Quick reference for maintainers releasing a new version of dep-rank.
These are already configured. Verify once if something seems broken:
- CI is passing on
main - PyPI Trusted Publisher configured for the
pypienvironment (Settings → Environments →pypi→ required reviewer set) - TestPyPI Trusted Publisher configured for the
testpypienvironment - Both environments use OIDC — no API tokens needed
-
Release BotGitHub App installed on this repo (needed to create lightweight tag refs with a non-GITHUB_TOKENidentity, so tag pushes trigger the downstream release workflow) -
RELEASE_BOT_APP_IDrepository variable set (Settings → Secrets and variables → Actions → Variables tab) -
RELEASE_BOT_PRIVATE_KEYstored in all three required stores (same key value in each — see "Release Bot Credential Topology" below):- Repo Actions secret (Settings → Secrets and variables → Actions → Repository secrets)
- Repo Dependabot secret (Settings → Secrets and variables → Dependabot → Repository secrets)
releaseenvironment secret (Settings → Environments →release→ Environment secrets)
-
releaseenvironment restricted tomain(Settings → Environments →release→ Deployment branches and tags → "Selected branches and tags" → patternmain)
One GitHub App (RELEASE_BOT_APP_ID) creates release tags and opens maintenance
PRs. Its private key is stored in three places because three workflows run in
contexts that cannot share a single store. All three intentionally hold the
same key value — this duplication is an accepted trade-off (a separate app
per trust level was considered and deferred).
| Store | Used by | Why this store |
|---|---|---|
release environment secret |
tag-release (Actions → Tag Release) |
The pinned shared tag-release reusable workflow declares environment: release. GitHub uses the environment secret instead of the caller-passed one, which is what scopes the release credential to the environment. |
| Repo Actions secret | pre-commit-autoupdate.yml |
Reads the secret directly in a normal scheduled job. |
| Repo Dependabot secret | ci.yml → update-lockfile |
Dependabot-triggered runs can read only Dependabot secrets. |
Access gating:
- The
releaseenvironment restricts the release credential to themainbranch (deployment-branch policy, patternmain). This is the GitHub-enforced guard thattag-releasecan only run frommain. - The
pypienvironment remains the human publication-approval gate: it requires a reviewer beforerelease.ymlpublishes to PyPI.tag-releaseis manually dispatched and intentionally has no reviewer/wait-timer onrelease— the human checkpoint lives atpypi.
Note: the GitHub API can confirm each secret exists and when it was updated, but not that the three values are equal. Treat equality as an invariant you maintain during rotation (below), not something you can verify after the fact.
GitHub Apps support multiple active private keys, so rotation is zero-downtime: add the new key everywhere, verify, then remove the old one.
- Generate a new private key for the Release Bot App
(App settings → Private keys → Generate a private key → downloads a
.pem). Leave the existing key active for now. - Update all three stores with the new key's full
.pemcontents (they must stay equal — see topology table):- Settings → Secrets and variables → Actions →
RELEASE_BOT_PRIVATE_KEY - Settings → Secrets and variables → Dependabot →
RELEASE_BOT_PRIVATE_KEY - Settings → Environments →
release→RELEASE_BOT_PRIVATE_KEY
- Settings → Secrets and variables → Actions →
- Verify each consumer with the new key before deleting the old one:
- pre-commit autoupdate (Actions store):
gh workflow run pre-commit-autoupdate.yml, then confirm the run succeeds (it mints an app token early). - lockfile update (Dependabot store): the
update-lockfilejob runs only on Dependabot PRs and cannot be dispatched manually. Confirm via the next Dependabot PR'supdate-lockfilejob succeeding, or re-run that job on the most recent open Dependabot PR's CI run. If no Dependabot PR is open, this store is validated by the next one. - tag-release (
releaseenv store): exercised by the next real release. A failed mint surfaces as the "Mint GitHub App token" step failing.
- pre-commit autoupdate (Actions store):
- Delete the old private key in the App settings once all three consumers have run successfully on the new key.
- Go to Actions → Tag Release → Run workflow
- Select the
mainbranch and pick abump:auto— infer from Conventional Commits since the last tag (feat:→ minor,fix:/chore:/docs:→ patch,<type>!:orBREAKING CHANGE:→ major)patch/minor/major— override the auto analysis
- Click Run workflow. The shared
tag-release.ymlreusable workflow computes the next version and creates a lightweightvX.Y.Ztag ref through the GitHub API using the Release Bot App. dep-rank has no.version-bump.json, so the workflow reports target-commit verification but does not hard-gate tag creation on that report; themainruleset's signed-commit requirement is the upstream enforcement point. - The
v*tag push triggersrelease.yml(test → build → TestPyPI → PyPI → GitHub Release)
The "auto" bump analyzer reads commit subjects since the last tag, so
Conventional Commits matter: every commit in a PR should match the PR's
user-visible intent, not the per-commit diff shape. A stray feat: in an
otherwise-fix: PR will flip a patch release to minor.
The UI only offers auto/patch/minor/major, so pre-releases use a manual
tag push. The same release.yml runs and the classifier flags the release
as prerelease automatically:
git checkout main && git pull origin main
git tag v0.2.0rc1 # or v0.2.0a1, v0.2.0b1, v0.2.0.dev1
git push origin v0.2.0rc1
Use PEP 440 canonical forms (no hyphens): a1 / b1 / rc1 / .dev1.
The release.yml workflow runs six jobs in sequence. Its leading test job is
a deliberate dep-rank-owned CI gate in addition to the shared-workflows
caller-owned PyPI template:
| Job | What it does |
|---|---|
test |
Runs linting, type checking, and tests as a CI gate |
build |
Verifies the tag is on main, builds sdist + wheel via uv build, uploads artifacts |
publish-testpypi |
Publishes the built artifacts to TestPyPI |
verify-testpypi |
Uses Python 3.13 and an explicit uv source to install only dep-rank from TestPyPI while resolving ordinary dependencies from PyPI, then runs dep-rank --version |
publish-pypi |
Waits for a required reviewer to approve the pypi environment, then publishes |
github-release |
Creates a draft GitHub Release with auto-generated notes and attached artifacts |
Monitor progress at:
https://github.com/j7an/dep-rank/actions
- Approve the
pypienvironment deployment when GitHub prompts you - Verify the live package:
pip install "dep-rank==${VERSION}" - Smoke-test:
dep-rank --version - Open the draft GitHub Release, review auto-generated notes, and click Publish release
The tag already exists. Fix the issue on main, then delete the tag and
re-run the Tag Release workflow:
git tag -d "v${VERSION}"
git push origin ":refs/tags/v${VERSION}"
# merge the fix to main, then:
# Actions → Tag Release → Run workflow → same bump as before
Same delete-and-re-push, but the re-tag is manual:
git tag -d "v${VERSION}"
git push origin ":refs/tags/v${VERSION}"
# merge the fix to main, then:
git tag "v${VERSION}" && git push origin "v${VERSION}"
The wheel and sdist are already uploaded to TestPyPI (immutable). You can publish to PyPI manually using the artifacts from the failed workflow run, or bump to a patch version and re-release via the Tag Release workflow.
The github-release job only runs if publish-pypi succeeds. If it was
skipped, create the release manually:
gh release create "v${VERSION}" dist/* \
--title "v${VERSION}" \
--generate-notes \
--draft