Repository navigation
425 lines (393 loc) · 18.1 KB
/
Copy pathbuild.yml
File metadata and controls
425 lines (393 loc) · 18.1 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
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# Full cross-platform build, gated on a marker in the commit message.
#
# WHAT COMES OUT
# macOS AudioHub-<version>.dmg (app + pkg + uninstaller, from app/build-app.sh)
# Windows AudioHub_<version>_x64-setup.exe (NSIS, from scripts/build-windows-installer.ps1)
#
# HOW IT IS TRIGGERED
# [build] push -> build both platforms, keep the artifacts on the run
# [release] push -> the same build, then publish a GitHub Release
# Manual runs via the Actions tab can pick either.
#
# Every other push does nothing at all. This repository has no CI history and
# its build is expensive (a WDK NuGet restore, two Cargo workspaces, a Tauri
# bundle per platform), so building on every commit would be the wrong default
# — the marker is the opt-in.
#
# ⚠ THESE ARTIFACTS ARE DEVELOPMENT BUILDS, NOT DISTRIBUTABLE ONES.
# * macOS: no Developer ID and no notarisation. `app/build-app.sh` falls back
# to an ad-hoc signature and says so. Gatekeeper will refuse it on any
# machine but the one that built it.
# * Windows: `-AuthenticodeMode Development` passes `--no-sign` to Tauri, and
# `-AllowUnsignedDriver` packages an unsigned AudioHubVad. That flag changes
# packaging only — nothing here installs a driver, imports a certificate, or
# enables testsigning.
# Shipping either to a user is a separate decision that needs real
# certificates; see plan.md §19 for the release form.
name: build
on:
push:
workflow_dispatch:
inputs:
mode:
description: What to produce
type: choice
default: build
options: [build, release]
# One build per ref. A second push cancels the first: these runners are slow
# and an obsolete artifact is worth nothing.
#
# Cancelling a superseded BUILD is right; cancelling a release mid-publish is
# not. The job-level group on `release` does NOT buy that: run-level
# cancellation takes the whole run, every job with it, so a job-level group is
# no exemption. A release therefore gets its own per-commit group that no later
# push can collide with, and only real releases are separated — ordinary builds
# still share `shared` and still supersede each other.
# Kept on one line on purpose: a folded scalar would carry newlines into the
# expression, and this is not the place to find out how they are parsed.
# The two operands are mutually exclusive without needing an event_name guard —
# a push has no `inputs`, a dispatch has no `head_commit`, and both nulls
# evaluate false.
concurrency:
group: build-${{ github.ref }}-${{ (contains(github.event.head_commit.message, '[release]') || github.event.inputs.mode == 'release') && github.sha || 'shared' }}
cancel-in-progress: true
permissions:
contents: read
env:
# Pinned in scripts/generate-third-party-licenses.mjs, which refuses any other
# version. Kept here as well so the failure is "the workflow is out of date"
# rather than a confusing mismatch three minutes into the run.
CARGO_ABOUT_VERSION: 0.9.1
CARGO_TERM_COLOR: always
jobs:
# ---------------------------------------------------------------- gate
#
# Decides whether to build at all, and whether to release. Split into its own
# job so the expensive matrix has a single, readable condition instead of the
# same `contains(...)` expression copied into every step.
gate:
runs-on: ubuntu-latest
outputs:
build: ${{ steps.decide.outputs.build }}
release: ${{ steps.decide.outputs.release }}
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
- id: decide
shell: bash
env:
# Through the environment, never interpolated into the script body: a
# commit message is attacker-controlled text and `${{ }}` inside a
# `run:` is textual substitution, so a message containing a quote and
# a semicolon would execute as shell.
MSG: ${{ github.event.head_commit.message }}
DISPATCH: ${{ github.event.inputs.mode }}
run: |
set -euo pipefail
build=false
release=false
if [ -n "$DISPATCH" ]; then
build=true
[ "$DISPATCH" = release ] && release=true
else
# Only the head commit's message is consulted. A push can carry many
# commits and marking an older one is almost always accidental.
case "$MSG" in
*'[release]'*) build=true; release=true ;;
*'[build]'*) build=true ;;
esac
fi
echo "build=$build" >> "$GITHUB_OUTPUT"
echo "release=$release" >> "$GITHUB_OUTPUT"
{
echo "### Build gate"
echo
echo "- build: \`$build\`"
echo "- release: \`$release\`"
echo
echo 'Add `[build]` or `[release]` to the head commit message to trigger one.'
} >> "$GITHUB_STEP_SUMMARY"
- id: version
shell: bash
run: |
set -euo pipefail
# The single source of truth for both bundlers; read it rather than
# duplicating a number that would silently drift. jq rather than node:
# this job installs no toolchain, and depending on whatever node the
# runner image happens to ship is a dependency nobody declared.
v=$(jq -re .version app/src-tauri/tauri.conf.json)
[ -n "$v" ] || { echo "could not read version"; exit 1; }
echo "version=$v" >> "$GITHUB_OUTPUT"
# ---------------------------------------------------------------- macOS
macos:
needs: gate
if: needs.gate.outputs.build == 'true'
# arm64. The product targets Apple Silicon and the HAL driver is built for
# the host arch by app/build-app.sh; an x86_64 runner would produce a bundle
# that does not match any development machine here.
runs-on: macos-14
timeout-minutes: 90
env:
AUDIOHUB_EXPECTED_APP_ARCH: arm64
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: app/frontend/package-lock.json
- name: Assert build architecture
run: |
set -euo pipefail
test "$(uname -m)" = "$AUDIOHUB_EXPECTED_APP_ARCH"
- name: Restore frontend dependencies without lifecycle scripts
working-directory: app/frontend
run: npm ci --ignore-scripts --no-audit --no-fund
- name: Rust toolchain
run: |
rustup toolchain install stable --profile minimal
rustup default stable
rustc -vV
# Two workspaces: the root one and the Tauri app. Keyed separately so a
# change in one does not throw away the other's cache.
- uses: Swatinem/rust-cache@v2
with:
workspaces: |
.
app/src-tauri
key: macos
- name: Cache cargo-installed tools
id: tools
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/cargo-about
~/.cargo/bin/cargo-tauri
key: macos-cargo-tools-about${{ env.CARGO_ABOUT_VERSION }}-tauri2
- name: Install cargo-about and cargo-tauri
if: steps.tools.outputs.cache-hit != 'true'
run: |
set -euo pipefail
# --features cli is required: the default build of cargo-about 0.9 has
# no binary, and the licence generator only checks `--version`.
cargo install --locked cargo-about --version "$CARGO_ABOUT_VERSION" --features cli
# BuildOnly refuses a missing CLI, so provisioning stays explicit and
# cacheable instead of executing an installer inside the product build.
cargo install --locked tauri-cli --version "^2"
- name: Build app, pkg and dmg
run: |
set -euo pipefail
# No AUDIOHUB_RELEASE: that flag is fail-closed and demands Developer
# ID identities plus a notarytool keychain profile, neither of which
# exists on a runner. The script then signs ad-hoc and says so.
zsh app/build-app.sh
- name: Locate the dmg
id: dmg
run: |
set -euo pipefail
# Newest first, like the Windows step. Identical on a clean runner,
# but an alphabetical pick would take AudioHub-0.1.0-dev.dmg over
# AudioHub-1.0.0.dmg in any tree that still holds an older bundle.
dmg=$(ls -t app/src-tauri/target/release/bundle/dmg/*.dmg | head -1)
[ -f "$dmg" ] || { echo "no dmg was produced"; exit 1; }
echo "path=$dmg" >> "$GITHUB_OUTPUT"
echo "name=$(basename "$dmg")" >> "$GITHUB_OUTPUT"
{
echo "### macOS"
echo
echo "- \`$(basename "$dmg")\` — $(stat -f%z "$dmg") bytes"
echo "- sha256 \`$(shasum -a 256 "$dmg" | cut -d' ' -f1)\`"
echo "- ad-hoc signed, **not** notarised"
} >> "$GITHUB_STEP_SUMMARY"
- uses: actions/upload-artifact@v4
with:
name: macos-dmg
path: ${{ steps.dmg.outputs.path }}
if-no-files-found: error
# ---------------------------------------------------------------- Windows
windows:
needs: gate
if: needs.gate.outputs.build == 'true'
# Pinned to 2022, NOT windows-latest. The image behind `latest` now ships
# Visual Studio 18, and the WDK NuGet package (10.0.26100.6584) resolves its
# MSBuild task assembly by VS major version: on 18 it looks for
# Microsoft.DriverKit.Build.Tasks.18.0.dll, which that package does not
# contain, and the driver build dies with MSB4062 before compiling a line.
# VS 2022 (v17) is also what builds this driver on the hardware test box, so
# the pin makes CI match the toolchain the driver is actually known to build
# under rather than tracking whatever `latest` becomes next.
runs-on: windows-2022
# Longer than macOS on purpose: a cold run also restores the WDK NuGet
# packages and builds the kernel-mode driver before Tauri is even reached.
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: app/frontend/package-lock.json
- name: Rust toolchain
shell: pwsh
run: |
# The build scripts invoke `cargo +stable-x86_64-pc-windows-msvc`, so
# the toolchain must exist under exactly that name. The runner's
# default is already msvc, but the explicit name is what they ask for
# and rustup does not alias it.
rustup toolchain install stable-x86_64-pc-windows-msvc --profile minimal
rustup default stable-x86_64-pc-windows-msvc
rustc -vV
- uses: Swatinem/rust-cache@v2
with:
workspaces: |
.
app/src-tauri
key: windows
- name: Cache cargo-installed tools
id: tools
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/cargo-about.exe
~/.cargo/bin/cargo-tauri.exe
key: windows-cargo-tools-about${{ env.CARGO_ABOUT_VERSION }}-tauri2
- name: Install cargo-about and cargo-tauri
if: steps.tools.outputs.cache-hit != 'true'
shell: pwsh
run: |
cargo install --locked cargo-about --version $env:CARGO_ABOUT_VERSION --features cli
cargo install --locked tauri-cli --version "^2"
# `app/ui` is gitignored, so a fresh checkout has none. Dependency
# restoration is explicit and lifecycle scripts stay disabled; the
# installer builder refuses an unprovisioned tree instead of running npm.
# tauri-build embeds frontendDist at COMPILE time, so a missing or stale
# app/ui produces an exe that runs and shows the wrong UI. That is the
# expensive kind of failure to notice, which is why this is its own step.
- name: Build the frontend into app/ui
shell: pwsh
run: |
npm --prefix app/frontend ci --ignore-scripts --no-audit --no-fund
npm --prefix app/frontend run build
if (-not (Test-Path 'app/ui/index.html')) { throw 'app/ui/index.html was not produced' }
- name: Build the NSIS installer
shell: pwsh
run: |
# The AudioHubVad driver builds from the WDK **NuGet packages**
# (drivers/windows-vad/tools/restore-wdk.ps1), not an installed WDK,
# so a stock windows-latest runner is enough: MSBuild and vswhere come
# with the image and signing is off (/p:SignMode=Off).
./scripts/build-windows-installer.ps1 -AllowUnsignedDriver -AuthenticodeMode Development
- name: Locate the installer
id: exe
shell: pwsh
run: |
$dir = 'app/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis'
$exe = Get-ChildItem -Path $dir -Filter '*setup*.exe' -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $exe) { throw "no installer was produced under $dir" }
"path=$($exe.FullName)" | Out-File $env:GITHUB_OUTPUT -Append
"name=$($exe.Name)" | Out-File $env:GITHUB_OUTPUT -Append
$h = (Get-FileHash $exe.FullName -Algorithm SHA256).Hash.ToLower()
@(
'### Windows'
''
"- ``$($exe.Name)`` — $($exe.Length) bytes"
"- sha256 ``$h``"
'- unsigned, packages an unsigned AudioHubVad'
) | Out-File $env:GITHUB_STEP_SUMMARY -Append
- uses: actions/upload-artifact@v4
with:
name: windows-installer
path: ${{ steps.exe.outputs.path }}
if-no-files-found: error
# ---------------------------------------------------------------- release
release:
needs: [gate, macos, windows]
if: needs.gate.outputs.release == 'true'
runs-on: ubuntu-latest
# Its own group, so a push landing while assets are being uploaded cannot
# cancel a half-published release.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
# Narrowed to this job. The build jobs never need to write to the repo, and
# the default token is read-only above.
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Describe what is being published
id: meta
shell: bash
env:
MSG: ${{ github.event.head_commit.message }}
run: |
set -euo pipefail
ls -l dist
# The version, plainly: `v1.0.0`. This used to carry a `-ci.<run>`
# suffix so that a rebuild could never collide with an existing tag,
# which was right while every run was a throwaway. It is wrong for a
# release people are meant to link to.
#
# The collision it was avoiding is now a feature: publishing the same
# version twice SHOULD fail, because the second one would silently
# replace assets other people already downloaded and checksummed.
# Releasing again means bumping `version` in tauri.conf.json first.
echo "tag=v${{ needs.gate.outputs.version }}" >> "$GITHUB_OUTPUT"
# First line only: release titles are one line, and the marker is noise.
subject=$(printf '%s' "$MSG" | head -1 | sed 's/\[release\]//g' | xargs || true)
[ -n "$subject" ] || subject="build ${{ github.run_number }}"
echo "subject=$subject" >> "$GITHUB_OUTPUT"
# Built OUTSIDE dist/ and moved in afterwards. Redirecting into
# dist/CHECKSUMS.md creates the file before the loop reads `dist/*`,
# so the table lists itself — with the checksum of an empty file.
notes=$(mktemp)
{
# The signing state goes FIRST. It is the one thing every reader has
# to know before clicking a download: both operating systems refuse
# these files on first launch, and a user who meets that without
# warning reads it as "the release is broken" rather than "this is
# what an unsigned build looks like".
echo '> **These builds are not signed for distribution.**'
echo '> The disk image is ad-hoc signed and not notarised, so Gatekeeper'
echo '> refuses it; the Windows installer is unsigned and carries an'
echo '> unsigned driver, so SmartScreen blocks it. Getting past either is'
echo '> a security decision you are making yourself —'
echo '> [Installation](https://github.com/Tularity/AudioHub/wiki/Installation)'
echo '> walks through it per platform.'
echo
echo '| file | bytes | sha256 |'
echo '| --- | ---: | --- |'
for f in dist/*; do
[ -f "$f" ] || continue
printf '| `%s` | %s | `%s` |\n' \
"$(basename "$f")" "$(stat -c%s "$f")" "$(sha256sum "$f" | cut -d' ' -f1)"
done
echo
echo 'Verify a download before running it: `shasum -a 256 <file>` on macOS,'
echo '`Get-FileHash <file>` on Windows.'
} > "$notes"
mv "$notes" dist/CHECKSUMS.md
cat dist/CHECKSUMS.md >> "$GITHUB_STEP_SUMMARY"
- uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.meta.outputs.tag }}
name: ${{ needs.gate.outputs.version }} — ${{ steps.meta.outputs.subject }}
target_commitish: ${{ github.sha }}
# A normal release, by the project owner's decision (2026-08-16).
#
# This flag used to be pinned true on the reasoning that nothing here
# is signed or notarised. That fact has not changed — but "prerelease"
# is GitHub's word for "not finished yet", and 1.0.0 is finished; it is
# unsigned, which is a different statement and one the release notes
# now make in their first line rather than leaving to a badge.
prerelease: false
generate_release_notes: true
body_path: dist/CHECKSUMS.md
files: |
dist/*.dmg
dist/*.exe
fail_on_unmatched_files: true