From 15acccd0e336163c51a14af2350b384aaeecc71f Mon Sep 17 00:00:00 2001 From: Ervins Strauhmanis Date: Fri, 15 May 2026 17:48:09 +0300 Subject: [PATCH 1/2] ci: align publish tag contract with release protocol --- .github/workflows/publish.yml | 8 -------- CHANGELOG.md | 4 ++++ docs/RELEASE_PROTOCOL.md | 22 ++++++++++++++++++++-- tests/test_architecture_contract.py | 12 ++++++++++++ tests/test_documentation_tooling.py | 10 ++++++++++ 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fdde0d8..2a2fc05 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -106,14 +106,6 @@ jobs: "::error::Release tag must point to a commit object" ) - verification = tag_object.get("verification") or {} - if not verification.get("verified"): - reason = verification.get("reason", "unknown") - raise SystemExit( - "::error::Release tag signature is not verified by GitHub " - f"(reason: {reason})" - ) - commit_sha = str(target_object.get("sha", "")) if len(commit_sha) != 40: raise SystemExit("::error::Resolved release commit SHA is malformed") diff --git a/CHANGELOG.md b/CHANGELOG.md index 25d2a21..323bdbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 already proved a detached bootstrap commit containing the full final payload. That removes the ambiguity around how to create `release/X.Y.Z` after bootstrap-path verification while keeping the PR diff against `origin/main` as the authoritative scope checkpoint. +- **Release publication now enforces annotated tags without depending on a separately configured GitHub-verified tag signature.** + The publish workflow and release runbook now agree that `vX.Y.Z` must be an immutable annotated + tag object, and the runbook includes the one allowed recovery path for replacing an accidentally + pushed lightweight tag before any public release object or assets exist. - **Runtime boundaries, release contracts, and validation semantics now fail closed earlier and more explicitly.** Streamed resource loading now enforces bounded input before allocation, filesystem loaders now apply bounded no-follow reads, custom-function and parsing diagnostics redact payloads by diff --git a/docs/RELEASE_PROTOCOL.md b/docs/RELEASE_PROTOCOL.md index 57a6d34..d9bdeef 100644 --- a/docs/RELEASE_PROTOCOL.md +++ b/docs/RELEASE_PROTOCOL.md @@ -298,10 +298,11 @@ Do not create the tag until the exact merged `main` commit you intend to tag has ## Step 6: Tag, Publish Workflow, And Asset Convergence -Create and push the version tag only after Step 5 is green: +Create and push the version tag only after Step 5 is green. Use an annotated tag object, not a +lightweight tag: ```bash -git tag vX.Y.Z +git tag -a vX.Y.Z -m "Release X.Y.Z" git push origin vX.Y.Z ``` @@ -340,6 +341,23 @@ the PyPI job fails — repair the workflow on `main`, merge the fix, and then re `workflow_dispatch` for the existing tag. Do not delete, move, or recreate the tag to retrigger publication. +If the publish workflow fails immediately because the tag is the wrong object type and GitHub +Release assets were never created, fix the contract first, then replace the tag exactly once with +an annotated tag on the intended release commit: + +```bash +gh release view vX.Y.Z --json tagName,isDraft,isPrerelease,publishedAt,url,assets || true +git tag -d vX.Y.Z +git push --delete origin vX.Y.Z +git tag -a vX.Y.Z -m "Release X.Y.Z" +git push origin vX.Y.Z +``` + +This recovery is allowed only when both of these are true: + +- the failed publish run exited before any public release object or assets were created; +- the replacement tag points to the same intended release commit you already verified in Step 5. + If GitHub Release assets need manual convergence after the workflow, use: ```bash diff --git a/tests/test_architecture_contract.py b/tests/test_architecture_contract.py index b091c69..a847efd 100644 --- a/tests/test_architecture_contract.py +++ b/tests/test_architecture_contract.py @@ -571,3 +571,15 @@ def test_release_workflows_do_not_depend_on_node20_compatibility_shims() -> None assert publish_workflow.count( "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" ) >= 3 + + +def test_publish_workflow_requires_annotated_tags_without_signature_verification_gate() -> None: + """The publish workflow should require annotated tags but not an external signing setup.""" + publish_workflow = (REPO_ROOT / ".github" / "workflows" / "publish.yml").read_text( + encoding="utf-8" + ) + + assert "Release tags must be annotated tag objects" in publish_workflow + assert 'ref_object.get("type") != "tag"' in publish_workflow + assert "Release tag signature is not verified by GitHub" not in publish_workflow + assert 'verification = tag_object.get("verification")' not in publish_workflow diff --git a/tests/test_documentation_tooling.py b/tests/test_documentation_tooling.py index c577776..95c42d8 100644 --- a/tests/test_documentation_tooling.py +++ b/tests/test_documentation_tooling.py @@ -549,6 +549,16 @@ def test_release_protocol_artifact_leak_check_uses_base_tooling() -> None: assert "| rg " not in text +def test_release_protocol_requires_annotated_tags_and_documents_lightweight_recovery() -> None: + """Release instructions should require annotated tags and bound the wrong-tag recovery path.""" + text = (REPO_ROOT / "docs" / "RELEASE_PROTOCOL.md").read_text(encoding="utf-8") + + assert 'git tag -a vX.Y.Z -m "Release X.Y.Z"' in text + assert "wrong object type" in text + assert "git push --delete origin vX.Y.Z" in text + assert "the failed publish run exited before any public release object or assets were created" in text + + def test_atheris_inventory_readme_matches_target_manifest() -> None: """The published Atheris inventory should stay aligned with the live target registry.""" readme = (REPO_ROOT / "fuzz_atheris" / "README.md").read_text(encoding="utf-8") From 47f8b6307aa81b27065f3e3909b8bb4f651caa66 Mon Sep 17 00:00:00 2001 From: Ervins Strauhmanis Date: Fri, 15 May 2026 18:08:22 +0300 Subject: [PATCH 2/2] ci: scope release-tag checks to release architecture --- CHANGELOG.md | 4 ++++ scripts/python_support_lib.py | 14 +++++++++----- tests/test_architecture_contract.py | 4 ++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 323bdbd..5f6bf06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 The publish workflow and release runbook now agree that `vX.Y.Z` must be an immutable annotated tag object, and the runbook includes the one allowed recovery path for replacing an accidentally pushed lightweight tag before any public release object or assets exist. +- **Release-tag immutability checks now live with the release architecture contract instead of the Python support owner.** + The repository validator that owns supported-interpreter truth now limits itself to Python + matrix wiring, while the publish workflow's annotated-tag requirements are enforced by the + release architecture tests that own publication semantics. - **Runtime boundaries, release contracts, and validation semantics now fail closed earlier and more explicitly.** Streamed resource loading now enforces bounded input before allocation, filesystem loaders now apply bounded no-follow reads, custom-function and parsing diagnostics redact payloads by diff --git a/scripts/python_support_lib.py b/scripts/python_support_lib.py index 2e72d64..6bf8d31 100644 --- a/scripts/python_support_lib.py +++ b/scripts/python_support_lib.py @@ -218,16 +218,20 @@ def _validate_workflows(root: Path, errors: list[str]) -> None: errors=errors, ) + # Premise: + # Python support owns interpreter-version truth for the publication workflow, + # but release-tag immutability and tag-object semantics are a separate + # release-contract concern. + # + # Reason: + # Keeping this validator scoped to Python-version wiring prevents unrelated + # release-tag policy changes from breaking the Python support owner while + # the release architecture tests remain responsible for tag semantics. publish_markers = ( "release-contract:", "fromJSON(needs.release-contract.outputs.supported-json)", "needs.release-contract.outputs.release-commit", "needs.release-contract.outputs.freethreaded-version", - "Resolve immutable annotated release tag", - "/git/ref/tags/", - "/git/tags/", - "Release tags must be annotated tag objects", - "Release tag signature is not verified by GitHub", ) for marker in publish_markers: _expect( diff --git a/tests/test_architecture_contract.py b/tests/test_architecture_contract.py index a847efd..f5b2ec3 100644 --- a/tests/test_architecture_contract.py +++ b/tests/test_architecture_contract.py @@ -579,7 +579,11 @@ def test_publish_workflow_requires_annotated_tags_without_signature_verification encoding="utf-8" ) + assert "Resolve immutable annotated release tag" in publish_workflow + assert "/git/ref/tags/" in publish_workflow + assert "/git/tags/" in publish_workflow assert "Release tags must be annotated tag objects" in publish_workflow assert 'ref_object.get("type") != "tag"' in publish_workflow + assert "Release tag must point to a commit object" in publish_workflow assert "Release tag signature is not verified by GitHub" not in publish_workflow assert 'verification = tag_object.get("verification")' not in publish_workflow