Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 40 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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).
Expand Down
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down