# Every push to main is a release candidate (docs/desktop-release.md, "Release candidates"). In parallel: # ci bun run ci (adapters, desktop version, tests, typecheck, desktop i18n / typecheck / build, the boot check # in headless Chromium, desktop Rust tests and clippy, scrub); tools/ci.ts runs it as a task graph with # parallel test shards on the one Linux runner; no browser test runs here (tests/browser/, `bun run # test:browser` by hand). The Rust steps run when the push changed a Rust input (STOREBOUND_CI_BASE). # acceptance the release acceptance matrix (tools/acceptance.ts): every user-facing command from the packed npm # tarball with real piped stdin, Linux and Windows x Node 20 / Node 22 / Bun. # linux AppImage + deb (path-free build), self-tests, both started under Xvfb, privacy scan. # windows NSIS installer, silent per-user install, self-test, uninstall, the Node prompt smoke, privacy scan. # record when all are green (the installers already scanned with the private terms): the draft release rc- with the installers, the acceptance runs and matrix and # green.json (tools/release-candidate.ts record). A v* tag on this commit only promotes it (release.yml). # gate decides first: a commit that differs from its green parent only in version copies + CHANGELOG (a release's # version bump, proven file by file: tools/release-candidate.ts gate) reuses the parent's ci and acceptance results # and builds only the installers, whose embedded version changed. # Pull requests run ci + acceptance (no installers, no record). rehearsal/* branches are release rehearsals # (tools/release.ts --rehearsal). site/ is still published by Jenkins. name: ci on: push: branches: [main, 'rehearsal/**'] pull_request: branches: [main] workflow_dispatch: env: BUN_VERSION: '1.4.0' CARGO_TERM_COLOR: never CARGO_INCREMENTAL: '0' jobs: gate: runs-on: ubuntu-22.04 # the self-hosted runners only run code from this repo: pull requests from forks are skipped if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository timeout-minutes: 45 outputs: reuse: ${{ steps.gate.outputs.reuse }} from: ${{ steps.gate.outputs.from }} steps: - uses: actions/checkout@v4 with: fetch-depth: 2 - name: Version-only commit of a green parent? id: gate env: FORGEJO_TOKEN: ${{ secrets.RELEASE_TOKEN }} FORGEJO_URL: ${{ github.server_url }} FORGEJO_ORIGIN: ${{ secrets.FORGEJO_ORIGIN }} EVENT: ${{ github.event_name }} SHA: ${{ github.sha }} run: | # tests never gate a release (the owner's decision 2026-10-16): no waiting on the parent's run, which the # release's own bump push cancels; every commit builds its installers and runs the tests in the background echo "reuse=false" >> "$GITHUB_OUTPUT"; echo "from=" >> "$GITHUB_OUTPUT"; echo "tests run in the background; installers build for this commit" # background tests (the owner's decision 2026-10-16): they run on every push and report, but the record job (the # release candidate) never waits for them; `bun tools/release.ts` and `bun tools/ci-status.ts` print a red result ci: needs: gate if: github.event_name != 'pull_request' || true # runner label: ubuntu-22.04:docker://ghcr.io/catthehacker/ubuntu:act-22.04 runs-on: ubuntu-22.04 timeout-minutes: 60 steps: - uses: actions/checkout@v4 with: fetch-depth: 2 - uses: ./.forgejo/actions/linux-setup with: cache-key: ci rust-components: clippy - uses: ./.forgejo/actions/ci-run # The release acceptance matrix (tools/acceptance.ts, docs/acceptance.md): every user-facing command from the packed # npm tarball, in fresh temp folders with piped stdin and the fake stores, on Linux and Windows x Node 20 / Node 22 # (npx --package=) / Bun (bun x). One red cell and the commit is no candidate. macOS runs on the owner's Mac # (tools/release.ts, cached per CLI key). # after the installers: the one Windows runner builds the release installer first, the background tests wait # (0.8.13's Windows installer queued behind six acceptance jobs) acceptance: needs: [gate, linux, windows] if: github.event_name != 'pull_request' || true strategy: fail-fast: false matrix: include: - { os: ubuntu-22.04, platform: Linux, runtime: node, node: '20', name: node20 } - { os: ubuntu-22.04, platform: Linux, runtime: node, node: '22', name: node22 } - { os: ubuntu-22.04, platform: Linux, runtime: bun, node: '22', name: bun } - { os: windows, platform: Windows, runtime: node, node: '20', name: node20 } - { os: windows, platform: Windows, runtime: node, node: '22', name: node22 } - { os: windows, platform: Windows, runtime: bun, node: '22', name: bun } runs-on: ${{ matrix.os }} timeout-minutes: 45 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - name: Bun (Linux) if: ${{ matrix.platform == 'Linux' }} shell: bash run: | curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" >/dev/null echo "$HOME/.bun/bin" >> "${FORGEJO_PATH:-$GITHUB_PATH}" - name: Acceptance matrix (Linux) if: ${{ matrix.platform == 'Linux' }} shell: bash run: | node --version && bun --version # npm pack runs prepack, which builds the package's declarations with the pinned TypeScript bun install --frozen-lockfile bun tools/acceptance.ts run --runtime ${{ matrix.runtime }} --platform Linux --jobs 4 --out acceptance/Linux-${{ matrix.name }}.json - name: Acceptance matrix (Windows) if: ${{ matrix.platform == 'Windows' }} shell: pwsh run: | node --version; bun --version # npm pack runs prepack, which builds the package's declarations with the pinned TypeScript bun install --frozen-lockfile if ($LASTEXITCODE -ne 0) { throw "bun install failed" } bun tools/acceptance.ts run --runtime ${{ matrix.runtime }} --platform Windows --jobs 3 --out acceptance/Windows-${{ matrix.name }}.json if ($LASTEXITCODE -ne 0) { throw "acceptance matrix: red cells" } - uses: actions/upload-artifact@v3 if: ${{ always() }} with: name: acceptance-${{ matrix.platform }}-${{ matrix.name }} path: acceptance/ retention-days: 14 linux: needs: gate if: github.event_name != 'pull_request' # runner label: ubuntu-22.04:docker://ghcr.io/catthehacker/ubuntu:act-22.04 (glibc 2.35: the AppImage and the # deb then run on Ubuntu 22.04 and newer, Debian 12) runs-on: ubuntu-22.04 timeout-minutes: 90 env: # the path-free build keeps the target folder outside every home folder CARGO_TARGET_DIR: /tmp/storebound-target # linuxdeploy (AppImage) runs without FUSE inside the container APPIMAGE_EXTRACT_AND_RUN: '1' # linuxdeploy must not strip: the storebound sidecar is a `bun build --compile` executable whose JS payload strip # removes (it then starts as plain bun). The Rust binary is already stripped by Cargo (profile.release strip). NO_STRIP: '1' # WebKitGTK's DMABUF renderer leaves the window blank on some GPU / VM setups; the app sets this itself at # start (src-tauri/src/lib.rs linux_webkit_env) unless the user did; set here too for anything run in CI WEBKIT_DISABLE_DMABUF_RENDERER: '1' steps: - uses: actions/checkout@v4 - uses: ./.forgejo/actions/linux-setup with: cache-key: desktop packages: patchelf file squashfs-tools p7zip-full xz-utils # the release target folder (dependencies compiled once; a version bump recompiles only the app crate) - uses: actions/cache@v4 with: path: /tmp/storebound-target key: desktop-target-${{ runner.os }}-${{ hashFiles('apps/desktop/src-tauri/Cargo.lock') }} restore-keys: desktop-target-${{ runner.os }}- - name: Install and check run: | bun install --frozen-lockfile bun tools/desktop-version.ts bun tools/build-sidecars.ts --target bun-linux-x64 # linuxdeploy (AppImage) sets the rpath of every executable with its own patchelf, which corrupts the bun # sidecar (segfault, then ldd fails in the gtk plugin). With the rpath already set (Ubuntu's patchelf, which # keeps it working), linuxdeploy leaves the file as it is. The sidecar needs only libc, so the rpath is inert. patchelf --set-rpath '$ORIGIN/../lib' apps/desktop/src-tauri/binaries/storebound-x86_64-unknown-linux-gnu apps/desktop/src-tauri/binaries/storebound-x86_64-unknown-linux-gnu version - name: Build AppImage and deb (path-free) working-directory: apps/desktop run: | rm -rf "$CARGO_TARGET_DIR/release/bundle" bun ../../tools/desktop-release.ts exec -- bunx tauri build --bundles appimage,deb --ci - name: Self-test the exact artifacts run: | V=$(bun tools/desktop-version.ts --print) B="$CARGO_TARGET_DIR/release/bundle" mkdir -p out && cp "$B/appimage/storebound_${V}_amd64.AppImage" "$B/deb/storebound_${V}_amd64.deb" out/ chmod +x out/*.AppImage (cd /tmp && rm -rf squashfs-root && "$GITHUB_WORKSPACE/out/storebound_${V}_amd64.AppImage" --appimage-extract >/dev/null && squashfs-root/usr/bin/storebound-desktop --self-test) rm -rf /tmp/deb && dpkg-deb -x "out/storebound_${V}_amd64.deb" /tmp/deb && /tmp/deb/usr/bin/storebound-desktop --self-test dpkg-deb -I "out/storebound_${V}_amd64.deb" | sed -n '1,20p' - name: Start the app under Xvfb (AppImage and deb) # --self-test checks only the sidecar; this opens the real window (GTK, WebKitGTK, the bundled libraries of the # AppImage) on a virtual display. "sidecar started" in host.log means the webview ran the UI and called the host. run: | sudo apt-get install -y -qq --no-install-recommends xvfb xauth dbus-x11 >/dev/null V=$(bun tools/desktop-version.ts --print) smoke() { #