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
31 changes: 26 additions & 5 deletions scripts/prepare_portal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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]:
Expand All @@ -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
Expand All @@ -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
Expand Down
66 changes: 66 additions & 0 deletions tests/test_portal_source_split.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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\n<span>Python 3.10+</span>\n'
'```python\nprint("complete example")\n```\n',
Expand All @@ -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"}]
Expand All @@ -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",
])
Expand Down
Loading