Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 70 additions & 5 deletions .github/workflows/quality-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ jobs:
- name: Checkout consumer repo
# In a reusable workflow the github context belongs to the CALLER, so a
# bare checkout lands the consumer repo at the triggering ref.
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 (node24)
with:
path: repo

- name: Set up Python 3.12
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 (node24)
with:
python-version: "3.12"

Expand All @@ -74,13 +74,50 @@ jobs:
# grant is ever removed this step fails loudly with a 404 — the fix is
# to restore the grant or pass a fine-grained PAT via the caller's
# `secrets: inherit`.
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 (node24)
with:
repository: BonfireAI/candyfactory-quality
ref: ${{ github.job_workflow_sha }}
path: .cf-quality
token: ${{ github.token }}

- name: Honor the consumer's declared kit pin (fail-safe — never floats)
# `github.job_workflow_sha` above is DOCUMENTED as the reusable
# workflow's commit but was OBSERVED empty at run time (notioncrm run
# 27370755155: the checkout received no `ref:` and fell back to
# `git checkout -B main refs/remotes/origin/main`), so SHA-pinned
# consumers were gauged by whatever kit MAIN carried — counts
# irreproducible, twice in one night. The pin of record is the one
# the consumer COMMITS in its caller stub; this step re-anchors the
# kit checkout to that SHA before anything is installed, and REFUSES
# to run when no pin can be determined — the default fails safe
# instead of silently floating to main.
working-directory: ${{ github.workspace }}/.cf-quality
env:
JOB_WORKFLOW_SHA: ${{ github.job_workflow_sha }}
run: |
HEAD_SHA="$(git rev-parse HEAD)"
mapfile -t PINS < <(grep -rhoE 'BonfireAI/candyfactory-quality/\.github/workflows/quality-gate\.yml@[0-9a-f]{40}' "$GITHUB_WORKSPACE/repo/.github/workflows" 2>/dev/null | sed 's/.*@//' | sort -u)
if [ "${#PINS[@]}" -eq 1 ]; then
PIN="${PINS[0]}"
if [ "$HEAD_SHA" = "$PIN" ]; then
echo "kit checkout already rides the declared pin $PIN"
else
echo "::warning::kit checkout was at $HEAD_SHA (github.job_workflow_sha='$JOB_WORKFLOW_SHA') — re-anchoring to the consumer's declared pin"
git fetch --depth 1 origin "$PIN"
git checkout --detach "$PIN"
echo "kit re-anchored to the declared pin $PIN"
fi
elif [ "${#PINS[@]}" -gt 1 ]; then
echo "::error::quality-gate refused: ${#PINS[@]} distinct kit pins committed in the consumer's workflows (${PINS[*]}) — one pin of record only"
exit 1
elif [ -n "$JOB_WORKFLOW_SHA" ] && [ "$HEAD_SHA" = "$JOB_WORKFLOW_SHA" ]; then
echo "no committed caller pin found; kit checkout rides github.job_workflow_sha $HEAD_SHA"
else
echo "::error::quality-gate refused: no full-SHA kit pin committed in the consumer's workflows and github.job_workflow_sha is unreliable ('$JOB_WORKFLOW_SHA') — a gate floating on kit MAIN gauges nothing reproducible; pin the caller stub per templates/quality.caller.yml"
exit 1
fi

- name: Install the gauge kit + pinned tool battery
# [dev] carries ruff/mypy/mypy-baseline/pytest/complexipy — one gauge
# block; the consumer never supplies its own copies of the gate tools.
Expand All @@ -98,9 +135,11 @@ jobs:
run: |
CF_SOURCE_ROOT="$(cf-repo-config source-root)"
CF_PACKAGE_DIR="$(cf-repo-config package-dir)"
CF_FIRST_PARTY="$(cf-repo-config first-party)"
echo "CF_SOURCE_ROOT=$CF_SOURCE_ROOT" >> "$GITHUB_ENV"
echo "CF_PACKAGE_DIR=$CF_PACKAGE_DIR" >> "$GITHUB_ENV"
echo "resolved source root: $CF_SOURCE_ROOT / package dir: $CF_PACKAGE_DIR"
echo "CF_FIRST_PARTY=$CF_FIRST_PARTY" >> "$GITHUB_ENV"
echo "resolved source root: $CF_SOURCE_ROOT / package dir: $CF_PACKAGE_DIR / first-party: $CF_FIRST_PARTY"

