-
Notifications
You must be signed in to change notification settings - Fork 0
234 lines (220 loc) · 10 KB
/
Copy pathrelease.yml
File metadata and controls
234 lines (220 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
name: Release
# Cut a release one of two ways, both landing on the same bottle pipeline below:
# 1. Push a vX.Y.Z tag (what `scripts/cut_release.sh` does from a clean `main`).
# 2. Run this workflow manually ("Run workflow" / the actions_run_trigger MCP
# tool) — the `tag` job resolves the version, creates the tag, and pushes it,
# so a Claude web session (which works on a feature branch and can't push
# tags itself) can cut a release without a local checkout.
# Either way the pipeline builds the arm64 macOS bottle, publishes it to the tag's
# GitHub Release, and opens a formula PR (url + sha256 + bottle block) to merge.
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
version:
description: "Release version X.Y.Z (blank = next patch above the latest vX.Y.Z tag)"
required: false
default: ""
dry_run:
description: "Build the bottle only — don't create the tag, GitHub Release, or formula PR"
type: boolean
default: false
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.inputs.version || github.ref }}
cancel-in-progress: false
jobs:
# Resolve the tag the rest of the pipeline builds. On a tag push it already
# exists (just echo it). On a manual real release it's created here from the
# same logic maintainers run locally; on a manual dry run we build an existing
# tag without creating anything. Always runs so `bottle` has a single source
# for the tag regardless of trigger.
tag:
name: resolve release tag
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write # only the manual real-release path pushes a tag; unused otherwise
outputs:
tag: ${{ steps.resolve.outputs.tag }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Release from main; full history brings the vX.Y.Z tags cut_release.sh
# bumps from. persist-credentials off (the real-release push below uses
# an explicit tokened remote, matching the publish job).
ref: main
fetch-depth: 0
persist-credentials: false
- name: Resolve the release tag
id: resolve
env:
EVENT_NAME: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
INPUT_VERSION: ${{ github.event.inputs.version }}
DRY_RUN: ${{ github.event.inputs.dry_run }}
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" = "push" ]; then
tag="$REF_NAME"
elif [ "$DRY_RUN" = "true" ]; then
# Build a bottle for an already-existing tag without publishing.
if [ -n "$INPUT_VERSION" ]; then
tag="v${INPUT_VERSION}"
else
tag="$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n1)"
fi
[ -n "$tag" ] || { echo "no existing vX.Y.Z tag to dry-run" >&2; exit 1; }
else
# Real manual release: resolve + validate + create the tag locally via
# the same script maintainers run, then push it with an explicit
# tokened remote (persist-credentials is off above).
# cut_release.sh makes an annotated tag (git tag -a), which needs a
# committer identity; the runner has none, so set one (matching the
# publish job) or the tag step fails with "empty ident name".
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
extra=()
[ -n "$INPUT_VERSION" ] && extra+=("$INPUT_VERSION")
./scripts/cut_release.sh --yes --no-push "${extra[@]}"
tag="$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n1)"
git push "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "refs/tags/${tag}"
fi
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
bottle:
name: build arm64 bottle (macOS)
needs: [tag]
runs-on: macos-14
timeout-minutes: 40
permissions:
contents: read
outputs:
tag: ${{ needs.tag.outputs.tag }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false # this job doesn't push
# Homebrew/actions is a monorepo (setup-homebrew is a subpath); pin it to a
# commit SHA like every other action here — Dependabot keeps it current.
- uses: Homebrew/actions/setup-homebrew@2ebcf16054461267868620b1414507f3ccc765c1
- name: Resolve source sha256
id: meta
env:
# Pass via env (not inline ${{ }}) to satisfy zizmor template-injection.
TAG: ${{ needs.tag.outputs.tag }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
tag="$TAG"
url="https://github.com/${REPO}/archive/refs/tags/${tag}.tar.gz"
curl -fL "$url" -o source.tar.gz
sha="$(shasum -a 256 source.tar.gz | awk '{print $1}')"
{
echo "source_sha=${sha}"
echo "root_url=https://github.com/${REPO}/releases/download/${tag}"
} >> "$GITHUB_OUTPUT"
- name: Pin the formula to the release tag
env:
TAG: ${{ needs.tag.outputs.tag }}
SOURCE_SHA: ${{ steps.meta.outputs.source_sha }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
python3 - <<'PY'
import os, re, pathlib
tag, sha, repo = os.environ["TAG"], os.environ["SOURCE_SHA"], os.environ["REPO"]
url = f"https://github.com/{repo}/archive/refs/tags/{tag}.tar.gz"
p = pathlib.Path("Formula/assembly.rb")
src = p.read_text()
src = re.sub(r'url ".*?"', f'url "{url}"', src, count=1)
src = re.sub(r"sha256 .*", f'sha256 "{sha}"', src, count=1)
p.write_text(src)
PY
grep -nE '^ (url|sha256) ' Formula/assembly.rb | head -2
- name: Build the bottle + merge the block into the formula
env:
ROOT_URL: ${{ steps.meta.outputs.root_url }}
run: |
set -euo pipefail
brew tap-new --no-git assembly/local
# Homebrew >=5.1.15 ignores formulae from untrusted non-official taps,
# which includes this just-created local tap; trust it or the install
# below fails ("tap trust is required").
brew trust assembly/local
tap_formula="$(brew --repository assembly/local)/Formula/assembly.rb"
cp Formula/assembly.rb "$tap_formula"
brew install --build-bottle --formula assembly/local/assembly
brew bottle --json --no-rebuild --root-url="$ROOT_URL" assembly/local/assembly
for f in assembly--*.bottle.tar.gz; do mv "$f" "${f/--/-}"; done
shopt -s nullglob
tarballs=( assembly-*.bottle.tar.gz )
shopt -u nullglob
if [[ ${#tarballs[@]} -ne 1 ]]; then
echo "Expected exactly one bottle tarball, found ${#tarballs[@]}: ${tarballs[*]:-none}" >&2
exit 1
fi
brew bottle --merge --write --no-commit assembly--*.bottle.json
cp "$tap_formula" Formula/assembly.rb
echo "--- finalized formula head ---"
sed -n '1,20p' Formula/assembly.rb
- name: Upload bottle + finalized formula
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-artifacts
path: |
assembly-*.bottle.tar.gz
Formula/assembly.rb
if-no-files-found: error
publish:
name: publish release + open formula PR
needs: [bottle]
# Publish for real tag pushes and for manual real releases; skip on a manual
# dry run (which only builds the bottle to prove the formula installs).
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.dry_run != 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false # push via explicit tokened remote instead
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: release-artifacts
path: artifacts
- name: Create the GitHub Release with the bottle attached
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.bottle.outputs.tag }}
run: |
set -euo pipefail
if ! gh release view "$TAG" >/dev/null 2>&1; then
gh release create "$TAG" --title "$TAG" --generate-notes
fi
gh release upload "$TAG" artifacts/assembly-*.bottle.tar.gz --clobber
- name: Open the formula PR
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.bottle.outputs.tag }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
branch="release/${TAG}-formula"
cp artifacts/Formula/assembly.rb Formula/assembly.rb
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -B "$branch"
git add Formula/assembly.rb
git commit -m "Bottle ${TAG}: pin url + sha256, add arm64_sonoma bottle"
git push --force "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$branch"
if ! gh pr view "$branch" >/dev/null 2>&1; then
gh pr create --base main --head "$branch" \
--title "Bottle ${TAG}" \
--body "Automated by release.yml: pins the formula to the ${TAG} source tarball and adds the arm64_sonoma bottle block. Merge with the admin override (a GITHUB_TOKEN PR does not trigger CI, so the required check will not report). The diff is formula-only."
fi