Files
fusion/.github/workflows/pr-checks.yml
gsxdsm 2771408bba ci: enforce the lifecycle-column ratchet — it has never actually run (#2654)
**The ratchet was advisory.** `scripts/lifecycle-column-census.mjs`
existed only as `pnpm census:lifecycle-columns` — without `--strict` —
and **no workflow invoked it**. Nothing has ever compared the tree to
the baseline. Every "the baseline ratchet holds them" assumption in this
program rested on a check that does not run.

That explains both classes of hole:

**1. Three PRs lowered counts without re-recording,** leaving allowances
the deleted guards could return through while every check stayed green.
I've tightened them across #2593 and earlier PRs, but nothing stops the
next one.

**2. #2621 GREW the count while its own title claimed "count 0 → 0".**
It added `column === "triage"` and `column === "todo"` at
`register-task-workflow-routes.ts:2681`, taking that file to **23
against an allowance of 22**. It landed unchallenged. This is the
failure mode the ratchet exists to prevent, and it happened *inside this
program*, in a PR that asserted the opposite.

## The change

Adds `check:lifecycle-columns` (the census with `--strict`) to the
`pr-checks.yml` lint job, next to `check:changesets` and
`check:routes-modular` — the established pattern. **~1.8s over ~1950
files**, so this is not a slow-test addition.

## Proven to fail, in both directions

A guard that reports success without checking anything is worse than no
guard, so:

| injected defect | result |
|---|---|
| `const __probe = (c: string) => c === "triage"` added to `moves.ts` |
`count ROSE — moves.ts: 39 -> 40`, exit 1 |
| run against main's current baseline | exit 1 on
`mission-feature-sync.ts: allows 5, tree has 0` |

Both reverted; exit 0 restored. Note the second row: **this check is RED
on main right now**, which is the point.

## Merge order

**Stacked on #2593**, which carries the `DELIBERATE-LITERAL` marker for
the #2621 site (a v1 IR declares no roles, so no trait can answer that
question) plus the baseline re-record. Standalone on main this PR is red
— correctly. **Merge #2593 first**, then this.

I stacked rather than duplicating those two edits because I already
caused one conflict today by appending related content from two
branches, and #2651 merged a correction ahead of the section it
corrected. Same-content edits in two PRs is the same mistake.

## Census

Unchanged by this PR: **776 total, triage 5, reviewed 16** — it adds no
guards and converts none. It only makes the numbers enforceable.

## For the fleet

This should land before the 776-guard fleet launches. The brief says
"the baseline ratchet must shrink by exactly the converted count" —
until now nothing verified that claim, so a batch worker could report a
shrink that did not happen, or grow the count while converting, and CI
would agree.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:20:31 -07:00

255 lines
11 KiB
YAML

