Skip to content

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-error reports 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:workflows is satisfied by ci.yml's own actionlint job (S09-SR-05) rather than a duplicate step.

  • S09-SR-01 check:traceability and check:doc-traceability MUST 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 to check:traceability too, not just the doc checker).

Path-Scoped Jobs ​

  • S09-SR-02 ci.yml's lint, type-check, compile, bundle-size, coverage (tests, coverage, and the maturity check, which depends on that job's coverage output), audit, security (Trivy), knip, and integration jobs, and codeql.yml's analyze job, 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 — before is 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 shared changes job's output, not as a workflow-level on.push.paths / on.pull_request.paths (or paths-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 via if: 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 weekly schedule run 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 of npm 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.yml MUST include a docs job that, whenever the triggering diff touches docs/**, installs docs/'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 own package.json/package-lock.json — neither root's npm audit/ check:audit nor any job gated by SR-02's path set inspects it, and docs.yml's build step (which does) only runs on push to main or manual dispatch (Out of Scope), never on a pull request. Without this job a docs-only PR that bumps a vulnerable or build-breaking docs/ dependency merges with no CI signal at all. check:docs-audit MUST fail only on a high/critical advisory that npm audit fix could resolve (fixAvailable is not false in npm audit --json's output) — a plain npm audit --audit-level=high would also fail on an advisory with no upstream fix (as GHSA-fx2h-pf6j-xcff currently 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 docs job's skip condition MUST follow SR-03's pattern: a job-level if: fed by a docs output the shared changes job computes alongside src (extending scripts/ci-detect-source-changes.sh), not a second workflow-level path trigger — for the same reason SR-03 requires it for src (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 every changes job (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.yml and codeql.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_run conditions, not on every PR, so they don't have the cost problem this spec addresses.
  • Promoting integration to a required check (tracked separately in S08-SR-08).

Acceptance Criteria ​

  1. A PR touching only specs/** or root *.md files shows the traceability job passing and lint/type-check/compile/ bundle-size/coverage/audit/security/knip/integration/docs (and CodeQL's analyze) reporting as skipped, not pending.
  2. A PR touching src/** runs every job as before this spec.
  3. A PR that only edits specs/traceability.json still runs check:traceability and check:doc-traceability.
  4. The Monday 03:27 UTC codeql.yml schedule run always performs a full analysis.
  5. ci.yml and codeql.yml each invoke scripts/ci-detect-source-changes.sh rather than defining the path pattern inline; changing the pattern is a one-file edit.
  6. A PR touching only docs/** shows lint/type-check/compile/ bundle-size/coverage/audit/security/knip/integration (and CodeQL's analyze) skipped, while docs (S09-SR-09) actually runs and fails the check if the VitePress build breaks or docs/'s lockfile gains a high/critical advisory npm audit fix could 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 to check:traceability as 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 the integration job specifically; this spec governs when that job (and the other ci.yml/codeql.yml jobs) 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-level if: 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 docs job, scoped to docs/** the same way SR-02's jobs are scoped to src/**. Found by gap, not by design: a Snyk weekly scan flagged high-severity nanoid/postcss advisories in docs/'s VitePress lockfile that had merged with zero CI signal — root npm audit doesn't see a nested lockfile, and docs.yml's build step (the one place that would have caught a broken build too) only runs on push to main, never on the PR that introduced the bump.
  • 2026-07-26 — Split ci.yml's build job into six independent jobs (lint, type-check, compile, bundle-size, coverage, audit) that all fan out from changes in 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 except coverage'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 ran npm 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.

RequirementStatusImplementationNote
S09-NFR-01manual.github/workflows/ci.yml, .github/workflows/codeql.ymldiff 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-02manualscripts/ci-detect-source-changes.sh, .github/workflows/ci.yml, .github/workflows/codeql.ymlsingle 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-01manual.github/workflows/ci.ymltraceability job runs unconditionally on every push/PR; CI workflow-scoping requirement: enforced/observable in .github/workflows, not unit-taggable
S09-SR-02manual.github/workflows/ci.yml, .github/workflows/codeql.ymlbuild/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-03manual.github/workflows/ci.yml, .github/workflows/codeql.ymljob-level if: conditions, not workflow-level paths/paths-ignore triggers; CI workflow-scoping requirement: enforced/observable in .github/workflows, not unit-taggable
S09-SR-04manual.github/workflows/codeql.ymlanalyze's if: always() && (schedule || changes) keeps the weekly cron unconditional; CI workflow-scoping requirement: enforced/observable in .github/workflows, not unit-taggable
S09-SR-05manual.github/workflows/ci.ymlactionlint job lints all workflow YAML on push/PR via version-pinned container image; CI-verified, no unit test
S09-SR-06manualscripts/lint-workflows.mjs, package.json, scripts/check-consistency.mjslint: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-07implemented.github/workflows/ci.ymlevery verify step reachable in CI; consistency/spec-effort/spec-value added to the unconditional job
S09-SR-08implemented.github/workflows/ci.yml, .github/workflows/refresh-gifs.yml, scripts/maturity-score.mjsno 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-09manual.github/workflows/ci.yml, scripts/check-docs-audit.mjsdocs job, gated on the changes job's docs output; CI workflow requirement, not unit-taggable
S09-SR-10manual.github/workflows/ci.yml, scripts/ci-detect-source-changes.shdocs 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 jobs
  • 451ae7b2026-07-26fix(ci): stop reporting failed jobs as green, and earn the check back
  • efd676d2026-07-26feat(specs): attribute defects via a Fixes: commit trailer, and close CI gaps
  • 096341d2026-07-22feat(S09): run actionlint locally too, closing the CI/local parity gap
  • 6de34152026-07-20feat(S14): delivery-performance (DORA) metrics + actionlint in CI
  • 01d109b2026-07-14chore: close out and remove todo.md
  • 924cfb32026-07-12ci: centralize source-diff detection into one script
  • 9eb39172026-07-12ci: scope CI/CodeQL jobs to source-relevant diffs

Released under the MIT License.