Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,20 @@ jobs:
- run: uv python install ${{ matrix.python-version }}
- run: just install
- run: just test --cov-report xml

action-smoke:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: semvertag
uses: ./
env:
SEMVERTAG_BRANCH_PREFIX__MINOR: '["feat/"]'
- name: Verify outputs were emitted
run: |
test -n "${{ steps.semvertag.outputs.status }}"
test -n "${{ steps.semvertag.outputs.bump }}"
24 changes: 12 additions & 12 deletions .github/workflows/semvertag.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
name: semvertag

# Dogfood semvertag against this repo. Auto-tags on push to main when the latest
# commit is a merge from `feat/...` (minor bump) or `bugfix/`/`hotfix/...` (patch).
# This repo's branch convention is `feat/...`, so SEMVERTAG_BRANCH_PREFIX__MINOR
# Dogfood the local composite action against this repo. Auto-tags on
# push to main when the latest commit is a merge from `feat/...` (minor
# bump) or `bugfix/`/`hotfix/...` (patch). This repo's branch
# convention uses `feat/...`, so SEMVERTAG_BRANCH_PREFIX__MINOR
# overrides the default `feature/` mapping.
#
# The workflow only creates a tag — it does NOT trigger publish.yml (which fires
# on GitHub release creation). To publish to PyPI, create a GitHub release pointed
# at the auto-tagged commit.
# The workflow only creates a tag — it does NOT trigger publish.yml,
# which fires on GitHub release creation. To publish to PyPI, create a
# GitHub release pointed at the auto-tagged commit.
#
# `uses: ./` exercises the action.yml in the current checkout, so any
# breaking change to action.yml fails the dogfood run before it can
# affect external users.

on:
push:
Expand All @@ -27,11 +32,6 @@ jobs:
- 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
- uses: ./
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SEMVERTAG_BRANCH_PREFIX__MINOR: '["feat/"]'
35 changes: 35 additions & 0 deletions .github/workflows/tag-major.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: tag-major

# Maintains the floating `v0` major tag so users can pin `uses:
# modern-python/semvertag@v0` and ride minor bumps. Skipped on
# prereleases so an `v0.5.0-rc1` does not drag `v0` ahead of the latest
# stable. When v1.0.0 ships, this same job creates `v1` automatically
# from the tag name's leading segment.

on:
release:
types: [published]

permissions:
contents: write

