Skip to content

test(cliffs): four fresh regions - the port generalises, the residual… #197

test(cliffs): four fresh regions - the port generalises, the residual…

test(cliffs): four fresh regions - the port generalises, the residual… #197

Workflow file for this run

# The whole quality gate, run automatically. Until this file existed, `pnpm run
# verify` only ran when a human remembered to (or at deploy time, which can be
# days after the breaking merge) - see issue #54.
#
# `verify` no longer runs as one job, because the test phase is ~95% of it and
# a runner is ~3x slower than a dev machine: the single job measured 9m03s on
# PR #116. The phases are now split across a `static` job and a sharded `tests`
# matrix that run in parallel, with a `verify` job aggregating them.
#
# The anti-drift rule that motivated the old VERBATIM comment still holds, and
# is now enforced differently: this file names only package.json SCRIPTS
# (`verify:static`, `verify:shard`), never the underlying commands. `verify`,
# `verify:static` and `verify:shard` are all composed from the same
# `verify:lint` script, so there is still exactly one definition of each phase
# and CI cannot drift from local. Do not inline `vp check` etc. here.
# Note it is never `check` - that is `vp check --fix` and CI must never rewrite
# files.
#
# WHY THE JOB NAMED `verify` STILL EXISTS: ruleset `EJ` requires status checks
# named `verify` and `build`. A required check that never appears blocks every
# PR forever, so the aggregating job below MUST keep the id `verify`. Renaming
# it needs a ruleset PUT in the same change - see CLAUDE.md on the two-step.
#
# WHY 3 SHARDS, MEASURED NOT GUESSED: vitest splits by FILE, so a single file
# is an unsplittable floor. `test/previewAgreement.spec.ts` is 67s of the 68s
# local suite (~200s on a runner). Ideal-split time is CI_test/N, so N=3 is
# where that first drops below the floor - and N=4+ buys literally nothing
# until that file is split into its three independent tests. Raise N only
# after splitting it, and re-measure rather than assuming.
#
# The production build runs too, in the separate `build` job below - see its
# own comment for why it is not a phase inside `verify`.
#
# What is deliberately NOT here:
# - `pnpm refs:sync`. It needs a Factorio binary and ~/GitHub/factorio-data,
# neither of which exists on a runner. `verify` is designed to pass with no
# Factorio installed and that property is what makes this workflow possible.
# - Any deploy step or credential. Cloudflare Pages does not build this repo
# (`deploy:app` uploads an already-built `dist`), so CI here is a check
# only and the job needs no secrets at all.
name: verify
on:
push:
branches: [main]
pull_request:
# So a run can be forced without an empty commit.
workflow_dispatch:
# Superseded pushes to the same PR are pointless at ~4 minutes a run, so those
# get cancelled. Pushes to `main` do NOT - `cancel-in-progress` there means a
# second merge kills the first one's run, and `main` is the branch the deploy
# ships from, so every commit on it should carry its own verdict.
#
# This is not hypothetical: on 2026-07-30 six PRs merged inside half an hour and
# the runs for `605a4cc` and `be5f592` were both cancelled by the merges that
# followed them. Neither commit is unverified in practice - each had a green
# `verify` on its own PR before the ruleset would let it merge - but "green on
# the PR" and "green as it sits on main" are different claims, and only the
# second one survives a rebase-free merge into a `main` that has since moved.
# Ruleset `EJ` has since set `strict_required_status_checks_policy: true`, which
# closes most of that gap by forcing a PR up to date before it can merge; a
# per-commit verdict on `main` is still the thing that proves it, so these runs
# stay uncancelled.
concurrency:
group: verify-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# Minimum scope: the job only reads the tree. It writes no statuses, comments,
# packages or releases, so nothing beyond `contents: read` is granted.
permissions:
contents: read
jobs:
# Everything in `verify` that is NOT the app test suite: `vp check`,
# `check:vue`, and `preview:test`. ~24s on a runner, so it finishes long
# before any shard and costs nothing to run as its own job.
static:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
# `preview:test` shells out to wrangler (`wrangler types --check`). Keep it
# from phoning home for telemetry on a runner where nobody can answer the
# opt-in prompt.
WRANGLER_SEND_METRICS: "false"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .node-version
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run verify:static
# The app test suite, split across runners. Each shard is a separate machine,
# which is the ONLY way to raise the parallelism ceiling - `maxWorkers` was
# already measured as a dead end on one box (see CLAUDE.md).
tests:
runs-on: ubuntu-latest
# A shard is ~1/3 of a ~9-minute suite. 15 minutes stays a hang detector.
timeout-minutes: 15
strategy:
# One failing shard must not cancel the others: a red `verify` should say
# everything that is broken, not just whichever shard tripped first.
fail-fast: false
matrix:
shardIndex: [1, 2, 3]
shardTotal: [3]
steps:
# Third-party actions are pinned to a full commit SHA, never a moving tag.
# The trailing comment names the release each SHA is, and Renovate updates
# the SHA and that comment together - see .github/renovate.json5.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# No `version:` input on purpose. pnpm/action-setup >= 6 reads
# `devEngines.packageManager` from package.json, so the pnpm pin stays in
# exactly one place. Hard-coding it here would be a second pin to drift.
# This step must precede setup-node: `cache: pnpm` below resolves the
# store path by running pnpm, so pnpm has to be on PATH already.
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
# `.node-version` (26.5.0) is the single source of truth for the Node
# version, and this is its first real consumer - the file went from
# documentation to machinery when this workflow landed. `engines.node`
# stays a permissive floor and is deliberately NOT what CI runs.
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .node-version
cache: pnpm
# A full workspace install. Anything narrower can leave a sibling
# workspace's symlink dangling and produce fake
# `TS2307: Cannot find module 'vitest'` errors; a real full install is the
# one that reports `Scope: all 3 workspace projects`.
- run: pnpm install --frozen-lockfile
- run: pnpm run verify:shard -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
# The required status check. Ruleset `EJ` requires a check named `verify`, so
# this job must keep that id even though the work now happens above.
#
# `!cancelled()` rather than `always()`: a cancelled run (superseded push, per
# the concurrency group) should stay cancelled, not be converted into a
# failure. But a FAILED dependency must fail this job - a `needs:` job whose
# dependency failed is otherwise skipped, and a skipped required check does
# not block a merge. Hence the explicit result assertions.
verify:
needs: [static, tests]
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Assert every phase passed
env:
STATIC: ${{ needs.static.result }}
TESTS: ${{ needs.tests.result }}
run: |
echo "static=$STATIC tests=$TESTS"
# `tests` is the matrix aggregate: 'success' only if every shard was.
[ "$STATIC" = "success" ] || { echo "::error::static phase: $STATIC"; exit 1; }
[ "$TESTS" = "success" ] || { echo "::error::test shards: $TESTS"; exit 1; }
# `verify` is check + type-check + tests. None of them build, so a change that
# passes all three and breaks the production build reached `main` unnoticed
# until somebody deployed - possibly days later, and landing on whoever was
# deploying rather than whoever broke it (issue #61). This job closes that.
#
# Deliberately a SEPARATE job rather than an extra phase inside `verify`:
# `deploy` already runs `pnpm build` immediately after `pnpm run verify`, so
# folding the build into `verify` would build twice on every deploy and slow
# the local gate people actually run by hand. Separate also means it runs in
# parallel with the test job instead of after it.
build:
runs-on: ubuntu-latest
# The build is ~1s of Rolldown after ~30s of setup. 15 minutes is a hang
# detector, matching the job above.
timeout-minutes: 15
steps:
# Default `fetch-depth: 1` is correct here, and that was MEASURED rather
# than assumed - #61 was filed believing the build needed deeper history.
# `scripts/buildStamp.ts` runs exactly three git commands:
# `rev-parse HEAD`, `rev-parse --short HEAD`, and `status --porcelain`.
# None of them read history, so a shallow checkout is enough and the
# earlier "the build stamp reads git history" was imprecise - it reads git
# *state*.
#
# Worth knowing rather than fixing: on a `pull_request` event checkout
# lands on the merge commit, so the stamp this job produces is a synthetic
# SHA that exists nowhere in the repo. Harmless, because CI never deploys
# its artifact - `deploy:app` builds locally and uploads that. If a CI
# build ever becomes the thing that ships, this needs revisiting.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .node-version
cache: pnpm
- run: pnpm install --frozen-lockfile
# This catches a build that FAILS. It does not enforce "zero warnings",
# which CLAUDE.md leans on ("anything that does appear is new and worth
# reading"). Both ways of enforcing that were considered and rejected:
#
# - Grepping the build output for /warn/i. `vp build` has no
# `--fail-on-warn` (checked against vp 0.2.6 `--help`), and this repo
# has already been burned once by grepping build output - a version
# grep returned zero because the minifier had rewritten the string.
# A false pass here is worse than no check.
# - A `build.rollupOptions.onLog` hook that throws on `warn`. Robust, but
# it would hard-fail every local build too the moment a dependency
# emits one benign warning, and dependency-sourced warnings are exactly
# what this repo has seen (zlib-asm needed two suppressions before #46
# removed it). The value is in reading a new warning, not in blocking on
# it.
#
# So warnings stay visible in this job's log and unenforced. If that ever
# needs to change, the `onLog` route is the sturdy one - not a grep.
- run: pnpm vp build