open-standards/.github/workflows/wai-quantum-desktop-release.yml
Transaction Science e694cb0966
Some checks failed
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 12s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 21s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 15s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 23s
wai-app and Create / wai-app · does this change touch it (push) Successful in 23s
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (push) Successful in 32s
WAI operation catalogue / does this change touch it (push) Successful in 21s
wai-key-proxy / does this change touch it (push) Successful in 13s
wai-quantum wasmQ / does this change touch it (push) Successful in 20s
wai-ledger service / does this change touch it (push) Successful in 31s
energy meters on real hardware / RAPL and NVML on jetson-hub (push) Successful in 1m5s
wai-ops integer operators / does this change touch it (push) Successful in 30s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 1m13s
wai-quantum contract / does this change touch it (push) Successful in 25s
wai-quantum command line / does this change touch it (push) Successful in 24s
wai-det-jpegai WAISM float-level module of record / does this change touch it (push) Successful in 56s
wai formal verification (integer core) / does this change touch it (push) Successful in 1m0s
wai-quantum docs, course and locales / does this change touch it (push) Successful in 12s
wai-audit collector and player integration / does this change touch it (push) Successful in 1m18s
smart-byte conformance gate / does this change touch it (push) Successful in 1m41s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 1m18s
wai-int-module WAISM module of record / does this change touch it (push) Successful in 1m8s
wai-quantum wasmQ / the reference modules rebuild to their modules of record · linux (push) Successful in 42s
smart-byte conformance gate / Spec changes require vector-pack updates (push) Has been skipped
wai-fleet service / does this change touch it (push) Successful in 1m11s
wai cross-architecture byte-equality / does this change touch it (push) Successful in 1m13s
wai-quantum resource estimation / does this change touch it (push) Successful in 23s
wai-quantum editors / does this change touch it (push) Successful in 29s
wai-quantum other formats / does this change touch it (push) Successful in 26s
wai-quantum OpenQASM reader / does this change touch it (push) Successful in 24s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 1m15s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m21s
wai formal verification (integer core) / every harness in one shard (push) Successful in 18s
wai-ops integer operators / independent checker (Python) (push) Successful in 31s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 1m22s
wai-quantum-landscape-fpga reference design / does this change touch it (push) Successful in 34s
wai-quantum variational and algorithm library / does this change touch it (push) Successful in 16s
wai-quantum for JavaScript / does this change touch it (push) Successful in 39s
wai-quantum verdicts and repairs / does this change touch it (push) Successful in 18s
wai-quantum pulse and dynamics / does this change touch it (push) Successful in 29s
wai-quantum views in agent hosts / does this change touch it (push) Successful in 20s
wai-ops integer operators / wasm32 under Node (push) Successful in 47s
wai-sink-android native sink library / does this change touch it (push) Successful in 19s
WAI Studio / does this change touch it (push) Successful in 16s
wai-quantum error correction and mitigation / does this change touch it (push) Successful in 46s
wai-key-proxy / proxy · linux (push) Successful in 1m34s
wai-quantum extension points / does this change touch it (push) Successful in 50s
energy meters on real hardware / sink decode table · per-task energy and the GPU rail on macOS (push) Successful in 2m31s
wai-quantum for Python / does this change touch it (push) Successful in 51s
wai-ops integer operators / wasm32-wasip2 (wasmtime) (push) Successful in 1m7s
wai-sink-swift native sink package / does this change touch it (push) Successful in 28s
wai-quantum studio and desktop / does this change touch it (push) Successful in 49s
wai-quantum service / does this change touch it (push) Successful in 51s
wai-ledger service / service · macos (push) Successful in 1m22s
wai-vector vector graphics / does this change touch it (push) Successful in 27s
wai-ops integer operators / linux-x86_64 (push) Successful in 1m31s
wai-ops integer operators / macos-arm64 (push) Successful in 1m34s
wai-quantum wasmQ / profile, engines, reference modules, adversarial, vectors · macos (push) Successful in 2m55s
wai-ledger service / service · linux (push) Successful in 2m49s
wai-fleet service / service · macos (push) Successful in 1m2s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m7s
wai-quantum docs, course and locales / the Studio's words in every locale · linux (push) Successful in 13s
wai cross-architecture byte-equality / macos-arm64 (reference) (push) Successful in 40s
wai-quantum command line / command line · macos (push) Successful in 1m57s
wai-quantum wasmQ / interpreter and vectors under wasm32-wasip2 (wasmtime) (push) Successful in 3m48s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 5m23s
energy meters on real hardware / sink decode table · RAPL on jetson-hub (NVML surveyed) (push) Successful in 4m5s
wai-quantum contract / the policy is linked, and the deprecations page reads (push) Successful in 6s
wai-quantum wasmQ / profile, engines, reference modules, adversarial, vectors · linux (push) Successful in 5m2s
wai-quantum pulse and dynamics / the same bits · macos (push) Successful in 2m7s
wai formal verification (integer core) / kani (stage) (push) Successful in 4m55s
smart-byte conformance gate / Validate conformance JSON files (push) Successful in 8s
wai-quantum editors / editors · macos (push) Successful in 3m5s
smart-byte conformance gate / Second implementation reproduces every vector (push) Successful in 9s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 6m37s
smart-byte conformance gate / VERSIONS.md and README.md must exist (push) Successful in 9s
wai-audit collector and player integration / collector · linux (push) Successful in 2m5s
wai-int-module WAISM module of record / the module of record in Chromium (push) Successful in 18s
wai-quantum docs, course and locales / the course and the command line's languages · linux (push) Successful in 5m7s
wai-quantum service / service energy · linux packages (push) Successful in 2m45s
wai cross-architecture byte-equality / linux-x86_64 (push) Successful in 53s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 8m52s
wai formal verification (integer core) / kani (integer-core) (push) Successful in 7m27s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 9m26s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 9m29s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 10m15s
wai-fleet service / service · linux (push) Successful in 3m19s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 10m55s
wai-quantum for Python / python · macos (push) Successful in 4m45s
wai-quantum OpenQASM reader / corpus under wasm32-wasip2 (wasmtime) (push) Successful in 55s
wai-quantum contract / API compatibility · linux (push) Successful in 8m0s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 7m51s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 6m55s
wai-quantum other formats / the set under wasm32-wasip2 (wasmtime) (push) Successful in 3m4s
wai-quantum OpenQASM reader / reader and corpus · linux (push) Successful in 2m21s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 8m48s
wai formal verification (integer core) / kani (arith) (push) Successful in 11m38s
wai-quantum other formats / readers, writers and the set · linux (push) Successful in 4m17s
wai-quantum variational and algorithm library / parameters, gradients, optimisers, VQE, QAOA and builders · linux, release (push) Successful in 2m16s
wai-vector vector graphics / macos-arm64 (push) Successful in 2m49s
wai-quantum verdicts and repairs / the intent set and the verdict under wasm32-wasip2 (wasmtime) (push) Successful in 1m44s
wai-quantum editors / editors · linux (push) Successful in 6m6s
wai-quantum contract / schemas and the committed tool lists · linux (push) Successful in 11m2s
wai-quantum service / service · macos (push) Successful in 5m56s
wai-quantum pulse and dynamics / reader, simulator and pulse jobs · linux, release (push) Successful in 2m45s
wai-int-module WAISM module of record / linux · both checkers, wasmtime (Cranelift, Winch), Wasmi and Node (push) Successful in 8m56s
wai-quantum pulse and dynamics / reader, simulator and pulse jobs under wasm32-wasip2 (wasmtime) (push) Successful in 2m2s
wai-quantum verdicts and repairs / verdicts, repairs and the intent set · linux (push) Successful in 3m44s
wai-quantum for JavaScript / javascript · linux (push) Successful in 4m41s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m30s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 16m38s
joule-code conformance gate / joule-moq-relay, every target (push) Failing after 5m40s
wai-quantum extension points / the registry under wasm32-wasip2 (wasmtime) (push) Failing after 1m32s
wai-quantum resource estimation / estimation and calibration · linux, release (push) Successful in 9m28s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 11m46s
wai-quantum views in agent hosts / views · the page and its engine (push) Successful in 4m41s
wai-vector vector graphics / wasm32-wasip2 (wasmtime) (push) Successful in 1m11s
joule-code conformance gate / joule-moq-ingest, every target (push) Successful in 7m3s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 1m51s
wai-quantum error correction and mitigation / codes, decoders, texts and mitigation · linux, release (push) Successful in 3m47s
wai-vector vector graphics / wasm32 under Node (push) Successful in 39s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 17m57s
wai formal verification (integer core) / kani (plan) (push) Successful in 18m32s
wai-vector vector graphics / fuzz, peak allocation, comparison count (push) Successful in 19s
wai-quantum extension points / the registry and the example · linux (push) Failing after 4m38s
wai-vector vector graphics / linux-x86_64 (push) Successful in 3m4s
wai-ops integer operators / linux-aarch64 (push) Successful in 2m3s
wai-det-jpegai WAISM float-level module of record / linux · both checkers, F1, F2 on wasmtime (Cranelift, Winch, Pulley) and Wasmi, the planted faults (push) Successful in 20m30s
wai-quantum views in agent hosts / views · a scripted host (push) Successful in 7m14s
wai-quantum service / service · linux (push) Failing after 5m10s
wai cross-architecture byte-equality / linux-aarch64 (push) Successful in 52s
wai-vector vector graphics / wasm32 in three browser engines (push) Successful in 2m57s
wai-vector vector graphics / independent checker (Python) (push) Successful in 3m15s
wai-quantum-landscape-fpga reference design / the board's code on the board's processor's architecture · linux-arm64, release (push) Successful in 1m32s
wai-quantum for Python / python · linux (push) Successful in 8m18s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 10m50s
wai formal verification (integer core) / kani (sound) (push) Successful in 24m18s
wai-int-module WAISM module of record / linux-arm64 · both checkers, wasmtime (Cranelift, Winch), Wasmi and Node (push) Successful in 6m33s
wai release binaries build on Windows / the seven release binaries · windows (push) Failing after 25m52s
wai-quantum variational and algorithm library / parameters, gradients, optimisers, VQE, QAOA and builders · linux-arm64, release (push) Successful in 2m56s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 9m40s
wai-quantum pulse and dynamics / the same bits · linux-arm64 (push) Successful in 2m55s
wai-quantum studio and desktop / studio · browser (push) Successful in 11m54s
wai cross-architecture byte-equality / windows-x86_64 (push) Successful in 2m40s
wai-quantum extension points / the registry and the example · linux-arm64 (push) Failing after 2m58s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 4m25s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 14m4s
wai formal verification (integer core) / kani (isqrt512) (push) Successful in 30m23s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 14m8s
WAI Studio / Studio in WebKit · linux (push) Successful in 17m32s
wai-vector vector graphics / linux-aarch64 (push) Successful in 7m20s
wai-quantum command line / command line · linux (push) Successful in 30m0s
wai formal verification (integer core) / kani (stage-long) (push) Successful in 35m31s
wai-quantum-landscape-fpga reference design / the member against the model of the logic, and the logic against the model · linux, release (push) Successful in 27m59s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (push) Successful in 38m42s
wai-det-jpegai WAISM float-level module of record / linux-arm64 · both checkers, F1, F2 on wasmtime (Cranelift, Winch, Pulley) and Wasmi, the planted faults (push) Successful in 24m21s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 14m27s
wai formal verification (integer core) / kani (rans64-t2) (push) Successful in 13m24s
ci: a push to main takes a concurrency group of its own, so no later push cancels its run
Every workflow grouped its runs per ref with cancel-in-progress: true, and
15 of them called that safe on main because main advanced by merge commits.
It no longer does: work lands as direct commits, several an hour, and
Forgejo hands runners the oldest queued job first, so each cancelled run
was replaced at the back of the queue. With the linux-arm64 queue hours
deep, a workflow main touched more often than that never reached the
front. In the day to 2026-11-10 01:11Z, 578 of the 810 runs main's pushes
started were cancelled; after 13:58Z on 11-09 no run of joule-moq-room or
joule-moq-remote on main finished (31 of joule-moq-room's next 32 were
cancelled, the 32nd waiting), which has held the wai 0.6.0 release check
since 00:49Z.

