diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d31535a..06b775a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -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 @@ -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 diff --git a/docs/sdk-reference.md b/docs/sdk-reference.md index 913a922..3bb9f59 100644 --- a/docs/sdk-reference.md +++ b/docs/sdk-reference.md @@ -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 @@ -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 diff --git a/scripts/prepare_portal.py b/scripts/prepare_portal.py index 7a54c8c..d9dbf3a 100644 --- a/scripts/prepare_portal.py +++ b/scripts/prepare_portal.py @@ -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()): @@ -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 @@ -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"] @@ -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 diff --git a/tests/test_portal_source_split.py b/tests/test_portal_source_split.py index c3d0bca..2ae0bce 100644 --- a/tests/test_portal_source_split.py +++ b/tests/test_portal_source_split.py @@ -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()