IMPORTANT: Developer documentation for the current development branch. This content is unreleased, may change without notice, and must not be treated as Buildish release documentation.

Self-Releasing Buildish Release Tooling

How Buildish Release Tooling uses its own lifecycles, workflows, and release controls.

Self-Releasing Buildish Release Tooling

Buildish Release Tooling uses its own release state, commands, and workflows to publish itself. This is the project’s reference deployment of Buildish rather than a configuration that every component must copy unchanged. Components can use different artifacts, approval policy, signing policy, environments, and publication targets while preserving the same trust and handoff boundaries.

The links on this page point to main and therefore describe the current, unreleased workflow. For a completed release, use the files at that release’s immutable tag instead.

Checked-in release definition

The production release uses the candidate lifecycle. TestPyPI uses a separate direct-lifecycle configuration so the project can exercise direct state composition without creating a test tag or GitHub Release.

File Role
release-config.yaml Production candidate and promotion policy, artifacts, signing, and publication targets
release-notes/ Reviewed version-specific Markdown fragments bound into candidate and final release state
release-testpypi-config.yaml Direct-lifecycle state used by the TestPyPI rehearsal
release-testpypi.yml Build, publish, recover, and verify an exact version on TestPyPI
release-candidate.yml Build and sign reproducible distributions, then publish a retained GitHub candidate
release-verify-candidate.yml Independently verify a named candidate and manifest digest
release-promote.yml Promote one exact candidate to the final GitHub Release and PyPI

The generic direct GitHub workflow is a harness fixture, not a live self-release entry point.

The production configuration requires buildish-release-tooling/release-notes/{version}.md. Candidate resolution reads the file from the selected source commit, not the dispatching workflow checkout, and binds its exact Markdown and SHA-256 into candidate-manifest.json. Promotion reuses those candidate-bound notes. Changing the notes therefore requires a new source commit and candidate. The TestPyPI-only configuration omits this block because it does not create a public GitHub Release.

Trust boundaries

The self-release workflows keep release source, executable workflow code, and credentials separate:

  1. The resolver checks out github.workflow_sha, resolves an exact source commit, and writes typed release state.
  2. The source commit must be reachable from protected main.
  3. The Required Checks result must come from .github/workflows/ci.yml, triggered by push on main. A same-name status or check from another producer does not satisfy the gate.
  4. Only the unprivileged build job checks out and executes the selected source.
  5. Jobs with signing material, an OIDC token, or contents: write execute the workflow revision identified by github.workflow_sha. Selected-source files enter those jobs only as data.
  6. Each same-run state or artifact handoff has a producer-emitted SHA-256 that the consumer verifies before signing or publication.

These boundaries are implemented jointly by the workflows, release configurations, repository rulesets, and GitHub Environments. The repository files cannot prove that the external settings are still configured as described below.

GitHub repository settings

Configure these controls before using the self-release workflows.

Rulesets

The main ruleset targets the default branch and currently requires:

  • pull requests with one approving review and approval after the last push;
  • squash merging and linear history;
  • the Required Checks check from the GitHub Actions integration;
  • protection against deletion and non-fast-forward updates.

Administrator and maintain-level bypasses are deliberate operational escape hatches. Using a bypass changes the assurance supplied by the branch gate and should therefore be an explicit maintainer decision.

