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
66 changes: 42 additions & 24 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
name: release

on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Existing signed release tag to publish (for example, v1.2.0)."
required: true
type: string
immutable_releases_preflight:
description: "Repository owner confirmed immutable releases through the administration API immediately before dispatch."
required: true
type: boolean

env:
RELEASE_TAG: ${{ inputs.tag }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}

permissions:
contents: read
Expand All @@ -20,6 +31,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
ref: ${{ env.RELEASE_TAG }}
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
Expand All @@ -31,25 +43,25 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
git fetch origin main
git merge-base --is-ancestor "$GITHUB_SHA" origin/main
test "$(git rev-parse "${GITHUB_REF}^{commit}")" = "$GITHUB_SHA"
VERSION="${GITHUB_REF_NAME#v}"
test "$GITHUB_REF" = "refs/heads/$DEFAULT_BRANCH"
SOURCE_COMMIT="$(git rev-parse "refs/tags/${RELEASE_TAG}^{commit}")"
git fetch origin "$DEFAULT_BRANCH"
git merge-base --is-ancestor "$SOURCE_COMMIT" "origin/$DEFAULT_BRANCH"
test "$(git rev-parse HEAD)" = "$SOURCE_COMMIT"
VERSION="${RELEASE_TAG#v}"
PACKAGE_VERSION="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
test "$VERSION" = "$PACKAGE_VERSION"
test "$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/check-runs" --jq '[.check_runs[] | select(.name == "conformance-gate" and .conclusion == "success")] | length')" -ge 1
test "$(gh api "repos/$GITHUB_REPOSITORY/commits/$SOURCE_COMMIT/check-runs" --jq '[.check_runs[] | select(.name == "conformance-gate" and .conclusion == "success")] | length')" -ge 1

- name: Require immutable GitHub releases before publication
- name: Require owner-admin immutable-release preflight
env:
GH_TOKEN: ${{ github.token }}
IMMUTABLE_RELEASES_PREFLIGHT: ${{ inputs.immutable_releases_preflight }}
run: |
test "$(gh api \
-H "X-GitHub-Api-Version: 2026-03-10" \
"repos/$GITHUB_REPOSITORY/immutable-releases" \
--jq '.enabled')" = true
test "$GITHUB_ACTOR" = "$GITHUB_REPOSITORY_OWNER"
test "$IMMUTABLE_RELEASES_PREFLIGHT" = "true"

- name: Require finalized release metadata in the exact tagged checkout
run: python scripts/check_release_metadata.py --version "${GITHUB_REF_NAME#v}"
run: python scripts/check_release_metadata.py --version "${RELEASE_TAG#v}"

- name: Re-run conformance on the tagged checkout
run: |
Expand All @@ -60,8 +72,8 @@ jobs:
- name: Build exact source, reproducible wheel, and external manifest
run: |
python scripts/release_artifacts.py build \
--ref "$GITHUB_REF" \
--release "${GITHUB_REF_NAME#v}" \
--ref "refs/tags/$RELEASE_TAG" \
--release "${RELEASE_TAG#v}" \
--output-dir dist

- name: Smoke-test the exact wheel
Expand Down Expand Up @@ -93,8 +105,8 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSION="${GITHUB_REF_NAME#v}"
TAG_OBJECT="$(git rev-parse "$GITHUB_REF_NAME")"
VERSION="${RELEASE_TAG#v}"
TAG_OBJECT="$(git rev-parse "$RELEASE_TAG")"
TAG_VERIFIED="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$TAG_OBJECT" --jq '.verification.verified')"
TAG_REASON="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$TAG_OBJECT" --jq '.verification.reason')"
if test "$TAG_VERIFIED" = true; then
Expand Down Expand Up @@ -122,23 +134,29 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$GITHUB_REF_NAME" \
gh release create "$RELEASE_TAG" \
dist/*.zip dist/*.whl dist/*.tar.gz dist/*.cdx.json dist/*.manifest.json \
--repo "$GITHUB_REPOSITORY" \
--verify-tag \
--draft \
--title "VSTD ${GITHUB_REF_NAME}" \
--title "VSTD $RELEASE_TAG" \
--notes-file release-notes.md

- name: Publish the complete draft atomically
env:
GH_TOKEN: ${{ github.token }}
run: gh release edit "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --draft=false
run: gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false

- name: Require the published release to be immutable
env:
GH_TOKEN: ${{ github.token }}
run: |
test "$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG" --jq '.immutable')" = true

