CI/CD Policy — Path-Aware Risk-Based Gating¶
Status: Active Last updated: 2026-10-07
Overview¶
This repository uses path-aware CI/CD gating to run the right checks for each pull request based on which files changed. The goals are:
- Minimize unnecessary compute — docs-only PRs should not wait for full OS-matrix CI, CodeQL, or package builds.
- Maintain security posture — secret scanning always runs; code analysis runs on code changes; dependency review runs when lockfiles change.
- Never break required checks — all branch-protection required checks report a status (success via no-op or full run) on every PR.
- Keep release workflows safe — real publish only happens on release/tag events with environment protection, OIDC, attestation, and cosign.
Change Categories¶
Each PR is classified by the files it touches. A PR can belong to multiple categories.
| Category | File Patterns |
|---|---|
| docs | docs/**, README.md, CONTRIBUTING.md, CHANGELOG.md, CODE_OF_CONDUCT.md, GOVERNANCE.md, MAINTAINERS.md, ROADMAP.md, SECURITY.md, SUPPORT.md, ARCHITECTURE.md, CITATION.cff, LICENSE, mkdocs.yml, assets/** |
| python | src/**, tests/**, pyproject.toml, uv.lock, uv.toml, scripts/**, conftest.py, pyrightconfig.json, .python-version, performance/**, evals/** |
| repo_tooling | package.json, pnpm-workspace.yaml, Dockerfile, docker-compose.yml, lefthook.yml, .pre-commit-config.yaml, Taskfile.yml, .mergify.yml, .github/dependabot.yml, .github/rulesets/**, sonar-project.properties, AGENTS.md, **/AGENTS.md |
| npm | packages/**, package.json, pnpm-lock.yaml, pnpm-workspace.yaml, .node-version, .npmrc, commitlint.config.cjs, .commitlintrc.json |
| schemas | packages/protocol-schemas/** |
| workflows | .github/workflows/**, .github/actions/** |
| dependencies | package.json, pnpm-lock.yaml, pyproject.toml, uv.lock, Dockerfile, .github/dependabot.yml |
| release | .release-please-manifest.json, release-please-config.json, server.json, Dockerfile, docker-entrypoint.sh, docker-compose.yml |
| kicad | src/kicad_mcp/tools/schematic*.py, src/kicad_mcp/tools/pcb*.py, tests/integration/**, tests/fixtures/**, scripts/kicad_canary.py, compatibility.yaml, test-fixtures/** |
A PR is classified as docs-only when it touches files in the docs
category and does not touch files in python, npm, schemas,
workflows, or dependencies. The dependencies exclusion matters: a PR
that edits README.md alongside Dockerfile or a lockfile must not be
treated as docs-only, or the Trivy filesystem scan (in the security job)
would incorrectly no-op on a real dependency/Dockerfile change.
Workflow Behavior by PR Type¶
| Job / Check | Docs-only PR | Python PR | NPM PR | Schema PR | Workflow PR | Full / Mixed PR |
|---|---|---|---|---|---|---|
Python changed-file quality |
no-op ✅ | changed-file Ruff | no-op ✅ | no-op ✅ | no-op ✅ | when Python changes |
mcp-server (3 OS) |
no-op ✅ | full | no-op ✅ | no-op ✅ | full | full |
CI Tests / Coverage |
no-op ✅ | full | no-op ✅ | no-op ✅ | full | full |
mcp-npm (3 OS) |
no-op ✅ | no-op ✅ | full | no-op ✅ | full | full |
protocol-schemas |
no-op ✅ | full | full | full | full | full |
scan (Gitleaks) |
full | full | full | full | full | full |
analyze (CodeQL) |
no-op ✅ | full | full | full | full | full |
| Dependency Review | full | full | full | full | full | full |
required-pr-gate |
✅ pass | ✅ pass/fail | ✅ pass/fail | ✅ pass/fail | ✅ pass/fail | ✅ pass/fail |
no-op means the job runs and reports success, but skips expensive steps. This ensures the required status check is never left pending.
Repository-tooling contracts are an explicit exception to the generic NPM no-op row above.
Changes to root hook/tooling and repository-policy inputs such as package.json,
pnpm-workspace.yaml, Dockerfile, docker-compose.yml, lefthook.yml,
.pre-commit-config.yaml, Taskfile.yml, .mergify.yml, Dependabot/ruleset
configuration, sonar-project.properties, or AGENTS.md force the mcp-server
OS matrix and full Python coverage lane to run their real unit/tooling-contract suite.
This prevents a required check from reporting a no-op success when Python tests consume a
non-Python repository contract.
The internal Python changed-file quality job reuses scripts/hook_pre_push.py
in --ruff-only mode against the exact pull-request/push diff. The shared selector
keeps maintained Python files under src/, tests/, and scripts/ aligned between
local pre-push and remote CI while avoiding unrelated legacy-script cleanup. It joins
the aggregate Required PR Gate rather than becoming a new branch-protection context.
Dependency Review runs on every pull request to main and is a required
status check. It must not be path-filtered at the workflow trigger because a
missing required context would leave an otherwise valid pull request blocked.
The action itself reports success when a pull request has no dependency-graph
delta.
Windows 10.0.7 bounded native qualification (non-required)¶
kicad-10-0-7-windows-qualification.yml runs the native CLI suite on
windows-2025 after selected main pushes and on a weekly Tuesday 05:17 UTC
schedule; maintainers can also dispatch it manually. It deliberately has no
pull_request, pull_request_target, or workflow_run event, is not a
branch-protection context, and does not introduce a new required PR check.
The path filter covers the KiCad canary implementation, differential/PCB/
export/validation/tool adapters, fixtures, and locked dependencies so ordinary
documentation or unrelated package changes do not download the large installer.
Scheduled events always execute the default branch. Separate concurrency groups
for push, schedule, and workflow_dispatch keep unattended runs from
coalescing with explicitly operator-dispatched runs; running jobs are not
force-cancelled. GitHub may replace an older pending event in a group.
Every run uses an ephemeral GitHub-hosted Windows runner, read-only token,
credential-free checkout, SHA-pinned third-party Actions, a bounded download
from official KiCad mirrors, an independently reviewed fixed SHA-256,
Authenticode signer validation, an isolated installer directory, exact CLI
version, the 32-step native canary/5 semantic comparisons, limited artifact
retention and always() cleanup. No repository secrets or elevated publishing
identity are passed to the checked-out source. The interactive GUI/IPC
workflow is operator-only and never launched by this automation.
This is a post-merge and scheduled qualification signal, not evidence that
KiCad 10.0.7 is supported on every platform, not a replacement for the
Ubuntu 22.04 legacy 10.0.6 lane, and not authorization to modify
kicad.latestVerified, autonomous manufacturing evidence or release policy.
If it is ever promoted to a mandatory pre-merge check, review PR/fork trust,
runner minutes, always-reporting branch-protection contexts, Mergify conditions,
and ruleset/documentation together as a separate change. Track promotion in
#1181.
Required Status Checks¶
The following checks are required by the main-standard branch ruleset and must report
a status on every PR:
| # | Check Name | Source Workflow | Source Job |
|---|---|---|---|
| 1 | mcp-server (ubuntu-24.04) |
ci.yml |
mcp-server |
| 2 | mcp-server (windows-2025) |
ci.yml |
mcp-server |
| 3 | mcp-server (macos-15) |
ci.yml |
mcp-server |
| 4 | mcp-npm (ubuntu-24.04) |
ci.yml |
mcp-npm |
| 5 | mcp-npm (windows-2025) |
ci.yml |
mcp-npm |
| 6 | mcp-npm (macos-15) |
ci.yml |
mcp-npm |
| 7 | protocol-schemas |
ci.yml |
protocol-schemas |
| 8 | scan |
gitleaks.yml |
scan |
| 9 | analyze (python) |
codeql.yml |
analyze |
| 10 | analyze (javascript-typescript) |
codeql.yml |
analyze |
| 11 | Required PR Gate |
ci.yml |
required-pr-gate |
| 12 | SonarCloud Scan |
ci.yml |
sonarcloud |
| 13 | dependency-review |
dependency-review.yml |
dependency-review |
Live Model Release Policy is intentionally not a main-ruleset PR context. live-model-assurance.yml runs protected provider smoke after main pushes, while release.yml enforces deterministic live-model release readiness on release-please PRs and published releases. A missing, stale, or contract-mismatched approved baseline blocks that release boundary until the protected full gate promotes reviewed evidence.
Step-Level No-Op Pattern¶
To ensure required checks always report success even on docs-only PRs, we use
step-level no-op rather than job-level if: skip:
- The job always runs (so GitHub receives a status).
- The first step checks the change detection outputs.
- If the PR is docs-only and the job is not needed, it prints a notice and sets an environment variable to skip remaining steps.
- All subsequent steps have
if: env.docs_only != 'true'.
This avoids the "pending check" problem that occurs when a required job is skipped entirely.
One-run Python coverage for SonarCloud¶
The required SonarCloud Scan is produced by ci.yml after CI Tests / Coverage.
For Python or workflow changes, the authoritative 83% full-suite and 90% changed-line
coverage checks run once. A report is uploaded as an ephemeral, run-local artifact
only after those checks pass. The Sonar job downloads it from that same workflow
run, verifies the exact merge-commit SHA, run ID, run attempt and report digest,
and then performs static analysis. The required check's name remains unchanged.
When CI legitimately skips full Python coverage for non-Python changes, but
Sonar still needs code analysis, the same trusted Sonar job executes the shared
full-suite action as a fallback. Release-metadata-only changes, docs-only changes,
fork pull requests, and Dependabot PRs retain explicit Sonar no-op handling.
sonarcloud.yml remains available for manual fallback diagnostics only, not
as a second pull-request workflow producing a duplicate required check.
A failed Python full suite or patch-coverage gate always blocks the Required PR
Gate, and is never replaced with an apparent successful Sonar report. Coverage
artifacts are only exchanged within one workflow run; cross-workflow artifact
reuse and untrusted PR credential promotion remain disallowed.
Aggregate Gate (required-pr-gate)¶
ci.yml includes required-pr-gate, which collects all mandatory job
results and fails closed on failure, cancelled, unknown results and
unexpected skipped. Only two skips are permitted at the aggregate job
level: changes on an explicit manual dispatch, and release-readiness for
a non-release-please event. Other path-aware test work is skipped within
its job so GitHub always receives a successful required status. The aggregate
step summary now records a rationale for every outcome.
For normal pull requests, CI concurrency cancels a superseded commit to
conserve runner minutes. On main pushes the concurrency key includes the
exact Git SHA and does not cancel ongoing post-merge CI evidence; every
merge receives its own immutable validation result. This is a conscious
tradeoff between complete main evidence and additional runner usage.
CI Health Report runs on Mondays (or manual dispatch) with read-only Actions
permissions. It publishes a seven-day PR/main p50/p95 duration sample, counts
of passed/failed/cancelled runs, frequent failing jobs, and the five longest
observed jobs. Its JSON snapshot is retained 14 days. Durations are only
approximate; the job statistics sample up to 12 recent CI runs, and the run
query is capped at the latest 100. This report is observability only, not
an additional required PR check or auto-retry mechanism.
Required PR Gate is active in the main-standard ruleset. The existing operating-
system matrix, protocol schema, secret scan, CodeQL, and dependency-review
contexts remain required alongside it. New internal jobs such as coverage
become merge-blocking by joining the aggregate gate; they should not be added
directly to the ruleset until they have a stable history and a deliberate
migration plan.
Codecov reporting¶
CI Tests / Coverage is the recognized full-test summary check for OpenSSF
Scorecard and the source of Codecov Python coverage and JUnit test analytics.
It authenticates with OIDC and runs upload steps after failed tests. Codecov
project and patch statuses use relative auto targets and are informational
during baseline collection; the pytest failure and 83% local coverage threshold
remain blocking through the
aggregate gate.
Security Workflows¶
| Workflow | Runs on docs-only PR? | Rationale |
|---|---|---|
Gitleaks (scan) |
✅ Always | Fast; secrets can appear in any file. |
CodeQL (analyze) |
no-op | Code analysis is irrelevant for docs changes. Scheduled full scan runs weekly regardless. |
| Dependency Review | ✅ Always | Required context; evaluates dependency-graph changes and succeeds when no dependency delta exists. |
| Scorecard | Not triggered on PR | Runs on push to main and weekly schedule. |
Trivy (in CI security job) |
no-op on docs-only | Filesystem vulnerability scan is code-focused. |
| SonarQube Cloud | Runs for trusted same-repository PRs; conditionally skips Dependabot/forks | Required as SonarCloud Scan; see below for skip semantics. |
Dependency Graph and Dependabot¶
GitHub's Dependency Graph (vulnerability-alerts API) is enabled on this
repository. It must stay enabled for Dependency Review and Dependabot alerts.
.github/dependabot.yml is the active dependency-update configuration. It covers
uv, npm/pnpm package locations, GitHub Actions, Docker/Docker Compose, and Cargo.
Dependabot security updates own automated vulnerable-dependency remediation. The
repository intentionally has no active Renovate configuration or Mend Renovate App
requirement, avoiding two bots opening overlapping update pull requests.
Dependabot update PRs use the same protected-branch path as maintainer PRs. Routine minor/patch version updates are grouped per ecosystem and package-manager boundary, while major and runtime-sensitive updates remain individually reviewable and security updates use separate groups. npm/pnpm update entries stay single-directory so independent pnpm workspace and npm lockfile scopes are never combined into one Dependabot updater job.
Routine version-update scans are staggered across Monday through Friday in the Europe/Istanbul maintenance window, with separate morning/noon slots where a day has two updaters. Each updater allows at most two open version-update pull requests and applies a five-day release cooldown. These backpressure controls do not delay or count Dependabot security-update pull requests.
Required status checks and the active main-standard ruleset remain authoritative; neither
Dependabot nor Mergify bypasses them. Mergify may automatically queue only the explicitly
allowlisted routine Dependabot groups, one PR at a time. Queue and merge conditions are
identical and explicitly mirror the required contexts declared in .github/rulesets/main.json,
which keeps Mergify compatible with the strict up-to-date ruleset using in-place checks.
Repository tests require exact ruleset and queue/merge-condition parity, so Mergify does
not rely on external ruleset-discovery APIs to decide that CI is complete. Human, release,
security, major, and other ungrouped PRs are
not auto-queued by repository policy. They may enter the command-only maintainer queue
rule when a user with write access comments @mergifyio queue; that rule mirrors the same
required contexts, requires resolved review threads, and has no automatic trigger.
SonarQube Cloud scope¶
This project runs CI-based analysis via
.github/workflows/sonarcloud.yml, which triggers on every push to main
and on pull requests. The workflow installs dependencies, runs the full test
suite with coverage, and invokes SonarSource/sonarqube-scan-action (pinned
to v8.2.2). The scanner reads sonar-project.properties from the repository
root — this is the CI-based configuration file (distinct from
.sonarcloud.properties, which is used only by SonarCloud's Automatic
Analysis mode and must not coexist with a CI workflow).
sonar-project.properties sets sonar.tests to classify tests/,
packages/protocol-schemas/test/, and packages/kicad-fixtures/test/ as
test code. This only affects path classification — it does not exclude any
file from analysis, and it does not change rule severities or Quality
Profiles (those require SonarQube Cloud UI/admin access, not a repo file).
SonarCloud Scan is a required status check. Trusted same-repository PRs run the real analysis; Dependabot, fork, and release-metadata-only PRs take an explicit successful no-op path that does not expose SONAR_TOKEN. Mergify requires literal check-success for this context rather than accepting skipped/neutral conclusions.
Publish Workflows¶
All publish workflows enforce strict safety:
- Trigger: Only
release(published) orworkflow_dispatchwith explicit inputs. - PR context: Dry-run only (build without push, validate without publish).
- Environment protection: publish jobs remain bound to their configured
GitHub environments (
pypi,testpypi,npm,mcp-registry, andghcras applicable). The repository-settings audit expects zero required reviewers onnpm,mcp-registry, andghcr; adding a reviewer there is policy drift unless the publish policy is intentionally changed. - Supply chain: OIDC trusted publishing, attestation, SBOM generation, SHA256 checksum verification, cosign signing.
- Idempotency: All publish jobs check if the version is already published before attempting to publish.
| Workflow | PR Behavior | Release Behavior |
|---|---|---|
| Publish Python | N/A | Build → attest → publish to PyPI with OIDC |
| Publish npm | N/A | Pack → attest → publish with provenance |
| Publish Container | Dry-run build (cacheonly) | Push to GHCR → Trivy scan → cosign sign |
| Publish MCP Registry | Dry-run validate | Wait for artifacts → publish to registry |
| Publish Protocol Schemas | N/A | Build → attest → publish to npm |
| GUI Release | N/A | Build Tauri → create GitHub Release |
Supply Chain — Action Pinning¶
Every third-party action reference across .github/workflows/ is pinned to a
commit SHA (not a mutable tag like @v4 or @main). This is verified by:
grep -rnE "uses:.*@(v[0-9]|main|master)" .github/workflows/ | grep -vE "@[0-9a-f]{40}"
returning no output. When bumping a pinned action, resolve the new tag to its commit SHA and reuse the same SHA across every workflow that references that action, to keep pins consistent repo-wide.
Rollback¶
Path-aware gating can be fully reverted by:
- Removing the
changesjob fromci.yml. - Removing
needs: changesfrom downstream jobs. - Removing
if:conditions on no-op steps.
This restores the original behavior where all jobs run unconditionally on every PR. No branch protection changes are needed for rollback.
Repository settings drift audit¶
.github/workflows/repository-settings-audit.yml runs daily and on manual dispatch from the
default branch. It mints a short-lived GitHub App token with Actions: read and
Administration: read permissions; a missing or under-scoped token fails the audit.
The workflow itself has contents: read permissions and is not exposed to pull requests.
scripts/check_github_repository_settings.py compares live Actions permissions,
selected-action allowlists, default workflow-token permissions, protected publish
environments, and the repository rulesets against the reviewed JSON policy.
GitHub's ruleset API withholds bypass_actors from read-only tokens (it requires
ruleset write access to disclose them). The scheduled audit therefore warns when this
field is absent and treats it as unverified, not as an empty actor list. All other
ruleset fields are compared strictly. When an authorized maintainer runs the same audit
with ruleset write access, a returned bypass_actors list is compared strictly as well.
This visibility constraint does not justify granting the scheduled audit write permissions,
and missing bypass visibility must not be reported as verified compliance.