Skip to content

Commit 705f0a1

Browse files
committed
ci: add manual build-only workflow with per-OS toggles
1 parent 03a90b6 commit 705f0a1

1 file changed

Lines changed: 258 additions & 0 deletions

File tree

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
name: 'Build Artifacts (Manual)'
2+
3+
# Manual-only: builds installers and uploads them as workflow artifacts.
4+
# Never creates a git tag or a GitHub Release.
5+
on:
6+
workflow_dispatch:
7+
inputs:
8+
windows:
9+
description: 'Build Windows (x64 + arm64)'
10+
type: boolean
11+
default: true
12+
macos:
13+
description: 'Build macOS (universal)'
14+
type: boolean
15+
default: true
16+
linux:
17+
description: 'Build Linux (x64 + arm64)'
18+
type: boolean
19+
default: false
20+
21+
concurrency:
22+
group: build-artifacts-${{ github.ref }}
23+
cancel-in-progress: true
24+
25+
jobs:
26+
# Step 0: Turn the platforms input into a matrix include list
27+
setup:
28+
runs-on: ubuntu-latest
29+
outputs:
30+
matrix: ${{ steps.set.outputs.matrix }}
31+
steps:
32+
- id: set
33+
shell: bash
34+
run: |
35+
ENTRIES=()
36+
37+
if [ "${{ inputs.windows }}" = "true" ]; then
38+
ENTRIES+=('{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","rust-targets":"x86_64-pc-windows-msvc","cache-key":"win-x64"}')
39+
ENTRIES+=('{"platform":"windows-latest","args":"--target aarch64-pc-windows-msvc","rust-targets":"aarch64-pc-windows-msvc","cache-key":"win-arm64"}')
40+
fi
41+
42+
if [ "${{ inputs.macos }}" = "true" ]; then
43+
ENTRIES+=('{"platform":"macos-14","args":"--target universal-apple-darwin -- --no-default-features","rust-targets":"aarch64-apple-darwin,x86_64-apple-darwin","cache-key":"macos-universal"}')
44+
fi
45+
46+
if [ "${{ inputs.linux }}" = "true" ]; then
47+
ENTRIES+=('{"platform":"ubuntu-22.04","args":"--target x86_64-unknown-linux-gnu","rust-targets":"x86_64-unknown-linux-gnu","cache-key":"linux-x64"}')
48+
ENTRIES+=('{"platform":"ubuntu-22.04","args":"--target aarch64-unknown-linux-gnu --bundles deb,rpm","rust-targets":"aarch64-unknown-linux-gnu","cache-key":"linux-arm64"}')
49+
fi
50+
51+
if [ ${#ENTRIES[@]} -eq 0 ]; then
52+
echo "No platforms selected — pick at least one of windows/macos/linux." >&2
53+
exit 1
54+
fi
55+
56+
JOINED=$(IFS=,; echo "${ENTRIES[*]}")
57+
echo "matrix={\"include\":[$JOINED]}" >> "$GITHUB_OUTPUT"
58+
59+
# Step 1: Build frontend once
60+
build-frontend:
61+
needs: setup
62+
runs-on: ubuntu-latest
63+
steps:
64+
- uses: actions/checkout@v4
65+
- name: Setup Node.js
66+
uses: actions/setup-node@v4
67+
with:
68+
node-version: lts/*
69+
- name: Cache node_modules
70+
uses: actions/cache@v4
71+
with:
72+
path: node_modules
73+
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
74+
restore-keys: |
75+
npm-${{ runner.os }}-
76+
- name: Install dependencies
77+
run: npm ci
78+
- name: Build frontend
79+
run: npm run build
80+
- name: Upload dist artifact
81+
uses: actions/upload-artifact@v4
82+
with:
83+
name: frontend-dist
84+
path: dist/
85+
retention-days: 1
86+
87+
# Step 2: Build native binaries (no release, no tag)
88+
build-tauri:
89+
needs: [setup, build-frontend]
90+
runs-on: ${{ matrix.platform }}
91+
strategy:
92+
fail-fast: false
93+
matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
94+
steps:
95+
- uses: actions/checkout@v4
96+
- name: Select latest stable Xcode
97+
if: runner.os == 'macOS'
98+
uses: maxim-lobanov/setup-xcode@v1
99+
with:
100+
xcode-version: latest-stable
101+
# Split into small steps, each with a short timeout: any of these
102+
# `security` subcommands can silently pop a GUI auth dialog on some
103+
# macOS versions, which hangs forever on a headless runner with no one
104+
# to click it. A per-step timeout turns that into a fast, clearly
105+
# attributed failure instead of a frozen job.
106+
- name: Create signing keychain (macOS)
107+
if: runner.os == 'macOS'
108+
timeout-minutes: 2
109+
run: |
110+
KEYCHAIN_PATH="$RUNNER_TEMP/signing.keychain-db"
111+
KEYCHAIN_PASSWORD=$(uuidgen)
112+
echo "::add-mask::$KEYCHAIN_PASSWORD"
113+
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
114+
echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> "$GITHUB_ENV"
115+
116+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
117+
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
118+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
119+
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | sed 's/"//g')
120+
121+
- name: Import certificate (macOS)
122+
if: runner.os == 'macOS'
123+
timeout-minutes: 2
124+
env:
125+
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
126+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
127+
run: |
128+
CERT_PATH="$RUNNER_TEMP/cert.p12"
129+
echo "CERT_PATH=$CERT_PATH" >> "$GITHUB_ENV"
130+
echo -n "$APPLE_CERTIFICATE" | base64 --decode -o "$CERT_PATH"
131+
security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
132+
133+
- name: Set key partition list (macOS)
134+
if: runner.os == 'macOS'
135+
timeout-minutes: 2
136+
run: |
137+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
138+
139+
- name: Extract plain cert from p12 (macOS)
140+
if: runner.os == 'macOS'
141+
timeout-minutes: 1
142+
env:
143+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
144+
run: |
145+
CERT_PEM_PATH="$RUNNER_TEMP/cert.pem"
146+
echo "CERT_PEM_PATH=$CERT_PEM_PATH" >> "$GITHUB_ENV"
147+
openssl pkcs12 -in "$CERT_PATH" -clcerts -nokeys -passin pass:"$APPLE_CERTIFICATE_PASSWORD" -out "$CERT_PEM_PATH"
148+
149+
- name: Trust self-signed cert (macOS)
150+
if: runner.os == 'macOS'
151+
timeout-minutes: 1
152+
run: |
153+
# Self-signed root: not trusted by default like a real Apple cert
154+
# chain would be, so codesign can't resolve it without this.
155+
# add-trusted-cert (with or without -d) normally pops a GUI auth
156+
# dialog for trust-settings changes, which hangs forever with no
157+
# one to click it. Relaxing this one authorization right lets sudo
158+
# satisfy it non-interactively instead - documented workaround for
159+
# exactly this CI hang.
160+
sudo security authorizationdb write com.apple.trust-settings.admin allow
161+
sudo security add-trusted-cert -d -r trustRoot "$CERT_PEM_PATH"
162+
163+
- name: Verify signing identity is resolvable (macOS)
164+
if: runner.os == 'macOS'
165+
timeout-minutes: 1
166+
run: security find-identity -v -p codesigning "$KEYCHAIN_PATH"
167+
- name: Install Linux dependencies (x64)
168+
if: matrix.cache-key == 'linux-x64'
169+
run: |
170+
sudo apt-get update
171+
sudo apt-get install -y build-essential pkg-config libssl-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libasound2-dev libxdo-dev
172+
- name: Install Linux cross-compile dependencies (arm64)
173+
if: matrix.cache-key == 'linux-arm64'
174+
run: |
175+
sudo dpkg --add-architecture arm64
176+
# Add arm64 sources from ports.ubuntu.com
177+
echo "deb [arch=arm64] http://ports.ubuntu.com/ jammy main restricted universe multiverse" | sudo tee /etc/apt/sources.list.d/arm64.list
178+
echo "deb [arch=arm64] http://ports.ubuntu.com/ jammy-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list.d/arm64.list
179+
# Restrict existing sources to amd64 only
180+
sudo sed -i 's/^deb /deb [arch=amd64] /' /etc/apt/sources.list
181+
sudo apt-get update
182+
sudo apt-get install -y build-essential pkg-config libssl-dev libssl-dev:arm64 gcc-aarch64-linux-gnu g++-aarch64-linux-gnu patchelf \
183+
libgtk-3-dev:arm64 libwebkit2gtk-4.1-dev:arm64 libappindicator3-dev:arm64 \
184+
librsvg2-dev:arm64 libasound2-dev:arm64 libxdo-dev:arm64
185+
echo "PKG_CONFIG_ALLOW_CROSS=1" >> $GITHUB_ENV
186+
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig" >> $GITHUB_ENV
187+
echo "PKG_CONFIG_SYSROOT_DIR=/" >> $GITHUB_ENV
188+
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
189+
- name: Download frontend
190+
uses: actions/download-artifact@v4
191+
with:
192+
name: frontend-dist
193+
path: dist/
194+
- name: Setup Node.js
195+
uses: actions/setup-node@v4
196+
with:
197+
node-version: lts/*
198+
- name: Install Rust stable
199+
uses: dtolnay/rust-toolchain@stable
200+
with:
201+
targets: ${{ matrix.rust-targets }}
202+
- name: Set up sccache
203+
uses: mozilla-actions/sccache-action@v0.0.7
204+
- name: Rust cache
205+
uses: swatinem/rust-cache@v2
206+
with:
207+
workspaces: './src-tauri -> target'
208+
shared-key: ${{ matrix.cache-key }}-${{ hashFiles('package.json') }}
209+
# sccache already caches compiled objects; caching the whole target/
210+
# dir here too would just double up on the repo's 10GB cache quota.
211+
cache-targets: false
212+
213+
- name: Cache node_modules
214+
uses: actions/cache@v4
215+
with:
216+
path: node_modules
217+
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
218+
restore-keys: |
219+
npm-${{ runner.os }}-
220+
- name: Install npm deps
221+
run: npm ci
222+
223+
- name: Skip frontend build
224+
run: node -e "const f='src-tauri/tauri.conf.json';const j=JSON.parse(require('fs').readFileSync(f,'utf8'));j.build.beforeBuildCommand='';require('fs').writeFileSync(f,JSON.stringify(j,null,4))"
225+
226+
- name: Build (no release)
227+
uses: tauri-apps/tauri-action@v0.6
228+
env:
229+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
230+
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
231+
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
232+
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
233+
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
234+
# Self-signed cert (no Apple Developer account): keeps the code
235+
# signature identity stable across builds so macOS TCC permissions
236+
# (Screen Recording) survive updates. Not notarized. Already
237+
# imported and trusted by the step above — only the identity name
238+
# is needed here, not the cert itself.
239+
APPLE_SIGNING_IDENTITY: 'Shotcove Self Signed'
240+
with:
241+
args: ${{ matrix.args }}
242+
# includeRelease controls whether `tauri build` actually runs, not
243+
# whether a GitHub Release gets created — that only happens if
244+
# tagName/releaseId is set below, which we deliberately omit.
245+
includeRelease: true
246+
247+
- name: Upload artifacts
248+
uses: actions/upload-artifact@v4
249+
with:
250+
name: shotcove-${{ matrix.cache-key }}
251+
path: |
252+
src-tauri/target/**/release/bundle/msi/*.msi
253+
src-tauri/target/**/release/bundle/nsis/*.exe
254+
src-tauri/target/**/release/bundle/dmg/*.dmg
255+
src-tauri/target/**/release/bundle/deb/*.deb
256+
src-tauri/target/**/release/bundle/rpm/*.rpm
257+
src-tauri/target/**/release/bundle/appimage/*.AppImage
258+
if-no-files-found: warn

0 commit comments

Comments
 (0)