Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ on:
description: 'Published stable SDK tag to deploy, or empty for current main'
type: string
required: false
reference_guide_commit:
description: 'Optional exact merged guide commit with source identical to the selected stable SDK'
type: string
required: false

permissions:
contents: read
Expand Down Expand Up @@ -174,6 +178,7 @@ jobs:
CLOUDFLARE_WEB_ANALYTICS_TOKEN: ${{ vars.CLOUDFLARE_WEB_ANALYTICS_TOKEN }}
STABLE_TAG: ${{ inputs.stable_tag }}
DOCS_WHEEL_SHA256: ${{ steps.release-channel.outputs.wheel_sha256 }}
DOCS_REFERENCE_GUIDE_COMMIT: ${{ inputs.reference_guide_commit }}
PYTHONDONTWRITEBYTECODE: '1'
run: |
if ! printf '%s' "$CLOUDFLARE_WEB_ANALYTICS_TOKEN" | grep -Eq '^[a-f0-9]{32}$'; then
Expand Down
8 changes: 5 additions & 3 deletions docs/sdk-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1152,7 +1152,7 @@ activities, for example `OrderInput(order_id=UUID(data["order_id"]), ...)`.
Adapter output is part of the durable history contract, so changing that shape
is a workflow compatibility change.

## Whole run histories and workflow lists (unreleased)
## Whole run histories and workflow lists

`Client.get_history(...)` and `Client.list_workflows(...)` each return one
Server page. Use the lazy iterators to visit all pages without collecting
Expand Down Expand Up @@ -1180,9 +1180,11 @@ earlier items have been yielded. A changing namespace is refused before the
next request. This traverses the Server's cursor results; it does not create a
snapshot of a concurrently changing workflow list.

