From 26d54fedb589234955bb55a89c7e60be203679fd Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Mon, 8 Jun 2026 22:48:58 +0300 Subject: [PATCH 01/11] spec: design action.yml composite wrapper Brainstorm output for the composite GitHub Action that lets users replace the 11-line install-and-run block with `uses: modern-python/semvertag@v0`. Scope: action.yml + tag-major.yml floating-tag workflow + dogfood/CI/docs migration + v0.4.0 release runbook including the manual Marketplace publication procedure. --- ...-08-action-yml-composite-wrapper-design.md | 375 ++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 planning/specs/2026-06-08-action-yml-composite-wrapper-design.md diff --git a/planning/specs/2026-06-08-action-yml-composite-wrapper-design.md b/planning/specs/2026-06-08-action-yml-composite-wrapper-design.md new file mode 100644 index 0000000..6eefcd9 --- /dev/null +++ b/planning/specs/2026-06-08-action-yml-composite-wrapper-design.md @@ -0,0 +1,375 @@ +# action.yml composite wrapper — design spec + +**Date:** 2026-06-08 +**Status:** Approved, ready for implementation planning +**Topic slug:** `action-yml-composite-wrapper` +**Predecessor:** `2026-05-31-v0-1-0-release-prep-design.md` (deleted the prior stub `action.yml`; this spec reintroduces it now that the GitHub provider has shipped in 0.3.0) + +## Goal + +Ship a composite `action.yml` at the repo root so GitHub Actions users can replace the current 11-line install-and-run block in `docs/providers/github.md` with two lines: + +```yaml +- uses: actions/checkout@v4 + with: { fetch-depth: 0 } +- uses: modern-python/semvertag@v0 +``` + +Pair it with a small release-time workflow that maintains a floating `v0` major tag, so consumers can pin to `@v0` and ride minor releases automatically. Migrate the existing dogfood workflow to consume the local action (`uses: ./`) so any breakage surfaces on the repo's own CI before it reaches users. + +## Background + +`semvertag` ships as a Python CLI plus a GitLab CI Catalog template (`templates/semvertag.yml`). The GitHub Actions story is currently a documented inline snippet: install Python, install uv, run `uvx semvertag tag`. That works but it's verbose (11 lines of `steps:`), couples consumer workflows to implementation details (Python version, uv install method, version pin), and forces every consumer to keep those details in sync as upstream evolves. + +A composite `action.yml` existed at v0 (pre-0.1.0) but was deleted during the v0.1.0 release prep because the GitHub provider was a stub at the time. The GitHub provider shipped in 0.3.0 (`f118670`) and was patched in 0.3.1 to recognise GitHub PR merge subjects (`c40a0da`). Both `docs/providers/github.md` (the "Composite wrapper pending" callout) and `planning/releases/0.3.0.md` (the "deferred" entry) explicitly flag this work as the remaining gap. + +## Non-goals + +- **Marketplace publication** — the spec produces the artifacts that make publication possible and a step-by-step runbook for the manual UI click, but does not perform the publication itself. +- **CLI changes** — `action.yml` is a wrapper. No edits to `semvertag/` source code, `--json` envelope shape, exit codes, or auto-detection logic. +- **GitLab Catalog work** — `templates/semvertag.yml` remains untouched. Catalog publication is blocked on the `modern-python` GitLab namespace not existing and is tracked separately from this spec. +- **`action.yml` inputs beyond `strategy` and `token`** — every other knob (GHE endpoint, repo override, branch-prefix lists) already works by setting `env:` at the workflow or step level. Adding them as first-class inputs would duplicate the env-var contract and create drift risk. Documented in §Decision log. +- **Outputs beyond `tag`, `bump`, `status`** — `commit` and `reason` are present in the `RunResult` JSON envelope but not exposed; they can be added without a breaking change if a real consumer demand emerges. + +## Target shape + +``` +.github/workflows/ +├── ci.yml ← add `action-smoke` job that runs `uses: ./` +├── semvertag.yml ← dogfood: drop 4 install/run steps, replace with `uses: ./` +└── tag-major.yml ← NEW: on release published, force-update `v0` floating tag +action.yml ← NEW: composite — setup-uv → run --json → expose outputs +README.md ← rewrite "Use it in GitHub Actions" section +docs/providers/github.md ← rewrite Quick Start; add Outputs section; keep inline snippet as fallback +planning/releases/0.4.0.md ← NEW: release runbook including Marketplace publication procedure +``` + +## `action.yml` + +```yaml +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 | no-bump | error.' + value: ${{ steps.run.outputs.status }} + +runs: + using: 'composite' + steps: + - uses: astral-sh/setup-uv@v8 + + - name: Run semvertag + id: run + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.token }} + SEMVERTAG_STRATEGY: ${{ inputs.strategy }} + run: | + set -euo pipefail + result=$(uvx 'semvertag>=0.4,<1' tag --json) + printf '%s\n' "$result" + printf 'tag=%s\n' "$(jq -r '.tag // ""' <<<"$result")" >> "$GITHUB_OUTPUT" + printf 'bump=%s\n' "$(jq -r '.bump' <<<"$result")" >> "$GITHUB_OUTPUT" + printf 'status=%s\n' "$(jq -r '.status' <<<"$result")" >> "$GITHUB_OUTPUT" +``` + +### Key choices + +- **No `SEMVERTAG_PROVIDER` forced.** Auto-detection from `GITHUB_ACTIONS=true` (shipped in 0.3.0) makes that unnecessary. Forcing it would suppress useful errors when someone runs `act` or otherwise exercises the action outside a real GHA environment. +- **`set -euo pipefail`.** If `uvx` fails, the step fails fast and jq never sees empty/garbage stdin. Avoids ambiguous half-states in `$GITHUB_OUTPUT`. +- **Echo the JSON before parsing.** Humans reading the job log see the full envelope; no second CLI invocation is needed for diagnostics. +- **`jq -r '.tag // ""'`.** Guards the `no-bump` case (where `tag` is JSON `null`) so the output becomes an empty string — predictable for downstream `if:` gates. `.bump` and `.status` are always present per `RunResult` schema_version 1.0; no fallback. +- **CLI version floor `>=0.4,<1`.** Pairs the action with the release that ships it. `@v0` follows minor bumps; the floor inside `action.yml` also bumps on each minor. Lower bound is the release minor; upper bound `<1` defers the 1.0 question. +- **`astral-sh/setup-uv@v8`.** The official Astral installer; one step, prebuilt binary, automatic cache. Used by every modern uv-in-CI project. Trade-off accepted: this adds a third-party action dependency we trust via major-version pin (v8.x.y). +- **`SEMVERTAG_STRATEGY` always exported.** Mirrors the GitLab Catalog template (`templates/semvertag.yml`). Trade-off: a workflow-level `env: SEMVERTAG_STRATEGY: ...` is overridden by the action's step-env. The fix in that rare case is to use the `with: strategy:` input, which is the documented path. +- **Internal step id `run`.** Lets the top-level `outputs:` mapping reference `steps.run.outputs.*`. Users wire their own `id:` on the calling `uses:` block to read the exposed outputs. +- **No checkout inside the action.** Established tag/release actions (mathieudutour/github-tag-action, googleapis/release-please-action, cycjimmy/semantic-release-action) uniformly skip it. Callers have heterogeneous checkout needs (refs, submodules, LFS, sparse, monorepo paths, custom tokens); a composite that silently re-checks out fights those needs. + +## `.github/workflows/tag-major.yml` + +```yaml +name: tag-major + +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 + 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" +``` + +### Key choices + +- **`release.types: [published]`.** Fires once per release, not on draft/edit/delete. Avoids spurious `v0` updates while a release is being composed. +- **`!github.event.release.prerelease`.** Protects the floating tag during RC cycles — a `v0.5.0-rc1` should not drag `v0` ahead of the latest stable. +- **`${RELEASE_TAG%%.*}` parameter expansion.** Pure bash; no awk/sed/jq dependency. Extracts `v0` from `v0.4.0`. +- **Force-push the tag.** `-f` is required to move a floating tag. If branch protection rules cover tags (uncommon but possible) the job fails loudly with a clear permissions error — operator can adjust rules and re-run. +- **First-time bootstrap is manual.** When v0.4.0 ships, `v0` does not yet exist (this workflow only runs on subsequent releases). The v0.4.0 release runbook includes the one-time `git tag -fa v0 v0.4.0 && git push -f origin v0` step. From v0.4.1 onward the workflow handles it. + +## Dogfood workflow migration + +`.github/workflows/semvertag.yml` collapses to: + +```yaml +name: semvertag + +on: + push: + branches: [main] + +permissions: + contents: write + +concurrency: + group: semvertag + cancel-in-progress: false + +jobs: + tag: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: ./ + env: + SEMVERTAG_BRANCH_PREFIX__MINOR: '["feat/"]' +``` + +### Key choices + +- **`uses: ./`.** Exercises the action.yml in the current checkout. Any PR that breaks `action.yml` fails its own dogfood-shaped sanity check on the next push to main. +- **`SEMVERTAG_BRANCH_PREFIX__MINOR` stays at step level.** The action's step-env only sets `GITHUB_TOKEN` and `SEMVERTAG_STRATEGY`; other env vars on the calling step pass through to the composite's run step. +- **Drop explicit `GITHUB_TOKEN`.** The action's `token` input defaults to `${{ github.token }}`; `permissions: contents: write` is what makes that writable. +- **No `with: strategy:`.** `branch-prefix` is the input default. + +## CI smoke test + +Add a third job to `.github/workflows/ci.yml`: + +```yaml +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 }}" +``` + +### What it covers + +| Check | Layer | +|---|---| +| YAML parses, inputs/outputs declared correctly | `astral-sh/setup-uv@v8` + GHA composite loader (failure shows up as a runtime parse error) | +| `astral-sh/setup-uv@v8` resolves and installs | Step 1 of the composite | +| `uvx 'semvertag>=0.4,<1' tag --json` runs to completion | Step 2 (failure → `set -euo pipefail` exits non-zero) | +| JSON parsing emits non-empty `status`/`bump` | The verify step | + +### What it does NOT cover + +- **Real tag creation against the GitHub API.** A PR-time job from a fork cannot have `contents: write` without footguns. Branch-prefix on a PR commit returns `status=no-bump` (no merge-commit shape on a feature branch), so the job lands in the no-tag-created path and the verify step still confirms output emission. +- **GitHub Enterprise endpoint resolution.** Out of scope; users configure `SEMVERTAG_GITHUB__ENDPOINT` themselves and the action passes it through. +- **End-to-end real-API tag push.** Covered by the dogfood workflow on `main` after merge. + +## Docs rewrite + +### `README.md` + +Replace the "Use it in GitHub Actions" block (lines 42–69) with: + +````markdown +## Use it in GitHub Actions + +Paste this workflow into `.github/workflows/semvertag.yml`: + +```yaml +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 +``` + +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, outputs, and troubleshooting. +```` + +### `docs/providers/github.md` + +1. **Delete the "Composite wrapper pending" callout** (lines 7–11). +2. **Replace the Quick Start workflow** (lines 27–50) with the action-based one above. +3. **Add an "Outputs" section** after "Required permissions": + + ````markdown + ## Outputs + + When you set `id: semvertag` on the step, downstream steps can read: + + | Output | Value | + |---|---| + | `tag` | The created tag (e.g. `v1.2.3`), or empty string on `no-bump`. | + | `bump` | `none` \| `patch` \| `minor` \| `major`. | + | `status` | `created` \| `no-bump` \| `error`. | + + ```yaml + jobs: + tag-and-release: + runs-on: ubuntu-latest + 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 }}" + ``` + ```` + +4. **Swap the Strategy section's inline `uvx ...` snippet** for the action-input form: + + ```yaml + - uses: modern-python/semvertag@v0 + with: + strategy: conventional-commits + ``` + +5. **Show strategy-specific env vars** as `env:` siblings of the `uses:` step: + + ```yaml + - uses: modern-python/semvertag@v0 + env: + SEMVERTAG_BRANCH_PREFIX__MINOR: '["feat/"]' + ``` + + With a note: the action only explicitly sets `GITHUB_TOKEN` and `SEMVERTAG_STRATEGY`; every other env var on the calling step passes through to the composite's run step. + +6. **Add a "Without the composite action" section** above Troubleshooting, preserving the existing 11-line install-and-run snippet for users on private GHE instances without Marketplace access, security-constrained orgs that forbid third-party actions, or anyone who wants explicit control over the uv install step. + +## Release runbook (`planning/releases/0.4.0.md`) + +New file capturing both the release process and the Marketplace publication procedure as a manual step the maintainer follows. + +Structure: + +1. **Pre-flight check.** + - `name: 'semvertag'` not already taken on Marketplace (search at https://github.com/marketplace?type=actions before publishing). + - `branding.icon` is a valid Feather icon name; `branding.color` is one of `white | yellow | blue | green | orange | red | purple | gray-dark`. + - `action.yml` parses (`actionlint ./action.yml` if available locally). + - CI green on the PR introducing the action. +2. **Cut the v0.4.0 release.** + - Tag and push `v0.4.0` per the project's existing release flow (PyPI publishing via `publish.yml` fires on GitHub release creation). +3. **Bootstrap the floating `v0` tag (one-time).** + ```sh + git fetch --tags + git tag -fa v0 v0.4.0 -m 'Update v0 to v0.4.0' + git push -f origin v0 + ``` + Subsequent releases are handled automatically by `tag-major.yml`. +4. **Publish to Marketplace (manual UI step).** + - Navigate to https://github.com/modern-python/semvertag/releases/tag/v0.4.0. + - Click "Edit release". + - Check "Publish this Action to the GitHub Marketplace". + - Accept the Marketplace Terms of Service if prompted. + - Select **Primary Category**: `Continuous integration`. + - Select **Secondary Category** (optional): `Utilities`. + - Save the release. +5. **Post-publish smoke test.** + - Confirm listing appears at https://github.com/marketplace/actions/semvertag. + - In a sandbox repo, paste the README's snippet (`uses: modern-python/semvertag@v0`) and confirm the workflow runs end-to-end. + +## Decision log + +| Decision | Choice | Why not the alternative | +|---|---|---| +| Checkout in the action? | No | Every published tag/release action skips it; callers have heterogeneous checkout needs (refs, submodules, LFS, sparse, monorepo paths). | +| uv installer? | `astral-sh/setup-uv@v8` | Faster than `pip install uv` (prebuilt binary, automatic cache). The third-party-action dependency is the ecosystem norm. | +| Inputs? | `strategy` + `token` only | Every other knob (GHE endpoint, repo override, branch-prefix lists) already works via `env:`. Duplicating the env-var contract as inputs creates drift risk. | +| Outputs? | `tag`, `bump`, `status` | CLI already emits `--json`; cost is ~10 YAML lines. Matches release-please / github-tag-action convention. `commit` and `reason` deferred; can be added non-breaking. | +| Always export `SEMVERTAG_STRATEGY`? | Yes | Matches the GitLab Catalog template. Trade-off: workflow-level env `SEMVERTAG_STRATEGY` is overridden — use the `with: strategy:` input instead. | +| Floating major tag? | Yes, automated workflow | Lets users pin `@v0` and ride minor releases. Manual-per-release is easy to forget; deferring forces every user to bump pins on every minor. | +| Marketplace publication? | Manual UI step in the runbook | Maintainer-only action; not automatable from a workflow. Pre-flight + post-publish checks documented in the runbook. | + +## Risks + +- **`astral-sh/setup-uv@v8` major bump.** A future v9 may introduce breaking changes to inputs we depend on. Mitigation: pin to `@v8` (major) and revisit on each minor semvertag release. +- **`name: 'semvertag'` Marketplace collision.** If the name is taken when we publish, the listing fails to create. Mitigation: pre-flight check in the runbook. If the name is taken, change `name:` in `action.yml` to `'semvertag tag'` (Marketplace permits spaces in display names) before re-attempting publication; the listing slug and the `uses: modern-python/semvertag@v0` syntax are unaffected. Low likelihood — "semvertag" is distinctive. +- **`jq` not on self-hosted runners.** Default on every GitHub-hosted runner; self-hosted runners may strip it. Mitigation: document the assumption in `docs/providers/github.md` as a known requirement. +- **`SEMVERTAG_STRATEGY` step-env override surprise.** Users setting it at workflow level may be confused when the action's input default `branch-prefix` wins. Mitigation: documented in the new "Strategy-specific env vars" docs section; the `with: strategy:` input is presented as the canonical knob. +- **First-time `v0` bootstrap forgotten.** If the maintainer skips the bootstrap step in the v0.4.0 runbook, `uses: …@v0` fails for early adopters until the next release fires `tag-major.yml`. Mitigation: bold callout in the runbook. + +## Acceptance + +- `action.yml` exists at repo root with the shape in §`action.yml`. +- `.github/workflows/tag-major.yml` exists with the shape in §`tag-major.yml`. +- `.github/workflows/semvertag.yml` consumes `uses: ./`. +- `.github/workflows/ci.yml` has an `action-smoke` job that asserts on `status` and `bump` outputs. +- `README.md` advertises `uses: modern-python/semvertag@v0`. +- `docs/providers/github.md` Quick Start uses the action; the "Composite wrapper pending" callout is gone; Outputs section exists; "Without the composite action" fallback section preserves the inline-CLI recipe. +- `planning/releases/0.4.0.md` exists with the five-step Marketplace publication procedure. +- CI green on a PR that introduces all of the above; the dogfood workflow runs to completion on main after the PR merges. From 0adf7f05d17aabd71f9b084029cb8dc7ebe808f0 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Mon, 8 Jun 2026 23:02:44 +0300 Subject: [PATCH 02/11] plan: action.yml composite wrapper implementation Bite-sized 8-task plan derived from the spec. Spec revision: CLI floor in action.yml is `>=0.3.1,<1` (not `>=0.4,<1`) so the PR landing this work satisfies its own action-smoke CI from PyPI today; the floor only bumps when a future release breaks CLI contract. --- ...2026-06-08-action-yml-composite-wrapper.md | 812 ++++++++++++++++++ ...-08-action-yml-composite-wrapper-design.md | 4 +- 2 files changed, 814 insertions(+), 2 deletions(-) create mode 100644 planning/plans/2026-06-08-action-yml-composite-wrapper.md diff --git a/planning/plans/2026-06-08-action-yml-composite-wrapper.md b/planning/plans/2026-06-08-action-yml-composite-wrapper.md new file mode 100644 index 0000000..bc159a3 --- /dev/null +++ b/planning/plans/2026-06-08-action-yml-composite-wrapper.md @@ -0,0 +1,812 @@ +# action.yml composite wrapper — implementation plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Reintroduce `action.yml` at repo root as a composite GitHub Action so users can replace the 11-line install-and-run block with `uses: modern-python/semvertag@v0`. Ship a floating-major-tag workflow, migrate the dogfood + CI to consume the local action, rewrite docs, and produce a v0.4.0 release runbook including the manual Marketplace publication procedure. + +**Architecture:** Pure-YAML + Markdown work. Two new workflow files (`action.yml`, `.github/workflows/tag-major.yml`), one modified workflow (`.github/workflows/semvertag.yml` dogfood), one modified workflow (`.github/workflows/ci.yml` adds an `action-smoke` job that runs `uses: ./` against the action being introduced — the chicken-and-egg is resolved by floor `'semvertag>=0.3.1,<1'` which is satisfiable from PyPI today). Docs rewrite touches README and `docs/providers/github.md`. Runbook captures the manual Marketplace publication procedure as five numbered steps for the maintainer to follow. + +**Tech Stack:** GitHub Actions composite action syntax, `astral-sh/setup-uv@v8`, bash + `jq` (default on `ubuntu-latest`), MkDocs (`mkdocs build --strict` as the docs gate). + +**Spec:** `planning/specs/2026-06-08-action-yml-composite-wrapper-design.md` + +**Branch convention:** This is a `feat/` branch (the dogfood workflow's `SEMVERTAG_BRANCH_PREFIX__MINOR: '["feat/"]'` maps it to a minor bump). Suggested branch: `feat/action-yml-composite-wrapper`. + +--- + +## File structure + +| File | Action | Purpose | +|---|---|---| +| `action.yml` | **create** | Composite action — `setup-uv` then `uvx 'semvertag>=0.3.1,<1' tag --json` with output parsing | +| `.github/workflows/tag-major.yml` | **create** | Force-update floating `v0` tag on each non-prerelease release | +| `.github/workflows/semvertag.yml` | **modify** | Collapse 4 install/run steps into `uses: ./` (dogfood the action) | +| `.github/workflows/ci.yml` | **modify** | Add `action-smoke` job that runs `uses: ./` and asserts outputs exist | +| `README.md` | **modify** | Replace 24-line "Use it in GitHub Actions" section (lines 42–69) with action-based snippet | +| `docs/providers/github.md` | **modify** | Delete pending callout, replace Quick Start, add Outputs section, add env-var passthrough note, add "Without the composite action" fallback | +| `planning/releases/0.4.0.md` | **create** | Release runbook with pre-flight + v0 bootstrap + Marketplace publication procedure | + +--- + +## Verification commands referenced throughout + +- **YAML parse check:** `python3 -c "import yaml; yaml.safe_load(open('PATH'))"` +- **Docs gate:** `mkdocs build --strict` (run from repo root with `.venv` active; if not active, `uv run mkdocs build --strict`) +- **Lint gate:** `just lint-ci` +- **Test gate (sanity, even though no Python changed):** `just test` + +If a step says "verify" and the command exits 0 with no output, the check passed. + +--- + +### Task 1: Create `action.yml` + +**Files:** +- Create: `action.yml` + +- [ ] **Step 1: Create the composite action file** + +Write `action.yml` at the repo root with exactly this content: + +```yaml +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 | no-bump | error.' + value: ${{ steps.run.outputs.status }} + +runs: + using: 'composite' + steps: + - uses: astral-sh/setup-uv@v8 + + - 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" + printf 'tag=%s\n' "$(jq -r '.tag // ""' <<<"$result")" >> "$GITHUB_OUTPUT" + printf 'bump=%s\n' "$(jq -r '.bump' <<<"$result")" >> "$GITHUB_OUTPUT" + printf 'status=%s\n' "$(jq -r '.status' <<<"$result")" >> "$GITHUB_OUTPUT" +``` + +- [ ] **Step 2: Verify YAML parses** + +Run: `python3 -c "import yaml; yaml.safe_load(open('action.yml'))"` +Expected: exits 0, no output. + +- [ ] **Step 3: Commit** + +```bash +git add action.yml +git commit -m "feat: add action.yml composite GitHub Action" +``` + +--- + +### Task 2: Create `.github/workflows/tag-major.yml` + +**Files:** +- Create: `.github/workflows/tag-major.yml` + +- [ ] **Step 1: Create the workflow file** + +Write `.github/workflows/tag-major.yml` with exactly this content: + +```yaml +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" +``` + +- [ ] **Step 2: Verify YAML parses** + +Run: `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/tag-major.yml'))"` +Expected: exits 0, no output. + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/tag-major.yml +git commit -m "ci: add tag-major workflow to maintain floating v0 tag" +``` + +--- + +### Task 3: Migrate `.github/workflows/semvertag.yml` to consume the local action + +**Files:** +- Modify: `.github/workflows/semvertag.yml` + +- [ ] **Step 1: Replace the file contents** + +Overwrite `.github/workflows/semvertag.yml` with exactly this content: + +```yaml +name: semvertag + +# 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. +# +# `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: + branches: [main] + +permissions: + contents: write + +concurrency: + group: semvertag + cancel-in-progress: false + +jobs: + tag: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: ./ + env: + SEMVERTAG_BRANCH_PREFIX__MINOR: '["feat/"]' +``` + +- [ ] **Step 2: Verify YAML parses** + +Run: `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/semvertag.yml'))"` +Expected: exits 0, no output. + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/semvertag.yml +git commit -m "ci: dogfood the composite action via uses: ./" +``` + +--- + +### Task 4: Add `action-smoke` job to `.github/workflows/ci.yml` + +**Files:** +- Modify: `.github/workflows/ci.yml` + +- [ ] **Step 1: Read current `ci.yml`** + +Open `.github/workflows/ci.yml`. It currently has two jobs: `lint` and `pytest`. You will add a third job `action-smoke` at the end, preserving the existing two jobs unchanged. + +- [ ] **Step 2: Append the `action-smoke` job** + +Add this job to the `jobs:` block (after the `pytest:` job, with one blank line separating them): + +```yaml + 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 }}" +``` + +After the edit, the full file should look like this (lint and pytest unchanged, action-smoke appended): + +```yaml +name: main + +on: + push: + branches: + - main + pull_request: {} + +concurrency: + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: extractions/setup-just@v2 + - uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: "**/pyproject.toml" + - run: uv python install 3.11 + - run: just install lint-ci + + pytest: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - "3.11" + - "3.12" + - "3.13" + - "3.14" + steps: + - uses: actions/checkout@v4 + - uses: extractions/setup-just@v2 + - uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: "**/pyproject.toml" + - 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 }}" +``` + +- [ ] **Step 3: Verify YAML parses** + +Run: `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))"` +Expected: exits 0, no output. + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/ci.yml +git commit -m "ci: add action-smoke job that exercises the composite action" +``` + +--- + +### Task 5: Rewrite README's "Use it in GitHub Actions" section + +**Files:** +- Modify: `README.md` (lines 42–69, the "Use it in GitHub Actions" section) + +- [ ] **Step 1: Read current `README.md`** + +The current section spans lines 42–69 and contains an 11-step `steps:` block. You will replace it with an action-based snippet. + +- [ ] **Step 2: Replace the section** + +Replace the entire block from `## Use it in GitHub Actions` (line 42) through `> [GitHub Actions docs](docs/providers/github.md) for token scopes,` and the closing `> GitHub Enterprise setup, and troubleshooting.` (line 75, inclusive) with: + +```markdown +## Use it in GitHub Actions + +Paste this workflow into `.github/workflows/semvertag.yml`: + +```yaml +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 +``` + +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, outputs, and troubleshooting. +``` + +Note: the inner fenced ```yaml block must use triple-backticks, and the outer instruction must show those backticks literally. In the file, type three real backticks for both the opening and closing of the yaml block. + +- [ ] **Step 3: Verify Markdown still renders (mkdocs gate)** + +Run: `uv run mkdocs build --strict` from the repo root. +Expected: build succeeds with no warnings. + +If mkdocs reports a strict warning, it most likely indicates a broken intra-repo link in the section you just edited. Verify the relative path `docs/providers/github.md` resolves from the repo root (it does). + +- [ ] **Step 4: Commit** + +```bash +git add README.md +git commit -m "docs: README — advertise the composite action as the GHA recipe" +``` + +--- + +### Task 6: Rewrite `docs/providers/github.md` + +**Files:** +- Modify: `docs/providers/github.md` + +This task has several sub-edits. Do them in order. Each sub-edit's "verify" step is run at the end (Step 8) as one mkdocs build. + +- [ ] **Step 1: Delete the "Composite wrapper pending" callout** + +Remove lines 7–11 (the `> **GitHub Actions composite wrapper pending.** …` blockquote). The "## Quick Start" header on line 13 should be the next thing after the lead paragraph (lines 1–5). + +After this edit, lines 1–13 should read: + +```markdown +# 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. + +## Quick Start + +The minimum useful workflow: auto-tag on every push to the default +branch. +``` + +Then update the lead paragraph (lines 1–5) to reflect that the action is now the recommended path. Replace lines 1–5 with: + +```markdown +# 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. +``` + +- [ ] **Step 2: Replace the Quick Start workflow** + +The current Quick Start workflow (the 11-step `steps:` block now around lines 21–44 after the prior edits) becomes: + +````markdown +> **Required setup.** Either rely on the workflow-scoped +> `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 +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 auto-detection / `fetch-depth: 0` / token paragraphs that follow the snippet remain unchanged. + +- [ ] **Step 3: Add an "Outputs" section** + +Insert this section directly after "## Required permissions" (currently the section ending around line 87) and before "## Token scope": + +````markdown +## 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 on `no-bump`. | +| `bump` | `none` \| `patch` \| `minor` \| `major`. | +| `status` | `created` \| `no-bump` \| `error`. | + +Example: trigger a downstream release-notes job only when a tag was +created. + +```yaml +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 }}" +``` +```` + +- [ ] **Step 4: Update the Strategy section** + +Replace the inline `uvx` snippet (currently around line 78 — the one-liner showing `--strategy conventional-commits`) with the action-input form: + +```yaml + - uses: modern-python/semvertag@v0 + with: + strategy: conventional-commits +``` + +- [ ] **Step 5: Add an env-var passthrough note** + +After the Strategy table (currently around lines 71–75), add this note: + +````markdown +> **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/"]' +> ``` +```` + +- [ ] **Step 6: Add a "Without the composite action" section** + +Insert a new section immediately above "## Troubleshooting": + +````markdown +## 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. +```` + +- [ ] **Step 7: Update Troubleshooting's `GITHUB_TOKEN` hint** + +The current troubleshooting note (around line 153–155) says: + +> 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. + +Replace that sentence with: + +> 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. + +- [ ] **Step 8: Verify mkdocs gate** + +Run: `uv run mkdocs build --strict` from the repo root. +Expected: build succeeds with no warnings. + +Common pitfalls if it fails: +- A fenced code block left unclosed during the multi-step edit. Search for orphan ` ``` ` markers. +- A broken intra-repo link. The only links you should have introduced/touched are `[Branch-prefix strategy](../strategies/branch-prefix.md)` and `[Conventional Commits strategy](../strategies/conventional-commits.md)` — both exist; do not modify them. + +- [ ] **Step 9: Commit** + +```bash +git add docs/providers/github.md +git commit -m "docs(providers/github): rewrite Quick Start around the composite action" +``` + +--- + +### Task 7: Create `planning/releases/0.4.0.md` runbook + +**Files:** +- Create: `planning/releases/0.4.0.md` + +- [ ] **Step 1: Write the runbook** + +Create `planning/releases/0.4.0.md` with this content: + +```markdown +# semvertag 0.4.0 — composite GitHub Action + +**Minor release shipping the `action.yml` composite wrapper that has been on the deferred list since 0.3.0.** Users can now write `uses: modern-python/semvertag@v0` instead of pasting an 11-line install-and-run block. No CLI changes; the action wraps the existing `semvertag tag --json` invocation and surfaces `tag` / `bump` / `status` as step outputs. + +If you're already using the documented pure-CLI snippet and don't want to consume third-party actions, you can stay on it — `docs/providers/github.md` preserves it as the "Without the composite action" recipe. + +## What landed + +- `action.yml` at repo root — composite action: `astral-sh/setup-uv@v8`, then `uvx 'semvertag>=0.3.1,<1' tag --json`, then parses the envelope into `tag` / `bump` / `status` step outputs. +- `.github/workflows/tag-major.yml` — fires on release published (non-prerelease) and force-updates the floating `v0` major tag so consumers can pin `@v0` and ride minor bumps automatically. +- Dogfood workflow migration — `.github/workflows/semvertag.yml` now consumes `uses: ./`, exercising the action against the working tree on every push to main. +- `action-smoke` CI job — runs `uses: ./` on every PR and asserts that `status` and `bump` outputs are non-empty. Real tag creation against the GitHub API is covered by the post-merge dogfood run, not the PR-time job (forks can't have `contents: write`). +- README + `docs/providers/github.md` rewrite — Quick Start leads with the action; Outputs section documents the three step outputs; "Without the composite action" preserves the pure-CLI fallback for constrained environments. + +## CLI version floor + +`action.yml` pins `'semvertag>=0.3.1,<1'`. 0.3.1 is the minimum CLI version that ships every feature the action depends on (`--json` envelope, `GITHUB_ACTIONS=true` auto-detection, branch-prefix GitHub merge subject recognition). The floor only needs to bump when a future release breaks CLI contract — not on every minor. + +## Release procedure (maintainer) + +### Step 1: Pre-flight check + +Before tagging: + +- Search https://github.com/marketplace?type=actions for "semvertag" — the listing name `semvertag` must not be taken by another action. + - **If it's taken:** edit `action.yml`'s `name:` field to `'semvertag tag'` (Marketplace permits spaces in display names) and re-PR before continuing. The `uses: modern-python/semvertag@v0` syntax depends on the repo slug, not the display name, so consumer-facing docs do not change. +- Confirm `branding.icon` is one of the Feather icon names GitHub accepts and `branding.color` is one of `white | yellow | blue | green | orange | red | purple | gray-dark`. (We ship `icon: tag`, `color: blue` — both valid.) +- Confirm CI is green on main, including the new `action-smoke` job. + +### Step 2: Cut the v0.4.0 release + +Follow the project's existing release flow: tag, push, create a GitHub release. `publish.yml` fires on release creation and pushes to PyPI via `just publish` (which uses `uv version $GITHUB_REF_NAME` to inject the version at build time). + +### Step 3: Bootstrap the floating `v0` tag (one-time) + +The `tag-major.yml` workflow handles the floating tag on every release from v0.4.1 forward. For v0.4.0 specifically — the first release after the workflow landed — the floating tag does not yet exist and must be bootstrapped manually: + +```sh +git fetch --tags +git tag -fa v0 v0.4.0 -m 'Update v0 to v0.4.0' +git push -f origin v0 +``` + +After this, `uses: modern-python/semvertag@v0` resolves successfully for consumers. + +### Step 4: Publish to Marketplace (manual UI step) + +1. Navigate to https://github.com/modern-python/semvertag/releases/tag/v0.4.0. +2. Click **Edit release**. +3. Check **Publish this Action to the GitHub Marketplace**. +4. Accept the Marketplace Terms of Service if prompted (one-time for the repo). +5. Select **Primary Category:** `Continuous integration`. +6. Select **Secondary Category** (optional): `Utilities`. +7. Save the release. + +### Step 5: Post-publish smoke test + +- Confirm the listing appears at https://github.com/marketplace/actions/semvertag (the slug derives from the `name:` field; if you renamed in Step 1 the slug will differ). +- In a sandbox repo, paste the README snippet (`uses: modern-python/semvertag@v0` after a `actions/checkout@v4` with `fetch-depth: 0`) and confirm the workflow runs end-to-end. + +## Breaking changes + +None. The action is additive; the pure-CLI recipe still works exactly as before (and remains documented as the fallback). + +## See also + +- Spec: `planning/specs/2026-06-08-action-yml-composite-wrapper-design.md` +- Implementation plan: `planning/plans/2026-06-08-action-yml-composite-wrapper.md` +``` + +- [ ] **Step 2: Verify mkdocs still passes** + +Run: `uv run mkdocs build --strict` from the repo root. +Expected: build succeeds with no warnings. + +(The file lives under `planning/`, which is not in the mkdocs nav, so it shouldn't affect the build — but run the gate to confirm nothing else regressed.) + +- [ ] **Step 3: Commit** + +```bash +git add planning/releases/0.4.0.md +git commit -m "docs(release): draft 0.4.0 notes (composite action + tag-major workflow)" +``` + +--- + +### Task 8: Final verification + +**Files:** none modified + +- [ ] **Step 1: Run the full lint gate** + +Run: `just lint-ci` +Expected: exits 0. No Python source files changed, but this confirms the lint configuration still applies cleanly and we haven't accidentally introduced an EOF / formatting issue in the modified YAML/Markdown files via eof-fixer. + +- [ ] **Step 2: Run the test gate (sanity)** + +Run: `just test` +Expected: exits 0 with the existing test suite green. No tests were added or removed; this is a regression sanity check. + +- [ ] **Step 3: Run the docs gate** + +Run: `uv run mkdocs build --strict` +Expected: build succeeds with no warnings. + +- [ ] **Step 4: Inspect git log on the branch** + +Run: `git log --oneline main..HEAD` +Expected: seven commits, one per Task 1–7, in order: + +``` + docs(release): draft 0.4.0 notes (composite action + tag-major workflow) + docs(providers/github): rewrite Quick Start around the composite action + docs: README — advertise the composite action as the GHA recipe + ci: add action-smoke job that exercises the composite action + ci: dogfood the composite action via uses: ./ + ci: add tag-major workflow to maintain floating v0 tag + feat: add action.yml composite GitHub Action +``` + +- [ ] **Step 5: Push the branch and open a PR** + +```bash +git push -u origin feat/action-yml-composite-wrapper +gh pr create --title "feat: add action.yml composite wrapper + supporting workflows" --body "$(cat <<'EOF' +## Summary + +- Add `action.yml` so users can write `uses: modern-python/semvertag@v0` instead of pasting the 11-line install-and-run block. +- Add `.github/workflows/tag-major.yml` to maintain the floating `v0` major tag on each non-prerelease release. +- Migrate the dogfood workflow to consume `uses: ./` and add a PR-time `action-smoke` CI job. +- Rewrite README + `docs/providers/github.md` around the action; preserve the pure-CLI recipe as the "Without the composite action" fallback. +- Add `planning/releases/0.4.0.md` runbook including the manual Marketplace publication procedure. + +## Spec and plan + +- Spec: `planning/specs/2026-06-08-action-yml-composite-wrapper-design.md` +- Plan: `planning/plans/2026-06-08-action-yml-composite-wrapper.md` + +## Test plan + +- [ ] CI green on this PR (lint, pytest matrix, action-smoke). +- [ ] After merge: dogfood workflow on main creates a tag (this PR is on a `feat/` branch, so branch-prefix maps it to a minor bump → v0.4.0). +- [ ] After v0.4.0 release: bootstrap the floating `v0` tag per the runbook. +- [ ] After Marketplace publish: smoke-test `uses: modern-python/semvertag@v0` in a sandbox repo. +EOF +)" +``` + +Expected: PR opens; the GH Actions run starts. Monitor it. + +- [ ] **Step 6: Verify CI passes on the PR** + +Wait for CI to complete on the PR. Expected: +- `lint` job: green (same as before this PR). +- `pytest` job (matrix of 4 Python versions): green (same as before this PR). +- `action-smoke` job: green. The new job will: + - Check out the repo with `fetch-depth: 0`. + - Run `uses: ./` which executes the action.yml: install uv via `setup-uv@v8`, then `uvx 'semvertag>=0.3.1,<1' tag --json`. + - The CLI emits a JSON envelope. On a PR commit (not a merge commit), branch-prefix returns `status=no-bump` because no merge subject matches. `tag` is empty; `bump` is `none`; `status` is `no-bump`. + - The verify step asserts that `status` and `bump` outputs are non-empty. Both are (`"no-bump"` and `"none"`), so the assertion passes. + +If `action-smoke` fails: +- **`Required module unavailable: jq`** — jq missing from the runner. Unexpected on `ubuntu-latest`; if this happens, add `sudo apt-get install -y jq` before the `uses: ./` step in `ci.yml` (and document the dependency in `docs/providers/github.md`). +- **`semvertag` resolution error** — verify PyPI has 0.3.1; run `uvx 'semvertag>=0.3.1,<1' --version` locally to reproduce. +- **`Token rejected: 401`** — the action-smoke job declared `permissions: contents: write` but the runner still issued a read-only token. This can happen on PRs from forks; CI from a branch in the same repo is fine. The job's verify step does not require write to succeed (branch-prefix returns no-bump before any write would occur), so the actual API write path is never exercised in PR CI. + +- [ ] **Step 7: Hand off to the maintainer for the release** + +Once the PR is reviewed and merged, post a comment linking to the runbook (`planning/releases/0.4.0.md`) so the maintainer can follow the manual Marketplace publication procedure. No further plan steps after this — the runbook drives the rest. + +--- + +## Acceptance against the spec + +| Spec acceptance criterion | Task that covers it | +|---|---| +| `action.yml` exists at repo root with the shape in §`action.yml` | Task 1 | +| `.github/workflows/tag-major.yml` exists with the shape in §`tag-major.yml` | Task 2 | +| `.github/workflows/semvertag.yml` consumes `uses: ./` | Task 3 | +| `.github/workflows/ci.yml` has an `action-smoke` job that asserts on `status` and `bump` outputs | Task 4 | +| `README.md` advertises `uses: modern-python/semvertag@v0` | Task 5 | +| `docs/providers/github.md` Quick Start uses the action; pending callout gone; Outputs section exists; "Without the composite action" fallback section preserves the inline-CLI recipe | Task 6 | +| `planning/releases/0.4.0.md` exists with the five-step Marketplace publication procedure | Task 7 | +| CI green on a PR that introduces all of the above; the dogfood workflow runs to completion on main after the PR merges | Task 8 (CI green) + post-merge dogfood (out of plan scope, observed in the release runbook) | diff --git a/planning/specs/2026-06-08-action-yml-composite-wrapper-design.md b/planning/specs/2026-06-08-action-yml-composite-wrapper-design.md index 6eefcd9..202fc26 100644 --- a/planning/specs/2026-06-08-action-yml-composite-wrapper-design.md +++ b/planning/specs/2026-06-08-action-yml-composite-wrapper-design.md @@ -89,7 +89,7 @@ runs: SEMVERTAG_STRATEGY: ${{ inputs.strategy }} run: | set -euo pipefail - result=$(uvx 'semvertag>=0.4,<1' tag --json) + result=$(uvx 'semvertag>=0.3.1,<1' tag --json) printf '%s\n' "$result" printf 'tag=%s\n' "$(jq -r '.tag // ""' <<<"$result")" >> "$GITHUB_OUTPUT" printf 'bump=%s\n' "$(jq -r '.bump' <<<"$result")" >> "$GITHUB_OUTPUT" @@ -102,7 +102,7 @@ runs: - **`set -euo pipefail`.** If `uvx` fails, the step fails fast and jq never sees empty/garbage stdin. Avoids ambiguous half-states in `$GITHUB_OUTPUT`. - **Echo the JSON before parsing.** Humans reading the job log see the full envelope; no second CLI invocation is needed for diagnostics. - **`jq -r '.tag // ""'`.** Guards the `no-bump` case (where `tag` is JSON `null`) so the output becomes an empty string — predictable for downstream `if:` gates. `.bump` and `.status` are always present per `RunResult` schema_version 1.0; no fallback. -- **CLI version floor `>=0.4,<1`.** Pairs the action with the release that ships it. `@v0` follows minor bumps; the floor inside `action.yml` also bumps on each minor. Lower bound is the release minor; upper bound `<1` defers the 1.0 question. +- **CLI version floor `>=0.3.1,<1`.** Locks to the minimum CLI version that ships every feature the action depends on (`--json` envelope, `GITHUB_ACTIONS=true` auto-detection, branch-prefix GitHub merge subject recognition). The floor only needs to bump when a future release breaks CLI contract — not on every minor. Upper bound `<1` defers the 1.0 question. This also resolves a chicken-and-egg: the floor is satisfiable from PyPI today, so the PR landing this work passes its own `action-smoke` CI on the first run. - **`astral-sh/setup-uv@v8`.** The official Astral installer; one step, prebuilt binary, automatic cache. Used by every modern uv-in-CI project. Trade-off accepted: this adds a third-party action dependency we trust via major-version pin (v8.x.y). - **`SEMVERTAG_STRATEGY` always exported.** Mirrors the GitLab Catalog template (`templates/semvertag.yml`). Trade-off: a workflow-level `env: SEMVERTAG_STRATEGY: ...` is overridden by the action's step-env. The fix in that rare case is to use the `with: strategy:` input, which is the documented path. - **Internal step id `run`.** Lets the top-level `outputs:` mapping reference `steps.run.outputs.*`. Users wire their own `id:` on the calling `uses:` block to read the exposed outputs. From c841e1806511db068752c8e83f44e987746d6a5a Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Mon, 8 Jun 2026 23:06:19 +0300 Subject: [PATCH 03/11] feat: add action.yml composite GitHub Action --- action.yml | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 action.yml diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..9679582 --- /dev/null +++ b/action.yml @@ -0,0 +1,47 @@ +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 | no-bump | error.' + value: ${{ steps.run.outputs.status }} + +runs: + using: 'composite' + steps: + - uses: astral-sh/setup-uv@v3 + + - 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" + printf 'tag=%s\n' "$(jq -r '.tag // ""' <<<"$result")" >> "$GITHUB_OUTPUT" + printf 'bump=%s\n' "$(jq -r '.bump' <<<"$result")" >> "$GITHUB_OUTPUT" + printf 'status=%s\n' "$(jq -r '.status' <<<"$result")" >> "$GITHUB_OUTPUT" From fe7958bdf9e70c8b023a8b451cc326d07b60d93c Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Mon, 8 Jun 2026 23:09:01 +0300 Subject: [PATCH 04/11] ci: add tag-major workflow to maintain floating v0 tag --- .github/workflows/tag-major.yml | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/tag-major.yml diff --git a/.github/workflows/tag-major.yml b/.github/workflows/tag-major.yml new file mode 100644 index 0000000..a135ea2 --- /dev/null +++ b/.github/workflows/tag-major.yml @@ -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" From b1500ac9c27fd77fca204acd3392f95fe0a7bbc2 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Mon, 8 Jun 2026 23:12:04 +0300 Subject: [PATCH 05/11] ci: dogfood the composite action via uses: ./ --- .github/workflows/semvertag.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/semvertag.yml b/.github/workflows/semvertag.yml index e1c5318..057a055 100644 --- a/.github/workflows/semvertag.yml +++ b/.github/workflows/semvertag.yml @@ -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: @@ -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/"]' From 3590efc6ae8f04fc4f328b1303e61951fa4fbc35 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Mon, 8 Jun 2026 23:14:24 +0300 Subject: [PATCH 06/11] ci: add action-smoke job that exercises the composite action --- .github/workflows/ci.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a58996..bb9209a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 }}" From e860811c30946b58bd41e87ae71b668a65e2db94 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Mon, 8 Jun 2026 23:18:13 +0300 Subject: [PATCH 07/11] =?UTF-8?q?docs:=20README=20=E2=80=94=20advertise=20?= =?UTF-8?q?the=20composite=20action=20as=20the=20GHA=20recipe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 5e8658d..5ed19d0 100644 --- a/README.md +++ b/README.md @@ -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 From 498c911bbb0a5dd4f444c4c6030e7b8b3f72e4db Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Mon, 8 Jun 2026 23:22:31 +0300 Subject: [PATCH 08/11] docs(providers/github): rewrite Quick Start around the composite action Co-Authored-By: Claude Sonnet 4.6 --- docs/providers/github.md | 125 ++++++++++++++++++++++++++++++--------- 1 file changed, 97 insertions(+), 28 deletions(-) diff --git a/docs/providers/github.md b/docs/providers/github.md index 4808346..333eeb1 100644 --- a/docs/providers/github.md +++ b/docs/providers/github.md @@ -1,14 +1,10 @@ # 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 @@ -16,13 +12,12 @@ 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 @@ -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 @@ -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 @@ -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 on `no-bump`. | +| `bump` | `none` \| `patch` \| `minor` \| `major`. | +| `status` | `created` \| `no-bump` \| `error`. | + +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: @@ -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 From e18e7b8e30553537f08a4ab5f9155029af435028 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Mon, 8 Jun 2026 23:34:42 +0300 Subject: [PATCH 09/11] docs(release): draft 0.4.0 notes (composite action + tag-major workflow) Co-Authored-By: Claude Sonnet 4.6 --- planning/releases/0.4.0.md | 68 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 planning/releases/0.4.0.md diff --git a/planning/releases/0.4.0.md b/planning/releases/0.4.0.md new file mode 100644 index 0000000..ed2588b --- /dev/null +++ b/planning/releases/0.4.0.md @@ -0,0 +1,68 @@ +# semvertag 0.4.0 — composite GitHub Action + +**Minor release shipping the `action.yml` composite wrapper that has been on the deferred list since 0.3.0.** Users can now write `uses: modern-python/semvertag@v0` instead of pasting an 11-line install-and-run block. No CLI changes; the action wraps the existing `semvertag tag --json` invocation and surfaces `tag` / `bump` / `status` as step outputs. + +If you're already using the documented pure-CLI snippet and don't want to consume third-party actions, you can stay on it — `docs/providers/github.md` preserves it as the "Without the composite action" recipe. + +## What landed + +- `action.yml` at repo root — composite action: `astral-sh/setup-uv@v8`, then `uvx 'semvertag>=0.3.1,<1' tag --json`, then parses the envelope into `tag` / `bump` / `status` step outputs. +- `.github/workflows/tag-major.yml` — fires on release published (non-prerelease) and force-updates the floating `v0` major tag so consumers can pin `@v0` and ride minor bumps automatically. +- Dogfood workflow migration — `.github/workflows/semvertag.yml` now consumes `uses: ./`, exercising the action against the working tree on every push to main. +- `action-smoke` CI job — runs `uses: ./` on every PR and asserts that `status` and `bump` outputs are non-empty. Real tag creation against the GitHub API is covered by the post-merge dogfood run, not the PR-time job (forks can't have `contents: write`). +- README + `docs/providers/github.md` rewrite — Quick Start leads with the action; Outputs section documents the three step outputs; "Without the composite action" preserves the pure-CLI fallback for constrained environments. + +## CLI version floor + +`action.yml` pins `'semvertag>=0.3.1,<1'`. 0.3.1 is the minimum CLI version that ships every feature the action depends on (`--json` envelope, `GITHUB_ACTIONS=true` auto-detection, branch-prefix GitHub merge subject recognition). The floor only needs to bump when a future release breaks CLI contract — not on every minor. + +## Release procedure (maintainer) + +### Step 1: Pre-flight check + +Before tagging: + +- Search https://github.com/marketplace?type=actions for "semvertag" — the listing name `semvertag` must not be taken by another action. + - **If it's taken:** edit `action.yml`'s `name:` field to `'semvertag tag'` (Marketplace permits spaces in display names) and re-PR before continuing. The `uses: modern-python/semvertag@v0` syntax depends on the repo slug, not the display name, so consumer-facing docs do not change. +- Confirm `branding.icon` is one of the Feather icon names GitHub accepts and `branding.color` is one of `white | yellow | blue | green | orange | red | purple | gray-dark`. (We ship `icon: tag`, `color: blue` — both valid.) +- Confirm CI is green on main, including the new `action-smoke` job. + +### Step 2: Cut the v0.4.0 release + +Follow the project's existing release flow: tag, push, create a GitHub release. `publish.yml` fires on release creation and pushes to PyPI via `just publish` (which uses `uv version $GITHUB_REF_NAME` to inject the version at build time). + +### Step 3: Bootstrap the floating `v0` tag (one-time) + +The `tag-major.yml` workflow handles the floating tag on every release from v0.4.1 forward. For v0.4.0 specifically — the first release after the workflow landed — the floating tag does not yet exist and must be bootstrapped manually: + +```sh +git fetch --tags +git tag -fa v0 v0.4.0 -m 'Update v0 to v0.4.0' +git push -f origin v0 +``` + +After this, `uses: modern-python/semvertag@v0` resolves successfully for consumers. + +### Step 4: Publish to Marketplace (manual UI step) + +1. Navigate to https://github.com/modern-python/semvertag/releases/tag/v0.4.0. +2. Click **Edit release**. +3. Check **Publish this Action to the GitHub Marketplace**. +4. Accept the Marketplace Terms of Service if prompted (one-time for the repo). +5. Select **Primary Category:** `Continuous integration`. +6. Select **Secondary Category** (optional): `Utilities`. +7. Save the release. + +### Step 5: Post-publish smoke test + +- Confirm the listing appears at https://github.com/marketplace/actions/semvertag (the slug derives from the `name:` field; if you renamed in Step 1 the slug will differ). +- In a sandbox repo, paste the README snippet (`uses: modern-python/semvertag@v0` after a `actions/checkout@v4` with `fetch-depth: 0`) and confirm the workflow runs end-to-end. + +## Breaking changes + +None. The action is additive; the pure-CLI recipe still works exactly as before (and remains documented as the fallback). + +## See also + +- Spec: `planning/specs/2026-06-08-action-yml-composite-wrapper-design.md` +- Implementation plan: `planning/plans/2026-06-08-action-yml-composite-wrapper.md` From d2ece8658228933e245105be90588a0dc8e0b884 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Mon, 8 Jun 2026 23:46:05 +0300 Subject: [PATCH 10/11] fix: pin astral-sh/setup-uv to @v7, not @v8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spec/plan picked @v8 as the "current latest" major, but astral only publishes floating major tags up to @v7 — specific v8.0.0/v8.1.0/v8.2.0 releases exist, but no `v8` ref. `@v8` resolved to nothing in CI (initially fixed via @v3 to match ci.yml; this commit upgrades to @v7 to stay closest to upstream's actual current). Spec, plan, and runbook updated to match. --- action.yml | 2 +- .../2026-06-08-action-yml-composite-wrapper.md | 8 ++++---- planning/releases/0.4.0.md | 2 +- ...26-06-08-action-yml-composite-wrapper-design.md | 14 +++++++------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/action.yml b/action.yml index 9679582..dcd98f0 100644 --- a/action.yml +++ b/action.yml @@ -30,7 +30,7 @@ outputs: runs: using: 'composite' steps: - - uses: astral-sh/setup-uv@v3 + - uses: astral-sh/setup-uv@v7 - name: Run semvertag id: run diff --git a/planning/plans/2026-06-08-action-yml-composite-wrapper.md b/planning/plans/2026-06-08-action-yml-composite-wrapper.md index bc159a3..3057a74 100644 --- a/planning/plans/2026-06-08-action-yml-composite-wrapper.md +++ b/planning/plans/2026-06-08-action-yml-composite-wrapper.md @@ -6,7 +6,7 @@ **Architecture:** Pure-YAML + Markdown work. Two new workflow files (`action.yml`, `.github/workflows/tag-major.yml`), one modified workflow (`.github/workflows/semvertag.yml` dogfood), one modified workflow (`.github/workflows/ci.yml` adds an `action-smoke` job that runs `uses: ./` against the action being introduced — the chicken-and-egg is resolved by floor `'semvertag>=0.3.1,<1'` which is satisfiable from PyPI today). Docs rewrite touches README and `docs/providers/github.md`. Runbook captures the manual Marketplace publication procedure as five numbered steps for the maintainer to follow. -**Tech Stack:** GitHub Actions composite action syntax, `astral-sh/setup-uv@v8`, bash + `jq` (default on `ubuntu-latest`), MkDocs (`mkdocs build --strict` as the docs gate). +**Tech Stack:** GitHub Actions composite action syntax, `astral-sh/setup-uv@v7`, bash + `jq` (default on `ubuntu-latest`), MkDocs (`mkdocs build --strict` as the docs gate). **Spec:** `planning/specs/2026-06-08-action-yml-composite-wrapper-design.md` @@ -81,7 +81,7 @@ outputs: runs: using: 'composite' steps: - - uses: astral-sh/setup-uv@v8 + - uses: astral-sh/setup-uv@v7 - name: Run semvertag id: run @@ -634,7 +634,7 @@ If you're already using the documented pure-CLI snippet and don't want to consum ## What landed -- `action.yml` at repo root — composite action: `astral-sh/setup-uv@v8`, then `uvx 'semvertag>=0.3.1,<1' tag --json`, then parses the envelope into `tag` / `bump` / `status` step outputs. +- `action.yml` at repo root — composite action: `astral-sh/setup-uv@v7`, then `uvx 'semvertag>=0.3.1,<1' tag --json`, then parses the envelope into `tag` / `bump` / `status` step outputs. - `.github/workflows/tag-major.yml` — fires on release published (non-prerelease) and force-updates the floating `v0` major tag so consumers can pin `@v0` and ride minor bumps automatically. - Dogfood workflow migration — `.github/workflows/semvertag.yml` now consumes `uses: ./`, exercising the action against the working tree on every push to main. - `action-smoke` CI job — runs `uses: ./` on every PR and asserts that `status` and `bump` outputs are non-empty. Real tag creation against the GitHub API is covered by the post-merge dogfood run, not the PR-time job (forks can't have `contents: write`). @@ -783,7 +783,7 @@ Wait for CI to complete on the PR. Expected: - `pytest` job (matrix of 4 Python versions): green (same as before this PR). - `action-smoke` job: green. The new job will: - Check out the repo with `fetch-depth: 0`. - - Run `uses: ./` which executes the action.yml: install uv via `setup-uv@v8`, then `uvx 'semvertag>=0.3.1,<1' tag --json`. + - Run `uses: ./` which executes the action.yml: install uv via `setup-uv@v7`, then `uvx 'semvertag>=0.3.1,<1' tag --json`. - The CLI emits a JSON envelope. On a PR commit (not a merge commit), branch-prefix returns `status=no-bump` because no merge subject matches. `tag` is empty; `bump` is `none`; `status` is `no-bump`. - The verify step asserts that `status` and `bump` outputs are non-empty. Both are (`"no-bump"` and `"none"`), so the assertion passes. diff --git a/planning/releases/0.4.0.md b/planning/releases/0.4.0.md index ed2588b..8be6050 100644 --- a/planning/releases/0.4.0.md +++ b/planning/releases/0.4.0.md @@ -6,7 +6,7 @@ If you're already using the documented pure-CLI snippet and don't want to consum ## What landed -- `action.yml` at repo root — composite action: `astral-sh/setup-uv@v8`, then `uvx 'semvertag>=0.3.1,<1' tag --json`, then parses the envelope into `tag` / `bump` / `status` step outputs. +- `action.yml` at repo root — composite action: `astral-sh/setup-uv@v7`, then `uvx 'semvertag>=0.3.1,<1' tag --json`, then parses the envelope into `tag` / `bump` / `status` step outputs. - `.github/workflows/tag-major.yml` — fires on release published (non-prerelease) and force-updates the floating `v0` major tag so consumers can pin `@v0` and ride minor bumps automatically. - Dogfood workflow migration — `.github/workflows/semvertag.yml` now consumes `uses: ./`, exercising the action against the working tree on every push to main. - `action-smoke` CI job — runs `uses: ./` on every PR and asserts that `status` and `bump` outputs are non-empty. Real tag creation against the GitHub API is covered by the post-merge dogfood run, not the PR-time job (forks can't have `contents: write`). diff --git a/planning/specs/2026-06-08-action-yml-composite-wrapper-design.md b/planning/specs/2026-06-08-action-yml-composite-wrapper-design.md index 202fc26..5842115 100644 --- a/planning/specs/2026-06-08-action-yml-composite-wrapper-design.md +++ b/planning/specs/2026-06-08-action-yml-composite-wrapper-design.md @@ -79,7 +79,7 @@ outputs: runs: using: 'composite' steps: - - uses: astral-sh/setup-uv@v8 + - uses: astral-sh/setup-uv@v7 - name: Run semvertag id: run @@ -103,7 +103,7 @@ runs: - **Echo the JSON before parsing.** Humans reading the job log see the full envelope; no second CLI invocation is needed for diagnostics. - **`jq -r '.tag // ""'`.** Guards the `no-bump` case (where `tag` is JSON `null`) so the output becomes an empty string — predictable for downstream `if:` gates. `.bump` and `.status` are always present per `RunResult` schema_version 1.0; no fallback. - **CLI version floor `>=0.3.1,<1`.** Locks to the minimum CLI version that ships every feature the action depends on (`--json` envelope, `GITHUB_ACTIONS=true` auto-detection, branch-prefix GitHub merge subject recognition). The floor only needs to bump when a future release breaks CLI contract — not on every minor. Upper bound `<1` defers the 1.0 question. This also resolves a chicken-and-egg: the floor is satisfiable from PyPI today, so the PR landing this work passes its own `action-smoke` CI on the first run. -- **`astral-sh/setup-uv@v8`.** The official Astral installer; one step, prebuilt binary, automatic cache. Used by every modern uv-in-CI project. Trade-off accepted: this adds a third-party action dependency we trust via major-version pin (v8.x.y). +- **`astral-sh/setup-uv@v7`.** The official Astral installer; one step, prebuilt binary, automatic cache. Used by every modern uv-in-CI project. `v7` is the highest major astral publishes as a floating tag — specific `v8.x.y` releases exist (v8.0.0–v8.2.0) but no floating `v8` ref has been tagged upstream, so `@v8` resolves to nothing. Trade-off accepted: this adds a third-party action dependency we trust via major-version pin (v7.x.y). - **`SEMVERTAG_STRATEGY` always exported.** Mirrors the GitLab Catalog template (`templates/semvertag.yml`). Trade-off: a workflow-level `env: SEMVERTAG_STRATEGY: ...` is overridden by the action's step-env. The fix in that rare case is to use the `with: strategy:` input, which is the documented path. - **Internal step id `run`.** Lets the top-level `outputs:` mapping reference `steps.run.outputs.*`. Users wire their own `id:` on the calling `uses:` block to read the exposed outputs. - **No checkout inside the action.** Established tag/release actions (mathieudutour/github-tag-action, googleapis/release-please-action, cycjimmy/semantic-release-action) uniformly skip it. Callers have heterogeneous checkout needs (refs, submodules, LFS, sparse, monorepo paths, custom tokens); a composite that silently re-checks out fights those needs. @@ -212,9 +212,9 @@ action-smoke: | Check | Layer | |---|---| -| YAML parses, inputs/outputs declared correctly | `astral-sh/setup-uv@v8` + GHA composite loader (failure shows up as a runtime parse error) | -| `astral-sh/setup-uv@v8` resolves and installs | Step 1 of the composite | -| `uvx 'semvertag>=0.4,<1' tag --json` runs to completion | Step 2 (failure → `set -euo pipefail` exits non-zero) | +| YAML parses, inputs/outputs declared correctly | `astral-sh/setup-uv@v7` + GHA composite loader (failure shows up as a runtime parse error) | +| `astral-sh/setup-uv@v7` resolves and installs | Step 1 of the composite | +| `uvx 'semvertag>=0.3.1,<1' tag --json` runs to completion | Step 2 (failure → `set -euo pipefail` exits non-zero) | | JSON parsing emits non-empty `status`/`bump` | The verify step | ### What it does NOT cover @@ -348,7 +348,7 @@ Structure: | Decision | Choice | Why not the alternative | |---|---|---| | Checkout in the action? | No | Every published tag/release action skips it; callers have heterogeneous checkout needs (refs, submodules, LFS, sparse, monorepo paths). | -| uv installer? | `astral-sh/setup-uv@v8` | Faster than `pip install uv` (prebuilt binary, automatic cache). The third-party-action dependency is the ecosystem norm. | +| uv installer? | `astral-sh/setup-uv@v7` | Faster than `pip install uv` (prebuilt binary, automatic cache). The third-party-action dependency is the ecosystem norm. `v7` is the highest major astral publishes as a floating tag today; `@v8` resolves to nothing. | | Inputs? | `strategy` + `token` only | Every other knob (GHE endpoint, repo override, branch-prefix lists) already works via `env:`. Duplicating the env-var contract as inputs creates drift risk. | | Outputs? | `tag`, `bump`, `status` | CLI already emits `--json`; cost is ~10 YAML lines. Matches release-please / github-tag-action convention. `commit` and `reason` deferred; can be added non-breaking. | | Always export `SEMVERTAG_STRATEGY`? | Yes | Matches the GitLab Catalog template. Trade-off: workflow-level env `SEMVERTAG_STRATEGY` is overridden — use the `with: strategy:` input instead. | @@ -357,7 +357,7 @@ Structure: ## Risks -- **`astral-sh/setup-uv@v8` major bump.** A future v9 may introduce breaking changes to inputs we depend on. Mitigation: pin to `@v8` (major) and revisit on each minor semvertag release. +- **`astral-sh/setup-uv@v7` major bump.** When astral publishes a floating `v8` (or later) tag, that becomes a candidate upgrade. Re-evaluate the pin on each minor semvertag release; bump only if the new major brings features we want and has stable floating-tag support. - **`name: 'semvertag'` Marketplace collision.** If the name is taken when we publish, the listing fails to create. Mitigation: pre-flight check in the runbook. If the name is taken, change `name:` in `action.yml` to `'semvertag tag'` (Marketplace permits spaces in display names) before re-attempting publication; the listing slug and the `uses: modern-python/semvertag@v0` syntax are unaffected. Low likelihood — "semvertag" is distinctive. - **`jq` not on self-hosted runners.** Default on every GitHub-hosted runner; self-hosted runners may strip it. Mitigation: document the assumption in `docs/providers/github.md` as a known requirement. - **`SEMVERTAG_STRATEGY` step-env override surprise.** Users setting it at workflow level may be confused when the action's input default `branch-prefix` wins. Mitigation: documented in the new "Strategy-specific env vars" docs section; the `with: strategy:` input is presented as the canonical knob. From ecea4b65804ac1c1d7abf809a961c755d4421d46 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Mon, 8 Jun 2026 23:56:40 +0300 Subject: [PATCH 11/11] fix(action): normalize status to a stable created|no-bump enum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI emits internal status strings (`no_tags`, `already_tagged`, `no_merge_commit`, `no_conforming_commit`, `created`) but the action's documented enum was `created | no-bump | error` — consumers writing `if: outputs.status == 'no-bump'` would silently never match. Normalize inside the composite via a bash `case`: `created` stays `created`; everything else collapses to `no-bump`. `set -euo pipefail` already exits on CLI errors, so no `error` value is ever written — documentation tightened to reflect that. Spec / plan / provider docs updated to match. --- action.yml | 12 ++++++++++-- docs/providers/github.md | 4 ++-- .../2026-06-08-action-yml-composite-wrapper.md | 16 ++++++++++++---- ...-06-08-action-yml-composite-wrapper-design.md | 14 +++++++++++--- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/action.yml b/action.yml index dcd98f0..d1b52a1 100644 --- a/action.yml +++ b/action.yml @@ -24,7 +24,7 @@ outputs: description: 'The computed bump: none | patch | minor | major.' value: ${{ steps.run.outputs.bump }} status: - description: 'The run status: created | no-bump | error.' + description: 'The run status: created (tag pushed) | no-bump (nothing to tag).' value: ${{ steps.run.outputs.status }} runs: @@ -42,6 +42,14 @@ runs: 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' "$(jq -r '.status' <<<"$result")" >> "$GITHUB_OUTPUT" + printf 'status=%s\n' "$status" >> "$GITHUB_OUTPUT" diff --git a/docs/providers/github.md b/docs/providers/github.md index 333eeb1..4d0225f 100644 --- a/docs/providers/github.md +++ b/docs/providers/github.md @@ -94,9 +94,9 @@ 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 on `no-bump`. | +| `tag` | The created tag (e.g. `v1.2.3`), or empty string when `status` is `no-bump`. | | `bump` | `none` \| `patch` \| `minor` \| `major`. | -| `status` | `created` \| `no-bump` \| `error`. | +| `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. diff --git a/planning/plans/2026-06-08-action-yml-composite-wrapper.md b/planning/plans/2026-06-08-action-yml-composite-wrapper.md index 3057a74..7576547 100644 --- a/planning/plans/2026-06-08-action-yml-composite-wrapper.md +++ b/planning/plans/2026-06-08-action-yml-composite-wrapper.md @@ -75,7 +75,7 @@ outputs: description: 'The computed bump: none | patch | minor | major.' value: ${{ steps.run.outputs.bump }} status: - description: 'The run status: created | no-bump | error.' + description: 'The run status: created (tag pushed) | no-bump (nothing to tag).' value: ${{ steps.run.outputs.status }} runs: @@ -93,9 +93,17 @@ runs: 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' "$(jq -r '.status' <<<"$result")" >> "$GITHUB_OUTPUT" + printf 'status=%s\n' "$status" >> "$GITHUB_OUTPUT" ``` - [ ] **Step 2: Verify YAML parses** @@ -495,9 +503,9 @@ 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 on `no-bump`. | +| `tag` | The created tag (e.g. `v1.2.3`), or empty string when `status` is `no-bump`. | | `bump` | `none` \| `patch` \| `minor` \| `major`. | -| `status` | `created` \| `no-bump` \| `error`. | +| `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. diff --git a/planning/specs/2026-06-08-action-yml-composite-wrapper-design.md b/planning/specs/2026-06-08-action-yml-composite-wrapper-design.md index 5842115..8e471ff 100644 --- a/planning/specs/2026-06-08-action-yml-composite-wrapper-design.md +++ b/planning/specs/2026-06-08-action-yml-composite-wrapper-design.md @@ -73,7 +73,7 @@ outputs: description: 'The computed bump: none | patch | minor | major.' value: ${{ steps.run.outputs.bump }} status: - description: 'The run status: created | no-bump | error.' + description: 'The run status: created (tag pushed) | no-bump (nothing to tag).' value: ${{ steps.run.outputs.status }} runs: @@ -91,9 +91,17 @@ runs: 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' "$(jq -r '.status' <<<"$result")" >> "$GITHUB_OUTPUT" + printf 'status=%s\n' "$status" >> "$GITHUB_OUTPUT" ``` ### Key choices @@ -101,7 +109,7 @@ runs: - **No `SEMVERTAG_PROVIDER` forced.** Auto-detection from `GITHUB_ACTIONS=true` (shipped in 0.3.0) makes that unnecessary. Forcing it would suppress useful errors when someone runs `act` or otherwise exercises the action outside a real GHA environment. - **`set -euo pipefail`.** If `uvx` fails, the step fails fast and jq never sees empty/garbage stdin. Avoids ambiguous half-states in `$GITHUB_OUTPUT`. - **Echo the JSON before parsing.** Humans reading the job log see the full envelope; no second CLI invocation is needed for diagnostics. -- **`jq -r '.tag // ""'`.** Guards the `no-bump` case (where `tag` is JSON `null`) so the output becomes an empty string — predictable for downstream `if:` gates. `.bump` and `.status` are always present per `RunResult` schema_version 1.0; no fallback. +- **`jq -r '.tag // ""'`.** Guards the no-bump case (where `tag` is JSON `null`) so the output becomes an empty string — predictable for downstream `if:` gates. `.bump` is always present per `RunResult` schema_version 1.0; no fallback. `.status` is normalized via a bash `case` to a stable consumer-facing enum (`created | no-bump`), encapsulating the CLI's internal status strings (`no_tags`, `already_tagged`, `no_merge_commit`, `no_conforming_commit`, ...) so future CLI additions don't break action consumers. - **CLI version floor `>=0.3.1,<1`.** Locks to the minimum CLI version that ships every feature the action depends on (`--json` envelope, `GITHUB_ACTIONS=true` auto-detection, branch-prefix GitHub merge subject recognition). The floor only needs to bump when a future release breaks CLI contract — not on every minor. Upper bound `<1` defers the 1.0 question. This also resolves a chicken-and-egg: the floor is satisfiable from PyPI today, so the PR landing this work passes its own `action-smoke` CI on the first run. - **`astral-sh/setup-uv@v7`.** The official Astral installer; one step, prebuilt binary, automatic cache. Used by every modern uv-in-CI project. `v7` is the highest major astral publishes as a floating tag — specific `v8.x.y` releases exist (v8.0.0–v8.2.0) but no floating `v8` ref has been tagged upstream, so `@v8` resolves to nothing. Trade-off accepted: this adds a third-party action dependency we trust via major-version pin (v7.x.y). - **`SEMVERTAG_STRATEGY` always exported.** Mirrors the GitLab Catalog template (`templates/semvertag.yml`). Trade-off: a workflow-level `env: SEMVERTAG_STRATEGY: ...` is overridden by the action's step-env. The fix in that rare case is to use the `with: strategy:` input, which is the documented path.