Files
accounted/.github/workflows/core-build.yml
T
Jakob Wennberg 304baf1089 chore(ci): ratchet TypeScript errors, because npm test does not typecheck (#1980)
Vitest transpiles and discards types, so a type error passes all 18 000 tests
and only surfaces in npm run build several minutes later. That happened twice
on 2026-08-27: a widened union in the MCP server that a second declaration in
lib/events/types.ts still contradicted, and an interface that would not assign
into Record<string, unknown>[] because interfaces have no implicit index
signature. Both were caught by the build. Neither was caught by the tests,
which is the wrong order to learn it in.

This is not just a faster copy of the build job. tsc --noEmit also covers
__tests__ files, which the Next.js build never compiles, and that is where all
539 baseline errors live.

Baselined per FILE rather than per error code, unlike the lint ratchet: the
legacy errors sit in a handful of old test files and TS2322 is common enough
that a code-keyed budget would let a real regression hide behind a legacy fix
somewhere else.

Measured: 36s cold, which is what CI pays, and 4.4s warm locally.

Verified the gate fires by introducing a deliberate type error and watching it
fail with the exact location, then restoring.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 17:53:04 +02:00

185 lines
8.5 KiB
YAML

name: Core Build (no extensions)
# Split out of a single serial job that took 7m43s wall-clock: npm ci 24s, lint
# ratchet 1m29s, build 2m00s, unit tests 3m31s, ratchets ~10s. None of those
# stages needed the previous one's output, so they were serial only by
# accident. Running them as independent jobs (and sharding the 897-file unit
# suite four ways) puts the critical path on `build` at roughly 2m30s.
#
# The prologue each job needs is in .github/actions/setup-core.
on: [pull_request]
# Six concurrent jobs per push makes stale runs far more expensive than they
# were when this was one job, and a superseded push has nothing worth finishing.
concurrency:
group: core-build-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
checks:
name: Checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
# Nothing here pushes, so leaving the token in .git/config only widens
# what a compromised dependency in the build could reach.
persist-credentials: false
- uses: ./.github/actions/setup-core
- name: Verify skill bodies are in sync with the seed migration
# Fails if a .claude/skills/**/SKILL.md changed without regenerating the
# seed migration (npm run skills:generate). Keeps prod skill content from
# silently drifting out of sync. No DB needed: reads files + manifest.
run: npm run skills:check
- name: Verify taxonomy registry is in sync with the element lists
# Fails if dev_docs/bokslut/taxonomi/** changed without regenerating
# lib/bokslut/ixbrl/taxonomy/generated/ (npm run taxonomy:generate).
# The iXBRL generator emits facts strictly from the generated registry,
# so drift here means filings tagged against a stale concept set.
run: npm run taxonomy:check
- name: Verify the accounted-api agent skill is in sync with the registry
# Fails if a v1 endpoint / registry schema / overlay changed without
# regenerating skills/accounted-api (npm run apiskill:generate). The
# skill is the installable API reference for external coding agents;
# drift here means agents integrate against stale endpoint contracts.
run: npm run apiskill:check
- name: Lint ratchet (no new ESLint errors)
# `npm run lint` was never wired into CI, so ~60 legacy errors
# accumulated. This ratchet (sibling of check:guards) fails only when
# a PR ADDS an error beyond scripts/checks/eslint-baseline.json; the
# baseline ratchets down as legacy errors get fixed.
run: npm run check:lint
- name: Typecheck ratchet (no new TypeScript errors)
# `npm test` does not typecheck: vitest transpiles and discards types,
# so a type error passes all 18 000 tests and only surfaces in the
# build. That happened twice on 2026-08-27. This also covers __tests__
# files, which the Next.js build never compiles, so it is not merely a
# faster copy of the build job.
#
# Baselined per FILE in scripts/checks/typecheck-baseline.json (539
# legacy errors across 83 files, all in test files) and ratchets down.
# Per-file rather than per-error-code on purpose: a code-keyed budget
# would let a real regression hide behind a legacy fix elsewhere.
run: npm run check:types
- name: Antipattern ratchet (no new MFA-bypassing routes / naive öre-rounding)
# Fails only if a PR ADDS a route that hand-rolls supabase.auth.getUser()
# instead of the MFA-enforcing guard, or a new Math.round(x*100)/100.
# Baseline lives in scripts/checks/antipatterns-baseline.json and ratchets
# down as the A1 (route auth) and D1 (rounding) migrations land.
run: npm run check:guards
- name: Validate konteringspaket catalogue
# The system booking templates are data files under packs/ rather than
# rows frozen inside a migration. This gate is what makes that safe: it
# checks the schema, that every BAS account actually exists in the 2026
# chart, and that each pack BALANCES when applied through the real
# applyTemplate(). PR #1321 shipped seeded reference data that
# contradicted the engine; that class of bug is structurally valid and
# only a semantic check catches it.
run: npm run validate:packs
- name: Validate community registry
# registry/ is the public source of truth for gnubok.se/community/registry
# and takes entries by PR from outside the team. The gate checks the
# frontmatter the website build consumes, and rejects JSX/import/export
# in entry bodies: the site renders bodies through MDX, which would
# execute those at build time.
run: npm run validate:registry
- name: Check no core imports from extensions
run: |
VIOLATIONS=$(grep -r "from '@/extensions/" lib/ app/api/ components/ --include="*.ts" --include="*.tsx" \
| grep -v "app/api/extensions/" \
| grep -v "components/extensions/" \
| grep -v "lib/extensions/_generated/" \
| grep -v "lib/extensions/loader.ts" || true)
if [ -n "$VIOLATIONS" ]; then
echo "ERROR: Core code imports from @/extensions/:"
echo "$VIOLATIONS"
exit 1
fi
build:
name: Build (zero extensions)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
# Nothing here pushes, so leaving the token in .git/config only widens
# what a compromised dependency in the build could reach.
persist-credentials: false
- uses: ./.github/actions/setup-core
- run: npm run build
env:
# The Next.js build worker OOMs on the runner's default Node heap
# since 2026-08-19 (bundle growth crossed the default old-space
# ceiling; the first branch containing the whole day's merges was the
# first to hit it). Public-repo runners have 16 GB, give the build 8.
NODE_OPTIONS: --max-old-space-size=8192
test:
# 897 unit test files (11,341 tests) take 3m31s in one process. Measured
# locally, four shards split them 225/224/224/224 with the slowest at 64s,
# which puts this comfortably under the build job so it stops being the
# critical path. Vitest hard-errors when the shard count exceeds the
# resolved file count, which is nowhere near a concern at this size.
name: Unit tests (${{ matrix.shard }}/4)
runs-on: ubuntu-latest
strategy:
# One shard failing should not hide failures in the other three: a red PR
# is more useful when it lists every broken test, not just the first.
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
# Nothing here pushes, so leaving the token in .git/config only widens
# what a compromised dependency in the build could reach.
persist-credentials: false
- uses: ./.github/actions/setup-core
# Via env rather than interpolated straight into the shell body: the value
# is a static matrix integer and harmless, but keeping every `run:` free of
# ${{ }} is the rule that makes the template-injection audit meaningful.
- run: npm test -- --shard="$SHARD/4"
env:
SHARD: ${{ matrix.shard }}
core-build:
# One stable check name covering all six jobs above, so the PR checks list
# (and any future required-status-check rule) has a single thing to read
# rather than a shard-numbered matrix. `needs` alone would not be enough:
# a needed job that is skipped or cancelled does not fail its dependents,
# so the results are asserted explicitly.
name: Core Build
if: always()
needs: [checks, build, test]
runs-on: ubuntu-latest
steps:
- name: Assert every core job succeeded
env:
CHECKS: ${{ needs.checks.result }}
BUILD: ${{ needs.build.result }}
TEST: ${{ needs.test.result }}
run: |
set -euo pipefail
echo "checks=$CHECKS build=$BUILD test=$TEST"
failed=0
for r in "$CHECKS" "$BUILD" "$TEST"; do
[ "$r" = "success" ] || failed=1
done
if [ "$failed" -ne 0 ]; then
echo "::error::One or more core-build jobs did not succeed"
exit 1
fi