Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ updates:

# Cargo / Rust (Tauri desktop backend)
# QNBS-v3: separate from npm — Cargo updates affect src-tauri/ only; group all together to reduce PR noise
# QNBS-v3: no cross-ecosystem coupling with the npm @tauri-apps/plugin-* counterparts — a multi-ecosystem-groups attempt was investigated and reverted as unsafe to verify, see docs/DEPENDABOT-TRIAGE.md.
- package-ecosystem: cargo
directory: /src-tauri
schedule:
Expand Down
31 changes: 30 additions & 1 deletion .github/workflows/tauri-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,42 @@ jobs:
GITHUB_TOKEN: ${{ github.token }}
run: node scripts/signing/verify-github-signatures.mjs

# QNBS-v3: cheap defense-in-depth gate (no pnpm install needed) so a Rust/npm plugin mismatch fails in seconds, not after the ~45min matrix — the v1.28.5 failure class.
# QNBS-v3: needs verify-release-tag so a tag that fails signature verification never gets this job's checkout+script execution either, not just the bundle matrix.
parity-preflight:
Comment thread
qnbs marked this conversation as resolved.
name: 🔍 Tauri plugin parity preflight
needs: [verify-release-tag]
if: >-
${{
always() &&
!cancelled() &&
(
github.event_name == 'workflow_dispatch' ||
needs.verify-release-tag.result == 'success'
)
}}
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version-file: .nvmrc
- name: Verify Tauri plugin Rust/npm version parity
run: node scripts/check-tauri-plugin-versions.mjs
Comment thread
qnbs marked this conversation as resolved.