- name: Install the consumer package
# Monorepo layouts (declared package_dir) install from the package's
Expand Down Expand Up @@ -134,7 +173,13 @@ jobs:
# Same doctrine as mypy below: the config the verdict is graded by
# comes from the kit checkout at the pinned SHA, so a consumer cannot
# weaken the gauge it is graded by (hollow-vendored-copy refusal).
run: ruff check . --config "$GITHUB_WORKSPACE/.cf-quality/configs/ruff-base.toml"
# known-first-party is DERIVED from the consumer's resolved source
# root (cf-repo-config first-party): a shared gauge file cannot name
# every consumer's packages, and ruff's on-disk detection mis-files
# first-party imports that do not resolve — the kit's I001 verdict
# then contradicts the repo's legacy isort config. The inline value
# is derived from committed state, never a caller knob.
run: ruff check . --config "$GITHUB_WORKSPACE/.cf-quality/configs/ruff-base.toml" --config "lint.isort.known-first-party=$CF_FIRST_PARTY"

- name: ruff format --check (the kit's pinned gauge)
run: ruff format --check . --config "$GITHUB_WORKSPACE/.cf-quality/configs/ruff-base.toml"
Expand Down Expand Up @@ -191,6 +236,26 @@ jobs:
echo "::notice::mypy ratchet SKIPPED — no Python files in this repo"
fi

- name: complexipy through the snapshot ratchet (a Python repo MUST carry the watermark)
# Cognitive complexity rides the committed complexipy-snapshot.json
# watermark: a plain run compares against the snapshot in CWD — no
# separate ratchet flag exists in 5.5.0; comparison is the default
# (docs/tool-spikes.md). NEVER piped: piping masks the exit code.
# Same presence doctrine as the mypy baseline: the conventions define
# this as a gate, and green-by-missing-file is opt-in gaming — a repo
# with Python REFUSES to pass without its snapshot; only a
# Python-free repo may skip, visibly. The snapshot does NOT
# auto-shrink: re-snapshot on merge stays the runbook's job.
run: |
if [ -f complexipy-snapshot.json ]; then
complexipy "$CF_SOURCE_ROOT"
elif find . -name '*.py' -not -path '*/.*' | grep -q .; then
echo "::error::complexipy ratchet REFUSED — Python files present but no complexipy-snapshot.json; boot the snapshot per configs/BASELINE-CONVENTIONS.md (complexipy <source-root> --snapshot-create from the repo root), even when clean"
exit 1
else
echo "::notice::complexipy ratchet SKIPPED — no Python files in this repo"
fi

- name: pytest
# Plain pytest: a repo with zero collected tests exits 5 and FAILS the
# floor on purpose — a floor without tests is prose, not a gate. Runs
Expand Down
10 changes: 8 additions & 2 deletions .github/workflows/self-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 (node24)

- name: Set up Python 3.12
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 (node24)
with:
python-version: "3.12"

Expand Down Expand Up @@ -67,5 +67,11 @@ jobs:
- name: mypy (zero baseline — the new repo owes 0 type errors, bare)
run: mypy src

- name: complexipy — the kit's own cognitive-complexity watermark
# The kit ratchets FIRST (it submits before it preaches): a plain run
# compares against the committed complexipy-snapshot.json in CWD;
# never piped (piping masks the exit code — docs/tool-spikes.md).
run: complexipy src

- name: pytest
run: pytest
38 changes: 32 additions & 6 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,15 @@ Full text in `docs/sha-pin-doctrine.md`; load-bearing points:
exact commit the caller pinned the reusable workflow to — so the gate
scripts and the workflow file are always the SAME commit. No gauge/workflow
skew, and a consumer cannot be gated by scripts newer or older than the
workflow that invokes them.
workflow that invokes them. **Hardened 2026-06-11:** the context value was
OBSERVED empty at run time (the kit checkout received no `ref:` and
actions/checkout floated to the default branch — SHA-pinned consumers were
gauged by kit MAIN, counts irreproducible). The gate now re-anchors the kit
checkout to the pin the consumer COMMITTED in its caller stub, refuses two
distinct pins, and refuses to run at all when no pin is determinable —
fail-safe, never a silent float. Proven by a functional control rod in
`tests/test_workflows.py` (a real git fixture whose floating checkout must
be moved to the declared pin).
- Dependabot (`github-actions` ecosystem) owns pin freshness via bump PRs;
the bump itself runs through the gate it bumps. Org Actions policy enforces
full-SHA pinning; the Constable's weekly sweep audits unpinned refs
Expand Down Expand Up @@ -419,7 +427,9 @@ ordinary PR.
- **complexipy — set-like per function** (new offender fails; baselined
offender worsening fails), but the snapshot does NOT auto-shrink: the
re-snapshot-on-merge step is runbook procedure (§3 step 6), not yet CI
mechanism, and the complexipy step is not yet in `quality-gate.yml`.
mechanism. The complexipy step IS in `quality-gate.yml` (and `self-ci.yml`)
as of 2026-06-11, with the mypy-style presence rule: a Python repo without
its committed snapshot FAILS; only a Python-free repo skips, visibly.
- **cf-exemptions — count-based by design**, and honestly so: `frozen_count`
refuses silent growth, but a 1-for-1 entry swap at equal count is
machine-visible only as an `exemptions.json` diff in review, not
Expand Down Expand Up @@ -510,9 +520,10 @@ Everything below is a known gap, on the record. Ordered by blood.
wire live today.
6. **Release tag guard** — the plan's floor carries "release refuses
tag != pyproject.version"; no such step exists in either workflow yet.
7. **complexipy in CI (§4.11)** — the watermark is runbook-only; no workflow
step, and re-snapshot-on-merge is manual. Improvements are not locked in
mechanically.
7. **complexipy re-snapshot (§4.11)** — the gate step shipped 2026-06-11
(snapshot-presence rule included), but re-snapshot-on-merge stays manual:
improvements are not locked in mechanically until a merge-time re-snapshot
exists.
8. **ruff-sync check (§4.9)** — not installed, not a step; vendored-gauge
content fidelity is unforced unless the consumer mirror-declares it.
9. **Required-status-check mounting + canary (§4.8)** — rulesets and the
Expand Down Expand Up @@ -589,10 +600,25 @@ unknown keys fail typed (a typo'd key silently ignored would be drift).
Absent declaration keeps the historical discovery exactly — `src/` when
present, else the repo root — and `mypy-baseline.txt` stays root-anchored.

**cf-exemptions honors the declaration (2026-06-11).** The mexxa main-green
pass measured the gap: cf-exemptions discovered the repo-root `src/` (JS in
mexxa's case) and ignored the declared `source_root`, so every registered
exemption was documentation-grade — the scanner never visited the files the
entries covered. The scan surface now resolves through `cf-repo-config`
exactly like the gauges (typed failure on an invalid declaration), with the
control rod pinned in `tests/test_exemptions.py`: an unregistered
suppression under a declared `server/src` MUST fail.

**known-first-party rides the declared layout too (2026-06-11).** The shared
ruff gauge cannot statically name consumer packages, and detection mis-files
first-party imports that do not resolve on disk — `cf-repo-config
first-party` derives the names from the resolved source root and the gate
passes them inline to ruff (committed state, never a caller knob).

Honest residue: a declared root holding one trivial `.py` while the real
code lives elsewhere passes the resolver — the floor is non-emptiness, not
completeness; review and the repo-root-wide gates (ruff, file-budget,
exemptions, recursion at `.`) still measure the whole tree.
recursion at `.`) still measure the whole tree.

---

Expand Down
1 change: 1 addition & 0 deletions complexipy-snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
11 changes: 11 additions & 0 deletions configs/ruff-base.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@
# line-length 100 · target py312
# Consumer mounts: copy this file to <repo>/ruff.toml (or `extend` it) and add
# a ruff-sync stanza pointing back at this kit's pinned SHA.
#
# known-first-party (isort) is deliberately NOT set here: a shared gauge file
# cannot name every consumer's packages, and ruff's on-disk detection mis-files
# first-party imports that do not resolve on disk (modules authored RED, tests
# addressed by package path) — measured on a consumer where fixing the kit's
# I001 created NEW legacy-isort I001. The gate DERIVES the names per consumer
# (`cf-repo-config first-party`, from the resolved source root) and passes them
# inline: `--config "lint.isort.known-first-party=[...]"`. A vendored local
# copy MAY add [lint.isort] known-first-party with the repo's own names for
# editor/pre-commit parity — like per-file-ignores, it cannot weaken the graded
# verdict (CI derives its own value from committed state).

line-length = 100
target-version = "py312"
Expand Down
2 changes: 1 addition & 1 deletion docs/sha-pin-doctrine.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SHA-pin doctrine

1. Every cross-repo workflow/action ref is pinned to a full commit SHA (40-hex) — never `@main`, never a tag (tags move; SHAs do not).
2. The caller stub pins `quality-gate.yml@<full SHA>`; the gate checks the kit out at `github.job_workflow_sha`, so gauge scripts and workflow are always the same commit.
2. The caller stub pins `quality-gate.yml@<full SHA>`; the gate checks the kit out at `github.job_workflow_sha` and RE-ANCHORS to the consumer's committed pin (the context value was observed EMPTY at run time, silently floating the checkout to kit main) — when no pin is determinable the gate refuses, never floats.
3. Org Actions policy enforces full-SHA pinning, so an unpinned ref cannot even be merged.
4. Dependabot (`github-actions` ecosystem, weekly) owns pin freshness: it opens pin-bump PRs in every consumer repo, so the pinned gauge cannot rot.
5. A pin-bump PR is reviewed and merged by Anta like any change — the bump itself runs through the gate it bumps.
Expand Down
43 changes: 21 additions & 22 deletions src/cf_quality/exemptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,14 @@
or ``scripts/check_host_free.py`` the gate runs them and propagates
their exit codes; absent scripts are skipped silently.

The measured surface is ``<root>/src`` when it exists; otherwise the layout
is DISCOVERED (top-level packages/modules outside tests/docs/scripts), so a
flat or app layout is measured, never a silent no-op (the refuter's
src-only escape).
The measured surface is the committed ``[tool.cf-quality] source_root`` when
declared — resolved through cf-repo-config exactly like the gauges (the
mexxa main-green pass proved the old behavior: a JS repo-root ``src/`` was
discovered, the declared ``server/src`` never visited, and every registered
exemption was documentation-grade). Absent a declaration, ``<root>/src``
when it exists; otherwise the layout is DISCOVERED (top-level
packages/modules outside tests/docs/scripts), so a flat or app layout is
measured, never a silent no-op (the refuter's src-only escape).

Comments are read via :mod:`tokenize` (COMMENT tokens only), so suppression
text inside string literals never trips the gate.
Expand All @@ -58,6 +62,7 @@
from pathlib import Path
from typing import Any

from cf_quality import repo_config
from cf_quality.errors import GateError, GateViolation

GATED_NOQA_RULES = frozenset({"C901", "PLR0915"})
Expand All @@ -71,21 +76,10 @@
GATED_TYPE_IGNORE_CODES = frozenset({"override"})
FOLD_IN_SCRIPTS = ("check_english.py", "check_host_free.py")
#: Directories never measured for suppressions (tests carry their own
#: per-file-ignores discipline; the rest is non-shipping surface).
EXCLUDED_SCAN_DIRS = frozenset(
{
"tests",
"test",
"docs",
"examples",
"scripts",
"__pycache__",
"node_modules",
"build",
"dist",
"venv",
}
)
#: per-file-ignores discipline; the rest is non-shipping surface). The set
#: itself lives in repo_config — one gauge-block, shared with the
#: first-party derivation, never two lists drifting apart.
EXCLUDED_SCAN_DIRS = repo_config.NON_SHIPPING_DIRS
REQUIRED_ENTRY_KEYS = ("file", "symbol_or_line", "rule", "reason", "approver")

_NOSEC_RE = re.compile(r"#\s*nosec\b(.*)")
Expand Down Expand Up @@ -226,11 +220,16 @@ def _is_gated_code(code: str) -> bool:


def _discover_scan_paths(root: Path) -> list[Path]:
"""``src/`` when present; otherwise every top-level Python location.
"""The declared ``source_root`` when committed; else ``src/`` when
present; otherwise every top-level Python location.

A flat or app layout is measured, never a silent no-op: top-level
``*.py`` files and every non-excluded directory holding Python count.
A declared layout is honored exactly like the gauges honor it (typed
failure on an invalid declaration, never a silent fallback). A flat or
app layout is measured, never a silent no-op: top-level ``*.py`` files
and every non-excluded directory holding Python count.
"""
if repo_config.load(root).source_root is not None:
return [repo_config.resolve_source_root(root)]
src = root / "src"
if src.is_dir():
return [src]
Expand Down
Loading
Loading