Auto-tags a GitLab or GitHub repository with a semantic-version git tag from CI:
it reads the head commit of the default branch and the tag history through the
forge's REST API, asks a strategy how far the version should move, and creates
the new tag. Shipped as a Python CLI plus two thin wrappers over it — a GitHub
Actions composite (action.yml) and a GitLab CI Catalog component
(templates/semvertag.yml).
A term is listed only when there is a synonym to reject, or a meaning subtle enough that code and docs must agree on it. General semver and CI vocabulary — tag, version, release, major/minor/patch — does not belong here, however heavily this project uses it.
Head commit:
The single commit a run judges: the tip of the default branch at the moment the run starts, fetched
by get_latest_commit_on_default_branch and passed whole to BumpStrategy.decide. Nothing between
the latest tag and the head is read.
Avoid: latest commit. "Latest" is already pinned by latest tag to mean highest by precedence,
not newest by date, and the head is newest by position.
Forge:
A repository-hosting service semvertag talks to over REST — GitLab or GitHub today.
Avoid: host. host is already spoken for by the same-origin pagination guard, whose two
user-visible error strings say a Link header "points to a different host"; there it means scheme +
netloc, not a forge.
Provider:
The adapter for one forge — GitHubProvider / GitLabProvider under semvertag/providers/, matched
structurally against the Provider protocol. Also the user-facing selector value
(SEMVERTAG_PROVIDER=github). Note the collision: semvertag/ioc.py imports modern_di.providers
in the same file, where a provider is a DI recipe. Say provider for the forge adapter and
modern-di provider for the other.
Strategy:
The rule that turns one commit into a Bump or a decline. It receives the head commit of the
default branch and nothing else — no network, no tag history, no commit range. A strategy never picks
a tag or a version; the use-case does that around it.
Decline:
A strategy's answer that the head commit warrants no bump, naming which of its rules applied as a
status token and a fixed reason. Each distinct rule gets its own token, so a status never claims
something false about the commit (a merge commit is never reported as no_merge_commit).
Avoid: "no bump" for the strategy's answer. NoBump is the Outcome variant a decline becomes.
Latest tag:
The bump baseline: the highest by SemVer precedence among the repo's SemVer-form tags, bare
(1.2.0) or v-prefixed (v1.2.0) — not the most recently created one, and not the tag
reachable from HEAD. _select_latest_semver_tag sorts by semver.Version and takes the maximum.
The new tag inherits the latest tag's tag prefix: after v1.2.0 comes v1.3.0, after 1.2.0
comes 1.3.0.
Avoid: last tag, most recent tag. Both read as "newest by date", which names a different tag the
moment a patch on an older line is pushed after a newer minor.
Tag prefix:
What precedes the SemVer version in a tag name: lowercase v, or nothing. Any other prefix
(V1.2.0, release-1.2.0) makes the tag non-SemVer-form, so it is never a latest-tag candidate.
Avoid: bare "prefix". The branch-prefix strategy owns that word for feature/, bugfix/, ….
Outcome:
What a run did, as the closed sum Created | DryRun | NoTags | AlreadyTagged | NoBump in
semvertag/_outcome.py. It is internal and free to grow — the renderers match it exhaustively, so
a new variant is a type error until handled. Distinct from RunResult, the JSON wire DTO it
projects onto, and from status, the one string field on that DTO. The wire status tokens are a
frozen public contract (schema_version "1.0", parsed by jq in action.yml): a token never
changes meaning, though new ones may be added. The Outcome variant names are not frozen, and neither
is reason, the human-facing sentence beside the status, which may be reworded.
Prerelease:
Two incompatible spellings live in this repo and only one is recognized as a bump baseline.
SemVer-form (1.0.0-rc.1) parses, sorts by SemVer precedence, and finalizes on the next bump.
PEP 440 (0.9.0rc1) does not parse as SemVer and is skipped from selection entirely — yet that
is the form semvertag publishes itself under, and what release.yml means by "pre-release". Say
which form you mean.
Release tag / floating major tag:
This repo's own release tags are bare semver, no v (0.4.0): what semvertag emits here because
its latest tag is bare, what just publish feeds to
uv version $GITHUB_REF_NAME, and what release.yml triggers on. The floating major tag is
v-prefixed (v0): a single moving ref release.yml force-updates so action consumers can pin
uses: modern-python/semvertag@v0. Two conventions, one repo; a v on a release tag breaks
just publish.