Rendered from specs/S09-ci-workflow-scoping.md — edit it there, not here.
S09 — CI Workflow Scoping
Overview
ci.yml's lint/type-check/compile/bundle-size/coverage/audit/ security/knip/integration jobs and codeql.yml's analyze job all exercise the TypeScript/JS build & test surface (lint, tsc, webpack, coverage, maturity, Trivy, knip, the two-OS integration suite, CodeQL). The first six used to be sequential steps inside one build job; they're independent of each other (aside from coverage's own two steps — the maturity scorer reads that job's coverage output, so those two stay sequential within the coverage job) and now run as separate jobs so they execute in parallel instead of paying for each other's wall-clock time in sequence. None of that can be broken by a docs-only or spec-only change, yet every PR — including one that only edits specs/traceability.json or a docs/** page — paid for all of it. Meanwhile the one check that does matter for exactly that kind of PR, requirement/documentation traceability drift, ran nowhere in CI at all — the opposite of what was needed.
This spec scopes the expensive jobs to diffs that could plausibly break them, while keeping the cheap traceability checks unconditional so they still catch drift on the PRs most likely to introduce it.
Requirements
Always-On Traceability
S09-SR-08 No workflow may use
continue-on-error: true. A job that should not block merging MUST be left out of branch protection's required status checks instead — that is the setting that decides whether a red check blocks the merge button, and it does so without falsifying the check's result.continue-on-errorreports success for a job that failed, so the failure is invisible in the checks list, in the API, and to any policy built on either; green stops meaning green, and every other check on the run loses value with it.The cost is not hypothetical. The maturity step carried it, and that is why five checks scoring a silent zero went unnoticed (S12-SR-15). The integration job carried it, which made S08-SR-08's "clean for 2 consecutive weeks" promotion criterion permanently unmeasurable.
S09-SR-07 Every step of the mandatory local gate (
npm run verify) MUST also be reachable in CI, so a commit that bypasses the git hook —--no-verify, a fresh clone whose hooks are not installed, an edit made through the repository host's web UI — still meets the same bar. The unconditional spec-checks job is where the cheap plain-Node checkers belong;lint:workflowsis satisfied byci.yml's ownactionlintjob (S09-SR-05) rather than a duplicate step.S09-SR-01
check:traceabilityandcheck:doc-traceabilityMUST run in CI (ci.yml) on every push and pull request, independent of which paths changed — these are the checks a docs/spec-only PR is most likely to need (S07-SR-08's guarantee extends tocheck:traceabilitytoo, not just the doc checker).
Path-Scoped Jobs
- S09-SR-02
ci.yml'slint,type-check,compile,bundle-size,coverage(tests, coverage, and the maturity check, which depends on that job's coverage output),audit,security(Trivy),knip, andintegrationjobs, andcodeql.yml'sanalyzejob, MUST be skipped when the triggering diff touches none of:src/**,scripts/**,package.json,package-lock.json,webpack.config.js,tsconfig*.json,eslint.config.js,knip.json,stryker.config.json,commitlint.config.js,playwright.e2e.config.ts, or.github/workflows/**. A diff with no resolvable base commit (new branch, history rewrite —beforeis unset or absent from history) MUST default to running everything rather than skipping. - S09-SR-03 The skip in SR-02 MUST be implemented as a job-level
if:condition fed by a sharedchangesjob's output, not as a workflow-levelon.push.paths/on.pull_request.paths(orpaths-ignore) trigger. Rationale: a path-filtered trigger never starts the workflow at all on a non-matching diff, and if that workflow's job is configured as a required status check in branch protection, GitHub shows it as permanently pending ("Expected — Waiting") instead of passing — it blocks merges rather than skipping cleanly. A job skipped viaif:still reports a (skipped, passing) conclusion for that job, so required-status-check semantics hold regardless of branch-protection configuration. - S09-SR-04
codeql.yml's weeklyschedulerun MUST always perform a full analysis regardless of SR-02's path filter — a scheduled run has no PR diff to gate against, and periodic full scans are the point of the schedule trigger. - S09-SR-05 The workflow files themselves MUST be statically checked in CI with
actionlint— the workflow YAML is code (shell scripts, expressions, action references) and deserves a linter like the rest of the source. The check MUST run on every push and pull request, and MUST NOT add an unpinned action reference (use a version-pinned container image, which is exempt from the SHA-pin rule the security metric enforces). - S09-SR-06 The same actionlint check MUST also be runnable locally (
npm run lint:workflows) and MUST be part ofnpm run verify, so the Husky pre-commit hook catches a broken workflow before it reaches CI —AGENTS.md's "Full local gate ... CI reaches the same checks" claim MUST hold for this check too, not only for lint/tsc/tests. The local script and CI's container image MUST pin the identical actionlint version; a consistency check (npm run check:consistency) MUST fail if they drift. - S09-SR-09
ci.ymlMUST include adocsjob that, whenever the triggering diff touchesdocs/**, installsdocs/'s dependencies from its own lockfile (npm ci --prefix docs), builds the VitePress site (npm run build --prefix docs), and audits that lockfile (npm run check:docs-audit).docs/is a separate npm project with its ownpackage.json/package-lock.json— neither root'snpm audit/check:auditnor any job gated by SR-02's path set inspects it, anddocs.yml's build step (which does) only runs on push tomainor manual dispatch (Out of Scope), never on a pull request. Without this job a docs-only PR that bumps a vulnerable or build-breakingdocs/dependency merges with no CI signal at all.check:docs-auditMUST fail only on a high/critical advisory thatnpm audit fixcould resolve (fixAvailableis notfalseinnpm audit --json's output) — a plainnpm audit --audit-level=highwould also fail on an advisory with no upstream fix (asGHSA-fx2h-pf6j-xcffcurrently is for vitepress's vite dependency), leaving the check permanently red through no fault of the PR that triggers it. Such findings MUST still be printed, just not treated as blocking. - S09-SR-10 The
docsjob's skip condition MUST follow SR-03's pattern: a job-levelif:fed by adocsoutput the sharedchangesjob computes alongsidesrc(extendingscripts/ci-detect-source-changes.sh), not a second workflow-level path trigger — for the same reason SR-03 requires it forsrc(a path-filtered trigger leaves a required check permanently "Expected — Waiting" on a non-matching diff instead of passing).
Non-Functional Requirements
- S09-NFR-01 The diff detection in SR-02 MUST use only
git/shell commands already available on GitHub-hosted runners (no third-party path-filtering action), consistent with this project's preference for standards over added vendor tooling (AGENTS.md, "Agnosticity & standardization"). - S09-NFR-02 The path set and diff logic MUST be defined exactly once, in
scripts/ci-detect-source-changes.sh, and referenced identically by everychangesjob (ci.yml,codeql.yml) rather than duplicated inline in workflow YAML — a second workflow needing the same scoping calls the same script instead of copying the pattern.
Out of Scope
- Path-scoping workflows other than
ci.ymlandcodeql.yml(docs.yml,mutation.yml,scorecard.yml,snyk.yml,maturity-refresh.yml,refresh-gifs.yml) — these already trigger on their own schedule/workflow_dispatch/workflow_runconditions, not on every PR, so they don't have the cost problem this spec addresses. - Promoting
integrationto a required check (tracked separately inS08-SR-08).
Acceptance Criteria
- A PR touching only
specs/**or root*.mdfiles shows thetraceabilityjob passing andlint/type-check/compile/bundle-size/coverage/audit/security/knip/integration/docs(and CodeQL'sanalyze) reporting as skipped, not pending. - A PR touching
src/**runs every job as before this spec. - A PR that only edits
specs/traceability.jsonstill runscheck:traceabilityandcheck:doc-traceability. - The Monday 03:27 UTC
codeql.ymlschedule run always performs a full analysis. ci.ymlandcodeql.ymleach invokescripts/ci-detect-source-changes.shrather than defining the path pattern inline; changing the pattern is a one-file edit.- A PR touching only
docs/**showslint/type-check/compile/bundle-size/coverage/audit/security/knip/integration(and CodeQL'sanalyze) skipped, whiledocs(S09-SR-09) actually runs and fails the check if the VitePress build breaks ordocs/'s lockfile gains a high/critical advisorynpm audit fixcould resolve — but not for one with no upstream fix yet (fixAvailable: false).
Relation to Existing Specs
- Closes the enforcement gap the investigation behind this spec found:
check:doc-traceability's CI guarantee (S07-SR-08) is extended here tocheck:traceabilityas well, and both are made structurally unconditional rather than steps inside a job that could later be scoped away. - Complements S08's CI-integration requirements (
S08-SR-08/09) for theintegrationjob specifically; this spec governs when that job (and the otherci.yml/codeql.ymljobs) run at all. - This is a CI/tooling change; per
specs/README.md's traceability workflow such changes don't strictly require a spec entry ("say so instead"), but it's specified here anyway since the design (job-levelif:vs. workflow-level path triggers) has a real correctness trade-off worth recording, not just a mechanical config tweak.
History
- 2026-09-01 — Added S09-SR-09/10: a
docsjob, scoped todocs/**the same way SR-02's jobs are scoped tosrc/**. Found by gap, not by design: a Snyk weekly scan flagged high-severity nanoid/postcss advisories indocs/'s VitePress lockfile that had merged with zero CI signal — rootnpm auditdoesn't see a nested lockfile, anddocs.yml's build step (the one place that would have caught a broken build too) only runs on push tomain, never on the PR that introduced the bump. - 2026-07-26 — Split
ci.yml'sbuildjob into six independent jobs (lint,type-check,compile,bundle-size,coverage,audit) that all fan out fromchangesin parallel, for maximum CI parallelism. Same skip condition (S09-SR-02), same steps, same npm scripts — only the job boundaries changed, since the six were already independent of each other exceptcoverage's own two internal steps (test run → maturity check, which reads that run's coverage output). - 2026-07-22 — Added S09-SR-06: actionlint must also run locally via
npm run verify, not only in CI, closing a local/CI parity gap (the Husky pre-commit hook rannpm run verify, which did not include the workflow-lint check CI already enforced).
Requirement traceability
Status of each requirement in specs/traceability.json. Test coverage is auto-discovered from [ID] tags in test titles and is not listed here.
| Requirement | Status | Implementation | Note |
|---|---|---|---|
S09-NFR-01 | manual | .github/workflows/ci.yml, .github/workflows/codeql.yml | diff detection uses plain git diff/grep, no third-party path-filter action; CI workflow-scoping requirement: enforced/observable in .github/workflows, not unit-taggable |
S09-NFR-02 | manual | scripts/ci-detect-source-changes.sh, .github/workflows/ci.yml, .github/workflows/codeql.yml | single script shared by both changes jobs instead of duplicated inline shell; CI workflow-scoping requirement: enforced/observable in .github/workflows, not unit-taggable |
S09-SR-01 | manual | .github/workflows/ci.yml | traceability job runs unconditionally on every push/PR; CI workflow-scoping requirement: enforced/observable in .github/workflows, not unit-taggable |
S09-SR-02 | manual | .github/workflows/ci.yml, .github/workflows/codeql.yml | build/security/knip/integration/analyze jobs gated by the changes job output; CI workflow-scoping requirement: enforced/observable in .github/workflows, not unit-taggable |
S09-SR-03 | manual | .github/workflows/ci.yml, .github/workflows/codeql.yml | job-level if: conditions, not workflow-level paths/paths-ignore triggers; CI workflow-scoping requirement: enforced/observable in .github/workflows, not unit-taggable |
S09-SR-04 | manual | .github/workflows/codeql.yml | analyze's if: always() && (schedule || changes) keeps the weekly cron unconditional; CI workflow-scoping requirement: enforced/observable in .github/workflows, not unit-taggable |
S09-SR-05 | manual | .github/workflows/ci.yml | actionlint job lints all workflow YAML on push/PR via version-pinned container image; CI-verified, no unit test |
S09-SR-06 | manual | scripts/lint-workflows.mjs, package.json, scripts/check-consistency.mjs | lint:workflows wired into npm run verify (Husky pre-commit + CI); PATH/go-run/docker fallback chain; check:consistency asserts the pinned version matches ci.yml's actionlint container tag |
S09-SR-07 | implemented | .github/workflows/ci.yml | every verify step reachable in CI; consistency/spec-effort/spec-value added to the unconditional job |
S09-SR-08 | implemented | .github/workflows/ci.yml, .github/workflows/refresh-gifs.yml, scripts/maturity-score.mjs | no workflow uses continue-on-error; non-blocking is a branch-protection setting. Scorer predicate ignores YAML comments so prose about it does not trip the check. |
S09-SR-09 | manual | .github/workflows/ci.yml, scripts/check-docs-audit.mjs | docs job, gated on the changes job's docs output; CI workflow requirement, not unit-taggable |
S09-SR-10 | manual | .github/workflows/ci.yml, scripts/ci-detect-source-changes.sh | docs output computed alongside src in the shared changes-detection script; CI workflow requirement, not unit-taggable |
Documented in
Documentation sections tagged spec:S09 (or one of its requirement ids) — 132 of ~768 expected words (17%). The expectation comes from this spec's evaluated complexity of 19.2 (12 documentable requirements — 10 SR, 2 NFR — weighted by kind and by each definition's length).
Changes over time
f1cd9022026-09-01feat(ci): add path-scoped docs build & audit job (S09-SR-09/10)bfeaabe2026-07-26ci: split the build job into independent parallel jobs451ae7b2026-07-26fix(ci): stop reporting failed jobs as green, and earn the check backefd676d2026-07-26feat(specs): attribute defects via a Fixes: commit trailer, and close CI gaps096341d2026-07-22feat(S09): run actionlint locally too, closing the CI/local parity gap6de34152026-07-20feat(S14): delivery-performance (DORA) metrics + actionlint in CI01d109b2026-07-14chore: close out and remove todo.md924cfb32026-07-12ci: centralize source-diff detection into one script9eb39172026-07-12ci: scope CI/CodeQL jobs to source-relevant diffs