GitHub-hosted CI

LeapView runs continuous integration on standard, ephemeral GitHub-hosted runners. GitHub owns runner provisioning, isolation, queueing, logs, permissions, cancellation, and cleanup. The repository owns the pinned toolchain setup, bounded remote caches, Taskfile commands, and validation tiers that define a valid change.

Every job starts from a clean Ubuntu image and loses its local filesystem when it completes. Correctness therefore cannot depend on an old checkout, Docker daemon, image, volume, or tool cache. This prevents accumulated runner state from making a later run fail or pass.

Trust boundary

Internal, external, and Dependabot pull requests use the same GitHub-hosted execution path. The pull-request workflow grants only contents: read; it does not expose deployment or registry credentials. Fork code never executes on a Flid-managed host.

GitHub Actions caches contain only reproducible dependency downloads, compiler outputs, browser binaries, Terraform providers, and BuildKit layers. They never contain credentials, runtime databases, Docker state, or qualification evidence. Cache restoration is a performance optimization: deleting every cache must still produce the same result.

Execution contract

Four Taskfile targets define the validation tiers:

task ci
task ci:pr
task ci:full
task ci:nightly

task ci is the local alias for the fast pull-request contract. task ci:pr prepares shared inputs, runs the bounded Go and frontend lanes, and checks generated artifacts. task ci:full adds desktop tests, static and selected race analysis, route QA, and deployment validation. task ci:nightly adds dependency and vulnerability scans. task ci:local remains a compatibility alias for the full current-machine contract.

GitHub Actions distributes those same Taskfile units across clean runners. Pull requests first run the existing ciplan dependency classifier against the tested candidate, then run selected APIGen, Go package/application, frontend, docs/site, PostgreSQL, dbt, and spatial validation lanes concurrently. Unknown and cross-cutting inputs, manual dispatch, the ci:full label, and deterministic audit PRs run the full PR tier. The repository validation jobs execute task ci:prepare; the independent APIGen module does not need generated or embedded application assets and skips that preparation. The merge queue adds bounded overlap through task ci:full:extras:hosted; local and nightly full validation retain the sequential task ci:full:extras contract, and the daily schedule also runs task ci:nightly:extras. Local composition remains available through the tier targets; the workflow does not duplicate individual test commands or introduce a runner-specific container wrapper.

PostgreSQL conformance topology

The source-inventoried PostgreSQL lane uses the same runner locally and on GitHub. It runs up to four Go packages concurrently. Each package gets one disposable, pinned PostgreSQL 18 server with its data on tmpfs; the testcontainers PostgreSQL module already disables fsync for these containers. The wrapper passes a server URL and a per-container token to its test binary; the harness verifies the token before it can create objects, and the wrapper terminates the server after the package exits. The lane fails closed if PostgreSQL is unavailable.

Each test fixture creates a separate database, which is dropped along with its roles after the test. Tests within a package run serially because PostgreSQL roles are cluster-wide and qualification tests depend on their exact production names. Parallelism is across packages, not across tests sharing one server. TLS-specific and direct-container qualifications retain their own containers. The lane does not substitute PGlite or skip migrations; changing those behaviors would need separate conformance evidence.

Frontend validation has five isolated shards: core, reports, chat, data, and site. Each hosted shard runs task ci:lane:frontend:shard SHARD=<name> on its own runner with a 180-second watchdog and at most one retry for a timeout, never for an assertion failure. This bounds each independent suite without treating the cumulative runtime of five healthy suites as a hang. Every selected shard must succeed for CI gate to pass. The gate requires successful planning, checks candidate/run/attempt provenance, and rejects missing lane results or a frontend matrix differing from the versioned plan. Unselected jobs must report skipped. Local task ci runs the same bounded shards sequentially to avoid browser and bundler contention on a shared machine.

Toolchain and caches

.github/actions/setup-ci installs pinned Go, Node.js, Bun, and Task versions. Jobs opt into the pinned Terraform and Playwright installations only when their validation requires them. The action is shared by pull-request, merge, nightly, and production qualification jobs so a toolchain change has one reviewable source.

The setup action uses separate GitHub Actions cache entries for:

  • Go modules and compiler outputs, scoped by validation workload, runner platform, installed Go version, module manifests and pinned tool definitions;
  • Bun downloads, keyed by the root and desktop lockfiles;
  • the pinned Playwright Chromium build;
  • Terraform providers, keyed by the deployment lockfiles.

