From c3b1e8c370f947a5d8da10508c33b84a13e9afa4 Mon Sep 17 00:00:00 2001 From: CRT Date: Thu, 15 Oct 2026 00:42:39 +0800 Subject: [PATCH] release candidates: every push to main runs the full CI, the acceptance matrix and the Linux / Windows installer builds in parallel (ci.yml) and records the green result per commit as the draft rc- (green.json last); a version-only bump commit (proven file by file with the version writers) reuses its parent's CI and matrix and rebuilds only the installers; a v* tag only promotes the candidate (release.yml), fails fast on a red or missing commit; acceptance runs carry a CLI key; tools/release.ts does the whole release on the Mac (bump, CHANGELOG cut, fast gate, push, tag, Mac build + notarize in parallel with Forgejo, cached macOS acceptance, add, verify, publish with one upload of the Mac files, /Applications update, npm commands) and --rehearsal; DMG notarization runs while the app and updater bundle are checked; rehearsal channel on the VDS; docs --- .forgejo/workflows/ci.yml | 296 ++++++++++++++-- .forgejo/workflows/release.yml | 425 +++-------------------- CHANGELOG.md | 12 + docs/desktop-release.md | 161 ++++++--- tests/desktop-release.test.ts | 2 +- tests/release-pipeline.test.ts | 113 ++++++ tools/acceptance.ts | 21 +- tools/cli-key.ts | 48 +++ tools/desktop-release.ts | 157 ++++++--- tools/forgejo-release.ts | 67 +++- tools/release-candidate.ts | 285 +++++++++++++++ tools/release.ts | 361 +++++++++++++++++++ tools/vds/apache-storebound-desktop.conf | 3 +- tools/vds/storebound-desktop-publish | 13 +- 14 files changed, 1438 insertions(+), 526 deletions(-) create mode 100644 tests/release-pipeline.test.ts create mode 100644 tools/cli-key.ts create mode 100644 tools/release-candidate.ts create mode 100644 tools/release.ts diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index d816b74..6966fcd 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -1,15 +1,23 @@ -# bun run ci on every push to main and every pull request into main: adapters, desktop version, tests, typecheck, -# desktop i18n / typecheck / build, desktop boot check (headless Chromium), desktop Rust tests and clippy, scrub. Runs on the VDS runner (docs/desktop-release.md). -# tools/ci.ts runs the steps as a task graph and the tests as parallel shards (CPU cores - 1, max 8) on the one -# self-hosted runner, so a job matrix would only queue; CI=full makes it run the Rust steps on every push. -# Publishes nothing: site/ is still published by Jenkins; releases come from release.yml (a v* tag). -# acceptance-quick: the quick subset of the acceptance matrix (tools/acceptance.ts) on the Linux and Windows runners. -# release.yml's ci job runs the same steps (the setup is shared: .forgejo/actions/linux-setup). +# 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. CI=full runs the Rust steps on every push. +# 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 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] + branches: [main, 'rehearsal/**'] pull_request: branches: [main] workflow_dispatch: @@ -21,11 +29,37 @@ env: CI: full 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: | + if [ "$EVENT" = pull_request ]; then echo "reuse=false" >> "$GITHUB_OUTPUT"; echo "pull request: everything runs"; exit 0; fi + curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" >/dev/null + export PATH="$HOME/.bun/bin:$PATH" + bun tools/release-candidate.ts gate --sha "$SHA" --wait 40 + ci: + needs: gate + if: needs.gate.outputs.reuse != 'true' # runner label: ubuntu-22.04:docker://ghcr.io/catthehacker/ubuntu:act-22.04 runs-on: ubuntu-22.04 - # the self-hosted runner only runs 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: 60 steps: - uses: actions/checkout@v4 @@ -35,43 +69,255 @@ jobs: rust-components: clippy - uses: ./.forgejo/actions/ci-run - # The quick subset of the release acceptance matrix (tools/acceptance.ts --quick: --version, init, check, scan, the - # desktop [Y/n] prompt answered n, mcp, a console plan, store --dry writes and confirm prompts answered n) from the - # packed npm tarball with real piped stdin, on Linux and Windows with Node 20. The full matrix gates the release. - acceptance-quick: - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + # 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). + acceptance: + needs: gate + if: needs.gate.outputs.reuse != 'true' strategy: fail-fast: false matrix: include: - - { os: ubuntu-22.04, platform: Linux } - - { os: windows, platform: Windows } + - { 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: 20 + timeout-minutes: 45 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '20' + 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 quick (Linux, Node 20) + - name: Acceptance matrix (Linux) if: ${{ matrix.platform == 'Linux' }} shell: bash - run: bun tools/acceptance.ts run --runtime node --platform Linux --quick --jobs 4 --out acceptance/Linux-node20-quick.json - - name: Acceptance quick (Windows, Node 20) + run: | + node --version && bun --version + 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: | - bun tools/acceptance.ts run --runtime node --platform Windows --quick --jobs 3 --out acceptance/Windows-node20-quick.json - if ($LASTEXITCODE -ne 0) { throw "acceptance quick: red cells" } + node --version; bun --version + 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-quick-${{ matrix.platform }} + 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() { #