Skip to content

feat: canonical component build system — one file per component, generated outputs, drift gate #4

feat: canonical component build system — one file per component, generated outputs, drift gate

feat: canonical component build system — one file per component, generated outputs, drift gate #4

name: Packages Checks
on:
pull_request:
paths:
- 'packages/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- '.github/workflows/packages-checks.yml'
- '.github/dependabot.yml'
- 'scripts/validation/detect-pr-changes.ts'
- 'scripts/validation/detect-pr-changes.test.ts'
# The canonical source, its committed output, and the drift gate's machinery.
- 'content/**'
- '.opencode/agent/**'
- '.oac/**'
- 'registry.json'
- 'Makefile'
- 'scripts/validation/check-build-drift.sh'
permissions:
contents: read
jobs:
check-changes:
name: Detect Package Changes
runs-on: ubuntu-latest
outputs:
has-packages: ${{ steps.filter.outputs.has-packages }}
has-canonical: ${{ steps.filter.outputs.has-canonical }}
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14
# The detector decides which gates run, so a broken detector silently skips them —
# including the drift gate. Its tests also pin the drift job's contract (that it is
# blocking, gated on has-canonical, and runs the same make target contributors do).
# They must execute somewhere, and nothing else in CI ran them.
- name: Test change detection
run: bun test scripts/validation/detect-pr-changes.test.ts
- name: Check changed files
id: filter
run: |
echo "Changed files:" >&2
git diff --name-only -z "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}" |
tee >(while IFS= read -r -d '' path; do printf ' %q\n' "$path" >&2; done) |
bun run scripts/validation/detect-pr-changes.ts
cli-checks:
name: CLI (build + test + typecheck)
runs-on: ubuntu-latest
timeout-minutes: 10
needs: check-changes
if: needs.check-changes.outputs.has-packages == 'true'
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14
- name: Setup pnpm
uses: pnpm/action-setup@b0f76dfb45f55f8421693e4803ac7bb65143bd34 # v6
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: 'pnpm'
cache-dependency-path: 'pnpm-lock.yaml'
- name: Install workspace dependencies
run: pnpm install --frozen-lockfile
# cli tsconfig paths point at ../compatibility-layer/dist, so build it first.
- name: Build compatibility-layer dependency
run: pnpm --dir packages/compatibility-layer run build
- name: Typecheck
run: pnpm --dir packages/cli run typecheck
- name: Build
run: pnpm --dir packages/cli run build
# packages/cli tests use Bun-only APIs (Bun.file, Bun.spawn, import.meta.dir)
# until Stage 5 removes them — they must run under `bun test`, not vitest.
- name: Test
run: pnpm --dir packages/cli run test
compatibility-layer-checks:
name: Compatibility Layer (build + test + lint)
runs-on: ubuntu-latest
timeout-minutes: 10
needs: check-changes
if: needs.check-changes.outputs.has-packages == 'true'
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@b0f76dfb45f55f8421693e4803ac7bb65143bd34 # v6
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: 'pnpm'
cache-dependency-path: 'pnpm-lock.yaml'
- name: Install workspace dependencies
run: pnpm install --frozen-lockfile
# build runs tsc, which is also the typecheck for this package.
- name: Build (tsc typecheck)
run: pnpm --dir packages/compatibility-layer run build
- name: Lint
run: pnpm --dir packages/compatibility-layer run lint
- name: Test (vitest)
run: pnpm --dir packages/compatibility-layer test
# The structural guarantee the canonical refactor rests on: the generated trees stay
# COMMITTED, so drift has to be impossible rather than merely discouraged. Rebuild them from
# content/agents/** and fail if the build changed anything — a hand-edit to generated output
# turns this red. BLOCKING by design: no continue-on-error.
canonical-drift:
name: Canonical Build (no drift)
runs-on: ubuntu-latest
timeout-minutes: 10
needs: check-changes
if: needs.check-changes.outputs.has-canonical == 'true'
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
# `oac build` runs from packages/cli/dist, which is bundled `--target bun`.
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14
- name: Setup pnpm
uses: pnpm/action-setup@b0f76dfb45f55f8421693e4803ac7bb65143bd34 # v6
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: 'pnpm'
cache-dependency-path: 'pnpm-lock.yaml'
- name: Install workspace dependencies
run: pnpm install --frozen-lockfile
# The identical target contributors run locally, so a red gate here is reproducible with
# one command rather than by reading this file and reassembling it by hand.
- name: Build canonical trees and check for drift
run: make check-canonical-drift
summary:
name: Packages Checks Summary
runs-on: ubuntu-latest
needs: [check-changes, cli-checks, compatibility-layer-checks, canonical-drift]
if: always()
steps:
- name: Generate summary
run: |
echo "## 📦 Packages Checks Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
report() {
local label="$1" result="$2"
case "$result" in
success) echo "✅ **${label}:** Passed" >> $GITHUB_STEP_SUMMARY ;;
skipped) echo "⏭️ **${label}:** Skipped (no relevant changes)" >> $GITHUB_STEP_SUMMARY ;;
*) echo "❌ **${label}:** ${result}" >> $GITHUB_STEP_SUMMARY ;;
esac
}
report "Change Detection" "${{ needs.check-changes.result }}"
report "CLI" "${{ needs.cli-checks.result }}"
report "Compatibility Layer" "${{ needs.compatibility-layer-checks.result }}"
report "Canonical Build (no drift)" "${{ needs.canonical-drift.result }}"
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ needs.canonical-drift.result }}" = "failure" ]; then
echo "> The generated trees drift from \`content/agents/**\`." >> $GITHUB_STEP_SUMMARY
echo "> Run \`make build-canonical\` and commit the result." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
FAILED=0
[ "${{ needs.check-changes.result }}" != "success" ] && FAILED=1
for result in "${{ needs.cli-checks.result }}" "${{ needs.compatibility-layer-checks.result }}" "${{ needs.canonical-drift.result }}"; do
if [ "$result" != "success" ] && [ "$result" != "skipped" ]; then
FAILED=1
fi
done
if [ $FAILED -eq 0 ]; then
echo "### ✅ Required Package Checks Passed" >> $GITHUB_STEP_SUMMARY
else
echo "### ❌ Some Package Checks Failed" >> $GITHUB_STEP_SUMMARY
exit 1
fi