Skip to content

Heavy CI jobs run in order: backend, frontend, then Docker. The Mac and Linux VM share physical resources; the frontend uses one test worker. A failed predecessor does not skip the later checks, but cancellation and fork restrictions still apply. Isolated drawing and citation suites have a five-minute process budget including cold imports and all assertions. Generated-tool integration tests use the unchanged production timeout; a separate regression verifies explicit timeout enforcement.

All CI runs use local machines: Linux ARM64 for backend, native smoke and Docker; macOS ARM64 for frontend; macOS X64 for documentation. If a matching runner is unavailable, jobs stay queued. No PR, push or release validation falls back to GitHub-hosted executors. Read-only permissions, job dependencies and same-repository guards remain in force; forks cannot execute on the owner's machines.

Workflow-level concurrency groups use a CI-specific prefix, the workflow name and the PR number. A newer commit cancels the earlier running and queued work for that PR only. Cancellation is enabled only for pull_request events; other events use the unique github.run_id, so pushes and reusable release checks do not cancel one another or collide with the caller's release lock. Required check names, complete test targets, read-only permissions and fork restrictions remain unchanged. Adding capacity does not remove the existing job dependencies.

The frontend job disables remote dependency caching in setup-node: it omits cache and explicitly sets package-manager-cache: false. This avoids waiting for stalled optional cache restores and skips remote cache uploads, while preserving pnpm's local store. pnpm install --frozen-lockfile still installs and verifies dependencies, and all frontend and desktop checks remain mandatory. This does not remove network access needed for missing dependencies.

The native frontend job explicitly requests managed macOS ARM64 Python 3.11 with UV_PYTHON=cpython-3.11-macos-aarch64-none and verifies platform.machine() before installing dependencies. An installed Intel interpreter must not silently select the x86_64 dependency set through Rosetta. The shared CI workflow sets a 120-second HTTP read timeout, three HTTP retries, at most four concurrent downloads and two installation threads for every direct Python installation: frontend, backend, native smoke and documentation. Jobs and steps must inherit these defaults without overrides. The frontend interpreter preflight has a five-minute budget. Complete uv sync --frozen installations have a forty-five- minute budget, including native smoke; the frozen docs-ci installation has fifteen minutes. This accommodates cold downloads on the local runners. These limits preserve fresh job-scoped environments, cache isolation and every test target; they do not suppress installation errors or guarantee network availability. Runner assignments, release packaging and test targets are unchanged.

Shared CI pins uv to 0.10.0 so the existing retry budget also covers read timeouts after receiving part of a wheel. With 0.9.15, that streaming failure could terminate installation without retrying, despite the configured HTTP retries. This uses the upstream streaming-timeout fix, not a retry wrapper or a longer deadline. Completed downloads remain in the same job-scoped cache; failed installs still fail the job. Frozen application dependencies, Pages and release-packaging installer pins are unchanged.

Test strategy

Quality layers

flowchart TB
    Static["Static checks\nPython syntax, ESLint, i18n"] --> Unit["Unit tests\nnormalizers, policies, algorithms"]
    Unit --> Integration["Integration tests\nroutes, storage, adapters"]
    Integration --> E2E["Playwright\nreal browser and running services"]
    E2E --> Visual["Visual inspection and regression snapshots"]
    Integration --> Deploy["Docker and package smoke tests"]

No single layer is sufficient. A frontend build catches imports and syntax but not a broken interaction. A route unit test does not prove browser integration. A screenshot does not prove persistence or authorization.

Unified pre-PR validation

Prepare the locked JavaScript, runtime and documentation dependencies first:

pnpm install --frozen-lockfile
uv sync --frozen --group docs-ci
uv run --frozen --no-sync python scripts/ci/pre_pr.py --base-ref origin/main

On macOS, use --resource-profile mac-idle for bounded local validation. It replaces the default heap with 1536 MiB, runs at priority 10 with one worker and computation thread, and supervises a 2 GiB RSS budget for the command and descendants. Each phase starts only after 30 seconds of normal memory pressure. Static checks, builds and suites have limits of 5, 10 and 15 minutes respectively.

