Skip to content

Commit dfd17e3

Browse files
johnscaclaude
andcommitted
summarise the workflow comments, dropping API and error-code detail
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 4cf3bea commit dfd17e3

2 files changed

Lines changed: 53 additions & 96 deletions

File tree

‎.github/workflows/cut-release.yml‎

Lines changed: 49 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,17 @@ jobs:
3737
env:
3838
HARDEN_RUNNER: ${{ secrets.STEP_SECURITY_API_KEY != '' }} # zizmor: ignore[secrets-outside-env]
3939
steps:
40-
# First, ahead of the guards below, because egress policy only covers
41-
# what runs after it. This job fetches and runs a third-party action.
42-
# The publish job holds the write token but runs nothing but `gh`.
40+
# First, because egress policy only covers what runs after it. This is
41+
# the job that runs a third-party action. The publish job runs only `gh`.
4342
- name: Harden Runner
4443
if: env.HARDEN_RUNNER == 'true'
4544
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
4645
with:
4746
use-policy-store: true
4847
api-key: ${{ secrets.STEP_SECURITY_API_KEY }} # zizmor: ignore[secrets-outside-env]
4948

50-
# The tag action reads an unrecognised `force_bump` as no bump at all and
51-
# falls back to the commit messages, so without this a typo publishes a
52-
# version nobody asked for.
49+
# An unrecognised bump does not fail the tag action. It falls back to
50+
# reading the commit messages and publishes a version nobody asked for.
5351
- name: Check the requested bump
5452
if: ${{ !contains(fromJSON('["patch", "minor", "major"]'), inputs.bump) }}
5553
env:
@@ -59,11 +57,10 @@ jobs:
5957
echo "'${BUMP}'." >&2
6058
exit 1
6159
62-
# workflow_dispatch takes a ref, so the caller can start against any
63-
# branch. A release cut from a topic branch names code that never merged,
64-
# and immutability makes that permanent. The branch comes from the
65-
# calling repository, so this holds for a caller whose default branch has
66-
# another name.
60+
# A dispatch can target any branch, and a release cut from a topic branch
61+
# names code that never merged. Immutability makes that permanent.
62+
# Reading the branch from the calling repository keeps this right for a
63+
# caller that does not use `main`.
6764
- name: Refuse to release from anywhere but the default branch
6865
if: ${{ github.ref != format('refs/heads/{0}', github.event.repository.default_branch) }}
6966
env:
@@ -75,27 +72,23 @@ jobs:
7572
- name: Checkout
7673
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
7774
with:
78-
# The next version comes from the tag history, and the action walks
79-
# the local checkout when the compare API is unavailable.
75+
# The version comes from the tag history, so all of it has to be here.
8076
fetch-depth: 0
8177
persist-credentials: false
8278

83-
# Works out the version and stops. `push: false` leaves every ref write to
84-
# the publish job, so one step owns the tag. Splitting them lets a failure
85-
# halfway through leave a bare tag with no release, which is mutable and
86-
# in the way of the retry.
79+
# Works out the version and stops. Every ref write belongs to the publish
80+
# job, so one place owns the tag.
8781
- name: Work out the next version
8882
id: version
8983
uses: mathieudutour/github-tag-action@af99e60ce8132224b8e6ebab5023449fe256ed46 # v7
9084
with:
9185
github_token: ${{ github.token }}
9286
force_bump: ${{ inputs.bump }}
9387
push: false
94-
# `release_branches` defaults to `^master$,^main$`, and a branch
95-
# outside that list gets a version postfixed with the commit hash
96-
# and no tag at all. The guard above already confines this job to
97-
# the caller's default branch, so widening it here is what makes
98-
# that guard's generality real.
88+
# The action tags only its own idea of a release branch, which does
89+
# not cover every default branch. The guard above already confines
90+
# this to the caller's default branch, so widening it here is what
91+
# makes that guard general.
9992
release_branches: '.*'
10093

