This document describes the continuous-integration and continuous-delivery chain of
method-kit: what each piece does, how the protections fit together, and — importantly —
what travels with a fork and what does not.
It complements CONTRIBUTING.md, which covers the day-to-day contributor
workflow. This document is the why; CONTRIBUTING.md is the how.
The chain is a gated pull-request flow. Nothing reaches main except through a pull
request that passes every required gate. On top of the gates, detection and maintenance
features run continuously, and the release runs on a version tag.
flowchart TD
PR["Pull request to main"]
PR --> Q["quality<br/>ruff · compile · skills"]
PR --> T["tests<br/>auto-discovered module floors"]
PR --> S["security<br/>Bandit SAST"]
PR --> DP["docs-privacy<br/>private-marker scan over docs/**"]
PR --> CQ["Analyze (python)<br/>CodeQL security-extended"]
Q --> G{"All required checks green?<br/>branch up to date?<br/>no CodeQL alert ≥ High / Errors?"}
T --> G
S --> G
DP --> G
CQ --> G
G -- yes --> M(["Merge to main"])
G -- no --> X(["Merge blocked"])
The enforcement has two layers:
- Layer A — files in the repository (travel with a fork): the workflows (including the
action-driftwatcher), and the tooling configuration inpyproject.toml. - Layer B — repository settings (do not travel with a fork): branch protection, the code-scanning ruleset, and the Advanced Security toggles.
Both layers are required for the full posture. A fork inherits Layer A by copying the branch but gets none of Layer B — until it is recreated, the workflows still run, but nothing blocks a merge. Section 6 lists exactly what a fork must reconfigure.
| Workflow | Trigger | Job (check name) | What it enforces |
|---|---|---|---|
ci.yml |
PR to main, push to main |
quality |
ruff check → ruff format --check → doc-drift (tracked docs — scripts/gates/doc_drift_targets.tsv) → compileall -q skills |
tests.yml |
PR to main, push to main |
tests |
the auto-discovered module floors + pytest tests/ (exit 0/1); the first non-zero exit fails the check |
security.yml |
PR to main, push to main |
security |
Bandit SAST: bandit -c pyproject.toml -r . |
docs-privacy.yml |
PR to main, push to main |
docs-privacy |
private-marker scan over docs/**: bash scripts/gates/docs-privacy.sh |
codeql.yml |
PR + push + weekly (Mon 06:00 UTC) | Analyze (python) |
CodeQL security-extended — the analysis runs (content gate is the Layer-B ruleset, below) |
release.yml |
push of a tag v* |
release |
package the .skill deliverable + attach it to a Release; a version guard aborts if the tag ≠ the artifact's metadata.version |
action-drift.yml |
weekly (Mon 06:00 UTC) + manual dispatch | drift (detector, not a gate) |
re-checks the SHA-pinned action versions against upstream tags; opens an issue on drift — never edits a workflow, never blocks a merge |
Design notes:
- Quality, security, and test tools are pip-installed and version-pinned via PEP 735
dependency-groups (
ruff==0.15.18,bandit[toml]==1.9.4,pytest==9.1.1,defusedxml==0.7.1), not run via a marketplace action — deterministic, with exact CI↔local parity and no extra action to maintain. - CodeQL uses advanced setup (a committed workflow), not GitHub's default setup — the two
are mutually exclusive.
security-extendedis broader than the default suite. - Scan scope: the whole repository (
.venvexcluded by the ruff config).compilealltargetsskills/(the source directory). The test gate auto-discovers the module floors and also runspytest tests/. No cross-repo pinned checkout is used.
[tool.ruff]—target-version = "py312",line-length = 100, lint rulesE/F/I/UP/B, double-quote format,extend-exclude = [".venv"].[tool.bandit]— read with-c pyproject.toml(the[toml]extra). Skips are deliberate and documented, not blanket silencing — the active set isB101 B404 B406 B603 B607:B404(import subprocess) — informational; the module is used in safe form.B603(subprocess withoutshell=True) — the safe form: a fixed argument list, never a shell string, no user-controlled tokens.B607(partial path) — processes invoked by name on a trusted devPATH.B406(xml.sax.saxutils.escape) — escapes text for safe XML output ingen_diagram; it parses nothing. A Bandit false positive, and the only XML skip that remains.- The dangerous case,
B602(shell=True), stays active. - The
xml.etreeparse channel is gone:check_diagramnow parses withdefusedxml(retire the channel, METHOD §6), so the formerB405/B314skips were removed — there is nothing left to flag.defusedxmlis a runtime dependency of the diagram checker (declared inskills/project-pilot/modules/diagram-generation/requirements.txt) and is installed by thetestsgate.
[tool.pytest.ini_options]—testpaths = ["tests"]; thetestsgate runs the auto-discovered module floors and thenpytest tests/.- Dependency story — the toolchain is declared in PEP 735 dependency-groups and pinned;
tzdatais pulled on Windows. There is no top-levelrequirements.txt; a module may declare its own runtime dependency in a modulerequirements.txt(defusedxmlfor the diagram checker,tzdatafor filename stamping). None of these is a pinned pip manifest, so this repository ships no Dependabot version-updates configuration at all (§4) — GitHub Actions are SHA-pinned and watched byaction-drift, not tracked by Dependabot.
Workflow actions are pinned to full commit SHAs, not floating tags — a version comment
records the human-readable tag (e.g. actions/checkout@9c091bb… # v7.0.0). Pinning by SHA
closes the mutable-tag supply-chain channel: a floating @v4 lets the upstream rewrite what
the runner executes, a SHA freezes it (retire the channel, METHOD §6).
Because a SHA never moves on its own, a pinned action can silently fall behind upstream. The
action-drift.yml workflow is the watcher: weekly (Mon 06:00 UTC) and on manual dispatch,
it runs scripts/check_action_drift.py over .github/workflows/ and, on drift, opens an
issue — it is a detector, not a gate (METHOD §8): it never edits a workflow, never
blocks a merge, and its job stays green so the signal surfaces in the Issues tab. Re-pinning
is manual and deliberate (resolve the new tag, update the SHA), never an auto-refresh.
This repository ships no Dependabot configuration: action versions are managed by SHA pinning plus the
action-driftwatcher above, not by automated tag bumps.
Configured via gh api (METHOD §7/§10) and recreated in any fork that wants the same
protection.
Branch protection on main: require a PR; require status checks
pushes; restrict deletions; do not allow bypassing (admins included).
Code-scanning ruleset (targeting main): require code-scanning results, tool CodeQL,
threshold security alerts ≥ High or higher, alerts = Errors, mode Active. This is
distinct from the Analyze (python) status check: the check verifies the analysis ran;
the ruleset blocks the merge when alerts exist above the threshold. The status check
gates on the job; the ruleset gates on the finding.
Advanced Security: secret scanning + push protection on; Dependabot alerts + security updates on.
Honest current state of this repository. Layer B is armed in files but not active. The ruleset and branch-protection setup live in
.github/rulesets/main.jsonandscripts/setup-layer-b.sh, ready to apply, but the GitHub API rejects them on a free private repository (HTTP 403 — "upgrade to Pro or make the repository public"). So the workflows run and are green, but no merge is currently blocked — the gated-PR rule is held by discipline. Likewise, CodeQL analyses every PR (nominal), but its SARIF upload is blocked on the free private tier, so thecodeqlrun shows red while the analysis itself is fine. Activating Layer B requires GitHub Pro or making the repository public.
A fork copies all of Layer A and none of Layer B. To get the same posture:
- Enable Actions (GitHub disables workflows on new forks).
- Public vs private fork. Code scanning, secret scanning, and push protection are free on public repositories; on a private fork they need GitHub's paid code-security features (this is exactly the 403 above).
- Enable Advanced Security (secret scanning + push protection; Dependabot alerts +
security updates). CodeQL is picked up from
codeql.ymlon first run — do not also enable default setup. - Recreate branch protection with the required checks and "require branches up to date".
- Recreate the code-scanning ruleset (CodeQL, High+/Errors, Active) so alerts block
merges.
scripts/setup-layer-b.shdoes steps 4–5 viagh api.
Until steps 3–5 are done, the workflows still run (you can read their results), but nothing blocks a merge — the blocking comes from Layer B.
| Control | Layer | Blocks |
|---|---|---|
quality required check |
B (branch) | broken lint / format / compile |
tests required check |
B (branch) | a failing skill floor |
security required check |
B (branch) | a Bandit finding |
docs-privacy required check |
B (branch) | a private-marker leak into docs/** |
Analyze (python) required check |
B (branch) | a CodeQL analysis failure (the run, not an alert) |
| Code-scanning ruleset (CodeQL ≥ High / Errors) | B (ruleset) | a CodeQL alert (the finding) |
| Secret scanning + push protection | B (Adv. Security) | a secret at push time |
| Dependabot alerts / security updates | B (Adv. Security) | a vulnerable dependency |
action-drift watcher |
A (action-drift.yml) |
(detector) stale action SHAs — opens an issue, does not block |
Caveats worth knowing:
- Layer B is not active on this repository's tier (see §5) — the table above describes the intended posture; today only the Layer-A workflows run.
release.ymlis only exercised on a tag push — a green PR does not prove the release still works. Validate it at the nextv*tag.- Action pins are full commit SHAs with a version comment (e.g.
actions/checkout@9c091bb… # v7.0.0,actions/setup-python@a309ff8b… # v6.2.0,github/codeql-action@8aad20d… # v4.36.2) — frozen execution, not floating tags. Theaction-driftwatcher (§4) opens an issue when a pin falls behind upstream; re-pin by hand.