Release Automation¶
Shaken Fist Python projects use a standardised release workflow based on GitHub Actions, PyPI trusted publishers, and Sigstore signing. This page describes the release infrastructure and how to add it to a new project.
How It Works¶
When a maintainer pushes a git tag matching v* (e.g. v0.6.0),
the release.yml workflow:
- Builds the package using
python3 -m buildin a clean venv - Validates the package with
twine check - Waits for approval from a required reviewer (via a GitHub environment)
- Signs the tag using Sigstore/gitsign (keyless, OIDC-based)
- Publishes to PyPI using trusted publishers (no API tokens)
- Creates a GitHub Release with the built artifacts and auto-generated release notes
Maintainer pushes v0.6.0 tag
|
v
Build package ──> Upload artifacts
|
v
Wait for reviewer approval (GitHub environment)
|
v
Sign tag with Sigstore ──> Publish to PyPI ──> Create GitHub Release
Security Properties¶
The release process is designed to eliminate long-lived secrets:
- No PyPI API tokens -- authentication uses OIDC trusted publishers, where PyPI verifies the GitHub Actions workflow identity directly
- No GPG keys -- tag signing uses Sigstore's keyless signing with OIDC identity certificates, recorded in the Rekor transparency log
- Multi-party approval -- the
releaseenvironment requires a reviewer to approve before publishing proceeds - Protected tags -- tag rulesets prevent unauthorized users from creating release tags
- Build provenance -- Sigstore attestations cryptographically link published artifacts to the exact source commit
Adding Release Automation to a Project¶
Prerequisites¶
The project must:
- Use
pyproject.tomlwithsetuptools_scm(or similar) for version detection from git tags - Not have an old
release.shscript (remove it first)
Step 1: Copy the Templates¶
Templates are in
templates/release-automation/:
| Template | Destination |
|---|---|
release.yml |
.github/workflows/release.yml |
RELEASE-SETUP.md |
RELEASE-SETUP.md (repo root) |
Replace the placeholders in the copied files:
| Placeholder | Description | Example |
|---|---|---|
{{PROJECT_DISPLAY_NAME}} |
Human-readable name | Occy Strap |
{{PYPI_PACKAGE_NAME}} |
PyPI package name | occystrap |
{{GITHUB_REPO_NAME}} |
GitHub repo name | occystrap |
Step 2: Configure PyPI Trusted Publisher¶
- Log in to pypi.org
- Navigate to your project's Publishing settings
- Add a trusted publisher:
- Owner:
shakenfist - Repository: your repo name
- Workflow:
release.yml - Environment:
release
Step 3: Create GitHub Environment¶
- Go to Settings > Environments in the repository
- Create an environment named
release - Add required reviewers
- Restrict deployment to tags matching
v*
Step 4: Configure Protected Tags¶
- Go to Settings > Rules > Rulesets
- Create a tag ruleset for
v*with restricted creation and deletion - Add maintainers to the bypass list
Step 5: Remove Old Release Scripts¶
Delete any existing release.sh and commit the removal.
Projects Using This Infrastructure¶
| Project | PyPI Package | Status |
|---|---|---|
| shakenfist | shakenfist |
Live |
| occystrap | occystrap |
Live |
| kerbside | kerbside |
Live |
| agent-python | shakenfist-agent |
Added |
Verifying a Release¶
Tag Signature¶
gitsign verify --certificate-identity-regexp='.*' \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com' \
v0.6.0
PyPI Attestation¶
Check the Provenance section on the package's PyPI page.