Go validation uses the go-validation-v1 cache namespace and the stable workflow job ID as its workload scope. Application, package and frontend validation jobs reuse their respective entries across PR, merge and nightly runs; full validation shares its scope between merge and nightly runs. Frontend matrix shards share their preparation scope. APIGen, security and native packaging cannot win another validation workload's cache key. Native packaging retains its separate setup-go cache.

The validation key covers root and nested go.mod/go.sum files, Taskfile.yml and the shared setup action, including pinned SQLC and toolchain definitions. There is no fallback to the old shared Go cache or another workload. Each successful job may publish its own scope; a cache hit never skips preparation or validation. Default-branch nightly runs can populate the scopes for subsequent candidates, subject to GitHub's branch access rules. More scopes consume more cache storage; eviction must remain a performance concern only.

Production and public-site image builds export BuildKit layers to independently scoped GitHub Actions caches. LeapView does not cache node_modules, /var/lib/docker, mutable worktrees, or application data. Exact keys are immutable, restore prefixes may seed a new dependency set, and the package manager or build tool always validates restored content. The main artifact workflow populates the default-branch Bun download cache so new pull requests do not inherit an empty cache entry from image qualification.

Each job owns its checkout's installed dependencies. node:deps uses Task's run: once mode so parallel and nested dependency paths await a single bun install --frozen-lockfile in that Task invocation. A later invocation verifies the tree again; it does not trust a cached executable marker. Download caches can be shared, but installed trees are never shared between runners. Run independent local Task processes in separate worktrees rather than allowing multiple installers to write the same checkout concurrently.

The repository currently works within GitHub's default 10 GB cache allowance. The intended operating limit is 50 GB or more so the independent Go, Bun, browser, Terraform, and BuildKit caches do not evict one another. Hitting the lower limit may reduce cache hits but cannot change validation behavior.

Workflow tiers

The pull-request workflow runs selected validation lanes on independent four-vCPU runners and reports the stable required CI gate check. Docs/site changes use a dedicated contract lane so embedded Go documentation tests remain covered when backend lanes are unselected. This prevents browser and Go test contention and shortens wall-clock feedback without increasing per-job machine size.

For a native GitHub pull-request stack, only the top pull request runs those validation lanes. Lower layers report a successful CI gate with a summary that validation is deferred to the stack tip. The workflow listens for the stacked action. CI and security concurrency are scoped to each PR ref: a new revision cancels obsolete runs for that PR while other stack layers finish their own required checks. A stack-wide key can cancel lower-layer planning and aggregate gates when the tip starts, leaving the stack unable to enter the merge queue. Standalone pull requests use the selected plan; manual dispatch runs the complete PR tier. Stack planning compares the cumulative candidate against its merge base with the fetched default branch, including lower-layer changes. For stacks targeting another branch this is a conservative broader diff. Label changes also trigger planning.

The main-branch ruleset must require GitHub's merge queue. A deferred lower-layer gate is feedback, not authorization to merge directly. The queue validates the exact candidate selected for main, whether that candidate is the complete stack or a contiguous prefix, by running the Go and frontend lanes plus the full extras. Merge-validation concurrency is scoped to the candidate ref: a rebuilt candidate cancels its obsolete run without making distinct queue candidates cancel one another. Full merge validation starts alongside the base lanes on a separate runner and prepares its own inputs. The always-running CI gate still requires every lane to succeed and the native desktop proof to succeed for the exact merge candidate. Local full validation remains sequential to avoid contention on a shared machine.

CI / Nightly validation also runs security scans in parallel. Post-merge artifact CI builds and pushes the production image using a BuildKit cache, then qualifies its immutable digest on a second clean runner.

Splitting production build and qualification prevents build layers from consuming the local disk needed by the qualification journey. The digest passed between jobs is the only product identity boundary; qualification never falls back to a mutable tag or source build.

Operations

There is no persistent CI VM to patch, prune, or recover. Monitor queue time, cache hit rate, cache eviction, job duration, and GitHub service health. A cache outage or eviction should make a job slower, not fail it. If a clean GitHub runner lacks enough per-job CPU, memory, or disk, split independent work into jobs before introducing persistent infrastructure.