diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index b92c349..2ff5ade 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -5,9 +5,34 @@ on: push: 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: + # 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: - name: Frozen lockfile install + name: Stage 1 — Frozen lockfile install (E00-S05-T01) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -22,11 +47,10 @@ jobs: - name: Verify workspace groups run: pnpm -r list --depth -1 - # E00-S02-T08: the static assertions of tests/secrets-not-embedded.test.mjs - # gate every PR (the docker-gated layer-scan probe inside the same file runs - # where a Docker daemon is available and skips cleanly otherwise). - secrets-not-embedded: - name: Secrets not embedded (E00-S02-T08) + # Stage 2 — typecheck (E00-S05-T01). + typecheck: + name: Stage 2 — Typecheck (E00-S05-T01) + needs: frozen-install runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -34,290 +58,78 @@ jobs: uses: actions/setup-node@v4 with: 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 - - # 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 + - name: Run the config-schema unit suite run: node --test tests/config-schema.test.mjs - - # E00-S04-T05: the static assertions of tests/env-example.test.mjs gate every - # PR — the suite locks in the committed `.env.example` template: it exists at - # the repo root, is un-ignored in .gitignore (real `.env` files stay ignored - # while the example is tracked), documents every configuration environment - # source (HOST/PORT/DATABASE_URL/EPPP_SESSION_SECRET), and contains - # placeholder values only — no credential URI, no long secret-looking value, - # 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 + - name: Run the config-startup-error unit suite + run: node --test tests/config-startup-error.test.mjs + - name: Run the config-log-redaction unit suite + run: node --test tests/config-log-redaction.test.mjs + - name: Run the config-env-adapter unit suite + run: node --test tests/config-env-adapter.test.mjs + - name: Run the env-example unit suite run: node --test tests/env-example.test.mjs - # E00-S03-T01: the static assertions of tests/compose-config.test.mjs (db - # image pinned to postgres:18.6-bookworm, health gate, volume persistence, - # build platforms) gate every PR (the docker-gated real-stack probes inside - # the same file run where a Docker daemon is available and skip cleanly - # otherwise). - compose-config: - name: Compose config (E00-S03-T01) + # Stage 5 — architecture tests (E00-S05-T01). Static structure and policy + # suites: dependency boundaries, workspace layout/configuration, strict + # TypeScript base, engine/TypeScript pins, root commands, frozen-install + # clean clone, container definition structure, and the CI baseline itself. + architecture: + name: Stage 5 — Architecture tests (E00-S05-T01) + needs: unit runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -325,5 +137,93 @@ jobs: uses: actions/setup-node@v4 with: 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 + - 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