-
Notifications
You must be signed in to change notification settings - Fork 17
336 lines (303 loc) · 13.7 KB
/
Copy pathrelease.yml
File metadata and controls
336 lines (303 loc) · 13.7 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
name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
permissions:
contents: write
jobs:
# ── Quality gate ─────────────────────────────────────────────────────
# Runs typecheck + unit tests on Ubuntu before any platform build starts,
# so a broken tag never reaches the release matrix. One cheap job
# instead of a separate `ci.yml` that fires on every push to main (that's
# the EchoBird pattern — a separate push-triggered CI is what makes every
# release look like "a big pile of CI"). Here the gate runs ONLY on tag
# push / workflow_dispatch, gated behind `needs:`, so a normal
# commit-to-main push pays zero CI cost.
quality:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: src-ui/package-lock.json
- name: Install frontend deps
run: |
cd src-ui
npm ci
# Full frontend build (tsc -b typecheck + vite build). The matrix
# also builds the frontend, but repeating it here is ~1s and keeps
# the gate self-contained + identical to local `npm run build`, so a
# type error the matrix would hit is caught before any platform
# runner spins up. NOTE: `npm run lint` is intentionally NOT gated
# here — the repo has long-standing lint errors (empty catch blocks,
# react-refresh exports) that predate this workflow and would block
# every release until cleaned up. Add lint back once those are fixed.
- name: Frontend build (typecheck)
run: |
cd src-ui
npm run build
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
# Tauri's Linux build pulls glib-sys / gobject-sys / webkit2gtk-sys
# crates, which pkg-config against the system dev libraries. The
# release matrix installs these for the ubuntu builds; the quality
# gate needs the SAME set or `cargo test` fails to compile the
# glib-sys build script (glib-2.0.pc not found). Mirror the matrix's
# install verbatim so the gate stays in lockstep with what actually
# ships.
- name: Install system dependencies (Linux)
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
key: quality
# Unit tests — includes the shell_probe detection / fallback-chain
# suite. On Linux this also exercises the Unix cfg branches that
# Windows-hosted dev never runs locally (#[cfg(not(target_os =
# "windows"))] tests are filtered out on Windows).
- name: Cargo test
run: cargo test --no-fail-fast
release:
needs: quality
strategy:
fail-fast: false
matrix:
include:
- platform: windows-latest
args: ''
label: Windows x64
- platform: macos-latest
args: '--target aarch64-apple-darwin'
target: aarch64-apple-darwin
label: macOS Apple Silicon
# GitHub Actions exposed a dedicated x86_64 macOS runner
# (`macos-15-intel`) in May 2026, after the `macos-13` Intel runner
# was deprecated. Without it, Intel Mac users had to run Coffee CLI
# under Rosetta from the arm64 DMG. Now we ship a native x64 DMG too.
- platform: macos-15-intel
args: '--target x86_64-apple-darwin'
target: x86_64-apple-darwin
label: macOS Intel
- platform: ubuntu-22.04
args: ''
label: Linux x64
- platform: ubuntu-22.04-arm
args: ''
label: Linux arm64
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Resolve release tag
id: tag
shell: bash
run: |
# Tag-pushes give refs/tags/v1.2.3; workflow_dispatch falls back to
# the latest tag in the working tree. Bare version (no leading "v")
# is what tauri stamps into bundle filenames; both forms are
# exposed for downstream steps.
TAG="${GITHUB_REF#refs/tags/}"
if [ "$TAG" = "$GITHUB_REF" ]; then
TAG=$(git describe --tags --abbrev=0)
fi
VERSION="${TAG#v}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Install system dependencies (Linux)
if: startsWith(matrix.platform, 'ubuntu-')
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf rpm
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: src-ui/package-lock.json
- name: Install frontend deps
run: |
cd src-ui
npm ci
# All platforms build identically on stable xterm 6.0.0. The Linux CJK
# IME punctuation-duplication bug (issue #37 / xtermjs#5374) is fixed in
# the app layer instead — see TierTerminal.tsx (__IS_LINUX__-gated
# suppression of xterm's composition events) — so there is no longer a
# Linux-only dependency override or a Linux-only tsc-skip build path.
- name: Build frontend (tsc + vite)
run: |
cd src-ui
npm run build
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target || '' }}
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
key: ${{ matrix.label }}
- name: Install Tauri CLI
run: cargo install tauri-cli --version "^2.0" --locked
# ── macOS: build .app, re-sign without hardened runtime, then bundle DMG ──
# tauri-bundler's default codesign for adhoc identities adds
# --options runtime, which without Apple notarization causes Gatekeeper
# to silently reject the GUI launch (click icon, nothing happens). We
# build the .app first, codesign --deep WITHOUT runtime, then bundle
# the DMG from the re-signed .app. Gatekeeper accepts adhoc-signed
# apps that aren't claiming hardened runtime.
- name: Build .app only (macOS)
if: runner.os == 'macOS'
run: cargo tauri build --bundles app ${{ matrix.args }}
- name: Re-sign without hardened runtime (macOS)
if: runner.os == 'macOS'
run: |
APP=$(find target/${{ matrix.target }}/release/bundle/macos -maxdepth 2 -name "*.app" | head -1)
if [ -z "$APP" ]; then
echo "::error::No .app found"
find target -name "*.app" 2>/dev/null
exit 1
fi
echo "Re-signing: $APP"
codesign --force --deep --sign - "$APP"
codesign -dv --verbose=4 "$APP" 2>&1
# Sanity assertion: no 'runtime' flag, sealed resources present.
if codesign -dv --verbose=4 "$APP" 2>&1 | grep -qE "flags=.*runtime"; then
echo "::error::runtime flag still present after re-sign"
exit 1
fi
if codesign -dv --verbose=4 "$APP" 2>&1 | grep -q "Sealed Resources=none"; then
echo "::error::Sealed Resources=none after re-sign"
exit 1
fi
- name: Build DMG from re-signed .app (macOS)
if: runner.os == 'macOS'
run: cargo tauri bundle --bundles dmg ${{ matrix.args }}
# ── Windows / Linux: standard one-shot bundle build ──
- name: Build all bundles (Windows / Linux)
if: runner.os != 'macOS'
run: cargo tauri build ${{ matrix.args }}
# ── Stage + rename artifacts to platform-labelled format ──
# Tauri's bundler emits names like `Coffee CLI_1.9.2_amd64.deb` which
# don't tell the end user which OS they're for and use inconsistent
# arch slugs (amd64/x86_64/x64 across formats, plus rpm's `-1` release
# suffix). We rename to the EchoBird-style convention:
# Coffee.CLI_<version>_<OS>_<arch>.<ext>
# so a glance at the GitHub Releases page makes the OS/arch obvious
# and the install scripts have a single uniform pattern to grep.
- name: Stage and rename artifacts
id: stage
shell: bash
env:
VERSION: ${{ steps.tag.outputs.version }}
run: |
set -e
STAGING="$RUNNER_TEMP/coffee-release"
mkdir -p "$STAGING"
rename_one() {
local src="$1"
[ -f "$src" ] || return 0
local base
base=$(basename "$src")
local out=""
case "$base" in
# Windows
*_x64-setup.exe) out="Coffee.CLI_${VERSION}_Windows_x64-setup.exe" ;;
*_x64_*.msi|*_x64.msi) out="Coffee.CLI_${VERSION}_Windows_x64.msi" ;;
# macOS — separate native DMGs for Apple Silicon and Intel.
# Tauri's bundler uses `aarch64` / `x64` suffixes on the dmg
# name (matching the target triple's first segment).
*_aarch64.dmg) out="Coffee.CLI_${VERSION}_macOS_arm64.dmg" ;;
*_x64.dmg) out="Coffee.CLI_${VERSION}_macOS_x64.dmg" ;;
# Linux .deb (Tauri uses Debian "amd64"/"arm64")
*_amd64.deb) out="Coffee.CLI_${VERSION}_Linux_x64.deb" ;;
*_arm64.deb) out="Coffee.CLI_${VERSION}_Linux_arm64.deb" ;;
# Linux .AppImage (upstream AppImage uses "amd64"/"aarch64")
*_amd64.AppImage) out="Coffee.CLI_${VERSION}_Linux_x64.AppImage" ;;
*_aarch64.AppImage) out="Coffee.CLI_${VERSION}_Linux_arm64.AppImage" ;;
# Linux .rpm (uses dot-separated `<name>-<ver>-<release>.<arch>.rpm`)
*.x86_64.rpm) out="Coffee.CLI_${VERSION}_Linux_x64.rpm" ;;
*.aarch64.rpm) out="Coffee.CLI_${VERSION}_Linux_arm64.rpm" ;;
*) echo " skip: $base"; return 0 ;;
esac
cp "$src" "$STAGING/$out"
echo " staged: $base → $out"
}
echo "Bundle scan roots:"
for root in \
target/release/bundle \
target/aarch64-apple-darwin/release/bundle \
target/x86_64-apple-darwin/release/bundle \
; do
[ -d "$root" ] || continue
echo " $root"
while IFS= read -r f; do
rename_one "$f"
done < <(find "$root" -type f \
\( -name "*.exe" -o -name "*.msi" \
-o -name "*.dmg" \
-o -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \) )
done
echo ""
echo "Final staged assets:"
ls -la "$STAGING"
echo "staging=$STAGING" >> "$GITHUB_OUTPUT"
# ── Create release if missing, then upload all staged artifacts ──
# Each matrix job races to create the release; `gh release create`
# fails idempotently if another job already created it (caught and
# ignored), then `gh release upload --clobber` overwrites any
# earlier-job placeholder of the same filename.
- name: Upload to GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -e
TAG="${{ steps.tag.outputs.tag }}"
STAGING="${{ steps.stage.outputs.staging }}"
# Wait briefly in case another matrix job is mid-create.
for i in 1 2 3 4 5 6; do
if gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
break
fi
echo "Release $TAG not yet present (attempt $i/6); waiting 10s..."
sleep 10
done
if ! gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "Creating release $TAG (draft)"
# Releases are created as drafts so the maintainer can review CI
# artifacts and edit the changelog before users see the new
# version. Clicking "Publish release" in the GitHub UI fires the
# release.published event consumed by
# .github/workflows/bump-version-json.yml, which bumps
# Web-Home/version.json on main → CF Pages redeploys → in-app
# update check picks up the new version. Until then, in-app
# users and install scripts still see the previous release.
NOTES_FILE="$GITHUB_WORKSPACE/install/release-notes-template.md"
if [ -f "$NOTES_FILE" ]; then
gh release create "$TAG" --repo "${{ github.repository }}" \
--draft \
--title "Coffee CLI $TAG" \
--notes-file "$NOTES_FILE" \
|| echo "Release already exists (created by sibling matrix job)"
else
gh release create "$TAG" --repo "${{ github.repository }}" \
--draft \
--title "Coffee CLI $TAG" \
--notes "Coffee CLI $TAG. See README for install instructions." \
|| echo "Release already exists (created by sibling matrix job)"
fi
fi
# Upload everything in staging. --clobber overwrites if a previous
# job already pushed an asset with the same name.
shopt -s nullglob
for f in "$STAGING"/*; do
echo "uploading: $(basename "$f")"
gh release upload "$TAG" "$f" --repo "${{ github.repository }}" --clobber
done