Sustained warning pressure for 2 seconds, critical pressure, timeout, excess RSS or failed supervision stop the owned processes and leave validation incomplete. There is no automatic retry or limit increase. Keep other validation jobs stopped; do not close user applications or load local models as part of this audit. Node contract and desktop suites explicitly use --test-concurrency=1.

uv run --frozen --no-sync python scripts/ci/pre_pr.py --base-ref origin/main --resource-profile mac-idle

The locked Python environment is checked without installing packages or removing extras.

The direct Python entry point avoids pnpm's outer automatic dependency check. The check:pre-pr package alias is also available after dependency installation. Use --quick for static checks and CI contracts, or --list to inspect the plan without executing it. Neither is full PR evidence. The default full plan also runs API compatibility, all backend/pipeline type checks, frozen resource policy, the complete Python, frontend and desktop suites, the production frontend build and the complete updating documentation gate.

The base supplied with --base-ref is resolved once to an immutable commit; the command does not fetch or change branches. It reports HEAD and validates the current working tree, including tracked staged and unstaged edits. Resolve conflicts and review/stage or ignore untracked files first so index-based checks cannot miss new source. Python 3.11 and Node must have matching architectures.

Phases run consecutively with the reviewed 4 GiB Node heap and one frontend test worker. The first error stops the gate, preserves its status and names the failed phase; it is never retried or converted into success. Missing tools and interrupted runs remain failures. Read the first failing phase's log before deciding whether the cause is source, dependencies or infrastructure.

The child environment uses GNOSI_VALIDATION_ROOT and disposable data/vault directories, disables live-test opt-ins, removes inherited credential variables and points live backend/browser targets at a closed loopback port. It keeps the real home directory unchanged. UV_NO_SYNC=1, UV_FROZEN=1 and pnpm_config_verify_deps_before_run=error prevent child commands from silently installing dependencies; use the explicit frozen preparation above if needed. The gate does not start the personal app, containers or release packaging.

Full mode intentionally regenerates documentation. Review and stage those diffs, then rerun the documentation pre-PR gate against the same base; it must produce no further diff. Regression tests compare the full plan with every existing frontend/backend CI validation command to catch omissions when CI changes.

Local success is not GitHub success: all five required checks must pass on the final PR commit, including real native startup and Docker persistence on their runners. Local validation does not reproduce cold dependency downloads, runner scheduling, other operating systems, installers or deployment. No push or merge is performed by the command.

If source or HEAD changes during validation, the gate fails and must be rerun; expected generated-catalog updates are the only excluded source differences.

Unified type checking

Run pnpm typecheck from the repository root. It runs frontend TypeScript, strict mypy across the complete backend (excluding tests), strict mypy across every indexed public pipeline Python file, and finally Python syntax checks for backend, pipeline, scripts and extensions, in that order. Each failure stops the remaining stages and returns its exit status.

The individual commands typecheck:backend-boundaries and typecheck:pipeline remain available. This is a static gate, not a substitute for lint, unit tests, builds, browser flows or deployment acceptance. A strict pass does not prove that all existing explicit Any boundaries are removed. The command regression checks complete targets and uses isolated POSIX shims to exercise order and failure propagation; it does not prove Windows execution.

Backend tests

Pytest covers services, route dependencies, normalization, storage, security, concurrency, and regression cases. Tests use temporary vault and local-data directories. External providers are stubbed unless a test is explicitly marked as live/E2E.

Important suites include:

  • Auth, PAT, workspace bootstrap, roles, and public surfaces.
  • Path containment, safe writes, ETags, races, registry and sidecar behavior.
  • Formulas, rollups, typed filters, relations, planning, and scheduling.
  • Mail MIME/CID, contacts merge/vCard, calendar containment and reminders.
  • AI routing, skills, MCP resilience, confirmations, and generated tools.
  • Plugins, imports, citations, reader normalization, XSS, and SSRF.

