From 6c9c19d411d22318737ba73c0666a5a15f45b97a Mon Sep 17 00:00:00 2001 From: Michael Yankelev Date: Mon, 14 Sep 2026 02:24:12 +0200 Subject: [PATCH] chore(ci): move the updater key check to the main gate The Updater Key job ran on every pull request with the Tauri release signing key in its step environment, after installing the pull request's own tree. It now runs on a push to main and on a dispatch that is pinned to main, so the secret only ever meets reviewed code. The drift check itself is unchanged. ci-repo.yml no longer declares the two secrets and ci.yml no longer forwards them. Updater Key is not a required status context, so branch protection is unchanged. --- .github/workflows/ci-repo.yml | 61 --------------------------- .github/workflows/ci.yml | 3 -- .github/workflows/updater-key.yml | 68 +++++++++++++++++++++++++++++++ blueprint/testing.md | 10 ++--- 4 files changed, 73 insertions(+), 69 deletions(-) create mode 100644 .github/workflows/updater-key.yml diff --git a/.github/workflows/ci-repo.yml b/.github/workflows/ci-repo.yml index 48508603a2..22a16ae449 100644 --- a/.github/workflows/ci-repo.yml +++ b/.github/workflows/ci-repo.yml @@ -4,11 +4,6 @@ name: CI Repo on: workflow_call: - secrets: - TAURI_SIGNING_PRIVATE_KEY: - required: false - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: - required: false permissions: {} @@ -69,62 +64,6 @@ jobs: - name: No source comment names a tracking issue run: node scripts/check-tracker-refs.mjs - # The updater refuses any release that its configured key did not sign. A - # pubkey that drifts from the CI signing secret thus ships an app that can - # never update again. This check answers to no path filter, because a key - # rotation touches none, and the signing key stays out of every job that runs - # the desktop crate build scripts. Dependabot runs receive no repository - # secrets, so the job skips there. - updater-key: - name: Updater Key - if: github.actor != 'dependabot[bot]' - runs-on: ubuntu-latest - permissions: - contents: read - steps: - # No git operation follows the checkout, and this job holds the signing - # secret, so the job token stays out of the workspace .git/config. - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version-file: 'package.json' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Install minisign - run: | - sudo apt-get update - sudo apt-get install -y minisign - - - name: Assert the updater pubkey matches the signing key - env: - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - run: | - set -euo pipefail - if [ -z "${TAURI_SIGNING_PRIVATE_KEY}" ]; then - echo "::error::TAURI_SIGNING_PRIVATE_KEY is unavailable to this run, so the updater key is unverifiable" - exit 1 - fi - WORK=$(mktemp -d) - # Tauri wraps both the public key and the signature file in base64; - # minisign wants the unwrapped bytes. - jq -er '.plugins.updater.pubkey' apps/desktop/src-tauri/tauri.conf.json \ - | base64 -d > "${WORK}/updater.pub" - echo 'cipherbox updater key check' > "${WORK}/payload" - # `exec`, not the package's `tauri` script: that wrapper appends a - # `--config` the `signer` subcommand rejects. - pnpm --filter @cipherbox/desktop exec tauri signer sign "${WORK}/payload" - base64 -d < "${WORK}/payload.sig" > "${WORK}/payload.minisig" - minisign -V -p "${WORK}/updater.pub" -m "${WORK}/payload" -x "${WORK}/payload.minisig" - # The mock /routing/v1 record store holds the monotonic-sequence rule that a # real routing endpoint holds, so a rollback cannot reach a client. The suite # runs here, under no path filter, because the Repo area is the only gate that diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ecbf43941b..56e462c30d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,9 +101,6 @@ jobs: permissions: contents: read uses: ./.github/workflows/ci-repo.yml - secrets: - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} repo-result: name: Repo Result diff --git a/.github/workflows/updater-key.yml b/.github/workflows/updater-key.yml new file mode 100644 index 0000000000..36e34604fa --- /dev/null +++ b/.github/workflows/updater-key.yml @@ -0,0 +1,68 @@ +# The updater refuses any release that its configured key did not sign. A +# pubkey that drifts from the CI signing secret thus ships an app that can +# never update again. The check has to compare the two, and the comparison +# needs the private key, so it runs on the main gate: a push to main carries +# reviewed code, while a pull-request job would hand the release signing key +# to a tree the author still controls. The cost is that drift is reported +# after the merge. The check answers to no path filter, because a key rotation +# touches none. +name: Updater Key + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: {} + +jobs: + updater-key: + name: Updater Key + # The secret stays on reviewed code: a dispatch cannot aim this job at a branch. + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + # No git operation follows the checkout, and this job holds the signing + # secret, so the job token stays out of the workspace .git/config. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: 'package.json' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install minisign + run: | + sudo apt-get update + sudo apt-get install -y minisign + + - name: Assert the updater pubkey matches the signing key + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + run: | + set -euo pipefail + if [ -z "${TAURI_SIGNING_PRIVATE_KEY}" ]; then + echo "::error::TAURI_SIGNING_PRIVATE_KEY is unavailable to this run, so the updater key is unverifiable" + exit 1 + fi + WORK=$(mktemp -d) + # Tauri wraps both the public key and the signature file in base64; + # minisign wants the unwrapped bytes. + jq -er '.plugins.updater.pubkey' apps/desktop/src-tauri/tauri.conf.json \ + | base64 -d > "${WORK}/updater.pub" + echo 'cipherbox updater key check' > "${WORK}/payload" + # `exec`, not the package's `tauri` script: that wrapper appends a + # `--config` the `signer` subcommand rejects. + pnpm --filter @cipherbox/desktop exec tauri signer sign "${WORK}/payload" + base64 -d < "${WORK}/payload.sig" > "${WORK}/payload.minisig" + minisign -V -p "${WORK}/updater.pub" -m "${WORK}/payload" -x "${WORK}/payload.minisig" diff --git a/blueprint/testing.md b/blueprint/testing.md index 3a7c63869c..a490f1bac3 100644 --- a/blueprint/testing.md +++ b/blueprint/testing.md @@ -219,11 +219,11 @@ is not the contract gate. Path-filtered like v1 (the dorny pattern and reusable-workflow structure port), reorganized into three tiers: -| Tier | Trigger | Contents | -| ---------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **PR gate** — merge-blocking | every PR | five areas, each one reusable workflow reported through a single stable result context — **Repo** (lint, the tracker-reference scan `Tracker Refs`, the updater-key check); **API** (typecheck, the unit suite, the real-Postgres integration suite, DB migration drift, OpenAPI freshness); **Rust** (fmt + clippy, `Core KATs (native + WASM)`, the engine simulation, the fuse operation core `FUSE Op Core`, the workspace tests, and one adapter leg per shipped desktop platform — macOS and Windows each run a workspace check over all targets, the tests of the OS adapter crates, and the keyring conformance suite against the real OS backend, and neither runs the engine simulation, which is platform-neutral); **Web** (the shared packages, the `apps/web` host suite, the engine WASM artifact, the bundle, the `packages/client` browser suite); **Desktop** (the shell frontend suites and the unsigned shell build on all three platforms). Beside the areas stand the contract suite on the CI stack (`Contract Suite Result`) and an e2e **smoke slice** (`Web E2E Smoke`, reported through the stable `Web E2E Smoke Result` context) — a bounded-minutes budget of web login-and-CRUD plus one timing-profile cross-client scenario. Branch protection requires the area result contexts and these standalone contexts, never a job inside an area (ADR 0018) | -| **Main gate** | push to main | the full web-e2e suite, the desktop mounted matrix (macOS/Linux/Windows), the full cross-client matrix. Failure is treated revert-first, not fix-forward — this tier exists to bound the blast radius of what the smoke slice missed, never to be the first line | -| **Dispatch / scheduled** | manual or cron | the load harness (`crates/load`, v1's `tests/load/` scenarios ported onto the v2 surface — it drives the engine's real API client, so it is Rust beside the contract suite rather than a TS package); long-horizon liveness — lease renewal at seq+1 and the republisher walk against a compressed-EOL profile; staging release gates (mechanics → [FSM1/cipher-box-next#48](https://github.com/FSM1/cipher-box-next/issues/48)); the per-stage profiling benches (`Perf Benches`, criterion over `crates/core` and `crates/engine` on the in-memory seam fakes and the virtual clock — shared-runner timings are too noisy to gate a merge on) | +| Tier | Trigger | Contents | +| ---------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **PR gate** — merge-blocking | every PR | five areas, each one reusable workflow reported through a single stable result context — **Repo** (lint, the tracker-reference scan `Tracker Refs`); **API** (typecheck, the unit suite, the real-Postgres integration suite, DB migration drift, OpenAPI freshness); **Rust** (fmt + clippy, `Core KATs (native + WASM)`, the engine simulation, the fuse operation core `FUSE Op Core`, the workspace tests, and one adapter leg per shipped desktop platform — macOS and Windows each run a workspace check over all targets, the tests of the OS adapter crates, and the keyring conformance suite against the real OS backend, and neither runs the engine simulation, which is platform-neutral); **Web** (the shared packages, the `apps/web` host suite, the engine WASM artifact, the bundle, the `packages/client` browser suite); **Desktop** (the shell frontend suites and the unsigned shell build on all three platforms). Beside the areas stand the contract suite on the CI stack (`Contract Suite Result`) and an e2e **smoke slice** (`Web E2E Smoke`, reported through the stable `Web E2E Smoke Result` context) — a bounded-minutes budget of web login-and-CRUD plus one timing-profile cross-client scenario. Branch protection requires the area result contexts and these standalone contexts, never a job inside an area (ADR 0018) | +| **Main gate** | push to main | the full web-e2e suite, the desktop mounted matrix (macOS/Linux/Windows), the full cross-client matrix, and the updater-key drift check (`Updater Key`), which compares the committed updater pubkey against the release signing secret and therefore stays out of the pull-request path. Failure is treated revert-first, not fix-forward — this tier exists to bound the blast radius of what the smoke slice missed, never to be the first line | +| **Dispatch / scheduled** | manual or cron | the load harness (`crates/load`, v1's `tests/load/` scenarios ported onto the v2 surface — it drives the engine's real API client, so it is Rust beside the contract suite rather than a TS package); long-horizon liveness — lease renewal at seq+1 and the republisher walk against a compressed-EOL profile; staging release gates (mechanics → [FSM1/cipher-box-next#48](https://github.com/FSM1/cipher-box-next/issues/48)); the per-stage profiling benches (`Perf Benches`, criterion over `crates/core` and `crates/engine` on the in-memory seam fakes and the virtual clock — shared-runner timings are too noisy to gate a merge on) | The cargo test profile builds its dependencies optimized: the workspace `Cargo.toml` sets `[profile.dev.package."*"] opt-level = 3`, and the workspace