- name: Stage only the Python distributions for PyPI
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: verifier-standard-${{ github.ref_name }}
name: verifier-standard-${{ env.RELEASE_TAG }}
path: |
dist/*.whl
dist/*.tar.gz
Expand All @@ -158,7 +176,7 @@ jobs:
- name: Download the exact tested Python distributions
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: verifier-standard-${{ github.ref_name }}
name: verifier-standard-${{ env.RELEASE_TAG }}
path: dist

- name: Publish through PyPI Trusted Publishing
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Use TIME for disagreements among normative documents, schemas, runtime behavior,
conformance tests, or public implementation claims. Do not use it for an honestly represented
`UNKNOWN`, a receipt or Graph `CONFLICTED` state, ordinary design work, or a roadmap item.
`TIME == CLEAR` is a release invariant for version 1.2.0. Development branches and normal
pull-request checks may retain exact unresolved entries, but the tag-triggered publication
pull-request checks may retain exact unresolved entries, but the owner-dispatched publication
workflow must reject the exact tagged checkout unless its status is `CLEAR`.

## 2. Prime directive
Expand Down
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,11 @@
subprocess.
- Test the advertised Python 3.10 through 3.13 range, add release-integrity and
installed-wheel jobs, and expose one required conformance gate for branch protection.
- Add a tag-triggered release workflow that refuses non-main or unconformed commits,
rebuilds and smoke-tests exact artifacts, records tag signature status without
relabeling it, and creates GitHub/Sigstore attestations for every uploaded asset.
- Add an owner-dispatched release workflow for an existing tag that refuses a
non-default-branch dispatch or a tag outside protected history, requires the protected
conformance check and an owner-confirmed immutable-release preflight, rebuilds and
smoke-tests exact artifacts, records tag signature status without relabeling it, and
creates GitHub/Sigstore attestations for every uploaded asset.
- Add structured ambiguity, counterexample, and implementation feedback surfaces plus
public conduct and pull-request consequence checks.

Expand Down
2 changes: 1 addition & 1 deletion HUMANS.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,6 @@ coordinates; resolve only from evidence.

Do **not** escalate a receipt's `CONFLICTED` evidence, an honest `UNKNOWN`, a roadmap item,
or speculative research to TIME. Development branches may keep precise open contradictions.
For publication, the tag-triggered workflow checks the exact tagged `TIME.md` and fails
For publication, the owner-dispatched release workflow checks the exact tagged `TIME.md` and fails
unless it contains exactly one `Status: CLEAR` line; maintainer judgment cannot override
that release invariant.
26 changes: 18 additions & 8 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ exact, publicly resolvable commit.

Development branches may record precise contradictions with [`TIME.md`](TIME.md) set to
`Status: OPEN`; normal pull-request checks do not prohibit that state. Publication is
different: the tag-triggered workflow runs `python scripts/check_time_status.py` against
different: the owner-dispatched release workflow runs `python scripts/check_time_status.py` against
the exact tagged checkout and fails unless it contains exactly one `Status: CLEAR` line.
There is no subjective override.

Expand All @@ -20,7 +20,7 @@ period, `CHANGELOG.md` says `UNRELEASED`, `CITATION.cff` identifies a release ca
has no `date-released`, and install instructions distinguish a source checkout from the
latest published package. Before tagging, land an explicit release-finalization change that
uses the actual publication date consistently in the changelog and citation metadata; do
not fabricate or backdate it. The tag workflow enforces this with
not fabricate or backdate it. The release workflow enforces this with
`python scripts/check_release_metadata.py --version <version>` and also refuses
release-candidate Zenodo metadata.

Expand Down Expand Up @@ -82,8 +82,14 @@ release-candidate Zenodo metadata.
[repository setting](https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/establish-provenance-and-integrity/prevent-release-changes)
and the
[versioned API](https://docs.github.com/en/rest/repos/repos?apiVersion=2026-03-10#check-if-immutable-releases-are-enabled-for-a-repository).
The tag workflow repeats this check and stops before publication when the setting is
disabled. Immutability locks the published tag and attached assets and generates a
GitHub documents this endpoint as requiring repository Administration read permission.
The default Actions token cannot request that permission, so the release workflow must
not present its token as an independent administrative recheck. Instead, the repository
owner performs this authenticated preflight immediately before dispatching the existing
`release.yml` workflow and supplies its required
`immutable_releases_preflight=true` control input. The workflow requires the dispatcher
to equal the repository owner and verifies the published release's `immutable` field as
a postcondition. Immutability locks the published tag and attached assets and generates a
GitHub release attestation; it does not correct false metadata. Corrections,
revocations, and superseding releases remain additive. Enable the setting only after
the draft-first workflow is present on the protected release commit.
Expand Down Expand Up @@ -119,11 +125,15 @@ release-candidate Zenodo metadata.
The manifest's source ref MUST resolve to its recorded public commit. The source ZIP
file set and every member byte MUST match that commit. CRLF/LF equivalence is not
accepted as byte identity.
8. Push the tag only after all preceding checks pass. The tag-triggered release workflow
rechecks protected-main ancestry, package version, the successful protected
8. Push the tag only after all preceding checks pass. Re-run the owner-authenticated
immutable-release API preflight, then dispatch `.github/workflows/release.yml` from the
protected default branch with the exact existing tag and
`immutable_releases_preflight=true`. The owner-dispatched release workflow
rechecks signed-tag identity, protected-main ancestry, package version, the successful protected
repository-check aggregate (the `conformance-gate` status context), the full test
suite, immutable-release setting, deterministic build, installed wheel, and artifact
manifest.
suite, final metadata, deterministic build, installed wheel, and artifact manifest.
The administrative preflight is owner-observed rather than Actions-token-observed; the
workflow stops unless that fact is explicitly supplied and refuses a non-owner dispatch.
It then attests the tested source ZIP, wheel, source distribution, SBOM, and external
release manifest. The workflow creates a draft, attaches the complete set, and only
then publishes it. A second job can
Expand Down
2 changes: 1 addition & 1 deletion TIME.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ When a contradiction is open, change the status to `Status: OPEN` and record the
coordinates, both incompatible claims, evidence for each side, and affected behavior. An
evidence-backed repair removes the resolved live entry and returns this file to
`Status: CLEAR`; Git history preserves the prior state. Development branches may remain
open. The tag-triggered publication workflow checks the exact tagged checkout and fails
open. The owner-dispatched publication workflow checks the exact tagged checkout and fails
unless this file contains exactly one `Status: CLEAR` line.
33 changes: 22 additions & 11 deletions tests/test_release_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,8 @@ def test_release_notes_use_the_github_tag_object_verification() -> None:

def test_release_is_drafted_with_attested_sbom_before_publication() -> None:
workflow = RELEASE_WORKFLOW.read_text(encoding="utf-8")
create = workflow.index('gh release create "$GITHUB_REF_NAME"')
publish = workflow.index('gh release edit "$GITHUB_REF_NAME"')
create = workflow.index('gh release create "$RELEASE_TAG"')
publish = workflow.index('gh release edit "$RELEASE_TAG"')

assert "dist/*.cdx.json" in workflow
assert "--draft" in workflow[create:publish]
Expand Down Expand Up @@ -482,20 +482,31 @@ def test_release_metadata_gate_accepts_one_final_consistent_coordinate(tmp_path:
release_metadata.require_finalized(tmp_path, "1.2.0")


def test_tag_release_contract_binds_main_version_gate_and_final_metadata() -> None:
def test_release_contract_binds_tag_owner_preflight_and_final_metadata() -> None:
workflow = RELEASE_WORKFLOW.read_text(encoding="utf-8")
required = (
'git merge-base --is-ancestor "$GITHUB_SHA" origin/main',
'git rev-parse "${GITHUB_REF}^{commit}"',
"workflow_dispatch:",
"immutable_releases_preflight:",
"ref: ${{ env.RELEASE_TAG }}",
'test "$GITHUB_REF" = "refs/heads/$DEFAULT_BRANCH"',
'git merge-base --is-ancestor "$SOURCE_COMMIT" "origin/$DEFAULT_BRANCH"',
'test "$(git rev-parse HEAD)" = "$SOURCE_COMMIT"',
'test "$VERSION" = "$PACKAGE_VERSION"',
'commits/$GITHUB_SHA/check-runs',
'commits/$SOURCE_COMMIT/check-runs',
'select(.name == "conformance-gate" and .conclusion == "success")',
'repos/$GITHUB_REPOSITORY/immutable-releases',
"--jq '.enabled')\" = true",
'python scripts/check_release_metadata.py --version "${GITHUB_REF_NAME#v}"',
'test "$GITHUB_ACTOR" = "$GITHUB_REPOSITORY_OWNER"',
'test "$IMMUTABLE_RELEASES_PREFLIGHT" = "true"',
'python scripts/check_release_metadata.py --version "${RELEASE_TAG#v}"',
'gh release create "$RELEASE_TAG"',
'releases/tags/$RELEASE_TAG',
"--jq '.immutable')\" = true",
)
for fragment in required:
assert fragment in workflow
assert workflow.index('repos/$GITHUB_REPOSITORY/immutable-releases') < workflow.index(
'gh release create "$GITHUB_REF_NAME"'
assert "repos/$GITHUB_REPOSITORY/immutable-releases" not in workflow
assert workflow.index('test "$IMMUTABLE_RELEASES_PREFLIGHT" = "true"') < workflow.index(
'gh release create "$RELEASE_TAG"'
)
assert workflow.index('gh release create "$RELEASE_TAG"') < workflow.index(
"--jq '.immutable')\" = true"
)