Releases
The auto-release pipeline, what triggers it, and how to opt out.
The pipeline
Push to main
│
▼ ci.yml runs (matrix: Ubuntu + Windows × Python 3.12 / 3.13 / 3.14)
│
▼ CI passes → workflow_run fires release.yml
│
▼
┌──────────────┐
│ bump-version │ patch +1 in pyproject.toml, push back to main via GITHUB_TOKEN
└──────────────┘
│
▼
┌──────────────┐
│ publish-pypi │ python -m build + twine upload (PYPI_API_TOKEN)
└──────────────┘
│
▼
┌──────────────────────┐
│ create-draft-release │ GitHub Release at tag v<X.Y.Z>, draft: true
└──────────────────────┘
│
▼
┌──────────────┐
│ build-nuitka │ Windows .zip with the standalone bundle
└──────────────┘
│
▼
┌────────────────┐
│ publish-release│ unmarks the draft, release appears in sidebar
└────────────────┘
What gets published per release
Artefact |
Where |
Size |
|---|---|---|
|
PyPI |
~120 KB |
|
PyPI |
~140 KB |
|
GitHub Release |
~250-350 MB (Nuitka bundle + PySide6) |
|
GitHub Release |
80 bytes |
The PyPI artefacts work on Linux / macOS / Windows; the zip is the standalone Windows bundle for users who don’t want a Python install.
Version bump strategy
Patch +1 on every CI-success push to main:
0.1.0 → 0.1.1 → 0.1.2 → 0.1.3 → …
The bump-version job:
Reads the current
version = "X.Y.Z"frompyproject.toml.Increments
Zby 1.Commits the bump back to
mainaschore: bump version to X.Y.<Z+1>via the workflow’sGITHUB_TOKEN.Outputs the new SHA, which downstream jobs check out.
The bump commit’s push does not re-trigger CI — GitHub’s
rule that GITHUB_TOKEN-driven pushes can’t start new workflow
runs naturally terminates the loop.
For a minor or major bump, edit pyproject.toml manually in a
PR (0.1.5 → 0.2.0); the bump-version job sees that the version
changed but its patch logic still adds +1 to the patch, so a
manual 0.2.0 becomes 0.2.1 on the next CI success. Plan
accordingly.
Opting out of a release: [skip release]
Include [skip release] anywhere in a commit message and the
auto-bump + every downstream job are gated off:
git commit -m "Tweak docs typo [skip release]"
git push
Use this for:
Docs / typo / comment-only changes.
Refactors that don’t change behaviour.
Test-only changes that don’t gate functionality.
Anything where the version number would just be noise.
CI still runs (it always runs on push to any branch). Only the release pipeline is skipped.
Loop prevention
Two layers of belt + suspenders:
GitHub’s native rule: pushes via
GITHUB_TOKENdon’t trigger workflows. So the bump commit’s push doesn’t fire CI, doesn’t fireworkflow_run, and the release pipeline can’t re-enter.Defensive check:
bump-versioncallsgit log -1 --pretty=%Band skips if the previous commit message starts withchore: bump version to. Survives even if GitHub’s rule changes.
Why --standalone not --onefile
The Nuitka build uses --standalone (produces a
thesisagents.dist/ folder, which we zip) rather than
--onefile (single self-extracting .exe).
Reasons:
Concern |
|
|
|---|---|---|
Startup latency |
Self-extracts to |
Runs in place, no extraction (sub-second) |
Antivirus |
Self-extracting binary often flagged |
Folder + .exe, treated normally |
Locked-down corporate machines |
|
No |
Distribution |
Just the .exe |
A .zip the user unzips once |
Update mechanism |
Replace the whole .exe |
Replace the folder (rsync-friendly) |
The zip pattern is the standard for Qt apps on Windows (Krita, OBS, KeePass all ship this way).
Why Windows only
Linux + macOS users install from PyPI. Shipping Nuitka binaries
for those platforms just inflates the release page without
serving a real use case — pip install thesisagents is already
the one-command install path. Windows is the only platform where
“download and double-click” is the common install pattern.
Build timings
Stage |
Cold (no cache) |
Warm (cache hit) |
|---|---|---|
|
~5 s |
~5 s |
|
~3-5 min |
~3-5 min |
|
~10 s |
~10 s |
|
~50-70 min |
~5-10 min |
|
~5 s |
~5 s |
Total |
~55-75 min |
~10-15 min |
The Nuitka cold build dominates because PySide6 + Qt are a huge
amount of C++ to link. The cache (keyed on pyproject.toml +
version) cuts subsequent builds dramatically. The timeout cap is
90 min — generous for the cold case.
Repo settings the pipeline needs
Configure once after forking / first deploy:
Settings → Actions → General → Workflow permissions→ “Read and write permissions”. Thebump-versionjob pushes the version commit viaGITHUB_TOKEN; defaultread-onlypermissions would block it.Settings → Secrets and variables → Actions → New repository secret→PYPI_API_TOKEN. Generate a project-scoped token at https://pypi.org/manage/account/token/.No GitHub Environment is needed. The
publish-pypijob intentionally does NOT attachenvironment: pypi— that would make GitHub treat the run as a Deployment and surface a “Deployments” sidebar widget, which is noise alongside the real Release entry.
Recovering from a failed release
The pipeline is stage-fail-fast. If build-nuitka times out
after publish-pypi succeeded:
PyPI has the new version. It cannot be re-published with the same version number (PyPI is immutable).
GitHub has a draft release at
v<version>. It can be deleted:gh release delete v0.1.X --yes.pyproject.tomlonmainhas the new version. The next push tomainwill bump to the next patch.
So a failed release just “skips” a version number — the gap
(v0.1.1 skipped between v0.1.0 and v0.1.2) is cosmetic.
Don’t try to recycle the PyPI version; bump to the next number
and try again.
Branch protection (recommended)
Optional but recommended for shared forks:
Require PR review before merging to
main.Require CI to pass before merging.
Require linear history (rebases) for cleaner version-bump commits.
The bump-version push uses the GitHub Actions bot identity, so
branch protections that require a CODEOWNER review will block
the bump. Add an exception for the GitHub Actions bot or use a
“Restrict who can push” rule that allows the bot.
Watching a release in progress
gh run watch # latest run, any workflow
gh run list --workflow=release.yml -L 5 # last 5 release runs
gh run view <run-id> --log # all logs
gh run view <run-id> --log-failed # only failed-step logs
The pipeline’s stages are linear — if bump-version runs but
publish-pypi is skipped, look at the bump-version outputs;
that’s usually a [skip release] tag triggering the skip path.