diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b8dd46e..ac6973d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,6 +40,11 @@ concurrency: permissions: contents: write + # Reading the merge's labels needs this. The block sets every scope it does + # not name to `none`, so with `contents` alone the label lookup could only + # ever be refused — and a refusal used to look exactly like an unlabelled + # merge. + pull-requests: read jobs: # Works out this merge's version, writes it to Version.swift, and creates the @@ -83,18 +88,44 @@ jobs: echo "tag=v$current" >> "$GITHUB_OUTPUT" echo "sha=$GITHUB_SHA" >> "$GITHUB_OUTPUT" echo "Version.swift already says $current — releasing that." \ - >> "$GITHUB_STEP_SUMMARY" + | tee -a "$GITHUB_STEP_SUMMARY" exit 0 fi - # Labels of the pull request this commit came from. A direct push to - # main belongs to no pull request, which is simply no labels. - labels="$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls" \ - --jq '.[].labels[].name' | sort -u || true)" + # Labels of the pull request this merge came from. + # + # Read from the number in the commit subject rather than by asking + # GitHub which pull request a commit belongs to. That association is + # not reliably there at the moment this runs: the same query answered + # correctly for two merges and returned nothing for the next two, all + # four carrying the label, and every one of those shipped as a patch + # because an empty answer and "no labels" are the same value. A squash + # merge writes the number into its own subject, so this reads what the + # merge itself says. + # + # A direct push to main has no number in its subject, which is simply + # no labels — that path still gets the patch default, on purpose. + labels="" + number="$(git log -1 --pretty=%s \ + | sed -n 's/.*(#\([0-9][0-9]*\))$/\1/p')" + if [ -n "$number" ]; then + # No `|| true`. The previous version swallowed every failure into an + # empty label list, so a broken lookup was indistinguishable from an + # unlabelled merge and silently shipped the wrong version. A lookup + # that cannot answer should stop the release, not guess at it. + # A number in the subject that is not a pull request stops the + # release. That is the intended trade: a blocked release is a + # message, and a release that quietly picks the wrong version is + # not. + labels="$(gh pr view "$number" --json labels --jq '.labels[].name' \ + | sort -u)" + fi + echo "Pull request: ${number:-none}" + echo "Labels: ${labels:-none}" if grep -qx 'release:skip' <<<"$labels"; then echo "Labelled release:skip — publishing nothing." \ - >> "$GITHUB_STEP_SUMMARY" + | tee -a "$GITHUB_STEP_SUMMARY" exit 0 fi @@ -105,13 +136,14 @@ jobs: if [ -n "$changed" ] \ && ! grep -qvE '^(docs/|\.github/|[^/]*\.md$)' <<<"$changed"; then echo "Documentation only — publishing nothing." \ - >> "$GITHUB_STEP_SUMMARY" + | tee -a "$GITHUB_STEP_SUMMARY" exit 0 fi level="patch" if grep -qx 'release:minor' <<<"$labels"; then level="minor"; fi if grep -qx 'release:major' <<<"$labels"; then level="major"; fi + echo "Bump: $level" IFS=. read -r major minor patch <<<"$current" case "$level" in @@ -146,7 +178,7 @@ jobs: echo "tag=v$next" >> "$GITHUB_OUTPUT" echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" echo "Releasing v$next ($level bump from $current)" \ - >> "$GITHUB_STEP_SUMMARY" + | tee -a "$GITHUB_STEP_SUMMARY" # What the notes say about the first launch depends on whether the build # is notarized, and NOTARY_PROFILE is what decides that (see package.yml). diff --git a/AGENTS.md b/AGENTS.md index cee8ffc..954efa1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -253,6 +253,13 @@ are no component-level AGENTS.md files. pull request says otherwise, so the size of a release is decided in review rather than remembered at merge time. Labels rather than Conventional Commit prefixes, because the commit style here is prose. + **The label is read from the number in the merge commit's subject**, not by + asking GitHub which pull request a commit came from — that association is + not reliably present when this runs, and an empty answer is + indistinguishable from an unlabelled merge. The lookup has no `|| true` + either: it must fail loudly rather than fall through to the default. + `permissions:` must keep `pull-requests: read`, since the block sets every + scope it does not name to `none`. - **Two ways to publish nothing:** a `release:skip` label, or a merge that touched only `docs/`, `.github/` and top-level `*.md`. A merge touching docs *and* code still ships. @@ -403,6 +410,12 @@ are no component-level AGENTS.md files. - **GPU card is greyed out**: this macOS version does not publish the `PerformanceStatistics` keys the source knows about. Add the new spelling to the candidate list in `GPUSource`. +- **A merge shipped a patch version when it was labelled `release:minor`**: + the label lookup came back empty. Check the "Version" step's log — it now + prints the pull request number, the labels it found and the bump it chose. A + number of `none` on a squash merge means the subject did not end in `(#N)`; + empty labels on a real number means the token could not read them, so check + `pull-requests: read` is still in the workflow's `permissions:`. - **A pull request reports no CI**: it came from a fork, and the jobs skip fork pull requests on purpose. Re-run from a branch in this repository. - **A pull request reports no CI and did not come from a fork**: its commit