@@ -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}" \
0 commit comments