From ec1f531715a1d4b3eccd9178d91f511eeb01e190 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Sat, 14 Nov 2026 01:44:40 +0000 Subject: [PATCH] Publish reference prose corrections for unchanged stable SDK source --- scripts/prepare_portal.py | 31 ++++++++++++--- tests/test_portal_source_split.py | 66 +++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/scripts/prepare_portal.py b/scripts/prepare_portal.py index 608fc08..7a54c8c 100644 --- a/scripts/prepare_portal.py +++ b/scripts/prepare_portal.py @@ -84,11 +84,24 @@ def published_landing(root: Path, sdk: Path, project: dict[str, Any], tag: str) return landing -def stage_published_docs(root: Path, sdk: Path, config: dict[str, Any], project: dict[str, Any], tag: str) -> None: +def published_reference(root: Path, sdk: Path) -> 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") + 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()): + raise ValueError("Reference examples differ from the selected published SDK documentation") + return revision, reference + + +def stage_published_docs( + root: Path, sdk: Path, config: dict[str, Any], project: dict[str, Any], tag: str, +) -> dict[str, str]: stage = Path(tempfile.mkdtemp(prefix=".docs-stage-", dir=root)) - # The complete SDK documentation content comes from tracked tag files. - # Only this closed set of presentation assets and the reviewed landing - # page come from the current documentation revision. + # Start from the published tag. The landing and presentation can advance + # independently. Reference prose can advance only for identical SDK source + # and unchanged examples; API pages and all other guides remain tag-bound. for source in source_files(sdk, "docs"): target = stage / source.relative_to(sdk / "docs") target.parent.mkdir(parents=True, exist_ok=True) @@ -97,12 +110,18 @@ def stage_published_docs(root: Path, sdk: Path, config: dict[str, Any], project: 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) + (stage / "sdk-reference.md").write_bytes(reference) config["docs_dir"] = str(stage) config["nav"] = yaml.safe_load((sdk / "mkdocs.yml").read_text())["nav"] documentation = next(plugin["mkdocstrings"] for plugin in config["plugins"] if isinstance(plugin, dict) and "mkdocstrings" in plugin) documentation["handlers"]["python"]["paths"] = [str(sdk / "src")] config["extra"]["published_sdk_version"] = tag + return { + "path": "sdk-reference.md", "source_revision": reference_revision, + "sha256": hashlib.sha256(reference).hexdigest(), + } def prepare(root: Path, environment: dict[str, str]) -> dict[str, Any]: @@ -114,12 +133,13 @@ def prepare(root: Path, environment: dict[str, str]) -> dict[str, Any]: raise ValueError("Documentation checkout differs from the selected workflow revision") sdk = root channel = "source-preview" + reference_guide = None tag = environment.get("STABLE_TAG", "") if tag: sdk = root / ".docs-sdk" revision, project = published_identity(root, sdk, tag, environment) version = project["version"] - stage_published_docs(root, sdk, config, project, tag) + reference_guide = stage_published_docs(root, sdk, config, project, tag) channel = "stable" else: revision = docs_revision @@ -137,6 +157,7 @@ def prepare(root: Path, environment: dict[str, str]) -> dict[str, Any]: "sdk_version": version, "sdk_source_inventory_sha256": hashlib.sha256(inventory_bytes).hexdigest(), "published_wheel_sha256": environment.get("DOCS_WHEEL_SHA256") if tag else None, + "reference_guide": reference_guide, } (root / ".docs-provenance.json").write_text(json.dumps(audit, sort_keys=True) + "\n") return audit diff --git a/tests/test_portal_source_split.py b/tests/test_portal_source_split.py index 56f4cdd..c3d0bca 100644 --- a/tests/test_portal_source_split.py +++ b/tests/test_portal_source_split.py @@ -42,6 +42,7 @@ def sources(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> tuple[Path, dict "src/durable_workflow/__init__.py": 'PUBLIC = "published"\n', "docs/index.md": '# Published\n```python\nprint("complete example")\n```\n', "docs/reference/metrics.md": "Published metric contract\n", + "docs/sdk-reference.md": '# Reference\nPublished explanation\n```python\nprint("published API")\n```\n', "docs/assets/logo.svg": "published-logo", "docs/stylesheets/layout.css": "published-style", "docs/javascripts/identity.js": "published-script", @@ -63,6 +64,9 @@ def sources(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> tuple[Path, dict (root / "pyproject.toml").write_text('[project]\nversion = "2.15.0"\n') (root / "src/durable_workflow/__init__.py").write_text('PUBLIC = "unreleased"\n') (root / "docs/reference/metrics.md").write_text("Unreleased metric contract\n") + (root / "docs/sdk-reference.md").write_text( + '# Reference\nCorrected explanation\n```python\nprint("published API")\n```\n', + ) (root / "docs/index.md").write_text( '# Redesigned\nPython SDK ยท 2.14.1 published\nPython 3.10+\n' '```python\nprint("complete example")\n```\n', @@ -89,6 +93,7 @@ def test_stable_build_combines_current_presentation_and_published_content(source config = yaml.safe_load((root / ".docs-build.yml").read_text()) stage = Path(config["docs_dir"]) assert (stage / "reference/metrics.md").read_text() == "Published metric contract\n" + assert (stage / "sdk-reference.md").read_text() == (root / ".docs-sdk/docs/sdk-reference.md").read_text() assert (stage / "stylesheets/layout.css").read_text() == "redesigned-style" assert (stage / "index.md").read_text().startswith("# Redesigned") assert config["nav"] == [{"Published metrics": "reference/metrics.md"}] @@ -101,6 +106,67 @@ def test_stable_build_combines_current_presentation_and_published_content(source assert str(root) not in (root / ".docs-provenance.json").read_text() +def match_published_api(root: Path, env: dict[str, str]) -> None: + source = root / "src/durable_workflow/__init__.py" + source.write_bytes((root / ".docs-sdk/src/durable_workflow/__init__.py").read_bytes()) + git(root, "add", ".") + git(root, "commit", "-m", "Same published API with corrected reference prose") + env["GITHUB_SHA"] = git(root, "rev-parse", "HEAD") + + +def test_same_published_api_can_deploy_corrected_reference_prose(sources: tuple) -> None: + root, env, _installed = sources + match_published_api(root, env) + audit = portal.prepare(root, env) + config = yaml.safe_load((root / ".docs-build.yml").read_text()) + stage = Path(config["docs_dir"]) + assert (stage / "sdk-reference.md").read_bytes() == (root / "docs/sdk-reference.md").read_bytes() + assert (stage / "reference/metrics.md").read_text() == "Published metric contract\n" + assert audit["reference_guide"]["source_revision"] == env["GITHUB_SHA"] + assert config["plugins"][1]["mkdocstrings"]["handlers"]["python"]["paths"] == [str(root / ".docs-sdk/src")] + + +@pytest.mark.parametrize("change", ["changed-source", "added-source"]) +def test_unreleased_api_keeps_reference_at_published_tag(sources: tuple, change: str) -> None: + root, env, _installed = sources + match_published_api(root, env) + source = root / "src/durable_workflow" / ("__init__.py" if change == "changed-source" else "future.py") + source.write_text('PUBLIC = "unreleased"\n') + git(root, "add", ".") + git(root, "commit", "-m", "Unreleased API") + env["GITHUB_SHA"] = git(root, "rev-parse", "HEAD") + 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() == ( + root / ".docs-sdk/docs/sdk-reference.md" + ).read_bytes() + assert audit["reference_guide"]["source_revision"] == audit["sdk_source_revision"] + + +def test_changed_reference_example_refuses_same_api_overlay(sources: tuple) -> None: + root, env, _installed = sources + match_published_api(root, env) + portal.prepare(root, env) + (root / "docs/sdk-reference.md").write_text('```python\nprint("future API")\n```\n') + git(root, "add", ".") + git(root, "commit", "-m", "Changed reference example") + env["GITHUB_SHA"] = git(root, "rev-parse", "HEAD") + with pytest.raises(ValueError, match="Reference examples differ"): + portal.prepare(root, env) + assert not (root / ".docs-provenance.json").exists() + + +def test_uncommitted_reference_edit_cannot_change_recorded_guide(sources: tuple) -> None: + root, env, _installed = sources + match_published_api(root, env) + committed = (root / "docs/sdk-reference.md").read_bytes() + (root / "docs/sdk-reference.md").write_text("Uncommitted explanation") + 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() == committed + assert audit["reference_guide"]["source_revision"] == env["GITHUB_SHA"] + + @pytest.mark.parametrize("fault", [ "sdk-source", "installed-code", "installed-version", "example", "wheel", "docs-revision", "support-floor", ])