-
Notifications
You must be signed in to change notification settings - Fork 0
372 lines (339 loc) · 15.5 KB
/
Copy pathrelease.yml
File metadata and controls
372 lines (339 loc) · 15.5 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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
name: Release
on:
push:
tags: ["v*"]
# Read-only by default. Only `release` actually creates anything on the repo,
# and it raises this to `contents: write` for itself — so the three jobs that
# merely build (and that handle the signing secrets) carry a token that cannot
# publish a release even if something in the toolchain gets hold of it.
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
# Fail fast and loudly if the tag and the tree disagree about the version.
# Everything downstream names its artifacts after the tag, so a mismatch here
# would ship binaries whose `--version` lies.
verify:
name: verify version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.check.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Tag must match the workspace version
id: check
run: |
tag="${GITHUB_REF_NAME}"
version="${tag#v}"
echo "tag=$tag version=$version"
bash scripts/bump-version.sh --check "$version"
echo "version=$version" >> "$GITHUB_OUTPUT"
# `pb` and `patchbay-mcp`, one tarball per arch. macos-latest is arm64; the
# x86_64 target is a cross-compile from the same runner.
binaries:
name: binaries (${{ matrix.target }})
needs: verify
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
target: [aarch64-apple-darwin, x86_64-apple-darwin]
# Neutral names on purpose — see the comment on the `app` job.
env:
SIGNING_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
# Pinned by SHA, so the channel cannot be inferred from the ref the
# way `@stable` does it — see the note in ci.yml.
toolchain: stable
targets: ${{ matrix.target }}
- name: Cache cargo
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
key: release-${{ matrix.target }}
- name: Build
run: cargo build --release --locked --target ${{ matrix.target }} -p patchbay-cli -p patchbay-mcp
- name: Import signing certificate
if: env.SIGNING_CERTIFICATE != '' && env.SIGNING_CERTIFICATE_PASSWORD != ''
run: bash scripts/ci/macos-keychain.sh import
# Bare CLI binaries are not notarized — a loose executable has nothing to
# staple a ticket to, and Gatekeeper does not quarantine-check something
# you extracted from a tarball and ran from a shell. Signing them is still
# worth it: it makes `codesign -dv` show a real identity instead of
# "adhoc", and it keeps the binaries usable in contexts (TCC prompts,
# hardened parent processes) where an ad-hoc signature is treated as
# untrusted.
- name: Sign binaries
if: env.SIGNING_CERTIFICATE != '' && env.SIGNING_IDENTITY != ''
run: |
set -euo pipefail
bin="target/${{ matrix.target }}/release"
for name in pb patchbay-mcp; do
codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime "$bin/$name"
codesign --verify --strict --verbose=2 "$bin/$name"
done
- name: Package
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
target="${{ matrix.target }}"
bin="target/$target/release"
mkdir -p dist
for name in pb patchbay-mcp; do
staging="dist/$name"
mkdir -p "$staging"
cp "$bin/$name" "$staging/"
cp README.md LICENSE "$staging/"
tar -czf "dist/$name-$tag-$target.tar.gz" -C "$staging" "$name" README.md LICENSE
rm -rf "$staging"
done
# Generated from inside dist/ so the file names in the checksum file
# are bare, and `shasum -a 256 -c` works next to the downloads.
(cd dist && shasum -a 256 ./*.tar.gz > "SHA256SUMS-$target.txt")
ls -l dist
- uses: actions/upload-artifact@v4
with:
name: binaries-${{ matrix.target }}
path: dist/*
if-no-files-found: error
- name: Remove signing keychain
if: always()
run: bash scripts/ci/macos-keychain.sh cleanup
# The desktop panel, as a signed + notarized universal (arm64 + x86_64) .dmg.
#
# The secrets are bound to neutral SIGNING_* / NOTARY_* names rather than
# straight to APPLE_*. This is not cosmetic: a fork (or this repo before the
# secrets existed) resolves every `secrets.*` to the empty string, and an
# APPLE_SIGNING_IDENTITY of "" is not the same as an absent one — tauri-bundler
# reads it as Some("") and tries to sign with an identity named empty string,
# which fails the build. Absent means "build unsigned". So the APPLE_* names
# are only ever created, further down and via $GITHUB_ENV, once a step has
# confirmed the values are non-empty. That keeps the unsigned fork path green.
app:
name: panel dmg
needs: verify
runs-on: macos-latest
env:
SIGNING_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
SIGNING_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
NOTARY_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
NOTARY_API_KEY_ID: ${{ secrets.APPLE_API_KEY }}
NOTARY_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }}
steps:
- uses: actions/checkout@v4
- name: Install bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: latest
- name: Install Rust
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
toolchain: stable
targets: aarch64-apple-darwin,x86_64-apple-darwin
- name: Cache cargo (src-tauri)
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
workspaces: app/src-tauri
key: release-src-tauri
# No lifecycle scripts — see the note on the same step in ci.yml. It
# matters more here: this job holds the signing secrets.
- name: Install front-end deps
run: bun install --frozen-lockfile --ignore-scripts
working-directory: app
- name: Import signing certificate
if: env.SIGNING_CERTIFICATE != '' && env.SIGNING_CERTIFICATE_PASSWORD != ''
run: bash scripts/ci/macos-keychain.sh import
# Only now, with the identity proven present in the keychain, do the
# APPLE_* names come into existence. tauri-bundler picks them up to sign
# the .app and to notarize it with the App Store Connect API key.
- name: Export signing environment
if: env.SIGNING_CERTIFICATE != '' && env.SIGNING_IDENTITY != ''
run: |
set -euo pipefail
echo "APPLE_SIGNING_IDENTITY=$SIGNING_IDENTITY" >> "$GITHUB_ENV"
echo "APPLE_TEAM_ID=$SIGNING_TEAM_ID" >> "$GITHUB_ENV"
- name: Prepare notarization key
if: env.NOTARY_API_KEY_ID != '' && env.NOTARY_API_ISSUER != '' && env.NOTARY_API_KEY_CONTENT != ''
run: |
set -euo pipefail
key_dir="$RUNNER_TEMP/private_keys"
mkdir -p "$key_dir"
key_path="$key_dir/AuthKey_${NOTARY_API_KEY_ID}.p8"
printf '%s' "$NOTARY_API_KEY_CONTENT" > "$key_path"
chmod 600 "$key_path"
echo "APPLE_API_ISSUER=$NOTARY_API_ISSUER" >> "$GITHUB_ENV"
echo "APPLE_API_KEY=$NOTARY_API_KEY_ID" >> "$GITHUB_ENV"
echo "APPLE_API_KEY_PATH=$key_path" >> "$GITHUB_ENV"
- name: Build bundle
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# APPLE_SIGNING_IDENTITY / APPLE_TEAM_ID / APPLE_API_* arrive from
# $GITHUB_ENV above, and only when the secrets were non-empty. Nothing
# sets them to "" — see the job comment.
#
# The minisign keypair behind the in-app updater. This one is NOT
# optional the way the Apple identity is: `plugins.updater.pubkey` in
# tauri.conf.json makes signing mandatory, so a checkout without the
# private key cannot build the panel at all — tauri stops with "a
# public key has been found, but no private key". That is the right
# trade (an unsigned updater artifact is worse than none), but it does
# mean a fork must either add these two secrets or drop the
# `plugins.updater` block before it can build the .app.
#
# The password is legitimately empty for a key generated without one,
# so it is passed through as-is rather than gated on being non-empty.
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
# No tagName: tauri-action then only builds. The release itself is
# assembled once, in the `release` job, so every artifact lands on one
# release instead of three racing to create it.
projectPath: app
args: --target universal-apple-darwin
- name: Collect dmg
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
mkdir -p dist
# The finished dmg is target/<triple>/release/bundle/dmg/*.dmg.
# Anchoring on that directory matters: while bundle_dmg.sh runs there
# is also a scratch read-write image at bundle/macos/rw.<pid>.*.dmg,
# and picking that one up would ship a half-written disk image.
found=0
while IFS= read -r dmg; do
cp "$dmg" "dist/patchbay-$tag-universal-apple-darwin.dmg"
found=1
done < <(find app/src-tauri/target -type f -name '*.dmg' -path '*/bundle/dmg/*')
if [ "$found" -eq 0 ]; then
echo "::error::tauri produced no .dmg under app/src-tauri/target"
find app/src-tauri/target -maxdepth 5 -type d -name bundle || true
exit 1
fi
ls -l dist
# The other half of the panel's release: what the in-app updater reads.
#
# `createUpdaterArtifacts` makes tauri tar the finished .app (after it has
# been notarized and stapled, so an updated copy verifies offline too) and
# sign the tarball with the minisign key. The feed is a `latest.json` we
# write here rather than one tauri produces, because the download URL is
# only knowable once we have decided what the asset is called.
#
# Both macOS platform keys point at the same file: the build is universal,
# so one bundle genuinely is the update for both architectures. The
# updater matches on the key, and an Intel Mac that finds no
# `darwin-x86_64` entry concludes there is no update for it.
- name: Collect updater bundle and latest.json
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
bundle="app/src-tauri/target/universal-apple-darwin/release/bundle/macos"
tarball="$bundle/patchbay.app.tar.gz"
if [ ! -f "$tarball" ] || [ ! -f "$tarball.sig" ]; then
echo "::error::no signed updater bundle at $tarball(.sig) — is TAURI_SIGNING_PRIVATE_KEY set?"
ls -l "$bundle" || true
exit 1
fi
asset="patchbay-$tag-universal-apple-darwin.app.tar.gz"
cp "$tarball" "dist/$asset"
cp "$tarball.sig" "dist/$asset.sig"
# Version without the leading v: the updater parses it as semver and
# compares it against the running app's own version.
url="https://github.com/${GITHUB_REPOSITORY}/releases/download/$tag/$asset"
jq -n \
--arg version "${{ needs.verify.outputs.version }}" \
--arg pub_date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg signature "$(cat "$tarball.sig")" \
--arg url "$url" \
'{
version: $version,
pub_date: $pub_date,
platforms: {
"darwin-aarch64": { signature: $signature, url: $url },
"darwin-x86_64": { signature: $signature, url: $url }
}
}' > dist/latest.json
cat dist/latest.json
# tauri notarizes and staples the .app, then signs the .dmg around it —
# but it does not notarize the .dmg itself. An un-notarized disk image
# still makes Gatekeeper do an online check on first open, and shows a
# scarier prompt when the machine is offline. Submitting the dmg and
# stapling the ticket onto it makes the download verify locally.
- name: Notarize and staple dmg
if: env.APPLE_API_KEY_PATH != '' && env.APPLE_SIGNING_IDENTITY != ''
run: |
set -euo pipefail
dmg="$(find dist -name '*.dmg' | head -1)"
xcrun notarytool submit "$dmg" \
--key "$APPLE_API_KEY_PATH" \
--key-id "$APPLE_API_KEY" \
--issuer "$APPLE_API_ISSUER" \
--wait
xcrun stapler staple "$dmg"
xcrun stapler validate "$dmg"
spctl --assess --type open --context context:primary-signature -vv "$dmg"
# After stapling: the ticket is written into the dmg, so a checksum taken
# before this step would not match what people download.
- name: Checksum dmg
run: |
set -euo pipefail
(cd dist && shasum -a 256 ./*.dmg > SHA256SUMS-dmg.txt)
cat dist/SHA256SUMS-dmg.txt
- uses: actions/upload-artifact@v4
with:
name: panel-dmg
path: dist/*
if-no-files-found: error
- name: Remove signing keychain
if: always()
run: bash scripts/ci/macos-keychain.sh cleanup
release:
name: publish release
needs: [verify, binaries, app]
runs-on: ubuntu-latest
# The one job that writes: it creates the GitHub release and uploads to it.
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Release notes
id: notes
run: |
set -euo pipefail
{
echo 'body<<PATCHBAY_EOF'
bash scripts/changelog-section.sh "${{ needs.verify.outputs.version }}"
echo
echo 'PATCHBAY_EOF'
} >> "$GITHUB_OUTPUT"
- name: Publish
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
name: patchbay ${{ github.ref_name }}
body: ${{ steps.notes.outputs.body }}
# Appends the commit/PR list under whatever body we supplied.
generate_release_notes: true
fail_on_unmatched_files: true
# `*.tar.gz` covers both the CLI tarballs and the panel's updater
# bundle. The `.sig` and `latest.json` beside it are what the in-app
# updater fetches — `latest.json` has to be a release asset under this
# exact name, because the endpoint in tauri.conf.json is the
# `releases/latest/download/latest.json` redirect.
files: |
artifacts/*.tar.gz
artifacts/*.tar.gz.sig
artifacts/latest.json
artifacts/*.dmg
artifacts/SHA256SUMS-*.txt