10194
- name: Report the version
@@ -112,9 +105,9 @@ jobs:
112105
echo "| Dry run | ${DRY_RUN} |"
113106
} >> "${GITHUB_STEP_SUMMARY}"
114107
115-
# The tag action reports v0.0.0 as the previous tag when no tag exists, so
116-
# this fires only on the first release. A patch bump from that baseline
117-
# lands v0.0.1 permanently, so the input description is not enough.
108+
# Only fires on the first release, where the baseline is 0.0.0. A patch
109+
# from there lands v0.0.1 permanently, so this is enforced rather than
110+
# left to the input description.
118111
- name: Require the first release to be major
119112
if: ${{ steps.version.outputs.previous_tag == 'v0.0.0' && inputs.bump != 'major' }}
120113
env:
@@ -126,27 +119,20 @@ jobs:
126119
127120
publish:
128121
needs: version
129-
# `success()` is implicit on a job `if:` that names no status function, and
130-
# it keeps the version job's three guards binding. Spelling it out matters
131-
# because adding `always()` or a `failure()` branch later detaches them
132-
# silently, and that fails open: `new_tag` is already set by the time the
133-
# first-release guard runs.
122+
# `success()` is implicit here, and it is what keeps the version job's
123+
# guards binding. Spelled out because adding `always()` or a `failure()`
124+
# branch later would detach them silently, and that fails open.
134125
#
135-
# `inputs.dry_run`, not `github.event.inputs.dry_run`. The former keeps a
136-
# boolean a boolean. The latter yields the string "false", which is truthy,
137-
# so the negation is always false and this job never runs.
126+
# `inputs.dry_run`, not `github.event.inputs.dry_run`. The latter is a
127+
# string, so the negation would never hold and this job would never run.
138128
if: ${{ success() && !inputs.dry_run }}
139129
runs-on: ubuntu-latest
140130
permissions:
141131
contents: write
142132
steps:
143-
# Re-running stays on offer for 30 days, and the two modes go wrong in
144-
# opposite directions. "Re-run all jobs" re-runs the version job, and
145-
# because `force_bump` overrides the no-new-commits early return it works
146-
# out the version after this one and publishes that too. "Re-run failed
147-
# jobs" replays the succeeded version job's outputs and republishes the
148-
# same tag. run_attempt catches both. Retry by dispatching again, which
149-
# reads the same latest tag and so arrives at the same number.
133+
# Both re-run modes go wrong. One recomputes the version and publishes
134+
# the one after this. The other replays the old outputs and republishes
135+
# the same tag. Dispatch again instead, which arrives at the same number.
150136
- name: Refuse a re-run
151137
if: ${{ github.run_attempt != 1 }}
152138
run: |
@@ -155,17 +141,12 @@ jobs:
155141
exit 1
156142
157143
# No checkout: the release is cut against a commit SHA and carries no
158-
# assets. That also keeps this job, the one holding `contents: write`,
159-
# free of third-party actions, which is why it has no Harden Runner step.
160-
#
161-
# The checkout was doing a second job though, which is why --repo appears
162-
# below. `gh` resolves the repository from --repo, then GH_REPO, then the
163-
# git remotes of the working directory. It never reads GITHUB_REPOSITORY,
164-
# so without one of those it exits 1 with "not a git repository". `gh api`
165-
# is unaffected because its path is literal.
144+
# assets. That keeps the job holding `contents: write` free of
145+
# third-party actions, which is why it has no Harden Runner step. Without
146+
# a checkout `gh` cannot infer the repository, hence `--repo` below.
166147
#
167148
# zizmor rejects a release action here as work the runner already does,
168-
# which is why this is the CLI rather than ncipollo/release-action.
149+
# hence the CLI.
169150
- name: Publish the release
170151
env:
171152
GH_TOKEN: ${{ github.token }}
@@ -179,53 +160,32 @@ jobs:
179160
echo "That should not happen on the default branch with an explicit bump." >&2
180161
exit 1
181162
fi
182-
# This check is for the clean error, not for correctness. The ref
183-
# write below is what actually claims the name. Without the check a
184-
# collision surfaces as a raw 422 from the API.
185-
#
186-
# `git/matching-refs` answers "no such tag" with 200 and an empty
187-
# array, so a non-zero exit means the check itself failed rather than
188-
# the name being free. `git/ref/tags/...` answers 404, which shares an
189-
# exit status with a 401, a 5xx, or a dropped connection, and would
190-
# leave the guard passing whenever the API misbehaved. Keeping the
191-
# call out of an `if` matters for the same reason: errexit is
192-
# suppressed for a condition. matching-refs is a prefix match, hence
193-
# the exact-ref filter.
163+
# A courtesy check, not the guard. It turns a name that is already
164+
# taken into a readable error rather than a raw API failure. Claiming
165+
# the ref below is what actually settles the name. The call stays out
166+
# of an `if` so that a failure of the check itself stops the run.
194167
matches=$(gh api "repos/${GITHUB_REPOSITORY}/git/matching-refs/tags/${TAG}" \
195168
--jq '[.[] | select(.ref == "refs/tags/" + env.TAG)] | length')
196169
if [ "${matches}" != "0" ]; then
197170
echo "Tag ${TAG} already exists, so this run would publish a release over" >&2
198171
echo "a tag it did not create. Refusing." >&2
199172
exit 1
200173
fi
201-
# Claiming the name and publishing the release are two calls, so the
202-
# order decides which way the gap between them fails. Creating the
203-
# tag first fails closed: `POST git/refs` answers an existing ref with
204-
# 422, so a tag that lands after the check above costs this run and
205-
# nothing else.
206-
#
207-
# Letting `gh release create` make the tag instead fails open. It
208-
# answers a name that is already taken by releasing the tag that is
209-
# there and ignoring --target, which publishes an immutable release
210-
# against a commit nobody chose. Immutability makes the name
211-
# unusable afterwards even if the release is deleted.
212-
# Between the ref write and the release there is a tag with nothing
213-
# locking it, and the version job reads the highest tag, so a tag
214-
# left behind makes the next dispatch work out the version after
215-
# this one and skip this number silently.
174+
# Claim the tag ref, then lock it in by publishing the release
175+
# against it. The other order fails open: handed a name already
176+
# taken, the release call publishes against whatever commit that tag
177+
# points at, and immutability makes that permanent.
216178
#
217-
# The cleanup is armed before the write rather than after it. Arming
218-
# it after leaves a window, short but real, where the tag exists and
219-
# nothing would take it back out. That ordering is only safe because
220-
# the cleanup checks that the tag is this run's before removing it,
221-
# so the 422 path below, where the tag belongs to someone else, does
222-
# not delete their tag. Every check that fails to confirm leaves the
223-
# tag alone, including an API error, since a tag left behind costs a
224-
# version number and a wrong delete costs a release.
179+
# Claiming first leaves the tag briefly unlocked, and a tag left
180+
# behind makes the next dispatch skip this version number. So the
181+
# cleanup is armed before the claim, leaving no window where the tag
182+
# is unowned, and it is conditional on the tag being this run's so
183+
# that it never deletes one the claim lost to. Anything it cannot
184+
# confirm it leaves alone: a stray tag costs a number, a wrong delete
185+
# costs a release.
225186
#
226-
# bash runs an EXIT trap when a signal terminates it, so this covers
227-
# a cancelled job as well as a failed command. SIGKILL defeats it,
228-
# and nothing here can change that.
187+
# An EXIT trap covers a cancelled job as well as a failed command,
188+
# though not a SIGKILL.
229189
cleanup() {
230190
local ref_sha
231191
ref_sha=$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG}" --jq .object.sha 2>/dev/null) || return 0
@@ -241,9 +201,8 @@ jobs:
241201
gh api "repos/${GITHUB_REPOSITORY}/git/refs" \
242202
-f "ref=refs/tags/${TAG}" \
243203
-f "sha=${COMMIT}" > /dev/null
244-
# --target is redundant now that the tag exists at the right commit,
245-
# and `gh` ignores it for an existing tag. It stays as the statement
246-
# of which commit this release is for.
204+
# Redundant once the tag exists, but it states which commit this
205+
# release is for.
247206
gh release create "${TAG}" \
248207
--repo "${GITHUB_REPOSITORY}" \
249208
--target "${COMMIT}" \

‎.github/workflows/release.yml‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,8 @@ on:
1818
type: boolean
1919
default: false
2020

21-
# Two runs would otherwise read the same latest tag and claim the same version.
22-
# The second fails at the release step rather than corrupting anything, but
23-
# serialising stops it happening at all. Only one run queues behind the active
24-
# one, so a third dispatch drops the second.
21+
# Two runs would otherwise claim the same version. The second fails safely at
22+
# the release step, but serialising stops it happening at all.
2523
#
2624
# This belongs to the dispatched workflow rather than the called one, so every
2725
# caller carries it. Copy this file rather than writing one.
@@ -33,8 +31,8 @@ jobs:
3331
release:
3432
uses: ./.github/workflows/cut-release.yml
3533
# The ceiling for the called workflow, not what each job gets. It narrows
36-
# this to read for the job running the third-party version action, and only
37-
# its publish job keeps write.
34+
# this to read for the job running the third-party action, and only its
35+
# publish job keeps write.
3836
permissions:
3937
contents: write
4038
secrets:

0 commit comments

Comments
 (0)