Skip to content

Commit e306806

Browse files
lesnik512claude
andauthored
ci: tag-driven release (PyPI + GitHub Release + v0 in one workflow) (#35)
* planning: spec + plan for tag-driven release (#bundle) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: replace publish.yml + tag-major.yml with tag-driven release.yml Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: run the semvertag dogfood in dry-run (stop auto-tagging) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: move the release runbook into CLAUDE.md (maintainer-only) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: normalize index.md trailing newline (eof-fixer) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: note post-PyPI partial-failure recovery in release runbook Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * planning: mark tag-driven-release shipped (#35) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 81e6afd commit e306806

10 files changed

Lines changed: 700 additions & 204 deletions

File tree

‎.github/workflows/publish.yml‎

Lines changed: 0 additions & 17 deletions
This file was deleted.

‎.github/workflows/release.yml‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: Release
2+
3+
# Tag-driven: pushing a bare semver tag publishes to PyPI, creates the matching
4+
# GitHub Release, and floats the `v0` action tag. Replaces the old
5+
# `release: published` publish.yml (deleted) and folds in tag-major.yml (deleted).
6+
#
7+
# The tag is the sole, deliberate entry point. semvertag.yml dogfoods in
8+
# DRY-RUN, so it never pushes a tag — only a maintainer's manual `git push` of
9+
# a tag reaches here (GitHub suppresses workflow triggers from
10+
# GITHUB_TOKEN-pushed refs, so even an auto-push would not fire this). By
11+
# convention a tag is cut only off green main, so there is no in-workflow CI gate.
12+
on:
13+
push:
14+
tags:
15+
- '[0-9]+.[0-9]+.[0-9]+' # stable: 0.9.0
16+
- '[0-9]+.[0-9]+.[0-9]+[a-z]+[0-9]+' # pre-release: 0.9.0rc1, 1.0.0a2
17+
18+
# Needed for softprops/action-gh-release to create the Release and for the
19+
# v0 force-push.
20+
permissions:
21+
contents: write
22+
23+
jobs:
24+
release:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@v6
28+
- uses: extractions/setup-just@v4
29+
- uses: astral-sh/setup-uv@v7
30+
31+
# PyPI is irreversible, so it runs FIRST: if it fails the job stops and no
32+
# GitHub Release or v0 move advertises a version that never reached PyPI.
33+
# `just publish` derives the version from $GITHUB_REF_NAME (the tag name).
34+
- run: just publish
35+
env:
36+
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
37+
38+
# Description source: planning/releases/<tag>.md if present (verbatim, no
39+
# auto-changelog appended); otherwise GitHub's generated notes. A tag with
40+
# a letter (0.9.0rc1) is a pre-release -> flagged so GitHub won't mark it
41+
# "Latest" and so the v0 float below is skipped.
42+
- name: Resolve release metadata
43+
id: meta
44+
run: |
45+
set -euo pipefail
46+
notes="planning/releases/${GITHUB_REF_NAME}.md"
47+
if [ -f "$notes" ]; then
48+
echo "body_path=$notes" >> "$GITHUB_OUTPUT"
49+
echo "generate_notes=false" >> "$GITHUB_OUTPUT"
50+
else
51+
echo "generate_notes=true" >> "$GITHUB_OUTPUT"
52+
fi
53+
if [[ "$GITHUB_REF_NAME" =~ [a-z] ]]; then
54+
echo "prerelease=true" >> "$GITHUB_OUTPUT"
55+
else
56+
echo "prerelease=false" >> "$GITHUB_OUTPUT"
57+
fi
58+
59+
- name: Publish GitHub Release
60+
uses: softprops/action-gh-release@v3
61+
with:
62+
body_path: ${{ steps.meta.outputs.body_path }}
63+
generate_release_notes: ${{ steps.meta.outputs.generate_notes }}
64+
prerelease: ${{ steps.meta.outputs.prerelease }}
65+
draft: false
66+
67+
# Floating major tag (folded-in tag-major.yml): consumers pin
68+
# `uses: modern-python/semvertag@v0` and ride minor bumps. Skipped on
69+
# pre-releases so a 0.9.0rc1 doesn't drag v0 ahead of the latest stable.
70+
# References HEAD (the tag commit), so no fetch-depth: 0 is needed.
71+
- name: Float major tag
72+
if: steps.meta.outputs.prerelease == 'false'
73+
run: |
74+
set -euo pipefail
75+
major="v${GITHUB_REF_NAME%%.*}" # 0.9.0 -> v0
76+
git config user.name 'github-actions[bot]'
77+
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
78+
git tag -fa "$major" -m "Update $major to $GITHUB_REF_NAME"
79+
git push -f origin "$major"

‎.github/workflows/semvertag.yml‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
name: semvertag
22

3-
# Dogfood the local composite action against this repo. Auto-tags on
4-
# push to main when the latest commit is a merge from `feat/...` (minor
5-
# bump) or `bugfix/`/`hotfix/...` (patch). This repo's branch
6-
# convention uses `feat/...`, so SEMVERTAG_BRANCH_PREFIX__MINOR
7-
# overrides the default `feature/` mapping.
3+
# Dogfood the local composite action against this repo in DRY-RUN: on every
4+
# push to main it computes the planned bump (exercising action.yml + the
5+
# published semvertag) but never pushes a tag. This keeps action.yml honest —
6+
# a breaking change fails the run before it can affect external users.
87
#
9-
# The workflow only creates a tag — it does NOT trigger publish.yml,
10-
# which fires on GitHub release creation. To publish to PyPI, create a
11-
# GitHub release pointed at the auto-tagged commit.
8+
# Releases are NOT cut here. A release is a maintainer pushing a bare semver
9+
# tag by hand, which triggers .github/workflows/release.yml (PyPI + GitHub
10+
# Release + v0). Dry-run is load-bearing: it guarantees the only tags in the
11+
# repo are deliberate release tags — do not give this job a push token or
12+
# remove `dry-run: true`.
1213
#
13-
# `uses: ./` exercises the action.yml in the current checkout, so any
14-
# breaking change to action.yml fails the dogfood run before it can
15-
# affect external users.
14+
# This repo's branch convention uses `feat/...`, so SEMVERTAG_BRANCH_PREFIX__MINOR
15+
# overrides the default `feature/` mapping.
1616

1717
on:
1818
push:
1919
branches: [main]
2020

2121
permissions:
22-
contents: write
22+
contents: read
2323

2424
concurrency:
2525
group: semvertag
@@ -33,5 +33,7 @@ jobs:
3333
with:
3434
fetch-depth: 0
3535
- uses: ./
36+
with:
37+
dry-run: true
3638
env:
3739
SEMVERTAG_BRANCH_PREFIX__MINOR: '["feat/"]'

‎.github/workflows/tag-major.yml‎

Lines changed: 0 additions & 43 deletions
This file was deleted.

‎CLAUDE.md‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ excluded from the mkdocs site automatically). When superpowers skills default to
4545
`docs/superpowers/specs/` or `docs/superpowers/plans/`, use the change bundle
4646
under `planning/changes/` here instead.
4747

48+
**Cutting a release (maintainers)** is tag-driven via
49+
[`.github/workflows/release.yml`](.github/workflows/release.yml): write the
50+
notes at `planning/releases/<version>.md` (used verbatim as the GitHub Release
51+
body), then push a bare semver tag off green `main` —
52+
`git tag 0.9.0 && git push origin 0.9.0`. The workflow runs `just publish` (the
53+
tag sets the version via `uv version $GITHUB_REF_NAME`; no `pyproject.toml`
54+
bump) to PyPI, then creates the GitHub Release, then floats the `v0` action tag
55+
— PyPI first, so a failed publish creates no Release. Pre-releases use the
56+
PEP 440 form (`0.9.0rc1`, not `0.9.0-rc1`). PyPI is irreversible; there is no
57+
CI gate (a tag is the commitment point). The dogfood (`semvertag.yml`) runs in
58+
dry-run and never auto-tags, so the tag you push is the only tag. If `just
59+
publish` succeeds but a later step fails (Release or `v0`), do **not** re-push
60+
the tag — PyPI rejects re-uploading an existing version. Create the GitHub
61+
Release and move `v0` by hand, or cut a new patch tag.
62+
4863
## Commit messages
4964

5065
Imperative present-tense, scoped where helpful:
@@ -95,8 +110,9 @@ Two distinct tag conventions coexist — confusing them is easy:
95110
strategy emits bare-semver tags by default; release URLs are
96111
`releases/tag/0.4.0`. When touching the CLI / `Justfile` / publish flow, think
97112
bare semver — `$GITHUB_REF_NAME` is `0.4.0`, not `v0.4.0`.
98-
- **Action floating tag: `v`-prefixed** (`v0`). `.github/workflows/tag-major.yml`
99-
strips any leading `v` from the release tag then prepends `v` to the major
100-
segment (`0.4.0` → `v0`), so consumers can pin `uses: modern-python/semvertag@v0`
101-
per the GHA ecosystem convention. When touching `tag-major.yml` or
113+
- **Action floating tag: `v`-prefixed** (`v0`). The `Float major tag` step in
114+
[`.github/workflows/release.yml`](.github/workflows/release.yml) prepends `v`
115+
to the release tag's major segment (`0.4.0` → `v0`) and force-updates the
116+
floating tag, so consumers can pin `uses: modern-python/semvertag@v0` per the
117+
GHA ecosystem convention. Skipped on pre-releases. When touching that step or
102118
action-consumer docs, think `v`-prefix.

‎docs/contributing/release.md‎

Lines changed: 0 additions & 121 deletions
This file was deleted.

‎docs/index.md‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,3 @@ semvertag ships with two bump-decision strategies:
5151

5252
Both strategies are configurable via environment variables — see the
5353
strategy pages for the full configuration surface.
54-
55-
## Contributing
56-
57-
- [Release runbook](contributing/release.md) — for maintainers cutting
58-
a new release of semvertag itself.

‎mkdocs.yml‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ nav:
1111
- Strategies:
1212
- Branch prefix: strategies/branch-prefix.md
1313
- Conventional commits: strategies/conventional-commits.md
14-
- Contributing:
15-
- Release runbook: contributing/release.md
1614

1715
theme:
1816
name: material

0 commit comments

Comments
 (0)