Skip to content

Releases: modern-python/semvertag

0.6.0 — dry-run on the composite action

09 Jun 14:27
efc5902

Choose a tag to compare

semvertag 0.6.0 — dry-run on the composite action

Minor release exposing --dry-run through modern-python/semvertag@v0 and removing the action-smoke side-effect smell. Pure action + CI change — the CLI is unchanged from 0.5.0 (no Python source touched). Consumers who don't need the new input can keep using @v0 exactly as before; the input is opt-in.

If you don't use the composite action and only consume semvertag directly via uvx, you can skip this release — there's no CLI change.

What landed

  • dry-run input on action.yml (default 'false'). When true, the composite passes --dry-run to semvertag tag; semvertag short-circuits before provider.create_tag and the existing case block normalizes status="dry_run" to the public status="no-bump". The action's bump / tag outputs reflect what would have happened. (PR #16)
  • action-smoke (this repo's CI) becomes structurally side-effect-free. Two layers of protection:
    • Layer 1: with: { dry-run: 'true' } on uses: ./ — semvertag never attempts a push.
    • Layer 2: permissions: contents: write removed — even a future regression that bypassed dry-run would 403 on the API.
  • Assertion reduced from two lines (status == no-bump AND bump == none) to one (status == no-bump). Under dry-run, bump is unstable (reflects the would-be value) — the old check would fail on any PR where main HEAD was an untagged feat//bugfix/ merge. Status normalization is the real wiring contract; the single check catches a regression loudly (a unwired dry-run would surface as created).
  • CLI version floor in action.yml bumps from >=0.3.1,<1 to >=0.5.0,<1 (the release that ships --dry-run).
  • docs/providers/github.md gets a ## Preview the next bump section with both the action-input example and the equivalent local uvx invocation.

Usage

- id: semvertag
  uses: modern-python/semvertag@v0
  with:
    dry-run: true
- if: steps.semvertag.outputs.bump != 'none'
  run: echo "Next release would be ${{ steps.semvertag.outputs.tag }} (${{ steps.semvertag.outputs.bump }} bump)"

Under dry-run: true, the action's status output is no-bump (no real tag was pushed). The bump and tag outputs reflect what would have happened, so a downstream conditional like the one above can branch on the planned bump.

Why

PR #14 (in 0.5.0) surfaced the smell: action-smoke ran semvertag against the real main with contents: write, and when main HEAD wasn't already tagged, the smoke test pushed a real release tag from a PR's CI run. 0.5.0 shipped the CLI half (--dry-run); 0.6.0 wires it through the composite and switches the smoke job to use it. After this release:

  • action-smoke is provably side-effect-free regardless of main HEAD's tag state.
  • The brittle "main HEAD is always already-tagged" assumption is gone — the test asserts the dry-run wiring, not an external invariant.
  • Any consumer who wants to preview the next bump from a workflow can opt in via the same input.

Breaking changes

None. dry-run defaults to 'false'; existing uses: modern-python/semvertag@v0 invocations behave identically to 0.5.x.

The version floor bump (>=0.3.1,<1>=0.5.0,<1) is internal to the action — consumers who pin @v0 don't see it directly. If anyone was somehow consuming action.yml outside the @v0 pin and pinning an old semvertag themselves, they'd need to upgrade to 0.5.0+. No known consumer pattern matches this.

Release procedure

tag-major.yml automatically updates the floating v0 tag on release-published, so the steps from 0.4.0's manual bootstrap are no longer needed.

gh release create 0.6.0 \
  --title '0.6.0 — dry-run on the composite action' \
  --notes-file planning/releases/0.6.0.md

publish.yml fires on release creation and pushes 0.6.0 to PyPI. tag-major.yml fires in parallel and force-updates v0 to point at the 0.6.0 commit.

See also

  • Spec: planning/specs/2026-06-09-action-yml-dry-run-design.md
  • Implementation plan: planning/plans/2026-06-09-action-yml-dry-run.md
  • PR: #16
  • Predecessor (CLI half): PR #15 — semvertag 0.5.0
  • Original smell surfacing: PR #14 — mkdocs deploy + the action-smoke side-effect discovery

0.5.0

09 Jun 13:25
ffbf4d7

Choose a tag to compare

New

  • semvertag tag --dry-run — compute the bump and print the result without pushing a tag. Useful for previewing the next version locally and for CI smoke tests that need to exercise the composite action side-effect-free. JSON output uses a new status: "dry_run" value; human-readable rendering says "Dry run: would create tag X on commit Y…". (#15)
  • Docs site auto-deploys to https://semvertag.modern-python.org on every push to main that touches docs/, mkdocs.yml, or the docs workflow. (#14)

CI

  • GitHub Actions pins bumped to current majors: actions/checkout@v6, extractions/setup-just@v4, astral-sh/setup-uv@v8.2.0. (#14)
  • action-smoke gated to pull_request events only. (#13)

0.4.0 — composite GitHub Action

09 Jun 05:25
923e212

Choose a tag to compare

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@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).
  • 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 0.4.0 GitHub release

This project's release tag convention is bare semver (0.4.0, no v prefix) — the same as 0.3.x and earlier. The 0.4.0 tag is created automatically by the dogfood workflow on PR merge, so it already exists when you reach this step.

Create the GitHub release pointing at the existing tag:

gh release create 0.4.0 \
  --title '0.4.0 — composite GitHub Action' \
  --notes-file planning/releases/0.4.0.md

(Or use the GitHub web UI: Releases → "Draft a new release" → choose existing tag 0.4.0.)

publish.yml fires on release creation and pushes 0.4.0 to PyPI via just publish (which uses uv version $GITHUB_REF_NAME to inject the bare semver tag name as the package version).

Step 3: Bootstrap the floating v0 tag (one-time)

Releases use bare semver tags (0.4.0) but the floating action tag is v-prefixed (v0) so consumers can write the conventional uses: modern-python/semvertag@v0. tag-major.yml handles the v-prefix bump automatically on every release from 0.4.1 forward. For 0.4.0 specifically — the first release after the workflow landed — the floating tag does not yet exist and must be bootstrapped manually:

git fetch --tags
git tag -fa v0 0.4.0 -m 'Update v0 to 0.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/0.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

0.3.1 — branch-prefix recognizes GitHub merge subjects

08 Jun 18:14
b21ce22

Choose a tag to compare

Patch release with one fix surfaced by dogfooding. Anyone using --strategy branch-prefix (the default) inside GitHub Actions was silently no-op'ing on every merge with status=no_merge_commit, even on real merge commits. 0.3.1 fixes the default to recognize GitHub's PR-merge subject format alongside GitLab's git merge format. No changes to the GitHub or GitLab providers themselves.

If you're not on branch-prefix, or your CI is GitLab, you can skip this release — no behavior change for you.

The bug

BranchPrefixStrategy.decide gated the bump computation on a single substring check:

if self.config.merge_mark_text not in subject:   # default: "Merge branch"
    return Bump.NONE

"Merge branch" is GitLab's default git merge feature/foo subject (Merge branch 'feature/foo' into main). GitHub's PR merges produce a different subject — Merge pull request #N from OWNER/BRANCH — which does not contain "Merge branch". So every branch-prefix consumer running inside GitHub Actions hit the gate, fell through to Bump.NONE, and reported no_merge_commit even when the commit was clearly a merge.

The docs in docs/strategies/branch-prefix.md explicitly called this out as a limitation under defaults — the fix turns "explicitly limited" into "works".

Surfaced concretely by dogfooding 0.3.0 on this repo's own CI (PR #5, PR #6 — both merged via GitHub PR, both no-op'd with no_merge_commit).

The fix

BranchPrefixConfig.merge_mark_text: str = "Merge branch" becomes a plural tuple with both formats in the default:

merge_mark_texts: tuple[str, ...] = pydantic.Field(
    default=("Merge branch", "Merge pull request"),
    min_length=1,
)

BranchPrefixStrategy.decide now checks any(mark in subject for mark in self.config.merge_mark_texts). Out-of-the-box correct for both providers.

Breaking change

The field rename merge_mark_textmerge_mark_texts (singular → plural tuple) affects anyone who was overriding the default via env or config. Specifically:

  • SEMVERTAG_BRANCH_PREFIX__MERGE_MARK_TEXT="X" (old) → SEMVERTAG_BRANCH_PREFIX__MERGE_MARK_TEXTS='["X"]' (new — note the plural field name + JSON-tuple env shape).
  • BranchPrefixConfig(merge_mark_text="X") (old) → BranchPrefixConfig(merge_mark_texts=("X",)) (new).

Acceptable pre-1.0: this knob shipped in 0.1.0 with the documented use case "non-default merge conventions" and has no known consumers overriding it. Users who were overriding it get a clear pydantic ValidationError at startup — easy to fix.

Migration

For the common case (using defaults): no action needed. 0.3.1 just works.

For anyone overriding the old field: rename per above. The new shape accepts a tuple of substrings, so existing single-string overrides become a 1-tuple — and you can now list multiple marks (e.g. squash-merge prefixes alongside the standard ones) without writing custom code.

See also

  • Bug surfacing: dogfood PR #5 (chore/, expected no-op) and PR #6 (bugfix/, should have patch-bumped but didn't — that's how the bug was caught)
  • Fix: PR #7
  • Strategy docs updated: docs/strategies/branch-prefix.md

0.3.0 — GitHub provider

08 Jun 17:32
24b330e

Choose a tag to compare

Feature release. Adds first-class GitHub support. semvertag now auto-tags both GitLab projects and GitHub repos from the same CLI, with auto-detection from the CI runner's environment. The package description "Auto-tag GitLab repos" becomes "Auto-tag GitLab and GitHub repos" — honest at last.

If you're on GitLab today, nothing changes: the default provider remains gitlab when no CI env signals otherwise. Drop straight to "What's new" — there are no breaking changes to the GitLab side.

What's new

  • GitHubProvider. Parallel to GitLabProvider, conforming to the same four-method Provider Protocol. Talks to api.github.com (or your GitHub Enterprise endpoint via SEMVERTAG_GITHUB__ENDPOINT), authenticates via Authorization: Bearer <token> + X-GitHub-Api-Version: 2022-11-28, creates tags via POST /repos/{owner}/{repo}/git/refs. Same retry posture, same decoder path, same error translator shape as the GitLab side — built on the httpware-backed infrastructure that landed in 0.2.x.
  • Provider auto-detection from CI env. semvertag reads GITHUB_ACTIONS and GITLAB_CI to pick the active provider. Inside GitHub Actions: GITHUB_ACTIONS=true is auto-set → provider=github. Inside GitLab CI: GITLAB_CI=trueprovider=gitlab. Outside CI: defaults to gitlab (back-compat with 0.2.x users). Explicit --provider github or SEMVERTAG_PROVIDER=github always wins.
  • New CLI flags.
    • --provider github|gitlab — explicit override
    • --repo OWNER/REPO — GitHub repo identifier (or set GITHUB_REPOSITORY / SEMVERTAG_REPO)
    • --github-endpoint — GitHub API endpoint (for GitHub Enterprise)
    • --token now routes to the active provider's token field (a --provider github --token ghp_... invocation populates Settings.github.token)
  • New env aliases picked up by pydantic-settings:
    • GITHUB_REPOSITORY, SEMVERTAG_REPOSettings.repo
    • SEMVERTAG_PROVIDER, PROVIDERSettings.provider
    • GITHUB_TOKEN, SEMVERTAG_GITHUB__TOKEN, SEMVERTAG_TOKENSettings.github.token
    • SEMVERTAG_GITHUB__ENDPOINTSettings.github.endpoint (default https://api.github.com)
  • Tag-already-exists detection on GitHub. GitHub returns 422 Unprocessable Entity with {"errors": [{"resource": "Reference", "code": "already_exists"}]} when you POST a tag ref that already exists. translate_create_tag_github_unprocessable inspects both the structured already_exists code (durable contract) and the "already exists" message string (safety net) and surfaces as a ConfigError ("Tag already exists: 'vX.Y.Z'.").
  • Inline GitHub Actions workflow recipe in docs/providers/github.md and README.md. Uses actions/setup-python + uvx semvertag tag. Workflow needs permissions: contents: write to use the auto-issued GITHUB_TOKEN; or supply a PAT with contents: write (fine-grained) / repo / public_repo (classic) as SEMVERTAG_TOKEN.

What's new under the hood

Internal cleanups that landed alongside the provider:

  • _link_pagination module — RFC 8288 Link-header walking extracted from providers/gitlab.py (GitHub paginates identically; one helper now serves both).
  • _translate_transport shared helper in providers/_errors.py — the transport-error translation branches (DecodeError, TimeoutError, NetworkError, RetryBudgetExhaustedError, fallback) were uniformly "<provider> <description>" between GitLab and GitHub, so they parameterize cleanly on provider_label: str. Per-status translation stays per-provider because the actionable hints differ.
  • Settings._resolve_provider validator — runs at construction time; auto-detects from env if Settings.provider is unset; enforces that provider=github requires repo and provider=gitlab requires project_id. Replaces the runtime guards that used to live in ioc.py.

Behavior notes

  • Settings() now requires either project_id (gitlab) or repo (github). Pre-0.3.0 you could construct Settings() with neither, and the runtime guard in ioc._build_gitlab_provider would raise ConfigError("Project id missing. ...") at provider-construction time. The new validator catches the same situation earlier with a clearer message. CI consumers were already setting CI_PROJECT_ID (auto-exported by GitLab CI) so this is enforcing what was implicitly required. Local-dev users running semvertag tag against gitlab.com with no --project-id will see the new validator message instead of the old one. Same exit code (2), more actionable diagnostic.
  • No retry-policy changes. Retry middleware, backoff, status codes, RetryBudget all unchanged from 0.2.x.
  • No CLI flag removals. All 0.2.x flags still work. Three new ones added; --token semantics extended to route by active provider.

Migration

For most users, no migration needed. GitLab callers see no behavior change.

To start using GitHub:

# .github/workflows/semvertag.yml
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: 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 }}

GITHUB_ACTIONS=true auto-resolves --provider github; GITHUB_REPOSITORY auto-resolves --repo. The minimum workflow above is the complete setup for github.com-hosted public/private repos using the workflow-scoped GITHUB_TOKEN.

See docs/providers/github.md for token scopes, fine-grained vs classic PATs, GitHub Enterprise setup, and troubleshooting.

See also

Known follow-ups

  • action.yml composite GitHub Action. A one-line uses: modern-python/semvertag@vX.Y.Z is nicer DX than the inline workflow. Deferred; the inline recipe is the supported path until the composite ships.
  • Bitbucket provider. Same pattern; separate work.
  • GitHub App authentication. PATs and the workflow-scoped GITHUB_TOKEN are supported; GitHub App JWT exchange is not.

0.2.0 — HTTP stack on httpware; Python 3.10 dropped

08 Jun 08:13
21cf4d0

Choose a tag to compare

Breaking release. The provider HTTP stack is rebuilt on top of httpware 0.8.2 — a maintained sibling client framework — replacing the in-tree RetryingTransport + HttpClient plumbing that shipped in 0.1.x. Operator-visible behavior changes are listed below. Python 3.10 is no longer supported.

If you pin semvertag in CI and use Python 3.11+ already, you can skip straight to "Behavior changes" — the rest is internal.

What's new

  • HTTP stack on httpware. GitLabProvider holds a httpware.Client directly. httpware.Retry middleware replaces the hand-rolled RetryingTransport. httpware.PydanticDecoder decodes response bodies via response_model= — no more in-tree validator plumbing.
  • Smaller surface. Net ~600 LOC of HTTP plumbing deleted across the two migrations. semvertag/_transport.py and semvertag/providers/_http.py are gone; their tests deleted too.
  • Uniform error contract. httpware.ClientError covers transport failures, status errors, AND decode failures uniformly. semvertag's error tree (ConfigError/2, AuthError/3, ProviderAPIError/4) is unchanged at the CLI boundary — exit codes are stable.
  • httpware[pydantic]>=0.8.2 is a new transitive dependency. End users who install semvertag from PyPI pick it up automatically.

Breaking changes

Python 3.10 dropped

requires-python = ">=3.11,<4". semvertag 0.1.x supported 3.10; 0.2.0 does not. The bump is forced by httpware's own floor.

Migration: upgrade your CI runners and local development environment to Python 3.11 or newer. If you cannot, stay on semvertag==0.1.1.

Behavior changes from the new retry middleware

The 0.1.x RetryingTransport is gone; httpware.Retry takes over. These are the user-observable differences in CI logs:

Knob 0.1.x 0.2.0
Retryable statuses 408, 429, 500, 502, 503, 504 same
Methods retried all (transport-level) idempotent only — POST not retried
Backoff base 1.0 s, full-jitter 0.1 s, full-jitter
Max sleep per attempt implicit via 30 s wall cap 5 s
Total-attempt wall cap 30 s per request none; httpware.RetryBudget (10 deposits + 20% retry ratio) caps storms across requests instead
Retry-After combine max(server_hint, local_backoff) server hint honored verbatim, capped at max sleep

Net effect for a typical CI run: faster transient-failure recovery (backoff starts 10× shorter), create_tag (POST) now fails immediately on a 429 instead of retrying — operator must rerun the job. All other surfaces unchanged.

Error message wording

Provider error messages produced by decoder failures (malformed JSON, schema mismatch) now read "GitLab _ProjectResponse response could not be decoded: <pydantic error>" instead of the 0.1.x "shape invalid: ..." / "malformed JSON in response body" wording. Only matters if you grep stderr for specific substrings. Exception types (ProviderAPIError) and exit codes (4) are unchanged.

Migration

For most users, the migration is one line in CI config:

-  python-version: "3.10"
+  python-version: "3.11"

If you grep stderr or test output for the old retry/decode error wording, update the strings. If you depend on create_tag POSTs being retried automatically on 429, add an outer retry loop in your CI script (e.g., gh workflow rerun on failure).

The semvertag CLI surface, exit codes, environment variables (SEMVERTAG_*), and the templates/semvertag.yml GitLab CI Catalog descriptor are all unchanged.

See also

Two design+implementation cycles fed this release:

0.1.1

31 May 21:30
77e82f4

Choose a tag to compare

Full Changelog: 0.1.0...0.1.1

0.1.0

31 May 21:03
833e366

Choose a tag to compare