Releasing TraceLens¶
TraceLens uses tag-driven releases. The package version comes from the git
tag, and CI publishes only from a release tag. The pipeline creates the tag:
the "Release on merge" workflow after CI passes on main, the "Release tag"
workflow when a release pull request is merged, or a maintainer pushes one by
hand.
This avoids hand-edited version numbers, release loops, and PyPI's immutable version constraint.
Release Model¶
- Every pull request and every
maincommit runs tests, lint, typecheck, and package build validation. - A tag named
vX.Y.Zbuilds package versionX.Y.Z. - Pushing that tag triggers
.github/workflows/release.yml. - After CI passes on a push to
main, the "Release on merge" workflow releases whatever the changelog has queued under[Unreleased]: it picks the version, moves the entries into a dated section, commitsrelease: vX.Y.Ztomain, tags it, and starts the release workflow (Automatic Releases). - For a version you name (a pre-release, or a jump the rules would not
make), the "Release prepare" workflow turns
[Unreleased]into a dated section and opens a release pull request; merging it tags the merge commit (the "Release tag" workflow). - The release workflow builds the package, verifies the tag matches the built
version, renders the release notes from the changelog's dated section,
publishes to PyPI using trusted publishing through the
releaseenvironment (a reviewer required there gates every path), and then creates the GitHub Release for the tag with those notes and the built files attached. - Nothing is published unless the build, the version check, and the notes
succeed first, and no GitHub Release is created unless publication
succeeded. Tags whose version is not a final
X.Y.Z(rc,a,b,dev,post) are marked as pre-releases on GitHub.
One-Time Setup¶
TraceLens already has a PyPI project and trusted publishing configured. Re-run
this section only if the repository, workflow name, or PyPI ownership changes,
when the "Release prepare" workflow reports that it may not open pull
requests, or when "Release on merge" reports that main rejected its push.
- Confirm the package metadata:
-
In PyPI, add a trusted publisher for this repository:
-
Owner:
ssf0409 - Repository:
tracelens - Workflow:
release.yml -
Environment:
release -
In GitHub, create the
releaseenvironment under repository settings. Add required reviewers if you want a manual approval gate before publishing. -
Let the "Release prepare" workflow open pull requests: Settings → Actions → General → Workflow permissions → enable "Allow GitHub Actions to create and approve pull requests". Without it the workflow pushes the release branch, fails to open the pull request, removes the branch again, and says so.
-
Let the "Release on merge" workflow push to
main. Without branch protection nothing is needed. Ifmainrequires pull requests, allow GitHub Actions to bypass that rule; otherwise the workflow cannot land its release commit and falls back to opening the release pull request through "Release prepare", which you then merge.
No PyPI API token is required when trusted publishing is configured correctly.
Automatic Releases¶
Every merge to main that leaves entries under [Unreleased] in
CHANGELOG.md becomes a release once CI is green on that commit. The
"Release on merge" workflow (.github/workflows/release-auto.yml) runs when
the CI workflow completes for a push to main, and:
- Skips a red or cancelled CI run; a release commit itself
(
release: vX.Y.Z, or the merge of arelease/vX.Y.Zpull request: the "Release tag" workflow owns those); a commit whose title carries[release: skip]; an empty[Unreleased]section; and a latest tag that is a pre-release (v1.0.0rc1: finish it with "Release prepare"). The job summary says which. - Picks the version (
scripts/next_version.py) from the latestvX.Y.Ztag and the commit title: [release: X.Y.Z]names the version outright;[release: major],[release: minor], or[release: patch]names the bump;- otherwise entries under
### Added,### Changed,### Removed, or### Deprecatedmake a minor release, and entries only under### Fixed(or### Security) a patch release. While0.x, breaking changes are minor bumps; use[release: major]when it is time for1.0.0.
Only the first line of the merge commit message is read (the pull
request title for a squash merge), so a pull request description can
discuss the markers freely.
3. Releases: scripts/prepare_release.py moves the entries into
## [X.Y.Z] - <today>, the notes are rendered to check the section, the
result is committed to main as release: vX.Y.Z, tagged vX.Y.Z, and
the release workflow is started with publish=true (PyPI through the
release environment, then the GitHub Release). The commit and the tag
are pushed with the workflow token, which raises no events: CI does not
run again and nothing loops.
Preview the decision for the commit you are about to merge:
git log -1 --format=%B > /tmp/msg.txt # or write the intended title
python scripts/next_version.py --message-file /tmp/msg.txt
A release freezes the section it creates. A pull request updated after a
release must keep its entries under [Unreleased] (a merge or rebase can
leave them inside the section that was just released); CI's changelog check
(scripts/check_changelog.py, run by the lint job on every pull request)
refuses entries added to a released section. The contributor side is in
CONTRIBUTING.md.
To batch several pull requests into one release, put [release: skip] in
every merge title but the last. To stop automatic releases altogether,
disable the "Release on merge" workflow under Actions; the other two paths
keep working.
If main moves on before the workflow can push (two merges in quick
succession), the run for the older commit stops and the CI run of the newer
commit releases everything queued by then. If main rejects the push
(branch protection against GitHub Actions), the workflow starts "Release
prepare" with the same version, which opens the release pull request for you
to merge, and says so in its job summary.
Cut A Release By Hand¶
For a pre-release, a version the rules above would not pick, or when the automatic path is off, the manual path is two clicks and one review.
- Run the "Release prepare" workflow (Actions → Release prepare → Run
workflow) with the version, for example
0.5.0, and an optional one-paragraph summary. It checks that the version is well-formed, not older than the latest tag, and unused; moves the changelog's[Unreleased]entries into## [0.5.0] - <today>(refusing an empty section); pushesrelease/v0.5.0; opens the pull requestrelease: v0.5.0with the rendered notes as its description; and starts CI on the branch. Preview the notes locally at any time:
python scripts/prepare_release.py --version 0.5.0 --check
python scripts/release_notes.py --version 0.5.0 # after the section exists
-
Review and merge the release pull request. Edit
CHANGELOG.mdon the branch if the wording needs work; the pull request description is a preview, the changelog section is the source. Its checks are theci.ymlrun the workflow started on the branch (pull requests opened by a workflow do not triggerpull_requestworkflows themselves). Merge it with any merge method: the "Release tag" workflow reads the version from the commit that lands onmain(release: v0.5.0after a squash or rebase merge, therelease/v0.5.0branch name in a merge commit), tags that commitv0.5.0, and starts the release workflow withpublish=true, which publishes to PyPI (thereleaseenvironment and any reviewer required there still apply) and creates the GitHub Release. -
Verify that every public channel tells the same story:
git ls-remote --tags origin "vX.Y.Z" # the tag exists on GitHub
python -m pip index versions tracelens # PyPI lists X.Y.Z
gh release view vX.Y.Z --json isPrerelease,assets,body # the GitHub Release exists,
# is (not) a pre-release, has the wheel
# and sdist, and its body is the
# changelog section
python scripts/release_notes.py --version X.Y.Z # same notes locally
mkdocs build --strict # docs still build
- Smoke test from a clean environment once PyPI lists the version:
python -m venv /tmp/tracelens-release-smoke
/tmp/tracelens-release-smoke/bin/python -m pip install tracelens
/tmp/tracelens-release-smoke/bin/tracelens --help
Whichever path creates it, the tag is new, the changelog has the dated
section for its version, and only a tag is ever published: the "Release tag"
workflow tags only the merge of a release/vX.Y.Z pull request, and
"Release on merge" only its own release: vX.Y.Z commit.
Manual fallback¶
If the workflows are unavailable, the tag-driven path still works on its own:
- Move the
[Unreleased]entries into## [X.Y.Z] - YYYY-MM-DD(or runpython scripts/prepare_release.py --version X.Y.Z) and commit. - Run the verification gate (
make verify,make docs) and, for packaging or dependency changes, the built-wheel smoke in Contributor Testing. git tag vX.Y.Z && git push origin vX.Y.Z. The pushed tag triggers the release workflow directly, exactly as the automatic path does.
If A Release Fails¶
- Build, version check, or release notes fail. Nothing was published. Fix the cause (usually a missing changelog section or a tag that does not match the version), delete the tag locally and remotely, and push it again.
- PyPI publication fails. No GitHub Release is created. PyPI versions are immutable, so if the failure happened after upload, do not retag; if it happened before, fix and re-run the workflow.
- PyPI succeeded but the GitHub Release step failed. Re-run the workflow
(either "Re-run failed jobs" or "Re-run all jobs"): publishing is skipped
for files PyPI already has (
skip-existing), and the release step updates an existing release instead of failing on "already exists". Re-running never uploads a different file under an already-published version. - Something else needs checking first. Use "Run workflow"
(
workflow_dispatch) on the release workflow withpublishleft off, on a branch or a tag: it builds, verifies, and shows the rendered notes in the job summary without publishing anything. - "Release prepare" fails with "GitHub Actions is not permitted to create or approve pull requests". Enable the setting in step 4 of One-Time Setup and run the workflow again; it removed the release branch it had pushed, so nothing needs cleaning up.
- A release pull request must be abandoned. Close it and delete the
release/vX.Y.Zbranch; nothing was tagged or published. Run "Release prepare" again later, with the same version if it is still right. - The tag was created but the release workflow never ran (the dispatch
in "Release tag" failed). Re-run the "Release tag" job: it keeps a tag that
already points at the release commit and only dispatches again. Or run the
release workflow by hand on the tag with
publish=true; both are safe to repeat. - "Release on merge" did not release a merge you expected it to. Its job
summary says why: CI was red or cancelled,
[Unreleased]was empty, the title said[release: skip], the latest tag is a pre-release, ormainmoved on before it could push (the next green commit releases everything queued). Fix the cause and merge again, or use "Release prepare". - The
release: vX.Y.Zcommit is onmainbut the tag or the publish run is missing. Re-run the "Release on merge" job: it recognises its own release commit on top of the tested commit, keeps a tag that already points there, and dispatches the release again. Or push the tag by hand (git tag -a vX.Y.Z -m "release: vX.Y.Z" <sha> && git push origin vX.Y.Z), which triggers the release workflow directly. - "Release on merge" fails with "CHANGELOG.md already has a section for X.Y.Z but there is no tag vX.Y.Z". A release commit landed without its tag. Re-run the "Release tag" job for that commit (or "Release on merge", or tag by hand as above); the next merge then releases normally.
- "Release on merge" opened a release pull request instead of releasing.
mainrejected its push. Merge that pull request to publish, and see step 5 of One-Time Setup to make the next release automatic.
Dependency Guidance¶
Downstream projects should depend on TraceLens from PyPI:
Public GitHub or PyPI dependencies do not need a CI secret. A secret is only needed when a downstream CI job checks out or installs a private repository.
For local pre-release checks, prefer the built-wheel and downstream smoke guidance in Contributor Testing. TestPyPI is optional and mainly useful when changing the publishing workflow itself.