ci: run the required PR quality stages in order (E00-S05-T01)

Restructure .gitea/workflows/ci.yml from a flat list of per-suite jobs into
the ordered stage baseline: frozen install -> typecheck -> formatting/lint ->
unit -> architecture -> PostgreSQL integration -> build of the admin and
server applications. Each stage gates on its predecessor through needs, so
the frozen install runs before every later stage and the pipeline halts on
the first failing stage. The docker-gated real-stack probes in the
postgres-integration (and container) suites keep running where a Docker
daemon is available and skipping cleanly otherwise.
This commit is contained in:
implementer
2026-08-30 06:12:25 +00:00
parent d9b493498e
commit 89fe0b52e6
+187 -287
View File
@@ -5,9 +5,34 @@ on:
push: push:
branches: [main] branches: [main]
# E00-S05-T01 — CI quality baseline (required PR stages).
#
# Every pull request runs the required quality stages in order, each gated on
# the previous stage through `needs`:
#
# 1. frozen-install — the committed lockfile installs cleanly
# 2. typecheck — every workspace package passes `tsc --noEmit`
# 3. formatting-lint — the dependency-free formatting/lint policy (`pnpm lint`)
# 4. unit — deterministic unit suites (health, config, env example…)
# 5. architecture — static workspace/container structure and policy suites
# 6. postgres-integration — PostgreSQL adapter suites (docker-gated real-stack
# probes run where a Docker daemon is available and
# skip cleanly otherwise)
# 7. build-apps — builds the workspace applications (apps/*: server
# today, admin when E06-S01 lands) and verifies the
# compiled artifact
#
# The stage order, the `needs` chain and the tests/ coverage are locked in by
# tests/ci-stages.test.mjs (architecture stage). Container/Compose smoke on
# main/release branches and the Docker Compose baseline stack (E00-S02) stay
# out of scope for this stage list.
jobs: jobs:
# Stage 1 — frozen install (E00-S05-T01). Runs before every later stage: the
# committed lockfile must install cleanly and be up to date with the
# manifests before any stage proceeds.
frozen-install: frozen-install:
name: Frozen lockfile install name: Stage 1 — Frozen lockfile install (E00-S05-T01)
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -22,11 +47,10 @@ jobs:
- name: Verify workspace groups - name: Verify workspace groups
run: pnpm -r list --depth -1 run: pnpm -r list --depth -1
# E00-S02-T08: the static assertions of tests/secrets-not-embedded.test.mjs # Stage 2 — typecheck (E00-S05-T01).
# gate every PR (the docker-gated layer-scan probe inside the same file runs typecheck:
# where a Docker daemon is available and skips cleanly otherwise). name: Stage 2 — Typecheck (E00-S05-T01)
secrets-not-embedded: needs: frozen-install
name: Secrets not embedded (E00-S02-T08)
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -34,290 +58,78 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '24' node-version: '24'
- name: Run secrets-not-embedded test suite - name: Enable pnpm (corepack, pinned to 11.23.0 via packageManager)
run: corepack enable
- name: Install dependencies (frozen lockfile)
run: pnpm install --frozen-lockfile
- name: Typecheck every workspace package
run: pnpm typecheck
# Stage 3 — formatting/lint policy (E00-S05-T01). `pnpm lint` runs the
# dependency-free formatting-policy suite (tests/formatting-policy.test.mjs):
# LF line endings, no BOM, no trailing whitespace, no tab indentation, final
# newline, and valid JSON with 2-space indentation and no duplicate keys.
formatting-lint:
name: Stage 3 — Formatting/lint policy (E00-S05-T01)
needs: typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js 24
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Enable pnpm (corepack, pinned to 11.23.0 via packageManager)
run: corepack enable
- name: Install dependencies (frozen lockfile)
run: pnpm install --frozen-lockfile
- name: Run the formatting/lint policy
run: pnpm lint
# Stage 4 — unit tests (E00-S05-T01). Deterministic suites that gate every
# PR without external services: the app health endpoint, the secrets scan
# and the configuration service suites (schema, startup error, log
# redaction, env adapter, .env.example). The config suites boot the
# committed server, so the config and database-postgres packages are built
# first.
unit:
name: Stage 4 — Unit tests (E00-S05-T01)
needs: formatting-lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js 24
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Enable pnpm (corepack, pinned to 11.23.0 via packageManager)
run: corepack enable
- name: Install dependencies (frozen lockfile)
run: pnpm install --frozen-lockfile
- name: Build the config and database-postgres packages (the probes boot the committed server which imports them)
run: pnpm --filter @personal-blog/config build && pnpm --filter @personal-blog/database-postgres build
- name: Run the health-endpoint unit suite
run: node --test tests/health-endpoint.test.mjs
- name: Run the secrets-not-embedded unit suite
run: node --test tests/secrets-not-embedded.test.mjs run: node --test tests/secrets-not-embedded.test.mjs
- name: Run the config-schema unit suite
# E00-S03-T02: the static assertions of tests/database-postgres-imports.test.mjs
# gate every PR — the scan proves pg/Kysely imports live only in
# packages/database-postgres and the mutation probes prove the scan catches
# a driver import injected into any other package.
database-postgres-imports:
name: Database-postgres import isolation (E00-S03-T02)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js 24
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Run database-postgres import isolation suite
run: node --test tests/database-postgres-imports.test.mjs
# E00-S03-T03: the static assertions of tests/database-postgres-ledger.test.mjs
# gate every PR — the suite locks in the migration ledger (schema_migrations
# table DDL, idempotent parameterized record, driver-boundary re-export)
# with mutation probes, and the docker-gated real-stack probe (migrate an
# empty database and confirm the ledger exists) runs where a Docker daemon
# is available and skips cleanly otherwise. The job installs the frozen
# workspace because the real-stack probe executes the committed ledger
# module from the host (it imports `pg` through the package's own links).
database-postgres-ledger:
name: Migration ledger (E00-S03-T03)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js 24
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Enable pnpm (corepack, pinned to 11.23.0 via packageManager)
run: corepack enable
- name: Install dependencies (frozen lockfile)
run: pnpm install --frozen-lockfile
- name: Run migration ledger test suite
run: node --test tests/database-postgres-ledger.test.mjs
# E00-S03-T04: the static assertions of tests/database-postgres-lock.test.mjs
# gate every PR — the suite locks in the migration advisory lock (session-
# scoped pg_advisory_lock/pg_try_advisory_lock over a stable keyed hash on a
# dedicated connection, re-entrant-safe in-flight acquire so concurrent
# acquire() calls share one connection, driver-boundary re-export) with
# mutation probes, and the docker-gated real-stack concurrent probe (a
# second runner waits or fails while the first holds the lock; concurrent
# acquire() checks out exactly one connection; the lock releases when the
# holding session ends) runs where a Docker daemon is available and skips
# cleanly otherwise. The job installs the frozen workspace because the
# real-stack probe executes the committed lock module from the host (it
# imports `pg` through the package's own links).
database-postgres-lock:
name: Migration advisory lock (E00-S03-T04)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js 24
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Enable pnpm (corepack, pinned to 11.23.0 via packageManager)
run: corepack enable
- name: Install dependencies (frozen lockfile)
run: pnpm install --frozen-lockfile
- name: Run migration advisory lock test suite
run: node --test tests/database-postgres-lock.test.mjs
# E00-S03-T05: the static assertions of tests/database-postgres-diagnostic.test.mjs
# gate every PR — the suite locks in the migration failure diagnostic (a
# structured MigrationFailedError whose diagnostic identifies the failing
# migration, the failure phase, the underlying cause, and the applied/pending
# ledger state, serializable via toJSON) with mutation probes, and a
# deterministic stub-pool behavioral probe (intentionally failing migration
# fixture -> structured diagnostic naming the failing migration) runs on
# Node 24; the docker-gated real-stack probe (the issue's test plan: "run an
# intentionally failing migration fixture and confirm the diagnostic") runs
# where a Docker daemon is available and skips cleanly otherwise. The job
# installs the frozen workspace because the probes execute the committed
# runner module from the host (it imports `pg` through the package's own
# links).
database-postgres-diagnostic:
name: Migration failure diagnostic (E00-S03-T05)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js 24
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Enable pnpm (corepack, pinned to 11.23.0 via packageManager)
run: corepack enable
- name: Install dependencies (frozen lockfile)
run: pnpm install --frozen-lockfile
- name: Run migration failure diagnostic test suite
run: node --test tests/database-postgres-diagnostic.test.mjs
# E00-S03-T06: the static assertions of tests/app-readiness.test.mjs gate
# every PR — the suite locks in the readiness gate (the app answers
# GET /health with 503 {"status":"not ready"} until the startup migration
# run completes, then 200 {"status":"ok"}) with mutation probes, the
# deterministic probes (boot the committed server: no DATABASE_URL ->
# ready immediately; unreachable DATABASE_URL -> stays not-ready) run on
# Node 24, and the docker-gated real-stack probe (the issue's test plan:
# "start with pending migrations and confirm readiness waits" — the app's
# migration run is blocked behind a held ACCESS EXCLUSIVE lock on the
# migration ledger, /health stays not-ready, then flips ready once the lock
# releases) runs where a Docker daemon is available and skips cleanly
# otherwise. The job installs the frozen workspace and builds the config
# and database-postgres packages because the probes boot the committed
# server from the host (it imports @personal-blog/config and
# @personal-blog/database-postgres through the packages' own links; the
# required EPPP_SESSION_SECRET is provided by the probe's boot env).
app-readiness:
name: App readiness after migrations (E00-S03-T06)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js 24
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Enable pnpm (corepack, pinned to 11.23.0 via packageManager)
run: corepack enable
- name: Install dependencies (frozen lockfile)
run: pnpm install --frozen-lockfile
- name: Build the config and database-postgres packages (the probes boot the committed server which imports them)
run: pnpm --filter @personal-blog/config build && pnpm --filter @personal-blog/database-postgres build
- name: Run app readiness test suite
run: node --test tests/app-readiness.test.mjs
# E00-S04-T02: the static assertions of tests/config-startup-error.test.mjs
# gate every PR — the suite locks in the field-specific startup error (a
# missing required setting fails startup with an error naming the missing
# field: packages/config's MissingRequiredSettingError/assertValidConfig,
# wired into the committed server before it binds) with mutation probes, and
# the deterministic probes execute the issue's test plan ("start with a
# missing required field and confirm the error names it"): booting the
# committed server without EPPP_SESSION_SECRET exits non-zero naming
# sessionSecret, while a valid secret boots to GET /health 200. The job
# installs the frozen workspace and builds the config and database-postgres
# packages because the probes boot the committed server which imports them.
config-startup-error:
name: Field-specific startup errors (E00-S04-T02)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js 24
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Enable pnpm (corepack, pinned to 11.23.0 via packageManager)
run: corepack enable
- name: Install dependencies (frozen lockfile)
run: pnpm install --frozen-lockfile
- name: Build the config and database-postgres packages (the probes boot the committed server which imports them)
run: pnpm --filter @personal-blog/config build && pnpm --filter @personal-blog/database-postgres build
- name: Run config startup error test suite
run: node --test tests/config-startup-error.test.mjs
# E00-S04-T03: the static assertions of tests/config-log-redaction.test.mjs
# gate every PR — the suite locks in automatic secret redaction from logs
# (packages/config's redactConfig/redactText + the server's redacting
# logger: every log line is scrubbed of the config's secret values) with
# mutation probes, and the deterministic probes execute the issue's test
# plan ("log configuration and confirm secret values are redacted"):
# booting the committed server logs its resolved configuration with the
# secret values replaced by [REDACTED], and no secret value appears in the
# log output. The job installs the frozen workspace and builds the config
# and database-postgres packages because the probes boot the committed
# server which imports them.
config-log-redaction:
name: Secret redaction from logs (E00-S04-T03)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js 24
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Enable pnpm (corepack, pinned to 11.23.0 via packageManager)
run: corepack enable
- name: Install dependencies (frozen lockfile)
run: pnpm install --frozen-lockfile
- name: Build the config and database-postgres packages (the probes boot the committed server which imports them)
run: pnpm --filter @personal-blog/config build && pnpm --filter @personal-blog/database-postgres build
- name: Run config log redaction test suite
run: node --test tests/config-log-redaction.test.mjs
# E00-S04-T04: the static assertions of tests/config-env-adapter.test.mjs
# gate every PR — the suite locks in the environment adapter (packages/config
# is the single owner of process.env reads; the server and every other module
# read no process.env, all settings flow through loadConfigFromEnv into the
# validated config) with a comment-stripped workspace scan, mutation probes
# (injecting a direct process.env read into any other module fails the scan),
# a deterministic boundary probe (full env mapping, defaults, bad-PORT
# fallback, HOST validated as hostname/IP with invalid values throwing a
# field-specific startup error, missing required secret ->
# MissingRequiredSettingError) and server-boot probes (a PORT/HOST override
# shows up in the resolved configuration; HOST=127.0.0.1 binds loopback only
# and the startup log reflects the actual bind; an invalid HOST fails startup
# naming the field without echoing the raw value; a missing required secret
# still fails startup). The job installs the frozen workspace and builds the
# config and database-postgres packages because the probes boot the committed
# server which imports them.
config-env-adapter:
name: Env adapter owns process.env (E00-S04-T04)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js 24
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Enable pnpm (corepack, pinned to 11.23.0 via packageManager)
run: corepack enable
- name: Install dependencies (frozen lockfile)
run: pnpm install --frozen-lockfile
- name: Build the config and database-postgres packages (the probes boot the committed server which imports them)
run: pnpm --filter @personal-blog/config build && pnpm --filter @personal-blog/database-postgres build
- name: Run config env adapter test suite
run: node --test tests/config-env-adapter.test.mjs
# E00-S04-T01: the static assertions of tests/config-schema.test.mjs gate
# every PR — the suite locks in the TypeBox/Ajv configuration schema
# (packages/config, golden-tuple pins @sinclair/typebox@0.34.52 +
# ajv@8.20.0) with mutation probes, and the deterministic probe executes
# the issue's test plan ("validate a full config against the TypeBox/Ajv
# schema") against the committed schema through Ajv. The job installs the
# frozen workspace and builds the config package because the probe also
# exercises the compiled package boundary (@personal-blog/config) exactly
# as the later configuration adapter will consume it.
config-schema:
name: TypeBox/Ajv config schema (E00-S04-T01)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js 24
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Enable pnpm (corepack, pinned to 11.23.0 via packageManager)
run: corepack enable
- name: Install dependencies (frozen lockfile)
run: pnpm install --frozen-lockfile
- name: Build the config package (the probe exercises the compiled package boundary)
run: pnpm --filter @personal-blog/config build
- name: Run config schema test suite
run: node --test tests/config-schema.test.mjs run: node --test tests/config-schema.test.mjs
- name: Run the config-startup-error unit suite
# E00-S04-T05: the static assertions of tests/env-example.test.mjs gate every run: node --test tests/config-startup-error.test.mjs
# PR — the suite locks in the committed `.env.example` template: it exists at - name: Run the config-log-redaction unit suite
# the repo root, is un-ignored in .gitignore (real `.env` files stay ignored run: node --test tests/config-log-redaction.test.mjs
# while the example is tracked), documents every configuration environment - name: Run the config-env-adapter unit suite
# source (HOST/PORT/DATABASE_URL/EPPP_SESSION_SECRET), and contains run: node --test tests/config-env-adapter.test.mjs
# placeholder values only — no credential URI, no long secret-looking value, - name: Run the env-example unit suite
# and no compose default credential — with mutation probes proving the
# assertions are non-vacuous. It also locks the fail-closed EPPP_SESSION_SECRET
# placeholder (shorter than the schema's 32-character minimum), builds the
# secret-shaped probe at runtime so the branch stays gitleaks-clean, and
# masks raw values in assertion messages. The test needs no dependencies, so
# the job only installs Node.
env-example:
name: .env.example placeholders only (E00-S04-T05)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js 24
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Run .env.example test suite
run: node --test tests/env-example.test.mjs run: node --test tests/env-example.test.mjs
# E00-S03-T01: the static assertions of tests/compose-config.test.mjs (db # Stage 5 — architecture tests (E00-S05-T01). Static structure and policy
# image pinned to postgres:18.6-bookworm, health gate, volume persistence, # suites: dependency boundaries, workspace layout/configuration, strict
# build platforms) gate every PR (the docker-gated real-stack probes inside # TypeScript base, engine/TypeScript pins, root commands, frozen-install
# the same file run where a Docker daemon is available and skip cleanly # clean clone, container definition structure, and the CI baseline itself.
# otherwise). architecture:
compose-config: name: Stage 5 — Architecture tests (E00-S05-T01)
name: Compose config (E00-S03-T01) needs: unit
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -325,5 +137,93 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '24' node-version: '24'
- name: Run compose-config test suite - name: Enable pnpm (corepack, pinned to 11.23.0 via packageManager)
run: corepack enable
- name: Install dependencies (frozen lockfile)
run: pnpm install --frozen-lockfile
- name: Run the architecture-import suite
run: node --test tests/architecture-import.test.mjs
- name: Run the no-core-extension-imports suite
run: node --test tests/no-core-extension-imports.test.mjs
- name: Run the workspace-layout suite
run: node --test tests/workspace-layout.test.mjs
- name: Run the workspace-config suite
run: node --test tests/workspace-config.test.mjs
- name: Run the strict-tsconfig suite
run: node --test tests/strict-tsconfig.test.mjs
- name: Run the typescript-pin suite
run: node --test tests/typescript-pin.test.mjs
- name: Run the node-engine suite
run: node --test tests/node-engine.test.mjs
- name: Run the frozen-install suite
run: node --test tests/frozen-install.test.mjs
- name: Run the root-commands suite
run: node --test tests/root-commands.test.mjs
- name: Run the compose-config suite
run: node --test tests/compose-config.test.mjs run: node --test tests/compose-config.test.mjs
- name: Run the build-targets suite
run: node --test tests/build-targets.test.mjs
- name: Run the non-root-user suite
run: node --test tests/non-root-user.test.mjs
- name: Run the readonly-rootfs suite
run: node --test tests/readonly-rootfs.test.mjs
- name: Run the database-postgres-imports suite
run: node --test tests/database-postgres-imports.test.mjs
- name: Run the ci-stages baseline suite
run: node --test tests/ci-stages.test.mjs
# Stage 6 — PostgreSQL integration tests (E00-S05-T01). The PostgreSQL
# adapter suites (migration ledger, advisory lock, failure diagnostic) and
# the app readiness suite: their docker-gated real-stack probes (migrate an
# empty database, hold/release the advisory lock, readiness waits on the
# startup migration run) run where a Docker daemon is available and skip
# cleanly otherwise; the static and deterministic probes always gate. The
# app-readiness probes boot the committed server, so the config and
# database-postgres packages are built first.
postgres-integration:
name: Stage 6 — PostgreSQL integration tests (E00-S05-T01)
needs: architecture
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js 24
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Enable pnpm (corepack, pinned to 11.23.0 via packageManager)
run: corepack enable
- name: Install dependencies (frozen lockfile)
run: pnpm install --frozen-lockfile
- name: Build the config and database-postgres packages (the probes boot the committed server which imports them)
run: pnpm --filter @personal-blog/config build && pnpm --filter @personal-blog/database-postgres build
- name: Run the database-postgres-ledger suite
run: node --test tests/database-postgres-ledger.test.mjs
- name: Run the database-postgres-lock suite
run: node --test tests/database-postgres-lock.test.mjs
- name: Run the database-postgres-diagnostic suite
run: node --test tests/database-postgres-diagnostic.test.mjs
- name: Run the app-readiness suite
run: node --test tests/app-readiness.test.mjs
# Stage 7 — build the applications (E00-S05-T01). Builds every workspace
# application under apps/ (apps/server today; apps/admin when E06-S01 lands
# — the pnpm apps-group glob picks it up automatically) and verifies the
# compiled server artifact.
build-apps:
name: Stage 7 — Build the admin and server applications (E00-S05-T01)
needs: postgres-integration
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js 24
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Enable pnpm (corepack, pinned to 11.23.0 via packageManager)
run: corepack enable
- name: Install dependencies (frozen lockfile)
run: pnpm install --frozen-lockfile
- name: Build the workspace applications (apps/* — server today, admin when E06-S01 lands)
run: pnpm --filter "./apps/**" run build
- name: Verify the compiled server application artifact
run: test -f apps/server/dist/index.js