GitHub Actions¶
Use semvertag in GitHub Actions via the published composite action
(uses: modern-python/semvertag@v0). The action installs uv, runs
semvertag tag, and surfaces the result as step outputs. A pure-CLI
fallback for environments that can't consume the action lives at the
bottom of this page.
Quick start¶
The minimum useful workflow auto-tags on every push to the default branch.
The job needs a token with write access. Either rely on the workflow-scoped
GITHUB_TOKEN, which is auto-issued per job, and declarepermissions: contents: writein the workflow, or provide a fine-grained PAT withcontents: write(single repo) or a classic PAT withrepo/public_reposcope. Store the PAT as a repo secret namedSEMVERTAG_TOKEN; the alias chain picks it up ahead ofGITHUB_TOKEN.
name: semvertag
on:
push:
branches: [main]
permissions:
contents: write
jobs:
tag:
runs-on: ubuntu-latest
steps:
- uses: modern-python/semvertag@v0
The job runs against the head commit on the default branch and, if a bump is warranted by the configured strategy, creates a new tag ref via the GitHub API. 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.semvertag detects GitHub Actions from the
GITHUB_ACTIONS=trueenv var that GHA sets automatically, so the--providerflag is optional inside GHA. Pass--provider githubexplicitly only when running outside GHA (e.g. on a developer laptop targeting a github.com repo).semvertag reads the head commit and the tag history over the GitHub API and never touches the working tree, so the job needs neither an
actions/checkoutstep nor afetch-depthsetting. Add a checkout only if other steps in the same job need the repository files.
Strategy¶
Pass --strategy (or 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. |
Strategy-specific env vars (e.g.
SEMVERTAG_BRANCH_PREFIX__MINOR) stay configured on the calling step. The composite action only explicitly setsGITHUB_TOKENandSEMVERTAG_STRATEGY; every other env var on the calling step passes through to the action's run step.
Required permissions¶
The job creates a tag ref, so the token it uses must carry write
access to the repository's contents. semvertag reads the token from
these env vars in order:
SEMVERTAG_GITHUB__TOKEN, SEMVERTAG_TOKEN, GITHUB_TOKEN. The
first set value wins.
Outputs¶
When you give the step an id:, downstream steps can read three outputs:
| Output | Value |
|---|---|
tag |
The created tag (e.g. 1.2.3, or v1.2.3 in a v-prefixed repo), or empty string when status is no-bump. |
bump |
none | patch | minor | major. |
status |
created (tag pushed) | no-bump (nothing to tag: no prior tag, already tagged, or the strategy declined the head commit). On CLI error the action itself exits non-zero and this output is not written. |
Example: trigger a downstream release-notes job only when a tag was created.
name: semvertag-and-release
on:
push:
branches: [main]
jobs:
tag-and-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- id: semvertag
uses: modern-python/semvertag@v0
- if: steps.semvertag.outputs.status == 'created'
run: |
echo "tagged ${{ steps.semvertag.outputs.tag }}"
echo "bump=${{ steps.semvertag.outputs.bump }}"
Preview the next bump¶
Pass dry-run: true to compute the bump without pushing a tag. Use it in
CI smoke tests, in PR previews, or to see what the next release would be:
When dry-run: true, the action's status output is no-bump (no real tag
was pushed) and bump / tag reflect what would have happened. The raw
CLI's status field is dry_run; the action surface normalizes it to
no-bump so callers see a stable two-value enum.
You can also run this locally without the action:
Output (example):
{"schema_version":"1.0","strategy":"branch-prefix","bump":"minor","status":"dry_run","tag":"0.6.0","commit":"abc1234..."}
Token scope: GITHUB_TOKEN vs Personal Access Tokens¶
Three cases govern which token the job should use:
- The workflow-scoped
GITHUB_TOKENis preferred for most projects. GitHub Actions issues a fresh token per job; it inherits the workflow'spermissions:block. Addpermissions: contents: writeat the workflow level (as in the snippet above). The token is auto-exported asGITHUB_TOKENand picked up by the alias chain. - A fine-grained PAT scoped to the single repository needs the
Contents: Read and writescope. Store it as a repo secret namedSEMVERTAG_TOKEN; the alias chain picks it up ahead ofGITHUB_TOKEN. Use this when the workflow runs across organizations or needs scopes the workflow token can't grant. - A classic PAT needs the
repo(private repos) orpublic_repo(public repos only) scope and is stored the same way as the fine-grained PAT. It is less preferred because classic PATs bleed scope across all of the user's repos.
The alias chain reads
SEMVERTAG_GITHUB__TOKEN→SEMVERTAG_TOKEN→GITHUB_TOKENin order and the first set value wins, so a staleSEMVERTAG_TOKENleft over from a prior PAT-based setup will silently override the workflow'sGITHUB_TOKEN. If you migrate from PAT → workflow-token, unsetSEMVERTAG_TOKENfrom the repo's secrets.
For GitHub Enterprise, set SEMVERTAG_GITHUB__ENDPOINT as a
workflow-level env or a repo secret pointing to the instance's API
root, e.g. https://github.example.com/api/v3. The default is
https://api.github.com. Note the double underscore: pydantic-settings
uses __ as the nested-key delimiter, so SEMVERTAG_GITHUB_ENDPOINT
with a single underscore is silently ignored.
For most consumers on github.com-hosted repos with the
workflow-scoped GITHUB_TOKEN, the minimal workflow snippet above
is the entire setup.
Branch-prefix vs conventional-commits¶
Pick branch-prefix if your team merges PRs 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.
Without the composite action¶
If your environment can't consume the action (GitHub Enterprise instances without Marketplace access, security-constrained orgs that forbid third-party actions, or anyone who wants explicit control over the uv install step), paste the pure-CLI recipe instead:
jobs:
tag:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install --quiet --no-cache-dir 'uv>=0.4,<1'
- run: uvx 'semvertag>=0.10.0,<1' tag
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
The behavior matches the composite action exactly; only the install
shape differs. Strategy is set via env (SEMVERTAG_STRATEGY) or CLI
flag (--strategy …). This shape produces no outputs. Read
the CLI stdout, or invoke semvertag tag --json and parse the
envelope yourself.
Troubleshooting¶
Token rejected: 401. Verify SEMVERTAG_TOKEN is valid.¶
The token is malformed, expired, or revoked. Verify it in the GitHub UI
(Settings → Developer settings → Personal access tokens) or rotate the
workflow secret. When using the composite action, GITHUB_TOKEN is set
automatically from the token input (which defaults to
${{ github.token }}). When using the pure-CLI recipe in "Without the
composite action", add env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
to the run step.
Token missing scope or insufficient permission: 403¶
The token lacks contents: write (fine-grained / workflow-scoped) or
repo / public_repo (classic). For workflow-scoped tokens, add
permissions: contents: write at the workflow level. For PATs,
re-issue with the right scope.
GitHub repo not found: repo='...'¶
GITHUB_REPOSITORY was not exported, or --repo OWNER/REPO was not
passed. Inside GHA, GITHUB_REPOSITORY is auto-exported in every job;
outside GHA, set it explicitly.
Tag already exists: 'v...'¶
A previous run (or a concurrent run) already created this tag. semvertag refuses to silently succeed on a duplicate. Roll forward by pushing another commit that changes the bump, or delete the duplicate tag.
GitHub Enterprise, but the job connects to api.github.com¶
The default endpoint is https://api.github.com. Set
SEMVERTAG_GITHUB__ENDPOINT (note the double underscore) as a
workflow-level env pointing to the instance's API root, e.g.
https://github.example.com/api/v3.
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.