Skip to main content

Summary

When building the STRATT CI GitHub Actions workflow, the natural temptation was to implement each of the 8 failure modes as separate shell steps — call stratt validate, grep for cycles, check fingerprints. Instead, the entire CI check sequence already existed as a single function (runCIPipeline()) in @stratt/graph/ci.ts with 35 passing tests. The workflow needed thin CLI wrappers, not reimplemented logic.

The Pattern

The @stratt/graph package had a fully tested runCIPipeline(input: CIInput): CIResult that orchestrated all 9 checks (schema, fingerprint, imports, DAG, protected agents, gate removal, contract breaks, draft isolation, blast radius) and returned a structured result with a pre-formatted markdown report. Two new CLI commands (stratt ci and stratt impact) simply marshalled I/O: reading files, fetching previous versions from git, loading config — then delegated to the library.

Business Impact

This preserved the single-source-of-truth property. When a new failure mode (FM-09+) is added to @stratt/graph, every CI workflow inherits it automatically. No workflow YAML changes needed. The workflow is a consumer of the graph API, not a parallel implementation that can drift.

Key Takeaway

If your CI pipeline logic lives in shell scripts inside YAML, you’ve duplicated your domain rules into an untestable format. Move the rules to a testable library, then make CI a thin caller. This is the same principle as keeping business logic out of HTTP handlers — CI workflows are just another I/O boundary.