These helpers are unreleased source tracked in
These iterators are included in the published SDK. Their actual multi-page
acceptance and cross-SDK guide delivery are tracked in
[the shared paging issue](https://github.com/durable-workflow/.github/issues/247).
Published-package and actual multi-page acceptance remain separate.
History pages are event dictionaries, not replay-export bundles. Use
`Client.export_history(...)` when a replay bundle is required.

## Typed activity values

Expand Down
27 changes: 22 additions & 5 deletions scripts/prepare_portal.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,23 @@ def published_landing(root: Path, sdk: Path, project: dict[str, Any], tag: str)
return landing


def published_reference(root: Path, sdk: Path) -> tuple[str, bytes]:
def published_reference(root: Path, sdk: Path, selected: str = "") -> tuple[str, bytes]:
published_revision = git(sdk, "rev-parse", "HEAD")
changed_api = git(root, "diff", "--name-only", published_revision, "HEAD", "--", "src/durable_workflow")
revision = published_revision if changed_api else git(root, "rev-parse", "HEAD")
current = git(root, "rev-parse", "HEAD")
revision = current
if selected:
if not re.fullmatch(r"[0-9a-f]{40}", selected):
raise ValueError("Reference guide requires an exact merged commit")
try:
git(root, "merge-base", "--is-ancestor", selected, current)
except subprocess.CalledProcessError as error:
raise ValueError("Reference guide commit is outside accepted documentation history") from error
revision = selected
changed_api = git(root, "diff", "--name-only", published_revision, revision, "--", "src/durable_workflow")
if changed_api:
if selected:
raise ValueError("Selected reference guide contains unpublished SDK source")
revision = published_revision
reference = subprocess.check_output(["git", "-C", str(root), "show", f"{revision}:docs/sdk-reference.md"])
published = (sdk / "docs/sdk-reference.md").read_bytes()
if CODE_BLOCKS.findall(reference.decode()) != CODE_BLOCKS.findall(published.decode()):
Expand All @@ -97,6 +110,7 @@ def published_reference(root: Path, sdk: Path) -> tuple[str, bytes]:

def stage_published_docs(
root: Path, sdk: Path, config: dict[str, Any], project: dict[str, Any], tag: str,
reference_commit: str = "",
) -> dict[str, str]:
stage = Path(tempfile.mkdtemp(prefix=".docs-stage-", dir=root))
# Start from the published tag. The landing and presentation can advance
Expand All @@ -110,7 +124,7 @@ def stage_published_docs(
shutil.copytree(root / "docs" / directory, stage / directory, dirs_exist_ok=True)
landing = published_landing(root, sdk, project, tag)
(stage / "index.md").write_text(landing)
reference_revision, reference = published_reference(root, sdk)
reference_revision, reference = published_reference(root, sdk, reference_commit)
(stage / "sdk-reference.md").write_bytes(reference)
config["docs_dir"] = str(stage)
config["nav"] = yaml.safe_load((sdk / "mkdocs.yml").read_text())["nav"]
Expand All @@ -135,11 +149,14 @@ def prepare(root: Path, environment: dict[str, str]) -> dict[str, Any]:
channel = "source-preview"
reference_guide = None
tag = environment.get("STABLE_TAG", "")
reference_commit = environment.get("DOCS_REFERENCE_GUIDE_COMMIT", "")
if reference_commit and not tag:
raise ValueError("Reference guide selection requires a published SDK tag")
if tag:
sdk = root / ".docs-sdk"
revision, project = published_identity(root, sdk, tag, environment)
version = project["version"]
reference_guide = stage_published_docs(root, sdk, config, project, tag)
reference_guide = stage_published_docs(root, sdk, config, project, tag, reference_commit)
channel = "stable"
else:
revision = docs_revision
Expand Down
48 changes: 48 additions & 0 deletions tests/test_portal_source_split.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,51 @@ def test_source_build_stays_preview_with_current_api_and_no_package_claim(source
assert audit["sdk_version"] == "2.15.0"
assert audit["docs_source_revision"] == audit["sdk_source_revision"]
assert audit["published_wheel_sha256"] is None


def test_merged_published_source_guide_can_advance_prose_after_main_api_changes(sources: tuple) -> None:
root, env, _installed = sources
match_published_api(root, env)
guide_commit = env["GITHUB_SHA"]
guide = (root / "docs/sdk-reference.md").read_bytes()
(root / "src/durable_workflow/__init__.py").write_text('PUBLIC = "unreleased"\n')
(root / "docs/sdk-reference.md").write_text('```python\nprint("future API")\n```\n')
git(root, "add", ".")
git(root, "commit", "-m", "Continue development after the stable guide")
env["GITHUB_SHA"] = git(root, "rev-parse", "HEAD")
env["DOCS_REFERENCE_GUIDE_COMMIT"] = guide_commit
audit = portal.prepare(root, env)
config = yaml.safe_load((root / ".docs-build.yml").read_text())
assert (Path(config["docs_dir"]) / "sdk-reference.md").read_bytes() == guide
assert audit["reference_guide"]["source_revision"] == guide_commit
assert audit["sdk_source_revision"] == git(root, "rev-parse", "2.14.1")
assert audit["docs_source_revision"] == env["GITHUB_SHA"]


@pytest.mark.parametrize("fault", ["symbolic", "unmerged", "source", "example", "preview"])
def test_explicit_guide_selection_preserves_publication_and_history_boundaries(sources: tuple, fault: str) -> None:
root, env, _installed = sources
match_published_api(root, env)
selected = env["GITHUB_SHA"]
if fault == "symbolic":
selected = "HEAD"
elif fault == "unmerged":
git(root, "checkout", "-b", "unmerged-guide", "2.14.1")
(root / "docs/sdk-reference.md").write_text("Unmerged guide")
git(root, "add", ".")
git(root, "commit", "-m", "Unmerged guide")
selected = git(root, "rev-parse", "HEAD")
git(root, "checkout", "-")
elif fault in {"source", "example"}:
file = "src/durable_workflow/__init__.py" if fault == "source" else "docs/sdk-reference.md"
(root / file).write_text('```python\nprint("future API")\n```\n')
git(root, "add", ".")
git(root, "commit", "-m", "Unpublished source or example")
selected = git(root, "rev-parse", "HEAD")
env["GITHUB_SHA"] = selected
else:
env["STABLE_TAG"] = ""
env["DOCS_REFERENCE_GUIDE_COMMIT"] = selected
with pytest.raises(ValueError):
portal.prepare(root, env)
assert not (root / ".docs-provenance.json").exists()
Loading