Auto-bump patch version and create git tag on merge to master (CI) #50
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Right now the only version automation in
.gitea/workflows/ci.ymlissync-version-on-tag: when a tag is pushed manually, it correctspyproject.toml's version to match the tag if they've drifted. Tags themselves, and every version bump, are still entirely manual.This flips that around: version bumps should drive tags, not the other way around.
bump-versionCI job, using bump-my-version as the bump mechanism instead of hand-rolleduv version --bump.master, auto-bump the patch version — but only if the merged branch didn't already bump the version itself (covers a deliberate minor/major bump, or a manual patch bump, done by hand in the PR branch).vX.Y.Zgit tag.mastermust not trigger a bump or tag.The existing
sync-version-on-tagjob stays as-is — it's still a useful safety net for a tag created/moved by hand that doesn't matchpyproject.toml.Design
Detecting "did the branch already bump the version": compare
pyproject.toml's version atgithub.event.before(master's tip right before this push) against the version after the push. This works regardless of merge strategy since it's diffing master's own history, not inspecting individual PR commits.Bump mechanism:
bump-my-version, added as adevextra dependency, invoked viauv run bump-my-version bump patch --current-version "$(uv version --short)". The explicit--current-versionoverride means we never rely on.bumpversion.toml's own storedcurrent_versionstaying in sync withpyproject.toml—pyproject.toml(read via the existinguv version --shortconvention) remains the single source of truth, exactly as today.Tagging: kept as plain
git tag/git push, done uniformly in its own step for both cases (auto-bumped or already-bumped-by-the-branch) — simpler than relying on bump-my-version's owntag=truemachinery, which would only cover the auto-bump path..bumpversion.tomlsetstag = falseaccordingly; bump-my-version's job is strictly "bump + commit."uv.lock:
giant's ownversionfield is duplicated insideuv.lock(seegrep -A2 'name = "giant"' uv.lock). bump-my-version'spre_commit_hooks = ["uv lock", "git add uv.lock"]regenerates and stages it before the commit, mirroring the existingsync-version-on-tagjob'sgit add pyproject.toml uv.lock.Only merge commits trigger this: a direct/regular commit pushed straight to
mastershould not trigger a bump or tag — only an actual merge (a commit with 2+ parents, as gitea produces for "Merge pull request" merges — see e.g.dc4cad7in this repo's history) should. Checked withgit rev-parse HEAD^@ | wc -l(parent count of the currentHEAD) in a dedicated first step, whose output gates every later step in the job. Squash-merges and rebase-merges produce single-parent commits, so under this rule they're treated like regular commits and won't auto-bump/tag.Guarding against loops: the bump commit's message ends in
[skip ci](the conventionsync-version-on-tagalready relies on), so pushing it doesn't re-trigger the full pipeline. Independently, the merge-commit gate above is itself already a second guard: the bump commit and the tag are single-parent, non-merge commits, so even without[skip ci]this job would ignore them. Thebump-versionjob is also gated togithub.ref == 'refs/heads/master' && github.event_name == 'push', so it never fires on PRs or on tag pushes (the top-levelon.pushtrigger covers both branches and tags).Ordering:
bump-versionlistsneeds: [ruff-check, ruff-format, type-check, test], so master only gets bumped/tagged after CI is green on that push.Changes
1.
pyproject.tomlAdd
bump-my-versionto thedevextra:2. New file
.bumpversion.toml(repo root)(
current_versionhere is cosmetic bookkeeping only — every CI invocation overrides it via--current-version.)3.
.gitea/workflows/ci.ymlAdd a new job (
needs: [ruff-check, ruff-format, type-check, test], gated togithub.ref == 'refs/heads/master' && github.event_name == 'push'):Reuses the same
CI_TOKENsecret, container image, and uv-cache volume as the other jobs; git identity matchessync-version-on-tag.HEAD^@(git's "all parents of HEAD" syntax) checked viagit rev-parseis what determines merge-vs-regular-commit; every step after the check carries the sameif:guard.Verification
uv sync --extra cpu --extra devlocally, then sanity-check the config without touching anything:uv run bump-my-version show-bump— confirms the parse/serialize path (0.3.0 → patch → 0.3.1).uv run bump-my-version bump patch --current-version "$(uv version --short)" --dry-run -v— confirms it targets the right line inpyproject.tomland would run theuv lockhook, without writing anything.uv run ruff check ./uv run ruff format --check ./uv run ty check .— new.bumpversion.tomlandpyproject.tomledit don't affect these, but run for hygiene.masteron the gitea remote (this job can't be dry-run in isolation) — the cleanest way is to let the merge of the PR implementing this issue be the first real test: watch the Actions run, confirmbump-versionbumps0.3.0 → 0.3.1, pushes a[skip ci]commit, and pushes tagv0.3.1, and confirm the[skip ci]commit doesn't re-trigger the pipeline.I'd also like to use the commit messages to auto-create a changelog for every version — worth folding into this bump-version CI job (or a follow-up) so each tagged release gets a generated changelog entry alongside the version bump.
Fixed in
5b478d2on fix/issue-50.Added a bump-version CI job (needs the four existing checks, gated to actual merge commits on master via HEAD^@'s parent count) that auto-bumps the patch version with bump-my-version when a merged branch didn't already bump it, then creates and pushes a matching vX.Y.Z tag — exactly as designed in the issue body. sync-version-on-tag is untouched.
Folded in the changelog request from the comment above (per your choice when I asked during planning): the same job now also runs git-cliff to generate a changelog entry and prepends it to CHANGELOG.md before pushing. cliff.toml is tuned to this repo's plain imperative commit style rather than Conventional Commits — commits are grouped Added/Fixed/Removed/Changed by leading verb, '(gitea #N)' is linkified to the issue, and merge/[skip ci] commits are dropped. CHANGELOG.md starts fresh (no backfill of v0.2.0-v0.3.3).
Added tests/test_release_tooling.py to check .bumpversion.toml's search pattern actually matches pyproject.toml, and that cliff.toml groups/links/filters commits correctly against a synthetic repo (skipped if the git-cliff binary isn't on PATH).
Left open: real end-to-end verification needs an actual merge to master — the cleanest test is watching this PR's own merge bump 0.3.3 -> 0.3.4, push the bump + changelog commits, and push tag v0.3.4, then confirming the [skip ci] commits don't re-trigger the pipeline.