jobs:
update-major-tag:
if: ${{ !github.event.release.prerelease }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Update major tag
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
# RELEASE_TAG = 'v0.4.0' → major = 'v0'
major="${RELEASE_TAG%%.*}"
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git tag -fa "$major" "$RELEASE_TAG" -m "Update $major to $RELEASE_TAG"
git push -f origin "$major"
16 changes: 5 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,14 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install --quiet 'uv>=0.4,<1'
- run: uvx 'semvertag>=0.3,<1' tag
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: modern-python/semvertag@v0
```

semvertag auto-detects GitHub Actions from `GITHUB_ACTIONS=true` and
creates the tag ref via the GitHub API. `fetch-depth: 0` matters —
the default `1` misses tag-relative history. See
semvertag auto-detects GitHub Actions, picks the bump from the latest
commit, and creates the tag ref via the GitHub API. `fetch-depth: 0`
matters — the default `1` misses tag-relative history. See
[GitHub Actions docs](docs/providers/github.md) for token scopes,
GitHub Enterprise setup, and troubleshooting.
GitHub Enterprise setup, outputs, and troubleshooting.

## Strategies

Expand Down
55 changes: 55 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: 'semvertag'
description: 'Auto-tag your GitHub repository with a SemVer git tag based on commits or branch prefixes.'
author: 'modern-python'

branding:
icon: 'tag'
color: 'blue'

inputs:
strategy:
description: 'Bump strategy: branch-prefix (default) or conventional-commits.'
required: false
default: 'branch-prefix'
token:
description: 'GitHub token with contents: write. Defaults to the workflow-issued github.token.'
required: false
default: ${{ github.token }}

outputs:
tag:
description: 'The created tag (e.g. v1.2.3), or empty string if no bump was warranted.'
value: ${{ steps.run.outputs.tag }}
bump:
description: 'The computed bump: none | patch | minor | major.'
value: ${{ steps.run.outputs.bump }}
status:
description: 'The run status: created (tag pushed) | no-bump (nothing to tag).'
value: ${{ steps.run.outputs.status }}

runs:
using: 'composite'
steps:
- uses: astral-sh/setup-uv@v7

- name: Run semvertag
id: run
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token }}
SEMVERTAG_STRATEGY: ${{ inputs.strategy }}
run: |
set -euo pipefail
result=$(uvx 'semvertag>=0.3.1,<1' tag --json)
printf '%s\n' "$result"
# Normalize the CLI's internal status (`no_tags`, `already_tagged`,
# `no_merge_commit`, `no_conforming_commit`, ...) to a stable
# consumer-facing enum. `set -euo pipefail` ensures we never reach
# here on CLI errors, so there is no `error` value to surface.
case "$(jq -r '.status' <<<"$result")" in
created) status='created' ;;
*) status='no-bump' ;;
esac
printf 'tag=%s\n' "$(jq -r '.tag // ""' <<<"$result")" >> "$GITHUB_OUTPUT"
printf 'bump=%s\n' "$(jq -r '.bump' <<<"$result")" >> "$GITHUB_OUTPUT"
printf 'status=%s\n' "$status" >> "$GITHUB_OUTPUT"
125 changes: 97 additions & 28 deletions docs/providers/github.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
# GitHub Actions

Use semvertag in GitHub Actions via a small workflow that installs
`uv` and runs `uvx semvertag tag`. No composite action in your repo,
no maintained workflow YAML beyond the snippet below.

> **GitHub Actions composite wrapper pending.** A one-line
> `uses: modern-python/semvertag@v…` via a published composite
> action is the eventual delivery path — but it has not been
> published. Paste the workflow below into
> `.github/workflows/semvertag.yml` until then.
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 and picked up via the
> alias chain) — in which case the workflow MUST declare
> `permissions: contents: write` — OR provide a fine-grained PAT with
> `contents: write` (single repo) or a classic PAT with `repo` /
> `public_repo` scope. Store the PAT as a repo secret named
> `SEMVERTAG_TOKEN`; the alias chain picks it up ahead of
> `GITHUB_TOKEN`.
> `GITHUB_TOKEN` (which is auto-issued per job) — in which case the
> workflow MUST declare `permissions: contents: write` — OR provide a
> fine-grained PAT with `contents: write` (single repo) or a classic
> PAT with `repo` / `public_repo` scope. Store the PAT as a repo
> secret named `SEMVERTAG_TOKEN`; the alias chain picks it up ahead
> of `GITHUB_TOKEN`.

```yaml
name: semvertag
Expand All @@ -40,13 +35,7 @@ jobs:
- 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' tag
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: modern-python/semvertag@v0
```

The job runs against the latest commit on the default branch and, if
Expand Down Expand Up @@ -75,9 +64,22 @@ Pass `--strategy` (or set `SEMVERTAG_STRATEGY`) to one of:
| `conventional-commits` | Bump from Conventional Commits headers since the last tag. |

```yaml
- run: uvx 'semvertag>=0.3,<1' tag --strategy conventional-commits
- uses: modern-python/semvertag@v0
with:
strategy: conventional-commits
```

> **Strategy-specific env vars** (e.g. `SEMVERTAG_BRANCH_PREFIX__MINOR`)
> remain configured on the calling step. The composite action only
> explicitly sets `GITHUB_TOKEN` and `SEMVERTAG_STRATEGY`; every other
> env var on the calling step passes through to the action's run step.
>
> ```yaml
> - uses: modern-python/semvertag@v0
> env:
> SEMVERTAG_BRANCH_PREFIX__MINOR: '["feat/"]'
> ```

## Required permissions

The job creates a tag ref, so the token it uses MUST carry write
Expand All @@ -86,6 +88,42 @@ 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.

```yaml
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 }}"
```

## Token scope: `GITHUB_TOKEN` vs Personal Access Tokens

Three cases govern which token the job should use:
Expand Down Expand Up @@ -144,15 +182,46 @@ everything else → none). See
[Conventional Commits strategy](../strategies/conventional-commits.md)
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:

```yaml
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. For workflow-scoped tokens, this
usually means `GITHUB_TOKEN` was not exported into the step's
`env:` — add the `env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}`
line shown in the Quick Start.
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
Expand Down
Loading
Loading