Skip to content

Scheduler: fail closed on an unreadable run ledger, narrow the first-run fan-out #238

Scheduler: fail closed on an unreadable run ledger, narrow the first-run fan-out

Scheduler: fail closed on an unreadable run ledger, narrow the first-run fan-out #238

Workflow file for this run

# The CANON repo's own CI — Claudinite's test suite + its own conformance sweep
# on every PR. Not consumer wiring (consumers ship no workflow, #385).
name: CI
on:
pull_request:
push:
branches: [main]
defaults:
run:
shell: bash
jobs:
test-and-conformance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Check out the PR's real head, not the synthetic refs/pull/N/merge commit
# actions/checkout uses by default — otherwise HEAD is an ephemeral merge of
# the branch into main, which the work-scoped squash-merge-history check would
# (correctly, for that synthetic commit) flag. This makes CI evaluate exactly
# what the local Stop hook does: the branch tip. Falls back to github.sha on push.
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Unit tests
# THE test roots — a closed set of FOLDERS, each swept to any depth. Depth is
# deliberately not enumerated: the previous form spelled out one glob per level
# (`packs-tests/*/skills/*/*.test.mjs` and so on), so a test at an unlisted depth
# was silently never run — which is exactly how vendoring/'s two files sat outside
# CI. `**` removes depth from the question; what remains is the root list, and
# engine-tests/ci-test-roots.test.mjs fails the moment a tracked test lands
# outside it. Adding a root is a deliberate CI edit; adding a test is not.
# engine-tests/ engine/ + the other top-level canon code (migrations/, …);
# mirrors engine/'s inner structure, and stays canon-side
# because engine/ ships to consumers exactly as-is (#385)
# packs-tests/ every canon pack's tests, mirroring packs/
# vendoring/ co-located with the vendor computer/writer they cover
# .claudinite/local/packs/ this repo's own local packs — tests live inside the tree
# nullglob: a root with no tests drops out instead of reaching node as a literal pattern.
run: |
shopt -s globstar nullglob
tests=(engine-tests/**/*.test.mjs packs-tests/**/*.test.mjs vendoring/**/*.test.mjs .claudinite/local/packs/**/*.test.mjs)
node --test "${tests[@]}"
- name: Conformance sweep
# The world sweep's home is the test/CI flow (not the Stop hook, which
# carries only the work scope) — this canon repo models the pattern
# bootstrap wires into consumers. World only: the work scope judges a
# session's change and runs at that session's Stop hook, not here.
run: node engine/checks/check_the_world.mjs
- name: Promote write-surface gate
# The growth promote stage (.claudinite/local/packs/canon-curation/tasks/growth-promote/task.md)
# is the one change bounded to packs/ + skills/ — an ordinary engine PR legitimately edits
# checks/ and engine/, so this cannot run on every PR. It keys on the promote branch prefix
# (the task doc pins it) and fails the promote PR if a changed path strays.
if: contains(github.head_ref, 'growth-promote')
run: node .claudinite/local/packs/canon-curation/promote-scope.mjs