GitLab CI¶
Use semvertag in GitLab CI via a small inline job that installs uv
and runs uvx semvertag tag. Your repo needs no PyPI install and no
pipeline YAML beyond the snippet below.
A one-line
include: - component: …via the GitLab CI Catalog is the eventual delivery path, and its descriptor lives attemplates/semvertag.yml. The component has not yet been published to gitlab.com's Catalog. Paste the job below into.gitlab-ci.ymluntil then.
Quick start¶
The minimum useful pipeline auto-tags on every push to the default branch.
Set
SEMVERTAG_TOKENas a project-level masked CI/CD variable holding a Project Access Token (or Personal Access Token) withapi+write_repositoryscope.CI_JOB_TOKENworks on projects where the job-token write scope is opted in (see Token scope below).
stages: [tag]
semvertag:
stage: tag
image: python:3.13-slim
resource_group: semvertag
variables:
SEMVERTAG_STRATEGY: branch-prefix
before_script:
- pip install --quiet --no-cache-dir 'uv>=0.4,<1'
script:
- uvx 'semvertag>=0.10.0,<1' tag
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
The job runs against the head commit on the default branch and, if a
bump is warranted by the configured strategy, pushes a new tag to the
project's origin. If no bump is warranted, the job exits 0 without
pushing.
semvertag bumps from the highest existing semver tag and never creates the first one. It reads plain semver tags such as
0.1.0andv-prefixed ones such asv0.1.0, and each new tag keeps the prefix of the one it bumps from. Other forms (V0.1.0,release-0.1.0) are ignored. Until a readable tag exists, every run reportsno_tagsand exits 0. Push one withgit tag 0.1.0 && git push origin 0.1.0(orv0.1.0forvtags).Upgrading from a release that ignored
vtags: if your history isv-prefixed and you seeded a bare tag such as0.1.0to get started, semvertag now bumps from whichever tag has the highest version, usually your latestvtag (afterv1.4.0comesv1.5.0, not0.2.0). When the same version exists in both forms, thevtag wins.
resource_group: semvertagmakes GitLab serialize concurrentsemvertagjobs across pipelines on the same project, so back-to-back pushes queue instead of racing thecreate_tagAPI. Drop or rename the group if you intentionally want concurrent tag pushes.
Strategy¶
Set SEMVERTAG_STRATEGY to one of:
| Value | Description |
|---|---|
branch-prefix (default) |
Bump from the source-branch prefix of the head commit, which must be a merge commit. |
conventional-commits |
Bump from the head commit's Conventional Commits message. |
When the Catalog component lands, this will become a typed inputs:
block on the include:. The values and default match
templates/semvertag.yml's
spec.inputs.strategy so the migration is a snippet swap.
Dry run¶
Pass --dry-run to compute the bump and report the planned tag
without pushing it:
The Catalog component exposes this as a boolean dry-run input,
false by default.
Required permissions¶
The job pushes a tag, so the token it uses must carry write access to
the repository. semvertag reads the token from these env
vars in order: SEMVERTAG_GITLAB__TOKEN, SEMVERTAG_TOKEN,
CI_JOB_TOKEN, GITLAB_TOKEN. The first set value wins.
Token scope: CI_JOB_TOKEN vs Project Access Tokens¶
Two cases govern which token the job should use:
- On GitLab projects where the maintainer has opted in to job-token
write scope (Settings → CI/CD → Token Permissions → Allow access
from the project's token to write to the repository),
CI_JOB_TOKENis auto-exported into every CI job and gets picked up by the alias chain with no further configuration. - On projects that have not opted in, or projects on older GitLab
versions where
CI_JOB_TOKENwas scoped read-only by default, the consumer creates a Project Access Token (preferred; scoped to the one project) or a Personal Access Token (works but bleeds the user's scope across all their projects). Token scopes required:api+write_repository. Store the token as a masked CI/CD variable namedSEMVERTAG_TOKEN; the alias chain picks it up ahead ofCI_JOB_TOKEN.
The alias chain reads
SEMVERTAG_GITLAB__TOKEN→SEMVERTAG_TOKEN→CI_JOB_TOKEN→GITLAB_TOKENin order and the first set value wins, so a staleSEMVERTAG_TOKENleft over from a prior PAT-based setup will silently override a freshly-rotatedCI_JOB_TOKEN. If you migrate from PAT → job-token, unsetSEMVERTAG_TOKEN(or rotate its value to empty) in the project's CI/CD variables.
For self-hosted GitLab, set SEMVERTAG_GITLAB__ENDPOINT as a project
CI/CD variable pointing to the instance's API root, e.g.
https://gitlab.example.com. The default is https://gitlab.com and
is not auto-derived from CI_SERVER_FQDN. Note the double underscore:
pydantic-settings uses __ as the nested-key delimiter, so
SEMVERTAG_GITLAB_ENDPOINT with a single underscore is silently
ignored.
Use scheme + host only for the endpoint. Do not append
/api/v4(the client adds it); a value likehttps://gitlab.example.com/api/v4produces…/api/v4/api/v4/…URLs and 404s. A missing scheme (gitlab.example.com) fails at request time with httpxConnectError. A trailing slash is tolerated (the client strips it).
For most consumers on gitlab.com-hosted projects with job-token
write scope, the minimal job snippet above is the entire setup.
Branch-prefix vs conventional-commits¶
Pick branch-prefix if your team merges merge requests with branch
names that follow a feature/..., bugfix/..., hotfix/...
convention and lands them as merge commits. semvertag reads the head
commit's source-branch prefix and bumps accordingly: feature/ bumps
minor, bugfix/ and hotfix/ bump patch, and a merge with any other
prefix bumps nothing (unmapped_branch_prefix). With squash merges the
head is not a merge commit and the run reports no_merge_commit.
This is the default. See
Branch-prefix strategy for the full
prefix-to-bump table and edge-case behavior.
Pick conventional-commits if your team writes
Conventional Commits messages
directly on the default branch (e.g. feat: add X, fix: handle Y,
feat!: drop Z), typically with squash merges so that each push is
one commit. semvertag reads the head commit's type prefix and body
(feat! or a BREAKING CHANGE: footer → major, feat: → minor,
fix: → patch,
everything else → none); it does not scan the commits since the
latest tag. See
Conventional Commits strategy
for the full type-to-bump mapping and the head-commit rule.
Set the strategy per project by swapping the SEMVERTAG_STRATEGY
value in the job:
Troubleshooting¶
Token missing scope or insufficient permission: 403¶
The token does not have api + write_repository scope, or the
project's protected-tag rules disallow the bot from creating tags.
Verify the SEMVERTAG_TOKEN scopes in the GitLab UI (Settings → Access
Tokens).
Project id missing. Set CI_PROJECT_ID or pass --project-id.¶
The CI runner did not export CI_PROJECT_ID. Every standard GitLab CI
job exports the variable, but a custom executor that strips CI
variables would suppress it. Set SEMVERTAG_PROJECT_ID as a
project-level CI/CD variable as the override.
Self-hosted GitLab, but the job connects to gitlab.com¶
The default endpoint is https://gitlab.com and is not auto-derived
from CI_SERVER_FQDN. Set SEMVERTAG_GITLAB__ENDPOINT as a
project-level CI/CD variable pointing to the instance's API root.
None of the repo's N tags is SemVer-form (1.2.0 or v1.2.0)¶
The repo has tags, but semvertag reads none of them as a bump baseline,
so the run reports no_tags and exits 0 without tagging. It reads only
plain SemVer tags (1.2.0, 1.0.0-rc.1) and the same with a lowercase
v (v1.2.0). It ignores other forms, such as V1.2.0,
release-1.2.0, PEP 440 prereleases like 0.9.0rc1, and floating
major tags like v1. Push a SemVer tag on a default-branch commit to
start from, e.g. git tag v0.1.0 && git push origin v0.1.0.
A bump-worthy push was never tagged¶
The run for that push failed or was skipped. Re-run it. Each run judges only the head commit of its own push and does not look back, so the next push cannot recover an earlier bump.