name: PR Checks
# The thin trusted merge gate (docs/plans/2026-06-04-001-refactor-fast-trusted-test-gate-plan.md).
# Blocking checks are exactly: Lint, Typecheck, Build, Gate.
#
# BRANCH-PROTECTION CUTOVER: required status checks are matched by job name.
# When this file changes job names, update the repo's branch-protection
# required checks to exactly [Lint, Typecheck, Build, Gate] — a stale required
# name (e.g. "Test shard 1/4") that no longer reports will block every PR
# with "Expected — waiting for status". Open PRs must rebase onto main after
# the cutover so they run this workflow shape.
#
# Everything that used to run here as shards / slow tier / inventory guard is
# non-blocking and lives in full-suite.yml (push to main).
on:
pull_request:
branches: [main]
concurrency:
group: pr-checks-${{ github.ref }}
cancel-in-progress: true
# Least-privilege token: every job here only reads the repo (checkout + cache).
permissions:
contents: read
# FN-4863: Opt JavaScript actions into Node 24 ahead of GitHub's forced cutover on 2026-06-02.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Node.js and pnpm
uses: ./.github/actions/setup-node-pnpm
- name: Lint
run: pnpm lint
- name: Changeset format
run: pnpm check:changesets
- name: Dashboard route modularity
run: pnpm check:routes-modular
# The lifecycle-column ratchet was advisory until now: the census existed only as
# `pnpm census:lifecycle-columns` (no --strict) and nothing ran it, so three PRs
# lowered counts without re-recording and left allowances the deleted guards could
# return through while this gate stayed green. ~2s over ~1950 files.
- name: Lifecycle-column ratchet
run: pnpm check:lifecycle-columns
typecheck:
name: Typecheck
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Node.js and pnpm
uses: ./.github/actions/setup-node-pnpm
# FNXC:CIGateSpeed 2026-07-23-00:05:
# tsc incremental buildinfo cache. tsconfig.base.json enables
# `incremental` with per-package dist/.tsbuildinfo; a restored buildinfo
# is SELF-VALIDATING (tsc hashes every input against it and re-checks
# whatever changed), so restore-keys can never let a stale check through
# — it only shrinks the re-checked set. Cold typecheck was ~4 min of the
# PR critical path. Keyed by SHA so every commit saves a fresh snapshot;
# restore-keys picks the nearest prior one (same PR, or main via the
# warm-gate-build-cache job in full-suite.yml, which must keep an
# identical path list — actions/cache versions caches by path list).
- name: Cache TypeScript incremental buildinfo
uses: actions/cache@v6
with:
path: |
packages/*/dist/.tsbuildinfo
packages/dashboard/dist/.tsbuildinfo-app
plugins/*/dist/.tsbuildinfo
key: typecheck-tsbuildinfo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
typecheck-tsbuildinfo-${{ runner.os }}-
- name: Typecheck
run: pnpm typecheck
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Node.js and pnpm
uses: ./.github/actions/setup-node-pnpm
# FNXC:CIGateSpeed 2026-07-23-00:05:
# RESTORE-ONLY tap of the gate's incremental dist cache (see the gate
# job's cache block for the full safety rationale: `pnpm build` always
# runs and reconciles a near-match restore per package by content hash).
# Restore-only (actions/cache/restore, never save) because this job runs
# FULL CLI packaging (CI=true → desktop + bundled plugins + DTS), and
# saving that shape would swap the cache's canonical fast-CLI contents
# out from under the Gate job. Full CLI packaging itself is never skipped
# regardless of cache state (ensureFullPackageCliPlanned force-plans the
# CLI in full mode), so this job's distinctive coverage is preserved.
# Path list must stay byte-identical to the gate job's block.
- name: Compute dist source hash
id: dist-hash
run: echo "hash=$(node scripts/ensure-test-artifacts.mjs --print-source-hash)" >> "$GITHUB_OUTPUT"
- name: Restore built dist artifacts (incremental, restore-only)
uses: actions/cache/restore@v6
with:
path: |
packages/core/dist
packages/dashboard/dist
packages/engine/dist
packages/plugin-sdk/dist
packages/cli/dist
plugins/fusion-plugin-dependency-graph/dist
plugins/fusion-plugin-hermes-runtime/dist
plugins/fusion-plugin-openclaw-runtime/dist
plugins/fusion-plugin-paperclip-runtime/dist
.fusion/cache/plugin-build-cache.json
key: gate-dist-${{ runner.os }}-${{ steps.dist-hash.outputs.hash }}
restore-keys: |
gate-dist-${{ runner.os }}-
- name: Build
run: pnpm build
# The only merge-blocking TEST signal (R3). Runs the boot smoke (the app
# starts and serves) plus the curated engine-core suite and the CI-shape
# test — see `test:gate` in the root package.json. Gate membership is the
# explicit allow-list in packages/engine/vitest.config.ts (engine-core
# project); a flaky gate test is evicted by removing it from that list.
gate:
name: Gate
runs-on: ubuntu-latest
# FNXC:FixPgTestsAndCi 2026-06-26-09:10:
# Provision a PostgreSQL service container so the postgres/*.pg.test.ts
# suites (pgDescribe) run in the merge gate. The pg-test-harness probe
# detects reachability via a TCP probe on localhost:5432 and skips when
# unavailable, so this service is what makes the 57 PG twin tests actually
# execute instead of being silently skipped.
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
# Mark the service healthy only when pg_isready succeeds on the mapped
# port, so job steps don't start before Postgres accepts connections.
options: >-
--health-cmd "pg_isready -h localhost -p 5432 -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
# Point the PG test harness at the service container. psql admin DDL
# (CREATE/DROP DATABASE) runs against this URL's maintenance database.
FUSION_PG_TEST_URL_BASE: "postgresql://postgres:postgres@localhost:5432"
PGPASSWORD: "postgres"
# The gate's value is speed; without a job timeout a hung build or
# deadlocked vitest worker blocks every PR for GitHub's default 6 hours.
# Expected runtime is ~3-5 min.
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Node.js and pnpm
uses: ./.github/actions/setup-node-pnpm
# FNXC:CIGateSpeed 2026-07-22-23:30:
# Gate-scoped INCREMENTAL dist cache (`gate-dist-*` namespace, distinct from
# the shard jobs' exact-match `dist-*` contract). Unlike the shard jobs —
# where restore-keys is forbidden because tests consume restored dist
# WITHOUT a build (stale dist was the FN-4232/FN-4605 failure mode) — the
# gate always runs `pnpm build` after restore. build-workspace.mjs verifies
# every package's git content hash against .fusion/cache/plugin-build-cache.json
# (cached below alongside dist) and rebuilds anything changed, missing, or
# unhashed, so a near-match restore can only speed the build up, never let
# stale dist through. Measured before this change: every PR missed the
# exact key and paid a full ~6-8 min build for ~45s of actual gate tests.
# NEVER add node_modules here (breaks Windows pnpm junctions elsewhere).
# The warm-gate-build-cache job in full-suite.yml saves this same cache
# (identical path list — actions/cache versions caches by path list, so
# the two blocks must stay in sync) on every push to main so a PR's FIRST
# gate run restores main's build instead of building cold.
- name: Compute dist source hash
id: dist-hash
run: echo "hash=$(node scripts/ensure-test-artifacts.mjs --print-source-hash)" >> "$GITHUB_OUTPUT"
- name: Cache built dist artifacts (incremental)
id: dist-cache
uses: actions/cache@v6
with:
path: |
packages/core/dist
packages/dashboard/dist
packages/engine/dist
packages/plugin-sdk/dist
packages/cli/dist
plugins/fusion-plugin-dependency-graph/dist
plugins/fusion-plugin-hermes-runtime/dist
plugins/fusion-plugin-openclaw-runtime/dist
plugins/fusion-plugin-paperclip-runtime/dist
.fusion/cache/plugin-build-cache.json
key: gate-dist-${{ runner.os }}-${{ steps.dist-hash.outputs.hash }}
restore-keys: |
gate-dist-${{ runner.os }}-
# Only seed the mtime-defeating artifact hash-cache on an EXACT hit; on a
# restore-keys near-hit the restored dist may be stale for changed
# packages, and `pnpm build` below is what reconciles it.
- name: Seed artifact hash-cache on cache hit
if: steps.dist-cache.outputs.cache-hit == 'true'
run: node scripts/ensure-test-artifacts.mjs --seed-artifact-cache
# FNXC:CIGateSpeed 2026-07-22-23:30:
# Boot smoke needs the built workspace including the CLI. Fast CLI
# packaging (bin.js + extension.js, no desktop/bundled-plugin/DTS staging)
# is sufficient for boot smoke + test:gate and is the same shape
# `pnpm verify:fast` proves locally; CI=true would otherwise force the
# multi-minute full packaging tail on every gate run. Full CLI packaging
# coverage stays blocking in the separate Build job.
- name: Build
run: pnpm build
env:
FUSION_CLI_FULL_PACKAGE: "0"
- name: Boot smoke (app starts and serves)
run: node scripts/boot-smoke.mjs
- name: Gate tests (curated engine-core + CI-shape)
run: pnpm test:gate
# Advisory desktop-packaging validation lives in its OWN workflow (desktop-packaging.yml) so this
# thin gate stays exactly [Lint, Typecheck, Build, Gate] — the job set here maps 1:1 to the
# branch-protection required checks (CI-shape test enforces the invariant).