-
Notifications
You must be signed in to change notification settings - Fork 0
333 lines (303 loc) · 13.5 KB
/
Copy pathrelease.yaml
File metadata and controls
333 lines (303 loc) · 13.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
name: release
# Every push to main publishes a downloadable build to the rolling `nightly`
# release. Assets are replaced in place, so the download URLs and the updater
# endpoint are stable.
#
# Signing: each Apple / Tauri signing step is gated on its secret (these live at
# the org level). With them present the build is Developer ID signed and
# notarized; without one it degrades to an ad-hoc signed build rather than
# failing.
on:
workflow_dispatch:
push:
branches:
- main
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
env:
RELEASE_TAG: nightly
jobs:
build:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15
arch: aarch64
target: aarch64-apple-darwin
- runner: macos-15-intel
arch: x86_64
target: x86_64-apple-darwin
runs-on: ${{ matrix.runner }}
env:
# `secrets` is not available in step-level `if:`, but `env` is.
APPLE_CERT_B64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_FILE_BASE64 }}
defaults:
run:
working-directory: Chord
steps:
# Both repos are checked out side by side under GITHUB_WORKSPACE, because
# `Cargo.toml` depends on rbun by relative path: from
# `Chord/apps/chord-tauri/src-tauri`, `../../../../rbun` resolves to the
# sibling `rbun` directory here. checkout refuses a `path` outside the
# workspace, so nesting Chord one level down is what makes that line up.
# The prebuilt dylib downloaded below only satisfies the *linker*; cargo
# still needs rbun's source manifest to resolve the dependency at all.
- uses: actions/checkout@v6
with:
path: Chord
- name: Checkout rbun
uses: actions/checkout@v6
with:
repository: KeyChord/rbun
path: rbun
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: lts/*
# setup-toolchain runs `proto install` at the workspace root, which no
# longer holds `.prototools` now that Chord is checked out one level down,
# and it has no working-directory input. Install the two pinned tools
# directly instead: the versions come from `.prototools` and
# `rust-toolchain.toml`.
- name: Read pinned bun version
id: tools
working-directory: .
run: |
set -euo pipefail
# setup-bun cannot read .prototools, so parse the pin out of it and
# keep .prototools the single source of truth.
v="$(sed -n 's/^bun[[:space:]]*=[[:space:]]*"\(.*\)"$/\1/p' Chord/.prototools)"
test -n "$v" || { echo "::error::no bun pin in .prototools"; exit 1; }
echo "bun=$v" >> "$GITHUB_OUTPUT"
echo "bun $v"
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ steps.tools.outputs.bun }}
- name: Setup rust
run: |
set -euo pipefail
# rust-toolchain.toml pins the channel and targets; rustup honours it
# automatically for every cargo invocation inside the repo.
rustup show active-toolchain || rustup toolchain install
rustup target add ${{ matrix.target }}
rustc --version
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: Chord/apps/chord-tauri/src-tauri -> target
key: ${{ matrix.target }}
# The `bun` cargo feature links libbun_embed.dylib from the sibling rbun
# checkout, which does not exist on CI. Pull the prebuilt dylib that
# KeyChord/rbun publishes for this architecture instead.
- name: Download libbun_embed
id: bunlib
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
dir="$RUNNER_TEMP/bun-embed"
mkdir -p "$dir"
asset="libbun_embed-${{ matrix.target }}.dylib"
# Newest bun-embed release that actually carries this arch's asset:
# the two matrix legs publish independently, so the newest tag may
# briefly have only one of them.
tag=""
for candidate in $(gh release list -R KeyChord/rbun --limit 100 \
--order desc --json tagName,createdAt \
--jq 'sort_by(.createdAt) | reverse | .[].tagName | select(startswith("bun-embed-"))'); do
if gh release view "$candidate" -R KeyChord/rbun --json assets \
--jq '.assets[].name' 2>/dev/null | grep -qx "$asset"; then
tag="$candidate"
break
fi
done
if [ -z "$tag" ]; then
echo "::error::no bun-embed release in KeyChord/rbun provides $asset; run its build-bun-embed workflow first"
exit 1
fi
echo "using $tag / $asset"
gh release download "$tag" -R KeyChord/rbun --pattern "$asset" --dir "$dir" --clobber
mv "$dir/$asset" "$dir/libbun_embed.dylib"
lipo -info "$dir/libbun_embed.dylib"
echo "dir=$dir" >> "$GITHUB_OUTPUT"
- name: Install dependencies
run: bun install --frozen-lockfile
# tauri.conf.json points `bundle.macOS.frameworks` at the local rbun
# checkout. Emit an overlay config pointing at the downloaded dylib, and
# stamp the version so each nightly build is identifiable.
- name: Build CI tauri config
id: cfg
run: |
set -euo pipefail
version="$(bun -e 'console.log(JSON.parse(await Bun.file("apps/chord-tauri/src-tauri/tauri.conf.json").text()).version)')"
short="${GITHUB_SHA:0:7}"
cfg="apps/chord-tauri/src-tauri/tauri.ci.conf.json"
bun -e '
const dylib = process.argv[1] + "/libbun_embed.dylib";
await Bun.write(process.argv[2], JSON.stringify({
bundle: { macOS: { frameworks: [dylib], signingIdentity: "-" } },
}, null, 2));
' "${{ steps.bunlib.outputs.dir }}" "$cfg"
cat "$cfg"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "short=$short" >> "$GITHUB_OUTPUT"
echo "config=src-tauri/tauri.ci.conf.json" >> "$GITHUB_OUTPUT"
- name: Import Apple certificate
id: cert
if: ${{ env.APPLE_CERT_B64 != '' }}
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_FILE_BASE64 }}
p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
- name: Resolve signing identity
id: signing
run: |
set -euo pipefail
if [ -n "${APPLE_CERT_B64:-}" ]; then
id="$(security find-identity -v -p codesigning signing_temp.keychain \
| grep "Developer ID Application" | head -n1 | awk -F'"' '{print $2}')"
echo "identity=$id" >> "$GITHUB_OUTPUT"
echo "signed=true" >> "$GITHUB_OUTPUT"
else
echo "no Apple certificate secret; ad-hoc signing"
echo "identity=-" >> "$GITHUB_OUTPUT"
echo "signed=false" >> "$GITHUB_OUTPUT"
fi
- name: Select bundle targets
id: bundles
env:
UPDATER_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
run: |
set -euo pipefail
# tauri hard-errors when an Updater-type bundle is requested while
# `plugins.updater.pubkey` is set but TAURI_SIGNING_PRIVATE_KEY is not
# ("A public key has been found, but no private key"). Building only
# dmg+app skips updater signing entirely, so the unsigned path works.
if [ -n "${UPDATER_KEY:-}" ]; then
echo "bundles=dmg,app,updater" >> "$GITHUB_OUTPUT"
else
echo "no updater signing key; building dmg,app only"
echo "bundles=dmg,app" >> "$GITHUB_OUTPUT"
fi
- name: Build Tauri app
working-directory: Chord/apps/chord-tauri
env:
RBUN_BUN_LIB_DIR: ${{ steps.bunlib.outputs.dir }}
APPLE_SIGNING_IDENTITY: ${{ steps.signing.outputs.identity }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_FILE_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
set -euo pipefail
bun tauri build \
--target ${{ matrix.target }} \
--config ${{ steps.cfg.outputs.config }} \
--bundles ${{ steps.bundles.outputs.bundles }}
- name: Stage assets
id: stage
run: |
set -euo pipefail
bundle="apps/chord-tauri/src-tauri/target/${{ matrix.target }}/release/bundle"
out="$RUNNER_TEMP/release-assets"
mkdir -p "$out"
suffix="${{ matrix.arch }}"
dmg="$(find "$bundle/dmg" -maxdepth 1 -name '*.dmg' | head -n1)"
test -n "$dmg" || { echo "::error::no .dmg produced"; exit 1; }
cp "$dmg" "$out/Chord-$suffix.dmg"
app="$(find "$bundle/macos" -maxdepth 1 -name '*.app' | head -n1)"
test -n "$app" || { echo "::error::no .app produced"; exit 1; }
# Sanity-check that the embedded Bun runtime actually shipped.
test -f "$app/Contents/Frameworks/libbun_embed.dylib" \
|| { echo "::error::libbun_embed.dylib missing from bundle"; exit 1; }
# When the updater bundle ran it already produced a signed archive;
# reuse it so the archive and its .sig stay a matched pair. Otherwise
# roll our own plain archive of the .app.
updater="$(find "$bundle/macos" -maxdepth 1 -name '*.app.tar.gz' | head -n1)"
if [ -n "$updater" ]; then
cp "$updater" "$out/Chord-$suffix.app.tar.gz"
if [ -e "$updater.sig" ]; then
cp "$updater.sig" "$out/Chord-$suffix.app.tar.gz.sig"
fi
else
tar -czf "$out/Chord-$suffix.app.tar.gz" -C "$(dirname "$app")" "$(basename "$app")"
fi
# spctl only exists on macOS, so record the Gatekeeper verdict here for
# the ubuntu publish job to read.
if spctl -a -vv "$app" 2>&1 | grep -qi notarized; then
echo notarized > "$out/gatekeeper.txt"
else
echo unsigned > "$out/gatekeeper.txt"
fi
cat "$out/gatekeeper.txt"
ls -la "$out"
echo "dir=$out" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v4
with:
name: chord-${{ matrix.arch }}
path: ${{ steps.stage.outputs.dir }}/*
if-no-files-found: error
publish:
needs: build
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v4
with:
path: assets
merge-multiple: true
- name: Publish rolling release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
ls -la assets
short="${GITHUB_SHA:0:7}"
# Say which it is rather than hardcoding a claim: the signing steps are
# secret-gated, so an unsigned build is possible if a secret is removed.
# The verdict comes from the macOS build job (spctl is not on ubuntu).
if grep -qx notarized assets/gatekeeper.txt 2>/dev/null; then
gatekeeper="Signed and notarized — just open it."
else
gatekeeper="This build is not notarized; on first launch, right-click the app and choose **Open**."
fi
rm -f assets/gatekeeper.txt
notes="Automatic build from \`main\` @ ${short}. Download the \`.dmg\` for your Mac: \`aarch64\` for Apple Silicon, \`x86_64\` for Intel. $gatekeeper"
if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
# Move the rolling tag onto this commit, then refresh the release.
git tag -f "$RELEASE_TAG" "$GITHUB_SHA"
git push -f origin "refs/tags/$RELEASE_TAG"
gh release edit "$RELEASE_TAG" --notes "$notes" --prerelease --latest=false
else
# Pin the tag to the commit that was actually built rather than
# whatever the default branch points at by the time this runs.
gh release create "$RELEASE_TAG" \
--target "$GITHUB_SHA" \
--title "Chord (latest from main)" \
--notes "$notes" \
--prerelease
fi
# `--clobber` replaces the previous build's assets in place. Names are
# commit-free, so the download URLs are stable across releases.
gh release upload "$RELEASE_TAG" assets/* --clobber
# Delete anything left from an older naming scheme; otherwise the
# release accumulates a stale set of assets on every push.
keep="$(cd assets && ls)"
gh release view "$RELEASE_TAG" --json assets --jq '.assets[].name' \
| while read -r existing; do
if ! grep -qxF "$existing" <<<"$keep"; then
echo "removing stale asset $existing"
gh release delete-asset "$RELEASE_TAG" "$existing" --yes
fi
done
gh release view "$RELEASE_TAG" --json assets --jq '.assets[].name'