Releasing
Releases are driven by Conventional Commits and automated with release-please. Nobody bumps a version by hand and nobody pushes a v* tag by hand.
How a release happens
- Feature and fix work lands on
mainwith conventional commit messages, for examplefeat(status): add a dashboardorfix(skills): handle non-UTF-8 files. - On every push to
main, the Release workflow runs release-please. When there is something to release it opens (or updates) a release PR titledchore(main): release X.Y.Z. That PR contains theCHANGELOG.mdentry, the bumpedpyproject.tomlversion and the bumpedsrc/shotgrid_mcp_server/__init__.pyversion. - The release PR runs the normal CI. Because the PR is created with a personal access token,
pull_requestworkflows do fire on it. - Merging the release PR makes release-please create the
vX.Y.Ztag and the GitHub Release. The same workflow then builds the wheel and sdist, publishes them to PyPI with trusted publishing, attaches them to the GitHub Release, and finally asserts that both artifacts are actually attached.
What bumps the version
Two layers decide this. The workflow's preflight job decides whether release-please runs at all, and release-please then decides how far the version moves.
| Commit type | Effect in this repo |
|---|---|
feat | minor (0.17.1 → 0.18.0) |
fix, perf, refactor, revert | patch (0.17.1 → 0.17.2) |
feat! / fix! / a BREAKING CHANGE: footer | minor — while the major is 0, breaking changes bump the minor and stay below 1.0 |
chore, ci, docs, test, style, build, deps | no release at all |
The last row is a property of this repository's preflight job, not of release-please. release-please's own rule is much blunter: in determineReleaseType, anything that is neither breaking nor a feat falls through to a patch bump, so left to itself it would also cut a release for a docs: or chore: commit — and therefore for any future workflow that regenerates uv.lock or reformats code on main. preflight exists precisely to stop that.
The gate is implemented in scripts/ci/check_releasable_commits.py and covered by tests/test_release_preflight.py. It deliberately fails open: if the commit range cannot be resolved it reports releasable=true, because a release silently suppressed is worse than an unwanted release PR.
A commit type with no entry in changelog-sections still bumps the version, it just produces an empty changelog entry. That is why revert and deps are listed explicitly in release-please-config.json.
Version source of truth
.release-please-manifest.json holds the released version. Everything else is a mirror that release-please rewrites:
| File | Path release-please updates |
|---|---|
pyproject.toml | project.version |
src/shotgrid_mcp_server/__init__.py | __version__ (the line carries a # x-release-please-version marker, which is what release-please looks for) |
CHANGELOG.md | new entries are inserted above the legacy section |
Do not edit these by hand. The Version Consistency workflow fails a PR when any of them disagree with the manifest.
The extra-files entries in release-please-config.json partly repeat what the python release type already updates on its own (it knows about pyproject.toml and derives __init__.py from the package name). They are kept explicit so a change in release-please's built-in behaviour cannot silently stop updating them, and the two updaters are idempotent — release-please merges duplicates for the same path into a CompositeUpdater and applies them in order.
The entries below ## Legacy changelog (commitizen, before release-please) in CHANGELOG.md were generated by commitizen and are kept for history only.
Backfilling a release
If the build, publish or attach job fails after release-please already created a release, fix the cause and re-run the Release workflow by hand with the tag_name input set to the existing tag (for example v0.17.2). The workflow rebuilds from that tag, publishes to PyPI, attaches the artifacts and re-runs the asset assertion. A manual backfill skips the preflight gate and checks the artifacts against the tag you asked for rather than against a release-please version.
PyPI filenames are immutable, so a version that already exists there cannot be re-uploaded. The publish step runs with skip-existing: true precisely so that a backfill of a release whose files reached PyPI but whose GitHub assets were never attached can still succeed and finish the job. If a version really is missing from PyPI, upload it under a new version number instead.
Secrets
| Secret | Used for |
|---|---|
RELEASE_PLEASE_TOKEN (falls back to PERSONAL_ACCESS_TOKEN) | creating the release PR, tag and GitHub Release. A personal access token is required: with the default GITHUB_TOKEN, GitHub does not run pull_request workflows on the release PR and does not start workflows from the tag it creates. |
PyPI publishing uses trusted publishing, so no PyPI token is stored.
