This document is for maintainers and contributors who need to understand the release flow, versioning scheme, CI gates, and secrets hygiene for qBit Manage.
Releases follow a three-step process involving two actors: CI automation and the operator (a repository maintainer).
Trigger the Release PR workflow from the GitHub Actions UI:
- Go to Actions → Release PR → Run workflow.
- Choose
version_bump_type(patch / minor / major; default: patch). - Optionally supply
release_notes_overrideto replace auto-generated notes.
The workflow:
- Checks out
develop, readsVERSION(e.g.4.7.2-develop5), strips the-developNsuffix, and computes the new release version per the bump type. - Creates a
release/v<NEW>branch as a snapshot of develop (so develop keeps moving while the release stabilizes — the release branch is a frozen copy at the snapshot point). - Writes the new version to
VERSIONon the release branch and pushes it. - Opens a PR from
release/v<NEW> → mastertitledRelease v<NEW>. - PR body is auto-generated from
git log origin/master..HEAD --oneline --no-merges, grouped by Conventional Commit prefix (feat,fix,chore, etc.). - Builds the 5-platform binary + Tauri desktop bundle matrix via the reusable
build-binaries.ymlworkflow. - Creates a draft GitHub release tagged
v<NEW>(target_commitishset to the release branch) with the auto-generated notes and built assets attached. The tag is not pushed yet — the draft is a testable preview.
Repo setting prerequisite: enable Settings → General → Pull Requests → Automatically delete head branches so the
release/v<NEW>branch is cleaned up on merge. If it's disabled, you'll need to delete the branch manually after merge.
The operator:
-
Reviews the release PR, checks the draft release notes, and optionally downloads and tests the binaries attached to the draft GitHub release.
-
Verifies all CI checks are green on the PR.
-
Merges
release/v<NEW>tomaster. UseRebase and merge(not squash) so the individual Conventional Commit messages (feat:,fix:, etc.) survive on master. Rationale:- The project relies on Conventional Commit prefixes for change-log
scanning (
git log --grep='^feat:') and for the release-notes auto-categorization above. Squashing collapses everything into one "Release v<X.Y.Z>" message and loses that signal. update-develop-branch.ymlresetsdeveloptomasterafter every release, so develop's history ends up matching master's. Rebase keeps that history granular and bisectable.- Individual commits remain revertible.
- Squash is hazardous: a squash commit message is built from the PR
title + the auto-generated changelog. If any line in it contains
[skip ci](VERSION-bump subjects historically did), GitHub skips every push-triggered workflow on master — notag.yml, so no tag, noversion.yml/pypi/develop-reset (this is exactly how the 4.9.0 release silently no-op'd).release-pr.ymlnow strips bump subjects and neutralizes CI-skip tokens in the notes as a backstop, but rebase-merge remains the supported path.
The release branch auto-deletes on merge (per the repo setting above).
- The project relies on Conventional Commit prefixes for change-log
scanning (
The master push triggers tag.yml (and update-develop-branch.yml) directly;
tag.yml then pushes the v<X.Y.Z> tag, which in turn triggers version.yml
and pypi-publish.yml. The cascade is two steps, not a single parallel burst:
| Workflow | What it does |
|---|---|
tag.yml |
Reads VERSION, creates and pushes the v<X.Y.Z> tag via Kometa-Team/tag-new-version. |
pypi-publish.yml |
Triggered by the new v* tag; builds the Python package and publishes to PyPI via trusted publishing (OIDC, no API token needed). |
version.yml |
Triggered by the v* tag; builds + pushes the Docker image, then publishes the draft GitHub release that release-pr.yml already prepared — flips it from draft to published. Does not rebuild binaries. |
update-develop-branch.yml |
Back-merges master into develop, bumps VERSION to the next patch-develop1 (e.g. 4.7.3-develop1); develop.yml chains automatically via workflow_run. |
After tag.yml pushes the v<X.Y.Z> tag, version.yml fires: it builds and
pushes the Docker image, then publishes the draft release (binaries and notes
were already attached by release-pr.yml in Step 1).
All production release runs share the fixed production-release concurrency
group and queue instead of cancelling in progress, serializing every release
run regardless of version tag. Both the pre-merge published-release guard in
release-pr.yml and the post-merge draft lookup in version.yml distinguish
an explicit not-found response from authentication, rate-limit, and transient
GitHub API failures. Unexpected API failures stop with their original message
instead of being reported as a missing release or allowing a published
release to reach the draft-update action.
Every push to develop triggers bump-version-develop.yml, which bumps VERSION
when needed. When that workflow finishes, develop.yml chains automatically via
workflow_run (same for update-develop-branch.yml after a master release).
The develop build always checks out the current develop tip so binaries, Docker,
and latest-develop match the bumped VERSION.
develop.yml then:
- Builds the full 5-platform binary + Tauri bundle matrix via
build-binaries.yml. - Pushes the
:developDocker image. - Deletes and recreates the
latest-developrolling GitHub pre-release at the current develop HEAD (gh release delete latest-develop --cleanup-tag, thengh release create latest-develop --prerelease --latest=false ...).
All develop-release runs share the fixed develop-release concurrency group and
queue instead of cancelling in progress. This prevents two runs from deleting or
creating the rolling release at the same time, and avoids cancellation between the
delete and create operations. Before deletion, the workflow checks whether the
release exists. Only an explicit not-found response skips deletion; authentication,
rate-limit, and transient GitHub API failures stop the job with the original error.
This distinction prevents an API outage from being misread as an absent release and
then reported misleadingly as a duplicate-tag failure during creation.
The latest-develop tag is intentionally non-v* so it never triggers tag.yml,
version.yml, or pypi-publish.yml. Use it to grab a development binary without
waiting for a full release.
By default, PRs do not run the 5-OS binary build — unlabeled PRs cost zero
runner minutes. When a maintainer adds the build label to a PR, pr-build.yml
triggers the full matrix via build-binaries.yml using the PR's head SHA. The
resulting qbit-manage-release-assets artifact is available for download from the
Actions → PR Build Artifacts workflow run, letting reviewers test all platforms
before merge. A concurrency group cancels superseded runs when new commits land on
the same labeled PR.
build-binaries.yml is a workflow_call reusable workflow — the single source of
truth for the PyInstaller + Tauri matrix. All three callers (release-pr.yml,
develop.yml, pr-build.yml) pass a ref input (branch / tag / SHA). The
workflow produces one consolidated qbit-manage-release-assets artifact containing
the 5 server binaries plus the per-platform desktop installer bundles.
For urgent fixes that cannot wait for the normal develop cycle:
- Create a branch from
masterwith thehotfix/prefix:git checkout master git pull origin master git checkout -b hotfix/fix-critical-crash
- Make the minimal fix. Open a PR directly to
master. - The PR requires explicit maintainer approval (branch protection rules apply).
- Once merged, the same Step 3 automation fires (tag → pypi → version → develop reset).
- The hot-fix commit is automatically backported to
developbyupdate-develop-branch.yml(develop is reset to master after every master push).
Never cherry-pick hot-fixes to develop manually — the reset workflow handles it, and manual cherry-picks create divergence.
Version strings live in a single file: VERSION at the repo root.
Format:
| Branch | Example | Meaning |
|---|---|---|
develop (active dev) |
4.7.2-develop5 |
5th auto-bump since 4.7.2 was cut |
master (release) |
4.7.2 |
Released version |
Auto-bump mechanics:
- The
bump-version-develop.ymlCI workflow (scripts/pre-commit/increase_version.shis still present locally for optional manual use) auto-increments thedevelopNcounter on every push todevelop. The pre-commitincrease-versionhook has been removed from.pre-commit-config.yaml; bumping is now CI-driven. - After a master merge,
update-develop-branch.ymlsets the next version: it strips the release suffix, bumps the patch segment by 1, and appends-develop1. Example:4.7.2→4.7.3-develop1. This keeps develop's version at a higher SemVer precedence than the release it follows, since it's an in-progress prerelease of at least the next patch.
Bump types are handled by the Release PR workflow's version_bump_type
input, which computes the new base version and writes it to the release/v<NEW>
branch (develop is not modified during this step):
patchpublishes develop's pre-guessed base as-is (no further increment) — e.g. develop at4.7.3-develop5→ release4.7.3. Adding another+1here would skip a version number, since develop's base is already the next-patch candidate.minor/majorintentionally discard the guessed patch and reset lower segments — e.g. develop at4.7.3-develop5→minorrelease4.8.0,majorrelease5.0.0.
Every PR to develop (and to master) is gated by tests.yml (triggered by
pull_request to master/main/develop), which runs pytest across Python
3.10–3.14. That is the only CI job that must pass before merge.
Ruff (lint/format) and yamllint are enforced as local pre-commit hooks, not as
CI jobs. develop.yml is a post-merge workflow triggered by
push: branches:[develop] — it is not a PR gate.
Branch protection on master requires at least one maintainer approval and all
status checks green.
A local pre-commit hook (check_no_tracker_secrets.py)
will scan staged files for patterns that match tracker credentials (API keys,
passkeys, announce URLs with embedded tokens). The hook blocks commits that
would accidentally include live tracker credentials sourced from a local
config/config.yml.
If the hook fires:
- Remove or redact the credential from the staged file.
- Add the file to
.gitignoreif it should never be committed (e.g. a personal config snippet). - If this is a false positive, contact a maintainer — do not bypass with
--no-verifywithout explicit approval.
| Secret | Used by | Purpose |
|---|---|---|
PAT |
tag.yml, update-develop-branch.yml, version.yml, release-pr.yml |
Push tags, force-push develop, publish releases, and open release PRs (bypasses branch protection) |
GITHUB_TOKEN |
Most workflows | Default Actions token for read operations and PR creation |
| PyPI OIDC | pypi-publish.yml |
Trusted publishing — no stored API token |
Secrets are managed in the repository's Settings → Secrets and variables → Actions. Never hardcode tokens in workflow files.
Release PR workflow fails on version parse:
The workflow expects VERSION to match X.Y.Z-developN on the develop
branch. If VERSION was manually edited to an unexpected format, correct it
before re-triggering.
update-develop-branch.yml fails to force-push develop:
This workflow requires the PAT secret (a personal access token with repo
scope and admin bypass for branch protection). Verify the secret is set and
has not expired.
tag.yml creates the wrong tag:
Kometa-Team/tag-new-version reads VERSION verbatim. If the version on
master contains a -develop suffix (it should not after a proper release
merge), the tag will be wrong. Fix VERSION on master and re-run the workflow.
PyPI publish fails with 400 Conflict: A release with the same version was already uploaded. Increment the version (patch bump) and issue a corrective release. PyPI does not allow re-uploading the same version.
bump-version-develop.yml triggers unexpectedly:
This workflow runs on every push to develop. To prevent it from bumping
VERSION on a specific push, include [skip-version-bump] in the commit
message (the bump commit itself uses this). Do not use [skip ci] — it
gets replayed into release notes and, on a squash-merge, into the master commit
message, which skips the entire release chain.
Release didn't run after a merge to master (no tag / no PyPI / no Docker):
The master commit message contained a CI-skip token ([skip ci] etc.), so
GitHub skipped tag.yml. Confirm with git log origin/master -1 --format=%B.
Recover:
gh release edit v<X.Y.Z> --draft=false --latest— publish the draft thatrelease-pr.ymlalready built (binaries + notes attached).gh workflow run tag.ymlandgh workflow run update-develop-branch.yml— manualworkflow_dispatchlevers to re-create the tag and reset develop.gh workflow run pypi-publish.yml --ref master— re-publish to PyPI. Prevention: rebase-merge release PRs; never put[skip ci]in a commit that can reach master.