diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index fc35b93ba..b72247f89 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -5,7 +5,7 @@ name: Docker on: push: - tags: ['v*'] + tags: ["v*"] workflow_dispatch: # QNBS-v3: top-level read-only follows principle of least privilege (CodeQL Token-Permissions). @@ -69,12 +69,49 @@ jobs: id: result run: echo "audited-sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + verify-release-tag: + name: Verify release tag + if: >- + ${{ github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v') && github.ref_protected == true }} + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + verified-tag-sha: ${{ steps.verify.outputs.verified-tag-sha }} + steps: + - name: Check out the pinned trusted verifier + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: 147470682b62d5bfe196bba803353d9f25cab8a7 + path: trusted-verifier + fetch-depth: 1 + persist-credentials: false + - name: Verify GitHub signatures for the release tag + id: verify + env: + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_EVENT_PATH: ${{ github.event_path }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_REF_TYPE: ${{ github.ref_type }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SHA: ${{ github.sha }} + GITHUB_TOKEN: ${{ github.token }} + shell: bash + working-directory: trusted-verifier + run: >- + set -euo pipefail && env -i PATH="$PATH" GITHUB_EVENT_NAME="$GITHUB_EVENT_NAME" + GITHUB_EVENT_PATH="$GITHUB_EVENT_PATH" GITHUB_REF_NAME="$GITHUB_REF_NAME" + GITHUB_REF_TYPE="$GITHUB_REF_TYPE" GITHUB_REPOSITORY="$GITHUB_REPOSITORY" + GITHUB_SHA="$GITHUB_SHA" GITHUB_TOKEN="$GITHUB_TOKEN" + node scripts/signing/verify-github-signatures.mjs && + echo "verified-tag-sha=$GITHUB_SHA" >> "$GITHUB_OUTPUT" + build-push: name: Build & push image - # QNBS-v3 (#911): every pushed tag (version, major.minor, and `latest` from metadata-action's latest=auto or the raw rule) waits on a successful audit of this exact SHA; no status function, so a failed, skipped or cancelled audit leaves this job unrun. - needs: [security-audit] + # QNBS-v3 (#911): publish only a verified signed v* tag push after the exact-SHA audit. + needs: [security-audit, verify-release-tag] if: >- - ${{ needs.security-audit.result == 'success' && needs.security-audit.outputs.audited-sha == github.sha }} + ${{ github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v') && github.ref_protected == true && needs.verify-release-tag.result == 'success' && needs.verify-release-tag.outputs.verified-tag-sha == github.sha && needs.security-audit.result == 'success' && needs.security-audit.outputs.audited-sha == github.sha }} runs-on: ubuntu-latest timeout-minutes: 20 # QNBS-v3: packages:write scoped to this job only — prevents privilege escalation in future jobs. diff --git a/.github/workflows/release-gate-proof.yml b/.github/workflows/release-gate-proof.yml index 9e011dd25..fe4e42640 100644 --- a/.github/workflows/release-gate-proof.yml +++ b/.github/workflows/release-gate-proof.yml @@ -1,8 +1,8 @@ -# Non-publishing proof of the #911 release security gate (same inlined audit job as tauri-build.yml / docker.yml). Dispatch it on main with: -# expected-sha = the dispatched commit -> audit succeeds, the publish stand-in runs (echo only) -# expected-sha = another full SHA -> audit may succeed, the stand-in is skipped (SHA mismatch) -# expected-sha = not-a-sha -> audit fails, the stand-in is skipped -# The stand-in's `if` is identical to docker.yml build-push (tests/unit/tooling/releaseSecurityGate.test.ts). +# Non-publishing proof of the #911 release security gate. Dispatch on main with: +# expected-sha = dispatch SHA; simulated_event=push; simulated_ref_type=tag; +# simulated_ref=refs/tags/v1.29.3; simulated_tag_signature=verified -> stand-in runs +# A wrong SHA, non-tag ref, unverified signature result, or non-push simulation keeps it skipped. +# Event/ref/signature inputs are data only; this workflow has no publishing permission or action. # Nothing here holds a write permission, logs in to a registry or creates a release. name: Release gate proof @@ -13,6 +13,29 @@ on: description: SHA to hand to the release security audit (use a wrong or invalid value for the negative proofs). required: true type: string + simulated_event: + description: Event modeled by the no-write stand-in; choose push only for the positive tag-path proof. + required: true + type: choice + options: [push, workflow_dispatch] + default: workflow_dispatch + simulated_ref_type: + description: Ref type modeled by the no-write stand-in. + required: true + type: choice + options: [branch, tag] + default: branch + simulated_ref: + description: Ref modeled by the no-write stand-in, for example refs/tags/v1.29.3. + required: true + type: string + default: refs/heads/main + simulated_tag_signature: + description: Simulated signature-verifier result; this input does not verify a real tag. + required: true + type: choice + options: [verified, rejected] + default: rejected permissions: contents: read @@ -77,7 +100,7 @@ jobs: name: Publish stand-in (no side effects) needs: [security-audit] if: >- - ${{ needs.security-audit.result == 'success' && needs.security-audit.outputs.audited-sha == github.sha }} + ${{ github.event_name == 'workflow_dispatch' && inputs.simulated_event == 'push' && inputs.simulated_ref_type == 'tag' && startsWith(inputs.simulated_ref, 'refs/tags/v') && inputs.simulated_tag_signature == 'verified' && needs.security-audit.result == 'success' && needs.security-audit.outputs.audited-sha == github.sha }} runs-on: ubuntu-latest timeout-minutes: 2 permissions: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dab8eded..e73c7371a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- **Container publishing:** GHCR releases now require a protected, signed annotated `v*` tag bound to the triggering commit, verified by a pinned source, and the exact-commit security audit; manual dispatch remains non-publishing (PR #1205). - **Release-tag verification:** Validate protected-main ancestry with the GitHub Compare API's returned base and merge-base commits, and bind the verified tag target to the triggering event SHA (PR #1207). diff --git a/README.md b/README.md index ec6d206b5..6dfc2e1b8 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ TypeScript native preview (tsgo) Tauri 2 19 locales — 2963 keys - 8705+ tests / 656 files + 8721+ tests / 656 files Codecov Coverage CI status MIT License @@ -807,7 +807,7 @@ The normal web PR pipeline is not the complete native release qualification surf | DOCX | `docx` + JSZip | Word-compatible export | | PWA | Service Worker + Web App Manifest | Offline shell/installability | | i18n | Custom React i18n context | 2963 keys × 19 locales | -| Testing | Vitest 5.x (8705+ tests / 656 files) + Playwright | Unit/integration/E2E | +| Testing | Vitest 5.x (8721+ tests / 656 files) + Playwright | Unit/integration/E2E | | Quality | Biome + tsgo + CodeQL/security tooling | Static and CI gates | | Desktop | Tauri 2 | Current native shell | @@ -884,7 +884,7 @@ WorldScript-Studio/ ├── locales/ # Source locale trees ├── public/ # PWA assets, manifest, SW, runtime locale bundles ├── tests/ -│ ├── unit/ # Vitest unit tests (the 8705+ total also includes components/ and packages/*/tests; tests/e2e is excluded) +│ ├── unit/ # Vitest unit tests (the 8721+ total also includes components/ and packages/*/tests; tests/e2e is excluded) │ └── e2e/ # Playwright ├── docs/ # Canonical product/engineering documentation + ADRs ├── src-tauri/ # Tauri v2 desktop shell / Rust @@ -1029,7 +1029,7 @@ Raw bundle-budget ceilings (KB per uncompressed asset): entry **2500 KB**, vendo Current source-synchronized README metrics: -- **8705+ unit tests** across **656 test files** +- **8721+ unit tests** across **656 test files** - i18n: **2963 keys × 19 locales** CI remains authoritative for actual pass/fail and live coverage. diff --git a/docs/CI.md b/docs/CI.md index a880f0253..6c1b7988e 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -12,10 +12,10 @@ For historical optimization notes (targets may predate the live workflow), see [ **Canonical quality gate:** GitHub Actions. Low-end local machines should **not** be expected to run the full heavy stack. -| Tier | Where | Commands / scope | -|------|--------|------------------| +| Tier | Where | Commands / scope | +| ----------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Quick (local)** | Developer laptop | `pnpm run ci:prepush` (change-aware: single-checker typecheck and i18n/content-guard checks run only when the outgoing change classification requires them, see [`ci:prepush` change-aware routing](#ciprepush-change-aware-routing); release/doc truth and lightweight guardrails run unconditionally); the pre-commit hook runs staged Biome checks; optional targeted `pnpm exec vitest run ` for a fast smoke | -| **Heavy (CI)** | `ci.yml` | Vitest **with** `--coverage` and thresholds, Playwright E2E (`CI=true`) including **mobile emulation** (Pixel 5 / Chromium), Lighthouse CI, Storybook static build, bundle budget + analyze. Mutation testing (Stryker) is **not** part of this pipeline — see [Mutation testing status](#mutation-testing-status). | +| **Heavy (CI)** | `ci.yml` | Vitest **with** `--coverage` and thresholds, Playwright E2E (`CI=true`) including **mobile emulation** (Pixel 5 / Chromium), Lighthouse CI, Storybook static build, bundle budget + analyze. Mutation testing (Stryker) is **not** part of this pipeline — see [Mutation testing status](#mutation-testing-status). | **Merge readiness:** A green workflow run on the PR/branch matters more than reproducing every E2E or LHCI step locally. Use CI **artifacts** (Playwright HTML report, coverage, Lighthouse output) to debug failures. @@ -65,17 +65,17 @@ coverage ratchet remains informational. ## Toolchain (CI parity) -| Requirement | Source | -|-------------|--------| -| Node.js | [`.nvmrc`](../.nvmrc) (currently **22**) | -| Package manager | **pnpm** 12.x, exact version pinned by [`package.json`](../package.json) `packageManager` | -| Lint / format | **Biome** (`pnpm run lint`, `lint:fix`) | -| i18n parity | **`pnpm run i18n:check`** — every locale must expose the same keys as `locales/en/*.json` (see [`scripts/check-i18n-keys.mjs`](../scripts/check-i18n-keys.mjs); optional `--fix` copies missing strings from EN) | -| Types | **TypeScript 7** (`tsgo`) `pnpm run typecheck` | -| Unit tests | **Vitest** with V8 coverage (`pnpm exec vitest run --coverage`) | -| E2E | **Playwright** (`pnpm run test:e2e` with `CI=true`) | +| Requirement | Source | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Node.js | [`.nvmrc`](../.nvmrc) (currently **22**) | +| Package manager | **pnpm** 12.x, exact version pinned by [`package.json`](../package.json) `packageManager` | +| Lint / format | **Biome** (`pnpm run lint`, `lint:fix`) | +| i18n parity | **`pnpm run i18n:check`** — every locale must expose the same keys as `locales/en/*.json` (see [`scripts/check-i18n-keys.mjs`](../scripts/check-i18n-keys.mjs); optional `--fix` copies missing strings from EN) | +| Types | **TypeScript 7** (`tsgo`) `pnpm run typecheck` | +| Unit tests | **Vitest** with V8 coverage (`pnpm exec vitest run --coverage`) | +| E2E | **Playwright** (`pnpm run test:e2e` with `CI=true`) | | Performance budgets | **Lighthouse CI** via `@lhci/cli` (`.lighthouserc.cjs`) — **accessibility** asserted at **`error`** level `minScore: 0.95` (blocks CI); performance `warn` ≥ 0.4; SEO `warn` ≥ 0.8; CLS `error` ≤ 0.1; FCP `warn` ≤ 5 s; LCP `warn` ≤ 7 s | -| Bundle guardrails | **`pnpm run bundle:budget`** (entry/vendor/JS/WASM ceilings) + **`pnpm run analyze`** (rollup visualizer → `dist/bundle-analysis.html`, artifact in CI) | +| Bundle guardrails | **`pnpm run bundle:budget`** (entry/vendor/JS/WASM ceilings) + **`pnpm run analyze`** (rollup visualizer → `dist/bundle-analysis.html`, artifact in CI) | --- @@ -136,36 +136,36 @@ scanned daily without running the full CI matrix. The existing `security` job re authoritative PR/main vulnerability gate; `pnpm audit` remains advisory because of its documented registry gzip-decoding failure mode, while OSV failures remain blocking. -| Job | Needs | Purpose | -|-----|--------|---------| -| `workflow-policy` | — | Structural (real-parser, not regex) validation of every `.github/workflows/*.yml` and `.github/actions/**/action.yml` — permissions, needs graph, SHA-pinned action references, publishing boundary (`scripts/workflow-policy-check.mjs`). Runs first, before any job invokes the governed `./.github/actions/setup` composite, using only external SHA-pinned actions directly (never that composite) and an `--ignore-scripts --ignore-pnpmfile` install, so a PR tampering with the composite (or its own install hooks) can't execute before the gate evaluates it. | -| `pr-size` | `workflow-policy` | PR-size governance (`scripts/check-pr-size.mjs`) — tiered file/line/commit limits, advisory below the absolute ceiling and blocking only above it. `pull_request` only. Runs the **base ref's** own copy of the checker (never the PR's working-tree copy) when it exists there, so a PR touching it can't raise its own limits; falls back to the PR's own copy one time only, for the introducing PR whose base ref has no checker yet. | -| `security` | `workflow-policy` | `pnpm audit --audit-level=high`; **OSV scanner** (`google/osv-scanner-action`) for npm + Rust lockfiles; `gitleaks` secrets scan; on PRs: `dependency-review-action` | -| `scheduled-osv` | — | Separate daily and manually triggerable (`workflow_dispatch`) `.github/workflows/security-scheduled.yml` scan of the same three lockfiles; `contents: read` only; fails closed and writes lockfile/package/advisory details to the step summary | -| `quality` | `security` | Matrix **Node 22** and **24** → Biome lint, **`pnpm run i18n:check`**, **`pnpm run docs:check`**, **`pnpm run csp:verify`**, **`pnpm run parity:check`**, `pnpm run typecheck`, Vitest + coverage (+ non-blocking coverage-ratchet suggestion), Codecov coverage (Node 22 only, optional token), Codecov Test Analytics per Node version (`unit-node22`/`unit-node24` flags), coverage + JUnit artifacts | -| `rust-tauri` | `security` | Rust `cargo fmt --check`, `cargo check --locked`, `cargo clippy --locked --all-targets -- -D warnings`, and `cargo test --locked` on stable, then `cargo +1.90 check --locked --all-targets` at the declared `rust-version` of `src-tauri` (the Core keeps its own 1.77.2 build in `secure-store-platform`); compile/lint signal for Tauri changes without building installers on every PR | -| `build` | `quality` | Production `pnpm run build`, **`bundle:budget`**, **`analyze`** (upload `bundle-analysis.html`; Codecov Bundle Analysis runs inside this same build — see below), **`pnpm run smoke:prod`** (headless-Chromium prod-build + CSP-runtime gate — see below), `dist` artifact; on `main` (non-PR): Pages artifact + **SLSA build provenance attestation**. No `if:` on the job itself — `smoke:prod` runs on every PR, not just `main` pushes. | -| `e2e` | `quality` | Playwright **Chromium** + **Mobile Chrome** (Pixel 5) — `CI=true`, 2× retries, 50 min timeout; browser cache via `actions/cache@v6.1.0`. Firefox optional locally. `PLAYWRIGHT_SKIP_VRT=true` (VRT runs in `browser-quality` instead). | -| `browser-quality` | `build` | **Consolidated (Wave 2 CI-performance)**: VRT then Lighthouse, sharing one checkout/setup/build-artifact download. VRT: visual regression against production `dist`, `toHaveScreenshot()` with committed PNG baselines (4 views × Chromium). Lighthouse (mobile): **accessibility error gate** `minScore: 0.95`; **CLS error** ≤ 0.1; performance/SEO warn — blocking. Lighthouse (desktop): accessibility/CLS are error-level LHCI assertions same as mobile and can fail the step itself, but the pre-existing `continue-on-error: true` (unchanged by this consolidation) keeps that failure from failing the job; promotion to blocking is gated on the 5-run exit criterion below. Each phase runs on `if: ${{ !cancelled() }}` relative to the others — a VRT or mobile-Lighthouse failure never skips a later phase's own evidence; only desktop Lighthouse's own failure is swallowed (job-level result). Artifacts uploaded always. Timeout 35 min. | -| `storybook` | `quality` | Cloud-first — Storybook build + test-runner only run in CI (not locally); Playwright browser cache `v6.1.0`; `--maxWorkers=2 --junit` (non-blocking, `continue-on-error: true` — see [exit criteria](#non-blocking-gates--exit-criteria-f-13)); artifacts uploaded always. Debug: manual `storybook-debug.yml` workflow. | -| `signatures` | `security` | Read-only GitHub API verification of every commit in the complete introduced range; pull-request commit pagination; and annotated release-tag plus target-commit verification. | -| `secure-store-platform` | `security`, `changes` | R-15 Gate 1b-platform evidence (matrix **Ubuntu**, **macOS**, **Windows**; only when `crates/` or its helper changed; deliberately uncached): clippy and guard tests with `--features platform-keystore`, the real OS secure-store lifecycle (Linux in an isolated D-Bus session with a throwaway gnome-keyring via `scripts/secret-service-session.sh`), and on Linux the fail-closed run without any Secret Service plus the Rust 1.77.2 MSRV build with the feature. | -| `ci-success` | `workflow-policy`, `pr-size`, `security`, `signatures`, `quality`, `changes`, `rust-tauri`, `core-rust`, `secure-store-platform`, `build`, `e2e`, `browser-quality` | Required-status **aggregator** — `if: always()`, fails if any required release-safety job does not resolve to `success`; signature verification is authoritative; Storybook and deep-E2E are explicitly advisory. Rust jobs are legitimately skipped when their paths are untouched; `pr-size` is legitimately skipped only on non-`pull_request` events. | -| `deploy` | `ci-success` | **Only** `main` push (not PR), and only after the aggregate gate succeeds; the Pages artifact is resolved from the same workflow run. | +| Job | Needs | Purpose | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `workflow-policy` | — | Structural (real-parser, not regex) validation of every `.github/workflows/*.yml` and `.github/actions/**/action.yml` — permissions, needs graph, SHA-pinned action references, publishing boundary (`scripts/workflow-policy-check.mjs`). Runs first, before any job invokes the governed `./.github/actions/setup` composite, using only external SHA-pinned actions directly (never that composite) and an `--ignore-scripts --ignore-pnpmfile` install, so a PR tampering with the composite (or its own install hooks) can't execute before the gate evaluates it. | +| `pr-size` | `workflow-policy` | PR-size governance (`scripts/check-pr-size.mjs`) — tiered file/line/commit limits, advisory below the absolute ceiling and blocking only above it. `pull_request` only. Runs the **base ref's** own copy of the checker (never the PR's working-tree copy) when it exists there, so a PR touching it can't raise its own limits; falls back to the PR's own copy one time only, for the introducing PR whose base ref has no checker yet. | +| `security` | `workflow-policy` | `pnpm audit --audit-level=high`; **OSV scanner** (`google/osv-scanner-action`) for npm + Rust lockfiles; `gitleaks` secrets scan; on PRs: `dependency-review-action` | +| `scheduled-osv` | — | Separate daily and manually triggerable (`workflow_dispatch`) `.github/workflows/security-scheduled.yml` scan of the same three lockfiles; `contents: read` only; fails closed and writes lockfile/package/advisory details to the step summary | +| `quality` | `security` | Matrix **Node 22** and **24** → Biome lint, **`pnpm run i18n:check`**, **`pnpm run docs:check`**, **`pnpm run csp:verify`**, **`pnpm run parity:check`**, `pnpm run typecheck`, Vitest + coverage (+ non-blocking coverage-ratchet suggestion), Codecov coverage (Node 22 only, optional token), Codecov Test Analytics per Node version (`unit-node22`/`unit-node24` flags), coverage + JUnit artifacts | +| `rust-tauri` | `security` | Rust `cargo fmt --check`, `cargo check --locked`, `cargo clippy --locked --all-targets -- -D warnings`, and `cargo test --locked` on stable, then `cargo +1.90 check --locked --all-targets` at the declared `rust-version` of `src-tauri` (the Core keeps its own 1.77.2 build in `secure-store-platform`); compile/lint signal for Tauri changes without building installers on every PR | +| `build` | `quality` | Production `pnpm run build`, **`bundle:budget`**, **`analyze`** (upload `bundle-analysis.html`; Codecov Bundle Analysis runs inside this same build — see below), **`pnpm run smoke:prod`** (headless-Chromium prod-build + CSP-runtime gate — see below), `dist` artifact; on `main` (non-PR): Pages artifact + **SLSA build provenance attestation**. No `if:` on the job itself — `smoke:prod` runs on every PR, not just `main` pushes. | +| `e2e` | `quality` | Playwright **Chromium** + **Mobile Chrome** (Pixel 5) — `CI=true`, 2× retries, 50 min timeout; browser cache via `actions/cache@v6.1.0`. Firefox optional locally. `PLAYWRIGHT_SKIP_VRT=true` (VRT runs in `browser-quality` instead). | +| `browser-quality` | `build` | **Consolidated (Wave 2 CI-performance)**: VRT then Lighthouse, sharing one checkout/setup/build-artifact download. VRT: visual regression against production `dist`, `toHaveScreenshot()` with committed PNG baselines (4 views × Chromium). Lighthouse (mobile): **accessibility error gate** `minScore: 0.95`; **CLS error** ≤ 0.1; performance/SEO warn — blocking. Lighthouse (desktop): accessibility/CLS are error-level LHCI assertions same as mobile and can fail the step itself, but the pre-existing `continue-on-error: true` (unchanged by this consolidation) keeps that failure from failing the job; promotion to blocking is gated on the 5-run exit criterion below. Each phase runs on `if: ${{ !cancelled() }}` relative to the others — a VRT or mobile-Lighthouse failure never skips a later phase's own evidence; only desktop Lighthouse's own failure is swallowed (job-level result). Artifacts uploaded always. Timeout 35 min. | +| `storybook` | `quality` | Cloud-first — Storybook build + test-runner only run in CI (not locally); Playwright browser cache `v6.1.0`; `--maxWorkers=2 --junit` (non-blocking, `continue-on-error: true` — see [exit criteria](#non-blocking-gates--exit-criteria-f-13)); artifacts uploaded always. Debug: manual `storybook-debug.yml` workflow. | +| `signatures` | `security` | Read-only GitHub API verification of every commit in the complete introduced range; pull-request commit pagination; and annotated release-tag plus target-commit verification. | +| `secure-store-platform` | `security`, `changes` | R-15 Gate 1b-platform evidence (matrix **Ubuntu**, **macOS**, **Windows**; only when `crates/` or its helper changed; deliberately uncached): clippy and guard tests with `--features platform-keystore`, the real OS secure-store lifecycle (Linux in an isolated D-Bus session with a throwaway gnome-keyring via `scripts/secret-service-session.sh`), and on Linux the fail-closed run without any Secret Service plus the Rust 1.77.2 MSRV build with the feature. | +| `ci-success` | `workflow-policy`, `pr-size`, `security`, `signatures`, `quality`, `changes`, `rust-tauri`, `core-rust`, `secure-store-platform`, `build`, `e2e`, `browser-quality` | Required-status **aggregator** — `if: always()`, fails if any required release-safety job does not resolve to `success`; signature verification is authoritative; Storybook and deep-E2E are explicitly advisory. Rust jobs are legitimately skipped when their paths are untouched; `pr-size` is legitimately skipped only on non-`pull_request` events. | +| `deploy` | `ci-success` | **Only** `main` push (not PR), and only after the aggregate gate succeeds; the Pages artifact is resolved from the same workflow run. | > **Desktop:** On-demand / tag-driven Tauri bundles live in [`tauri-build.yml`](../.github/workflows/tauri-build.yml); **`v*` tags** additionally publish installers on a **GitHub Release**. See [`docs/TAURI-CI.md`](TAURI-CI.md). Desktop CI does not block the web deploy graph above. > -> **Release publication security gate (#911):** Tauri `release` (GitHub Release assets and the updater `latest.json`) and GHCR `build-push` (version, `major.minor` and `latest`, including metadata-action's `latest=auto`) each need a `security-audit` job inlined in the same workflow. That job: +> **Release publication security gate (#911):** Tauri `release` (GitHub Release assets and the updater `latest.json`) and GHCR `build-push` (version, `major.minor` and `latest`, including metadata-action's `latest=auto`) each need a `security-audit` job inlined in the same workflow. GHCR additionally requires a real push to a `v*` tag and successful exact-tag GitHub signature verification; `workflow_dispatch` only runs the Docker audit and cannot publish. That audit job: > 1. validates `EXPECTED_SHA: ${{ github.sha }}`; > 2. checks out exactly that commit and proves `HEAD` equals it; > 3. runs the same pinned OSV scanner and `src-tauri/osv-scanner.toml` over `pnpm-lock.yaml`, `src-tauri/Cargo.lock` and `crates/Cargo.lock`; > 4. only then emits `audited-sha`. > -> Each publish job's `if` requires `needs.security-audit.result == 'success'` and `needs.security-audit.outputs.audited-sha == github.sha`, with no status function. A finding, scanner or database error, skip, cancellation, timeout or SHA mismatch therefore leaves it unrun, for tag pushes and `workflow_dispatch` alike. +> Each actual publish job's `if` requires `needs.security-audit.result == 'success'` and `needs.security-audit.outputs.audited-sha == github.sha`, with no status function. Docker additionally requires `needs.verify-release-tag.result == 'success'`; that read-only job verifies the real annotated `v*` tag and its target commit through the repository's GitHub signature verifier. Docker's `workflow_dispatch` event may run the audit for diagnosis, but `build-push` remains skipped for every manual dispatch, including when the dispatched ref is a tag. > -> **Why inlined, and where it is enforced:** the trusted workflow-policy checker on `main` rejects local reusable-workflow calls, and a PR cannot change that trusted root, so the audit job is inlined. [`scripts/release-security-gate.mjs`](../scripts/release-security-gate.mjs) checks the gated jobs and the three identical audit jobs. Its unit tests run in the Quality Gate, with negative fixtures; promoting these rules into the trusted checker is an owner step. +> **Why inlined, and where it is enforced:** the trusted workflow-policy checker on `main` rejects local reusable-workflow calls, and a PR cannot change that trusted root, so the audit job is inlined. [`scripts/release-security-gate.mjs`](../scripts/release-security-gate.mjs) checks exact conjunctive publish conditions, the Docker tag-verification dependency, and the three identical audit jobs. Its unit tests run in the Quality Gate, with negative fixtures; promoting these rules into the trusted checker is an owner step. > -> **Live proof without publishing:** [`release-gate-proof.yml`](../.github/workflows/release-gate-proof.yml) is dispatch-only. Its publish stand-in uses the identical condition. +> **Live proof without publishing:** [`release-gate-proof.yml`](../.github/workflows/release-gate-proof.yml) is dispatch-only. Its echo-only stand-in uses explicit inputs to model a valid `push` to a `v*` tag, a successful tag-signature-verification result, and the exact audited SHA. Those inputs are a simulation: the workflow has no registry credentials or write permission and does not claim to verify a real tag. A manual dispatch, even with a tag ref, can never publish; the actual Docker workflow requires the real push event and verifier job. ### Codecov integration @@ -181,14 +181,14 @@ artifact upload already listed above). `if: ${{ !cancelled() }}`, not the default `success()`, so a real test failure still uploads its result instead of silently disappearing. -| Suite | Flag | JUnit source | -|-------|------|--------------| -| Unit (Node 22) | `unit-node22` | `reports/junit.xml` (Vitest, `--reporter=junit`) | -| Unit (Node 24) | `unit-node24` | `reports/junit.xml` (Vitest, `--reporter=junit`) | -| Required E2E | `e2e` | `tests/e2e/results/junit.xml` (Playwright) | -| Deep E2E (advisory) | `e2e-deep` | `tests/e2e/results/junit.xml`, same runner as above | -| Storybook (advisory) | `storybook` | `test-results/storybook-junit.xml` (`JEST_JUNIT_OUTPUT_FILE` pins the path — `--junit`'s own default, jest-junit's `/junit.xml`, was never captured by the existing artifact upload) | -| VRT | `vrt` | `tests/e2e/results/junit.xml`, same runner as E2E | +| Suite | Flag | JUnit source | +| -------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Unit (Node 22) | `unit-node22` | `reports/junit.xml` (Vitest, `--reporter=junit`) | +| Unit (Node 24) | `unit-node24` | `reports/junit.xml` (Vitest, `--reporter=junit`) | +| Required E2E | `e2e` | `tests/e2e/results/junit.xml` (Playwright) | +| Deep E2E (advisory) | `e2e-deep` | `tests/e2e/results/junit.xml`, same runner as above | +| Storybook (advisory) | `storybook` | `test-results/storybook-junit.xml` (`JEST_JUNIT_OUTPUT_FILE` pins the path — `--junit`'s own default, jest-junit's `/junit.xml`, was never captured by the existing artifact upload) | +| VRT | `vrt` | `tests/e2e/results/junit.xml`, same runner as E2E | Vitest runs once per Node version with two reporters in the same invocation (`--reporter=json --reporter=junit`) — never a second test run merely to get JUnit; the `junit` @@ -252,11 +252,11 @@ that broke. See [ADR-0013](adr/0013-csp-wasm-and-blob-frames.md) for the full in **Layer A alone was never sufficient, and never will be**, for anything shaped like this defect — that's the standing lesson, not a one-time fix: -| Layer | Where | Checks | What it CANNOT catch | -|-------|-------|--------|-----------------------| -| **A — Consistency** | `tests/unit/csp.test.ts`, `tests/unit/deploymentHeaders.test.ts` | Cross-surface consistency — the 5 CSP surfaces agree with each other (identical strings / no header looser than the meta tag) | Whether the *agreed-upon* policy is itself correct — 5 identically-broken CSPs pass Layer A with a clean bill of health | -| **B — Correctness** | `tests/unit/cspCorrectness.test.ts` | Specific directives contain the tokens a shipped feature actually needs (`'wasm-unsafe-eval'`, `frame-src blob:`, …), forbidden tokens are absent (`'unsafe-eval'`, `'unsafe-inline'` in `script-src`), and every inline `