The Release Tags ruleset targets v* and prevents update, deletion, and non-fast-forward changes without a bypass. Candidate and final tags both match this pattern. The separately configured Release Branches ruleset targets release/**; the current self-release path selects protected main, so it does not require a release branch.

Actions policy

GitHub Actions is enabled with selected actions and required full-SHA pinning. GitHub-owned actions are allowed. The release workflows additionally require these third-party publishers:

  • astral-sh/setup-uv;
  • pypa/gh-action-pypi-publish.

The repository pins every action invocation to a full commit SHA and leaves the corresponding version in a comment for Renovate and human review.

Environments

Environment Used by Required configuration
testpypi TestPyPI OIDC publication Restrict deployment branches to main
pypi Production PyPI OIDC publication Restrict deployment branches to main; add required reviewers if production publication needs explicit approval
release-candidate Candidate signing and GitHub candidate publication Create this environment, restrict it to main, and preferably require a reviewer

An environment protects only jobs that declare its name. Adding a secret to an environment does not protect a different contents: write job automatically.

Create all three under Settings > Environments. Select only main in each environment’s deployment-branch policy. The testpypi and pypi environments need no stored package-index credential: their publishing jobs use GitHub OIDC and the Trusted Publisher registrations below. For release-candidate, add the two environment secrets listed in the next section. If dispatching and approving a signing operation should be separate decisions, add a required reviewer and enable the option that prevents the initiating user from approving their own deployment.

Secrets and variables

The candidate workflow reads these values:

Kind Name Purpose
Environment secret in release-candidate BUILDISH_RELEASE_GPG_SECRET_SUBKEY ASCII-armored signing-secret export
Environment secret in release-candidate BUILDISH_RELEASE_GPG_PASSPHRASE Passphrase for that signing subkey
Organization or repository variable BUILDISH_RELEASE_GPG_SIGNING_FINGERPRINT Expected signing-subkey fingerprint

Store both sensitive values as secrets of this repository’s release-candidate environment. Do not grant this repository an organization-level or repository-level secret with either name: such a secret could be referenced by a job that does not pass the environment gate. If another component uses the same signing key, give that repository’s signing environment its own protected copy.

If either secret is currently provided at organization or repository scope, first create the environment-scoped copy, verify its name, and then remove this repository’s access to the broader copy. GitHub resolves a same-named environment secret for an environment-bound job, but leaving a broader copy available would still let a different job request it without passing this gate.

The workflow references the private key and passphrase only in the prepare-assets job, after its release-candidate environment gate; the unprivileged build job does not receive them. The public fingerprint is not secret and can remain an organization variable. The corresponding public key is published in the KEYS file. See the release-signing key guide for generation, backup, renewal, and revocation procedures.

PyPI Trusted Publishers

Neither package-index workflow uses a long-lived PyPI token. Configure the following GitHub Trusted Publisher identities exactly:

Index Owner Repository Workflow Environment
TestPyPI buildish-tooling buildish-release-tooling release-testpypi.yml testpypi
PyPI buildish-tooling buildish-release-tooling release-promote.yml pypi

Only the publishing jobs receive id-token: write. After publication, an unprivileged job reads the package-index state and PEP 740 provenance back, requires the expected repository, workflow, and environment identity, ignores bundles from unrelated publishers, and requires a cryptographically verified PyPI Publish Attestation for each retained distribution.

Release sequence

1. Rehearse on TestPyPI

Dispatch release-testpypi.yml with the exact version and, optionally, an exact source commit. The workflow:

  • resolves and retains DirectReleaseState;
  • applies the protected-main and trusted-check gates;
  • builds the wheel and sdist reproducibly;
  • preflights existing TestPyPI state;
  • publishes through the testpypi environment when files are absent or partially present;
  • verifies the exact remote files and their PEP 740 provenance.

TestPyPI and PyPI have independent project/version namespaces, so this rehearsal does not consume the production PyPI version.

2. Publish a production candidate

Dispatch release-candidate.yml with the final version, reviewed source commit, candidate label rc, and normally none for the vote profile. The workflow builds the wheel and sdist twice, rebuilds the wheel from the sdist, smoke tests both formats, creates checksums and detached OpenPGP signatures, and publishes a GitHub prerelease with candidate-manifest.json.

Retain the exact candidate tag and SHA-256 of candidate-manifest.json. Reviewers can run release-verify-candidate.yml with those values without mutating the candidate.

3. Promote the selected candidate

After the external approval decision, dispatch release-promote.yml with the final version, exact candidate tag, and retained candidate-manifest SHA-256. Promotion:

  • downloads and verifies the candidate manifest and every candidate asset;
  • creates the final tag at the candidate’s exact source commit;
  • stages, verifies, and publishes the GitHub Release;
  • publishes only the retained wheel and sdist to PyPI through the pypi environment;
  • reads back and verifies the PyPI files and PEP 740 provenance;
  • selects the verified PyPI Publish Attestations from the configured Trusted Publisher, retains those exact evidence documents as GitHub Release assets, and binds their asset URLs, SHA-256 digests, distribution digests, and publisher claims into the final release manifest;
  • attaches that final release manifest after both publication channels verify successfully.

The external approval process is intentionally outside Release Tooling. It selects one immutable candidate identity; the promotion workflow does not discover or substitute the “latest” candidate.

Recovery behavior

PyPI and GitHub publication cannot be atomic. Both package-index workflows therefore inspect the index before requesting an upload:

  • absent files proceed to publication;
  • a matching partial set uploads only after every existing file matches by name, size, and SHA-256;
  • a complete exact set skips publication and continues with verification;
  • any unexpected or conflicting file stops the workflow.

The publisher’s skip-existing option is used only after that preflight. A successful rerun must still pass the exact-set and attestation verification steps. The verification handoff uses a SHA-256 inventory covering the publication result, downloaded provenance, and selected attestation evidence. Candidate and promotion state files, artifact inventories, and publication results are retained as workflow artifacts to support failed-job analysis and recovery. The final manifest remains durable with the GitHub Release and identifies the exact retained PyPI Publish Attestation assets independently of that temporary workflow-artifact retention. The live Integrity API response is only the mutable source from which that exact evidence was selected. If interruption occurs between uploading the reserved buildish-verification-evidence--* assets and attaching the stable manifest, rerun the complete promotion workflow with the same immutable inputs. Earlier jobs tolerate only the exact configured evidence names in that reserved orphan namespace, and the attachment job accepts each existing asset only when its size and SHA-256 match the freshly verified evidence exactly.

Adapting the example

A component adopting this design should copy the invariants, not necessarily the repository’s names:

  • execute trusted tooling in credential-bearing jobs;
  • treat the selected source and built artifacts as untrusted data until verified;
  • bind cross-job and cross-run handoffs to immutable digests;
  • give write, signing, and OIDC credentials only to the jobs that need them;
  • configure and independently audit the repository settings that the workflow assumes;
  • verify external publication after mutation rather than treating a successful upload step as final evidence.

See Workflow composition for the general model and Candidate release and exact promotion for the lifecycle walkthrough.