Every workflow now groups by
  ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
with cancel-in-progress: true. A pull request's newer push still
supersedes its older run; a push to main (or a tag) has a group of its own
that no later push shares; a scheduled run keeps its branch's group, which
no push shares now, so only the next scheduled run supersedes it.
wai-breaking, wai-key-keychain, wai-key-windows and
wai-release-windows-build already grouped a push by its commit; the last
gains the explicit cancel-in-progress: true.

Not cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}. Forgejo
16.0.6 does evaluate the expression (EvaluateWorkflowConcurrency in the
runner's jobparser), but false with a group is Forgejo's QueueBehind mode
(ConfigureActionRunConcurrency, services/actions/workflows.go): a run is
not offered to a runner while another run of its group is running or an
older one is pending (getConcurrencyCondition, models/actions/task.go),
and nothing cancels a waiting one. That is GitHub's false without its
one-pending limit: main's runs of a workflow would go one at a time, each
through the whole label queue, and every push would add one more.

What bounds the queue: main holds one run per push that touches a
workflow, and Forgejo cancels a job left waiting 24 hours
(cancel_abandoned_jobs, ABANDONED_JOB_TIMEOUT; the forge keeps the
default), so a runner host that is down cannot keep more than a day's
pushes queued. Estimated from each workflow's last finished main run, a
day of main's pushes with every run kept needs about 950 linux-arm64 and
2700 macos slot-minutes, against at least 6 linux-arm64 slots (8640 a
day) and 3 macos (4320).

scripts/check-workflow-concurrency.sh now requires this block: it still
refuses a missing block and a group that does not vary by ref, and now
refuses a group per ref alone and cancel-in-progress other than true. Its
self-test first checks that the untouched tree passes, then that each
broken form fails with its own error.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 20:46:02 -05:00

417 lines
22 KiB
YAML

name: wai-quantum desktop release
# On a tag wai-quantum-desktop-v<version> (the version wai/wai-quantum-desktop/Cargo.toml
# and its tauri.conf.json state), builds Quantum Studio for the desktop on each runner and
# attaches its installers to the forge release of that tag, each with its SHA-256:
# macOS aarch64-apple-darwin and x86_64-apple-darwin: a disk image (.dmg), with the
# GPU state vector;
# Linux x86_64- and aarch64-unknown-linux-gnu: an AppImage, a Debian package and an
# RPM package;
# Windows x86_64-pc-windows-msvc: an installer (-setup.exe) and a portable archive
# (.zip) for package managers.
# Each build runs what it made before it is attached: the app's `check` (a Bell pair run in
# its worker, sealed and verified) from the bundle where the runner can run it, and its
# `update-status` (the app, the AppImage and the Debian package's app know their installer;
# the portable archive's copy knows none, so it never updates itself), the macOS app's code
# signature, the disk image's checksums, and the Debian package unpacked.
#
# Signing. Each kind is skipped, and the log says so, when its secrets are absent:
# - Updates: with the secret WAI_QUANTUM_DESKTOP_UPDATER_KEY (and, if the key has one,
# WAI_QUANTUM_DESKTOP_UPDATER_KEY_PASSWORD), every installer an update installs is
# signed (the macOS app as a .app.tar.gz, the AppImage, the packages, the Windows
# installer), and the built app's `verify-update` checks each signature with the public
# key in tauri.conf.json, for this version, before anything is attached. Without the
# secret the release has installers and no update: copies already installed are not
# offered it, and update themselves no further until a release is signed.
# - macOS: with APPLE_CERTIFICATE (a Developer ID Application certificate, .p12 in
# base64), APPLE_CERTIFICATE_PASSWORD and APPLE_SIGNING_IDENTITY, the app is signed with
# that identity; with APPLE_API_ISSUER, APPLE_API_KEY and APPLE_API_KEY_P8 (or APPLE_ID,
# APPLE_PASSWORD and APPLE_TEAM_ID) it is also notarised and stapled. Without them the
# app is signed ad hoc and not notarised, and the cask says how to open it.
#
# A last job writes the Homebrew cask and the Scoop manifest from the assets just
# attached, and, when the builds were signed for updates, the update manifest
# (latest.json), and attaches them to this version's release. The app asks the website
# for the update manifest (plugins.updater.endpoints), and the website takes it from
# this release when it is next deployed. Nothing is pushed to the tap or the bucket:
# their repositories take the cask and the manifest by hand.
#
# Uploads go to the forge directly over the private network (FORGE_API), never through
# the public tunnel. A runner that is down leaves its platform's assets missing: the
# release lists what each build attached, and the manifests leave that platform out.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: true
on:
push:
tags: ['wai-quantum-desktop-v*']
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
FORGE_API: http://100.64.224.0:3400/api/v1
FORGE_REPO: Transaction-Science/open-standards
PUBLIC: https://git.transaction.science/Transaction-Science/open-standards
# The command that bundles, pinned (cargo install --locked).
TAURI_CLI: 2.12.3
jobs:
version:
name: the tag is the app's version
runs-on: linux
outputs:
version: ${{ steps.v.outputs.version }}
steps:
- uses: actions/checkout@v5
- id: v
shell: bash
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
v="${tag#wai-quantum-desktop-v}"
crate="$(sed -n 's/^version = "\(.*\)"$/\1/p' wai/wai-quantum-desktop/Cargo.toml | head -1)"
conf="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' wai/wai-quantum-desktop/tauri.conf.json)"
[ "$v" = "$crate" ] || { echo "::error::the tag says $v, wai-quantum-desktop/Cargo.toml says $crate"; exit 1; }
[ "$v" = "$conf" ] || { echo "::error::the tag says $v, wai-quantum-desktop/tauri.conf.json says $conf"; exit 1; }
echo "version=$v" >> "$GITHUB_OUTPUT"
macos:
name: macOS · ${{ matrix.triple }}
needs: version
runs-on: macos
timeout-minutes: 120
strategy:
fail-fast: false
max-parallel: 1
matrix:
triple: [aarch64-apple-darwin, x86_64-apple-darwin]
env:
SECRET_UPDATER_KEY: ${{ secrets.WAI_QUANTUM_DESKTOP_UPDATER_KEY }}
SECRET_UPDATER_KEY_PASSWORD: ${{ secrets.WAI_QUANTUM_DESKTOP_UPDATER_KEY_PASSWORD }}
SECRET_APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
SECRET_APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
SECRET_APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
SECRET_APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
SECRET_APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
SECRET_APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
SECRET_APPLE_ID: ${{ secrets.APPLE_ID }}
SECRET_APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
SECRET_APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
steps:
- uses: actions/checkout@v5
# launchd's PATH omits cargo and node on the macOS host runner.
- name: Put cargo and node on PATH
shell: bash
run: |
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
echo "/opt/homebrew/bin" >> "$GITHUB_PATH"
# The app embeds the Studio's build, so it is built first.
- name: Build the Studio
shell: bash
working-directory: wai/quantum-studio
run: |
set -euo pipefail
npm ci --no-audit --no-fund
node build.mjs
# In a folder of its own, by version: the host's own cargo bin may hold another
# version for other work, which this leaves alone.
- name: The bundler, and the target
shell: bash
run: |
set -euo pipefail
rustup target add "${{ matrix.triple }}"
root="$HOME/.cache/wai-ci/tauri-cli-$TAURI_CLI"
[ -x "$root/bin/cargo-tauri" ] || nice -n 10 cargo install --locked tauri-cli --version "$TAURI_CLI" --root "$root" --target-dir "$RUNNER_TEMP/tauri-cli-build"
rm -rf "$RUNNER_TEMP/tauri-cli-build"
echo "$root/bin" >> "$GITHUB_PATH"
"$root/bin/cargo-tauri" tauri --version
- name: Build, sign where the secrets allow, and check
shell: bash
working-directory: wai/wai-quantum-desktop
env:
CI: 'true'
run: |
set -euo pipefail
t="${{ matrix.triple }}"; v="${{ needs.version.outputs.version }}"
cli="$(cargo tauri --version)"; [[ "$cli" == *" $TAURI_CLI" ]] || { echo "::error::cargo tauri is $cli, not $TAURI_CLI"; exit 1; }
source release/signing.sh
if [ -n "${SECRET_APPLE_SIGNING_IDENTITY:-}" ]; then
export APPLE_SIGNING_IDENTITY="$SECRET_APPLE_SIGNING_IDENTITY"
if [ -n "${SECRET_APPLE_CERTIFICATE:-}" ]; then
export APPLE_CERTIFICATE="$SECRET_APPLE_CERTIFICATE" APPLE_CERTIFICATE_PASSWORD="${SECRET_APPLE_CERTIFICATE_PASSWORD:-}"
fi
echo "macOS signing: with the identity APPLE_SIGNING_IDENTITY names"
if [ -n "${SECRET_APPLE_API_KEY_P8:-}" ]; then
(umask 077; printf '%s\n' "$SECRET_APPLE_API_KEY_P8" > "$RUNNER_TEMP/notary-key.p8")
export APPLE_API_ISSUER="$SECRET_APPLE_API_ISSUER" APPLE_API_KEY="$SECRET_APPLE_API_KEY" APPLE_API_KEY_PATH="$RUNNER_TEMP/notary-key.p8"
echo "notarisation: with the API key"
elif [ -n "${SECRET_APPLE_ID:-}" ]; then
export APPLE_ID="$SECRET_APPLE_ID" APPLE_PASSWORD="$SECRET_APPLE_PASSWORD" APPLE_TEAM_ID="$SECRET_APPLE_TEAM_ID"
echo "notarisation: with the Apple ID"
else
echo "::warning::notarisation skipped: neither APPLE_API_KEY_P8 nor APPLE_ID is set; the app is signed and not notarised"
fi
else
echo "macOS signing skipped: the secret APPLE_SIGNING_IDENTITY is not set; the app is signed ad hoc and not notarised"
fi
nice -n 10 cargo tauri build --ci --target "$t" --bundles app,dmg --features metal --config "$UPDATER_CONFIG" -- --locked
rm -f "$RUNNER_TEMP/notary-key.p8"
b="target/$t/release/bundle"
app="$b/macos/Quantum Studio.app"
exe="$app/Contents/MacOS/$(plutil -extract CFBundleExecutable raw -o - "$app/Contents/Info.plist")"
codesign --verify --deep --strict --verbose=2 "$app"
for d in "$b"/dmg/*.dmg; do hdiutil verify "$d"; done
if [ "$t" = "$(rustc -vV | sed -n 's/^host: //p')" ]; then
"$exe" check
# The app knows it was installed as an app (so, with a key, it updates itself).
s="$("$exe" update-status)"; echo "update-status: $s"
[[ "$s" == *'"installed_by":"app"'* ]] || { echo "::error::the bundled app does not know it is the installed app"; exit 1; }
if [ "$UPDATES_SIGNED" = 1 ]; then
for f in "$b"/macos/*.app.tar.gz; do "$exe" verify-update "$f" "$f.sig" "$v"; done
fi
else
echo "note: the $t app is not run here (another architecture)"
fi
bash release/collect.sh "$b" "$v" "$t" "$RUNNER_TEMP/dist"
- name: Attach to the release
shell: bash
working-directory: wai/wai-quantum-desktop
env:
FORGE_TOKEN: ${{ github.token }}
run: bash release/publish.sh "$GITHUB_REF_NAME" "Quantum Studio ${{ needs.version.outputs.version }}" "$RUNNER_TEMP"/dist/*
linux:
name: Linux · ${{ matrix.triple }}
needs: version
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- runner: linux
triple: x86_64-unknown-linux-gnu
- runner: linux-arm64
triple: aarch64-unknown-linux-gnu
env:
SECRET_UPDATER_KEY: ${{ secrets.WAI_QUANTUM_DESKTOP_UPDATER_KEY }}
SECRET_UPDATER_KEY_PASSWORD: ${{ secrets.WAI_QUANTUM_DESKTOP_UPDATER_KEY_PASSWORD }}
steps:
- uses: actions/checkout@v5
- name: Install Rust 1.99.1 and the web view's libraries
shell: bash
run: |
export PATH="$HOME/.cargo/bin:$PATH"
command -v rustup >/dev/null 2>&1 || \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.99.1
rustup toolchain install 1.99.1 --profile minimal
rustup default 1.99.1
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
$SUDO apt-get update -qq
$SUDO apt-get install -y -qq libwebkit2gtk-4.1-dev libxdo-dev libayatana-appindicator3-dev librsvg2-dev file xdg-utils
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Build the Studio
shell: bash
working-directory: wai/quantum-studio
run: |
set -euo pipefail
npm ci --no-audit --no-fund
node build.mjs
- name: The bundler
shell: bash
run: |
set -euo pipefail
cargo install --locked tauri-cli --version "$TAURI_CLI"
cargo tauri --version
- name: Build, sign for updates if the key is set, and check
shell: bash
working-directory: wai/wai-quantum-desktop
run: |
set -euo pipefail
t="${{ matrix.triple }}"; v="${{ needs.version.outputs.version }}"
cli="$(cargo tauri --version)"; [[ "$cli" == *" $TAURI_CLI" ]] || { echo "::error::cargo tauri is $cli, not $TAURI_CLI"; exit 1; }
source release/signing.sh
cargo tauri build --ci --target "$t" --bundles appimage,deb,rpm --config "$UPDATER_CONFIG" -- --locked
b="target/$t/release/bundle"
"target/$t/release/wai-quantum-desktop" check
# The AppImage runs (unpacked first: a container has no FUSE), and knows it is one.
for a in "$b"/appimage/*.AppImage; do
APPIMAGE_EXTRACT_AND_RUN=1 "$a" check
s="$(APPIMAGE_EXTRACT_AND_RUN=1 "$a" update-status)"; echo "update-status: $s"
[[ "$s" == *'"installed_by":"appimage"'* ]] || { echo "::error::the AppImage does not know it is one"; exit 1; }
done
# The Debian package holds the app, which runs and knows the package installed it.
for d in "$b"/deb/*.deb; do
dpkg-deb --info "$d" | grep -E '^ (Package|Version|Depends):'
rm -rf "$RUNNER_TEMP/deb" && dpkg-deb -x "$d" "$RUNNER_TEMP/deb"
"$RUNNER_TEMP/deb/usr/bin/wai-quantum-desktop" check
s="$("$RUNNER_TEMP/deb/usr/bin/wai-quantum-desktop" update-status)"; echo "update-status: $s"
[[ "$s" == *'"installed_by":"deb"'* ]] || { echo "::error::the Debian package's app does not know the package installed it"; exit 1; }
done
if [ "$UPDATES_SIGNED" = 1 ]; then
for f in "$b"/appimage/*.AppImage "$b"/deb/*.deb "$b"/rpm/*.rpm; do
"target/$t/release/wai-quantum-desktop" verify-update "$f" "$f.sig" "$v"
done
fi
bash release/collect.sh "$b" "$v" "$t" "$RUNNER_TEMP/dist"
- name: Attach to the release
shell: bash
working-directory: wai/wai-quantum-desktop
env:
FORGE_TOKEN: ${{ github.token }}
run: bash release/publish.sh "$GITHUB_REF_NAME" "Quantum Studio ${{ needs.version.outputs.version }}" "$RUNNER_TEMP"/dist/*
windows:
name: Windows · x86_64-pc-windows-msvc
needs: version
runs-on: windows
timeout-minutes: 120
env:
SECRET_UPDATER_KEY: ${{ secrets.WAI_QUANTUM_DESKTOP_UPDATER_KEY }}
SECRET_UPDATER_KEY_PASSWORD: ${{ secrets.WAI_QUANTUM_DESKTOP_UPDATER_KEY_PASSWORD }}
steps:
- uses: actions/checkout@v5
- name: Build the Studio
shell: pwsh
working-directory: wai/quantum-studio
run: |
$ErrorActionPreference = 'Stop'
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
node --version
npm ci --no-audit --no-fund
if ($LASTEXITCODE -ne 0) { throw "npm ci failed" }
node build.mjs
if ($LASTEXITCODE -ne 0) { throw "the Studio's build failed" }
- name: Build, sign for updates if the key is set, check and package
shell: pwsh
working-directory: wai/wai-quantum-desktop
run: |
$ErrorActionPreference = 'Stop'
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
$t = 'x86_64-pc-windows-msvc'
$v = "${{ needs.version.outputs.version }}"
$name = "wai-quantum-desktop-$v-$t"
$dist = Join-Path $env:RUNNER_TEMP 'dist'
$stage = Join-Path $env:RUNNER_TEMP $name
Remove-Item $stage, $dist -Recurse -Force -ErrorAction Ignore
New-Item -ItemType Directory -Force -Path $stage, $dist | Out-Null
# A run of the built app, waited for (it is a windowed program), its output kept.
function Run-App($exe, [string[]] $appArgs) {
$out = Join-Path $env:RUNNER_TEMP 'app.out'; $err = Join-Path $env:RUNNER_TEMP 'app.err'
$quoted = ($appArgs | ForEach-Object { '"' + $_ + '"' }) -join ' '
$p = Start-Process -FilePath $exe -ArgumentList $quoted -Wait -PassThru -NoNewWindow -RedirectStandardOutput $out -RedirectStandardError $err
$o = (Get-Content $out -Raw) + ''
Write-Host $o (Get-Content $err -Raw)
if ($p.ExitCode -ne 0) { throw "$exe $($appArgs -join ' ') exited $($p.ExitCode)" }
return $o
}
# The portable copy first, as cargo builds it: the installer's copy is marked as
# installed by it (and so updates itself), the archive's is not (a package manager
# updates it).
cargo build --locked --release --target $t
if ($LASTEXITCODE -ne 0) { throw "the build failed" }
$exe = "target\$t\release\wai-quantum-desktop.exe"
Copy-Item $exe, README.md, ..\LICENSE $stage
Run-App (Join-Path $stage 'wai-quantum-desktop.exe') @('check') | Out-Null
# The portable copy knows no installer made it, so it never updates itself.
$s = Run-App (Join-Path $stage 'wai-quantum-desktop.exe') @('update-status')
if ($s -notmatch '"installed_by":null' -or $s -notmatch '"updates_itself":false') { throw "the portable copy would update itself: $s" }
$updaterConfig = '{}'
if ($env:SECRET_UPDATER_KEY) {
$pub = (Get-Content tauri.conf.json -Raw | ConvertFrom-Json).plugins.updater.pubkey
if (-not $pub) { throw "the update key is set, but tauri.conf.json has no plugins.updater.pubkey: put the public key there first" }
$env:TAURI_SIGNING_PRIVATE_KEY = $env:SECRET_UPDATER_KEY
$env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD = "$env:SECRET_UPDATER_KEY_PASSWORD"
$updaterConfig = '{"bundle":{"createUpdaterArtifacts":true}}'
Write-Output "update signing: on, with the key the secret WAI_QUANTUM_DESKTOP_UPDATER_KEY holds"
} else {
Write-Output "update signing skipped: the secret WAI_QUANTUM_DESKTOP_UPDATER_KEY is not set; this release has installers and no update, and installed copies are not offered it"
}
# The bundler in a folder of its own, by version, ahead of any other on PATH.
$root = Join-Path $env:USERPROFILE ".cache\wai-ci\tauri-cli-$env:TAURI_CLI"
if (-not (Test-Path (Join-Path $root 'bin\cargo-tauri.exe'))) {
cargo install --locked tauri-cli --version $env:TAURI_CLI --root $root --target-dir (Join-Path $env:RUNNER_TEMP 'tauri-cli-build')
if ($LASTEXITCODE -ne 0) { throw "the bundler did not install" }
Remove-Item (Join-Path $env:RUNNER_TEMP 'tauri-cli-build') -Recurse -Force -ErrorAction Ignore
}
$env:PATH = (Join-Path $root 'bin') + [IO.Path]::PathSeparator + $env:PATH
$cli = (cargo tauri --version) -join ''
if ($cli -notmatch " $([regex]::Escape($env:TAURI_CLI))$") { throw "cargo tauri is $cli, not $env:TAURI_CLI" }
# '--' quoted, so PowerShell hands it to cargo as it is.
cargo tauri build --ci --target $t --bundles nsis --config $updaterConfig '--' --locked
if ($LASTEXITCODE -ne 0) { throw "the bundle failed" }
$setup = @(Get-ChildItem "target\$t\release\bundle\nsis\*-setup.exe")
if ($setup.Count -ne 1) { throw "the bundle holds $($setup.Count) installers, not one" }
$installer = Join-Path $dist "$name-setup.exe"
Copy-Item $setup[0].FullName $installer
if ($updaterConfig -ne '{}') {
Copy-Item "$($setup[0].FullName).sig" "$installer.sig"
Run-App (Join-Path $stage 'wai-quantum-desktop.exe') @('verify-update', $installer, "$installer.sig", $v) | Out-Null
}
Compress-Archive -Path $stage -DestinationPath (Join-Path $dist "$name.zip") -Force
foreach ($f in "$name-setup.exe", "$name.zip") {
$h = (Get-FileHash (Join-Path $dist $f) -Algorithm SHA256).Hash.ToLower()
Set-Content -NoNewline -Path (Join-Path $dist "$f.sha256") -Value "$h $f`n"
Write-Output "packaged $f ($h)"
}
- name: Attach to the release
shell: pwsh
env:
FORGE_TOKEN: ${{ github.token }}
run: |
$ErrorActionPreference = 'Stop'
$H = @{ Authorization = "token $env:FORGE_TOKEN" }
$base = "$env:FORGE_API/repos/$env:FORGE_REPO/releases"
$tag = $env:GITHUB_REF_NAME
$rel = $null
for ($i = 0; $i -lt 6 -and -not $rel; $i++) {
try { $rel = Invoke-RestMethod -Headers $H -Uri "$base/tags/$tag" } catch {
try { Invoke-RestMethod -Headers $H -Method Post -Uri $base -ContentType 'application/json' -Body (@{ tag_name = $tag; name = "Quantum Studio ${{ needs.version.outputs.version }}" } | ConvertTo-Json) | Out-Null } catch {}
Start-Sleep -Seconds 3
}
}
if (-not $rel) { throw "no release for $tag" }
foreach ($f in Get-ChildItem (Join-Path $env:RUNNER_TEMP 'dist')) {
foreach ($a in (Invoke-RestMethod -Headers $H -Uri "$base/$($rel.id)/assets" | Where-Object { $_.name -eq $f.Name })) {
Invoke-RestMethod -Headers $H -Method Delete -Uri "$base/$($rel.id)/assets/$($a.id)" | Out-Null
}
curl.exe -fsS -H "Authorization: token $env:FORGE_TOKEN" -X POST "$base/$($rel.id)/assets?name=$($f.Name)" -F "attachment=@$($f.FullName);type=application/octet-stream" -o NUL -w "uploaded $($f.Name): http %{http_code}`n"
if ($LASTEXITCODE -ne 0) { throw "the upload of $($f.Name) failed" }
}
# The package managers' entries and the update manifest, for the assets just attached:
# a Homebrew cask (macOS), a Scoop manifest (Windows) and latest.json. A platform whose
# build did not attach its assets is left out, and the log says so.
manifests:
name: Homebrew cask, Scoop manifest and update manifest
needs: [version, macos, linux, windows]
if: always() && needs.version.result == 'success'
runs-on: linux
steps:
- uses: actions/checkout@v5
- name: Write and attach them
shell: bash
working-directory: wai/wai-quantum-desktop
env:
FORGE_TOKEN: ${{ github.token }}
MACOS_NOTARISED: ${{ secrets.APPLE_SIGNING_IDENTITY != '' && (secrets.APPLE_API_KEY_P8 != '' || secrets.APPLE_ID != '') }}
run: |
set -euo pipefail
v="${{ needs.version.outputs.version }}"
out="$RUNNER_TEMP/manifests"; mkdir -p "$out"
python3 release/manifests.py "$v" "$GITHUB_REF_NAME" "$FORGE_API" "$FORGE_REPO" "$PUBLIC" "$out"
files=()
for f in wai-quantum-desktop.rb wai-quantum-desktop.json latest.json; do
if [ -f "$out/$f" ]; then files+=("$out/$f"); fi
done
bash release/publish.sh "$GITHUB_REF_NAME" "Quantum Studio $v" "${files[@]}"
if [ -f "$out/latest.json" ]; then
echo "latest.json is on this release; installed copies find $v once the website serves it (scripts/sync-desktop-update.sh $v in its repository)"
fi