Frontend tests

Vitest covers components, hooks, registries, formatting utilities, typed view logic, and state behavior. ESLint and the production Vite build are mandatory. check:i18n verifies that referenced user-facing keys exist in every locale.

The build must finish with zero errors. Existing warnings are not permission to add new warnings without review.

Frontend ownership checks use gnosi/feature-boundaries in ESLint. The reviewed extension is an exact public-entry manifest at frontend/feature-public-entries.json, with a reason per listed path. Cross-feature consumers use the feature root/index or an explicitly reviewed entry; unlisted siblings remain private. Check static imports, reexports, literal lazy imports and TypeScript import types. The manifest must not create an eager aggregate or change existing lazy boundaries.

The rules shared → no features/app and features → no app are unconditional, including types and manifest entries. Feature internals may use local imports. Global source contracts belong in frontend/tests/contracts/; the source guardrail complements AST lint. Verify implementation after the relocation; this documentation does not establish a passing global gate.

Run CPU-heavy build/typecheck gates separately from the complete real-DOM suite on constrained machines. If parallel work causes test deadlines to expire, repeat the affected suite in isolation and then the full suite with bounded workers (for example, pnpm --filter @gnosi/frontend exec vitest run --maxWorkers=2 --minWorkers=2). Keep the assertions and test deadlines intact; an isolated pass alone does not establish that the full suite is green.

Production bundle budgets

The frontend build runs scripts/check-bundle-size.ts after Vite. Fixed limits apply to uncompressed JavaScript bytes: entry file 1,400,000; largest chunk 1,800,000; editor vendor 1,550,000; tldraw vendor 1,350,000; settings route 600,000. Missing or duplicate reviewed chunks fail the check. Tests cover relative, root and prefixed deployment URLs, growth and missing chunks. The entry-file metric is not the complete initial dependency graph, compressed transfer size or a startup-time measurement. Vite's existing 1,500 kB advisory remains visible; these budgets prevent growth, not prove optimal performance.

End-to-end and visual tests

Playwright runs as a host-level project against the native application. An anonymous setup covers boot and public behavior; authenticated setup covers workspace functionality. Domain tests exercise Vault, dashboard, mail, calendar, contacts, drawings, automation, agent chat, and navigation.

Visual snapshots cover representative desktop and mobile pages. For a UI change, inspect the actual rendered page, click the changed control, watch the console, and take a screenshot. Confirm that modals, overlays, toasts, and menus use the registered z-index system and do not trap interaction.

Accessibility gate

The Playwright accessibility project is a blocking WCAG 2.2 AA gate. It runs axe against twelve selected product routes in light and dark themes, including color contrast, labels, landmarks, and ARIA relationships. The suite keeps application-owned markup in scope and does not maintain a permanent violation allowlist. Its deterministic fixture enables the optional modules represented by the route matrix, and every route also fails on unhandled browser page errors so a crashed surface cannot pass axe.

Before scanning, each case requires the expected canonical URL and a visible feature-specific surface, with no route skeleton or disabled-plugin fallback. It does not reload the page to retry failed startup. The skip-link check verifies its visible two-pixel border and keyboard underline; graph navigation follows the vault-scoped link. Media and control-center screenshots preserve light/dark contrast evidence. A green run covers these fixtures and states, not every interaction, assistive technology, user dataset or complete WCAG conformance.

Interaction assertions complement axe for behavior that static analysis cannot prove: skip navigation, visible focus, logical focus order, complete keyboard operation, mobile tab roving focus, cancelable-dialog Escape handling, focus trap and restoration, accessible names, and live route announcements. Shared focus, modal, navigation, or color-token changes must pass this project before release.

Global focus styling uses the data-focus-modality attribute on the document root. Pointer activation suppresses generic outlines; keyboard activation uses contextual indicators: existing borders for fields, underlines for links, and outlines for borderless controls. Editable Vault page titles retain their caret without an enclosing ring. Unit tests must cover modality transitions, while browser checks cover pointer and keyboard focus in light and dark themes.