bundle:
name: Bundle (${{ matrix.os }})
needs: [verify-release-tag]
needs: [verify-release-tag, parity-preflight]
Comment thread
qnbs marked this conversation as resolved.
if: >-
${{
always() &&
!cancelled() &&
needs.parity-preflight.result == 'success' &&
(
github.event_name == 'workflow_dispatch' ||
needs.verify-release-tag.result == 'success'
Expand Down
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`@puppeteer/browsers@3.x` dependency replaced `extract-zip` with `modern-tar` entirely — the
package now has zero occurrences in the resolved graph. Lighthouse 13.4.1 requires Node
`>=22.19`, so the repository's own `engines.node` floor is raised to match. PR #682.

- **Tauri release build now fails fast on a plugin version mismatch:** `tauri-build.yml`'s
cross-platform bundle matrix (~45 min) previously started right after tag-signature
verification, with no cheap check for the exact Rust/npm plugin mismatch that broke every
platform's `v1.28.5` release build. Added a `parity-preflight` job (checkout + one dependency-free
Node script, no `pnpm install`) that runs `check-tauri-plugin-versions.mjs` before the bundle
matrix starts, gated behind `verify-release-tag` so no repository code runs on an unverified
release tag. On both `workflow_dispatch` and tag pushes.
### Documentation

- **Post-release v1.28.6 truth sync:** removed the now-stale release-candidate markers from
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<img src="https://img.shields.io/badge/Storage-IndexedDB_v8-F59E0B" alt="IndexedDB v8">
<img src="https://img.shields.io/badge/PWA-v3.0-5BB974?logo=pwa" alt="PWA v3.0">
<img src="https://img.shields.io/badge/i18n-19_locales-2942_keys-0EA5E9" alt="i18n 19 locales — 2942 keys">
<img src="https://img.shields.io/badge/Tests-7643%2B_%2F_604_files-22C55E" alt="7643+ tests / 604 files">
<img src="https://img.shields.io/badge/Tests-7644%2B_%2F_604_files-22C55E" alt="7644+ tests / 604 files">
<img src="https://img.shields.io/codecov/c/github/qnbs/WorldScript-Studio?logo=codecov&label=Coverage" alt="Codecov Coverage">
<img src="https://img.shields.io/badge/License-MIT-22C55E" alt="License MIT">
<img src="https://img.shields.io/github/actions/workflow/status/qnbs/WorldScript-Studio/.github/workflows/ci.yml?branch=main&logo=github" alt="CI Status">
Expand Down Expand Up @@ -511,7 +511,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and
| **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) |
| **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking |
| **i18n** | Custom React Context (`I18nContext.tsx`) | 2942 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence |
| **Testing** | Vitest 4.x (7643+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Testing** | Vitest 4.x (7644+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy |
| **Visualization** | Force-directed graph | Interactive character relationship network |
| **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` |
Expand Down Expand Up @@ -549,7 +549,7 @@ WorldScript-Studio/
│ ├── sw.js # PWA Service Worker
│ └── manifest.json # PWA Web App Manifest v3
├── tests/
│ ├── unit/ # Vitest unit tests (7643+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ ├── unit/ # Vitest unit tests (7644+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths
│ │ └── settings/ # WebLlmPanel, AiSections
│ └── e2e/ # Playwright specs + helpers.ts
Expand Down Expand Up @@ -714,7 +714,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt
Raw bundle-budget ceilings (KB per uncompressed asset): entry **2500 KB**, vendor **6200 KB**, other JavaScript **2500 KB**, and WASM **30000 KB**.

**Current test metrics (2026-09-07, source-synchronized; CI remains authoritative for pass/fail):**
- **7643+ unit tests** across **604 test files** — CI is authoritative for pass/fail
- **7644+ unit tests** across **604 test files** — CI is authoritative for pass/fail
- Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics)
- i18n: **2942 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta)

Expand Down
2 changes: 1 addition & 1 deletion docs/DEPENDABOT-TRIAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ These need more than a changelog skim because of documented quirks elsewhere in
| `vite` | Production build uses **rolldown**, not esbuild/rollup; verify the current `vite.config.ts` and `package.json` `sideEffects` contract. Rolldown ignores `rollupOptions.treeshake` and ties tree-shaking to `package.json "sideEffects"` — a Vite bump that changes how a dependency's `sideEffects` field is honored can produce a blank-screen prod build that CI's `vite dev`-based E2E suite won't catch. Run `pnpm run build && pnpm run smoke:prod` after any Vite bump, not just CI green. |
| `zod` | Has a repo-local patch (`patches/zod@4.4.3.patch`, forces `"sideEffects": true`) applied via `pnpm patch`. A version bump may need the patch re-applied/re-verified against the new version. |
| `react` / `react-dom` | Already grouped — must stay in lockstep, split bumps cause version-mismatch errors at test time. |
| `tauri*` / `wry` / `tao` | Already grouped (`tauri-deps`) — same lockstep concern for the desktop backend. |
| `tauri-plugin-*` (Cargo) and `@tauri-apps/plugin-*` (npm) | **Not coupled — investigated and reverted (2026-09-10), not a documentation gap.** GitHub supports multi-ecosystem groups, but under the currently documented Dependabot configuration constraints there is no safely verified way to selectively route only these Tauri plugin dependencies into a multi-ecosystem group while simultaneously preserving the existing broad npm `/` and Cargo `/src-tauri` update coverage without overlapping update entries. A first implementation added `patterns: ['tauri-plugin-*']`/`patterns: ['@tauri-apps/plugin-*']` plus `multi-ecosystem-group: tauri-plugins` directly onto the existing broad cargo and npm update entries; this passed schema validation but was caught by review (Cubic) as very likely disabling Dependabot for every *other* dependency in those two entries (React, dev-tooling, `tauri`/`wry`/`tao`, and all other root npm/Cargo packages) — GitHub's multi-ecosystem tutorial's "Use `[\"*\"]` to include all dependencies" note strongly implies the top-level `patterns` key restricts an entry's *entire* scope, not just multi-ecosystem-group membership, and the standalone `patterns` key isn't even documented on GitHub's main `dependabot.yml` configuration-options reference page (only the multi-ecosystem tutorial page), suggesting genuinely new, not-yet-fully-reconciled behavior. A safer "separate dedicated entry per directory" architecture was also considered, but GitHub's own docs state plainly that two `updates:` entries for the same ecosystem+directory are not permitted, with no confirmed exception for multi-ecosystem-group participants. Neither variant could be verified safe without live-testing against a real Dependabot-enabled repository, which isn't observable synchronously from this tooling — schema validity alone is not proof of runtime scanning behavior. Given the severe, silent blast radius of guessing wrong, the coupling was reverted entirely rather than shipped unverified. **This does not weaken the actual safety net**, which was never contingent on Dependabot config: `pnpm run tauri-plugins:check` (`scripts/check-tauri-plugin-versions.mjs`) runs unconditionally in `ci.yml`'s `quality` job on every PR — including a Cargo-only or npm-only Dependabot PR — and fails closed on any mismatch; `tauri-build.yml` additionally runs the same check as a cheap `parity-preflight` job before its ~45min cross-platform bundle matrix. If a one-sided Dependabot PR fails this check, the fix must land **on that same PR's branch** (Dependabot PRs accept additional pushed commits) — a separate companion PR cannot make the failing PR's own CI pass, since each PR's CI only sees its own branch; never dismiss or loosen the check itself. Revisit multi-ecosystem coupling only with hands-on verification against a real Dependabot-enabled sandbox repository. |
Comment thread
qnbs marked this conversation as resolved.
| `github/codeql-action*` | Already grouped — see incident above. |
| `@biomejs/biome` | Check the installed package version against `biome.json`'s `$schema` URL after every bump; the schema URL is versioned independently from the npm dependency declaration. |
| Any WASM/WebGPU-adjacent package (`@huggingface/transformers`, `@mlc-ai/web-llm`, `onnxruntime-web`) | These ship in `vendor-*` SW-excluded chunks (`vite.config.ts` `globIgnores`) — verify a version bump didn't change the package's exported chunk structure in a way that breaks the manual-chunk mapping. |
Expand Down
26 changes: 25 additions & 1 deletion tests/unit/workflowPolicy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,10 @@ describe('Tauri release workflow policy', () => {

it('requires successful tag verification before tagged bundles, while allowing manual builds', () => {
const bundle = extractJobBlock(tauriWorkflowSource, 'bundle');
expect(extractNeeds(tauriWorkflowSource, 'bundle')).toEqual(['verify-release-tag']);
expect(extractNeeds(tauriWorkflowSource, 'bundle')).toEqual([
'verify-release-tag',
'parity-preflight',
]);
expect(bundle).toContain('always()');
expect(bundle).toContain('!cancelled()');
expect(bundle).toMatch(/github\.event_name == 'workflow_dispatch'/);
Expand All @@ -255,6 +258,27 @@ describe('Tauri release workflow policy', () => {
expect(bundle).toContain('Skip updater signing for workflow_dispatch test builds');
});

it('requires the plugin parity preflight to pass before bundling, and never runs it against an unverified tag', () => {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const bundle = extractJobBlock(tauriWorkflowSource, 'bundle');
const preflight = extractJobBlock(tauriWorkflowSource, 'parity-preflight');
// bundle requires both gates independently, with parity-preflight's success required
// structurally BEFORE the workflow_dispatch/tag OR branch — not nested inside it, where an
// OR would let a manual build bypass the parity check entirely.
expect(extractJobIf(bundle)).toMatch(
/always\(\)\s*&&\s*!cancelled\(\)\s*&&\s*needs\.parity-preflight\.result == 'success'\s*&&\s*\([\s\S]+github\.event_name == 'workflow_dispatch'[\s\S]+needs\.verify-release-tag\.result == 'success'/,
);
// parity-preflight itself never checks out/runs against a tag that failed signature
// verification — it depends on verify-release-tag, with the manual-build exception
// structurally OR'd (not AND'd, which would also skip it on every manual dispatch build).
expect(extractNeeds(tauriWorkflowSource, 'parity-preflight')).toEqual(['verify-release-tag']);
expect(extractJobIf(preflight)).toMatch(
/always\(\)\s*&&\s*!cancelled\(\)\s*&&\s*\(\s*github\.event_name == 'workflow_dispatch'\s*\|\|\s*needs\.verify-release-tag\.result == 'success'/,
);
expect(preflight).toContain('scripts/check-tauri-plugin-versions.mjs');
expect(preflight).not.toContain('pnpm install');
expect(preflight).toMatch(/^ {4}permissions:\n {6}contents: read\s*$/m);
});

it('keeps release publication tag-only and downstream of bundle output', () => {
const release = extractJobBlock(tauriWorkflowSource, 'release');
expect(extractNeeds(tauriWorkflowSource, 'release')).toEqual(['bundle']);
Expand Down
Loading