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..5f6bf06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,14 @@ 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. +- **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/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/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 b091c69..f5b2ec3 100644 --- a/tests/test_architecture_contract.py +++ b/tests/test_architecture_contract.py @@ -571,3 +571,19 @@ 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 "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 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")