Deployment tests

The CI image builder records the checked-out Git revision in org.opencontainers.image.revision and a unique invocation identifier in io.gnosi.ci.build-id. Existing target images must be listed and removed successfully, without forcing in-use images. After every build, including exit zero, inspection must return both matching labels. A post-load client failure can recover only with this evidence; an older image from the same commit still fails because its invocation identifier differs. Missing or malformed metadata, failed inspection and failed removal remain failures. The real startup and persistence smoke remains mandatory.

The backend image now uses uv 0.10.0 with UV_HTTP_TIMEOUT=120, UV_HTTP_RETRIES=3, UV_CONCURRENT_DOWNLOADS=4 and UV_CONCURRENT_INSTALLS=2 explicitly on its frozen installation command. Workflow variables are not implicitly inherited inside Docker builds. These values affect installation only, not the application's runtime environment. Regression tests compare the installer and budgets with shared CI and reject missing, changed or duplicated settings. Application dependencies, lockfiles, runner routing and release packaging are unchanged.

Docker CI validates Compose, builds both images and runs scripts/smoke_docker.sh. The smoke checks the live backend and frontend, then recreates the containers and verifies a synthetic marker in /data. Its exit cleanup owns only its uniquely named Compose project, including that project's test volumes.

Final cleanup runs scripts/ci/prepare_docker_runner.py --cleanup: it removes only the gnosi-frontend:ci and gnosi-backend:ci image tags without forcing in-use images, clears regenerable unused build cache, and verifies at least 12 GiB free. It never globally prunes containers, networks or volumes. A completed build-cache command reporting context deadline exceeded may retry up to three total attempts, five seconds apart. Each attempt has a 120-second process limit; both capacity and final-cleanup steps have ten-minute limits. Other errors, an expired process limit, exhausted retries or insufficient space still fail the check. Builds and the persistence smoke are never skipped.

Image removal has a separate bounded recovery: after its 60-second process limit expires, a successful query must confirm whether the exact CI tag remains. Verified absence completes that removal. A remaining tag permits one retry after five seconds, with another presence check before deletion. Failed inspection, in-use images and a second timeout with the tag still present remain failures. The ten-minute outer cleanup limit and final 12 GiB capacity check still apply.

The frontend job applies its reviewed 4 GiB Node heap budget at job scope so lint, type checking, tests and production build run under the same predictable memory contract. Desktop release-policy tests verify exact local runner assignments and the complete Node/Python resource environment. Mutation tests reject hosted runners, changed local assignments, missing budgets and step-level overrides. CI changes require the full desktop test suite as well as Python scheduling contracts.

Electron release CI configures packaging for macOS arm64/x64, Linux arm64 and Windows x64. Configuring that matrix, running desktop unit tests or checking a synthetic browser-profile migration does not prove installer or frozen-backend acceptance. Each architecture requires installation, startup, persistent-data and 2.x-upgrade evidence. macOS currently uses manual installer updates. Do not infer cross-platform acceptance from a local macOS run, or publish 3.0 before the complete release matrix passes.

Change-to-test mapping

Change Minimum evidence
Pure reviewed documentation Generator check, validator, strict docs build, browser docs smoke.
Generated catalog logic Generator unit tests, two-run determinism, validator, strict docs build.
Backend behavior Narrow pytest regression plus affected integration suite.
Frontend behavior Vitest where feasible, i18n check, production build, browser action and screenshot.
Accessibility or shared UI token Vitest for the shared primitive, four-locale parity, axe route matrix in light and dark, keyboard interaction suite, and browser screenshot.
Auth/security/path behavior Negative tests and cross-scope attempts, not only the golden path.
Deployment/dependency Native verification plus Docker or package CI as applicable.

Test catalog

The generated test catalog lists owned test files and navigation signals. Runner collection remains authoritative for executable test counts.