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-tag on every push to the default branch.
Required setup. Either rely on the workflow-scoped
GITHUB_TOKEN(which is auto-issued per job) — in which case the workflow MUST declarepermissions: contents: write— 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: actions/checkout@v4
with:
fetch-depth: 0
- uses: modern-python/semvertag@v0
The job runs against the latest 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.
Auto-detection. semvertag detects GitHub Actions from the
GITHUB_ACTIONS=trueenv var that GHA sets automatically. The--providerflag is therefore optional inside GHA — explicit--provider githubis only needed when running outside GHA (e.g. on a developer laptop targeting a github.com repo).
fetch-depth: 0matters: semvertag walks commit history to determine the bump.actions/checkout@v4's defaultfetch-depth: 1only fetches the single tip commit and will miss tag-relative history.
Strategy¶
Pass --strategy (or set SEMVERTAG_STRATEGY) to one of:
| Value | Description |
|---|---|
branch-prefix (default) |
Bump from the source-branch prefix of the latest merge commit. |
conventional-commits |
Bump from Conventional Commits headers since the last tag. |
Strategy-specific env vars (e.g.
SEMVERTAG_BRANCH_PREFIX__MINOR) remain 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. v1.2.3), 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, no merge commit, or non-conforming 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:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- 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 — useful 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:
- Workflow-scoped
GITHUB_TOKEN(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. - Fine-grained PAT scoped to the single repository. Required
scope:
Contents: Read and write. Store 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. - Classic PAT. Required scope:
repo(private repos) orpublic_repo(public repos only). Same storage shape as the fine-grained PAT. Less preferred — classic PATs bleed scope across all of the user's repos.
Masking caveat. Because the alias chain reads
SEMVERTAG_GITHUB__TOKEN→SEMVERTAG_TOKEN→GITHUB_TOKENin order and the first set value wins, 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.
GitHub Enterprise: set SEMVERTAG_GITHUB__ENDPOINT (note the
double underscore — pydantic-settings uses __ as the nested-key
delimiter, so SEMVERTAG_GITHUB_ENDPOINT with a single underscore is
silently ignored) 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.
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 fix/..., feat/..., chore/... convention. semvertag
reads the most recent merge commit's source-branch prefix and bumps
accordingly — fix/ bumps patch, feat/ bumps minor, chore/
bumps nothing. 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). semvertag scans commits since the last tag and
chooses the highest bump implied by their type prefixes (feat! or
BREAKING CHANGE: → major, feat: → minor, fix: → patch,
everything else → none). 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/checkout@v4
with:
fetch-depth: 0
- 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.3.1,<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 …). No outputs are produced in this shape — 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 in GitHub UI (Settings → Developer settings → Personal access tokens) or rotate the workflow secret. When using the composite action,GITHUB_TOKENis set automatically from thetokeninput (which defaults to${{ github.token }}). When using the pure-CLI recipe in "Without the composite action", addenv: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}to the run step. -
Token missing scope or insufficient permission: 403— the token lackscontents: write(fine-grained / workflow-scoped) orrepo/public_repo(classic). For workflow-scoped tokens, addpermissions: contents: writeat the workflow level. For PATs, re-issue with the right scope. -
GitHub repo not found: repo='...'—GITHUB_REPOSITORYwas not exported, or--repo OWNER/REPOwas not passed. Inside GHA,GITHUB_REPOSITORYis 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 ishttps://api.github.com. SetSEMVERTAG_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.