Skip to content

chore(build): add a Makefile so local checks predict CI (backend#1606) - #235

Merged
LukasWodka merged 5 commits into
developfrom
chore/1606-makefile
Aug 12, 2026
Merged

chore(build): add a Makefile so local checks predict CI (backend#1606)#235
LukasWodka merged 5 commits into
developfrom
chore/1606-makefile

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

This repo publishes the org's reusable workflows and runs real CI against itself — actionlint.yml, the org code-quality suite, four gate selftests, conformance-gate.yml — and had no Makefile. So there was no single local command that predicts CI, which is the gap backend#1606 exists to close. 14 of 20 repos already have one; this was one of the two notable holdouts with real CI behind it.

Adds a Makefile with the house target set (help, setup, check, check-all), matching the shape of backend/Makefile, e2e-test-agent/Makefile and docs/Makefile: .DEFAULT_GOAL := help, ?=-overridable tool vars, pinned versions mirroring the workflows, per-tool guards with install hints, and the install-hooks pre-push hook.

It is a thin wrapper. Every command is copied from the workflow that already runs it — no new tool, no new config, no new rule.

What make check runs, and where each command comes from

TargetCommandSource workflow
ruffruff check --isolated --select E4,E7,E9,F .code-quality.yml ruff job
shellcheckshellcheck --severity=error --format=gcc --exclude=SC1091 over the gate's own file selectioncode-quality.yml shellcheck job
house-rules./scripts/house-rules.sh --allcode-quality.yml house-rules job
action-pinsthe gate's own python, extracted from the workflowcode-quality.yml action-pins job
actionlintactionlint -no-color -oneline -shellcheck shellcheckactionlint.yml
selftest-caller-driftpython3 scripts/tests/caller-drift-selftest.pycaller-drift.yml selftest job
selftest-blocked-markerpython3 scripts/tests/blocked-marker-selftest.pyblocked-gate-selftest.yml
selftest-standards-syncpython3 scripts/tests/standards-sync-selftest.pystandards-sync.yml selftest job
selftest-version-bump-gatebash scripts/tests/version-bump-gate-selftest.shversion-bump-gate-selftest.yml

Three details worth review, because each is a place a lazier version would silently check less than CI:

shellcheck reproduces the gate's file selection, not a glob. The job does not take *.sh — it takes every tracked file whose extension is .sh/.bash/.kshor whose first line is a sh/bash/dash/ksh shebang, and explicitly skips .bats/.ps1/.psm1/.zsh. A looser or tighter local glob checks a different set of files and then disagrees with the gate. Today it selects 2 files.

action-pins is extracted from code-quality.yml, not reimplemented. The job body is a python heredoc. A hand-copied second version of a supply-chain gate is a version that can silently drift from the one actually gating merges — the exact #1606 failure mode, in the file where it would cost the most. Extraction runs the gate's own source byte for byte, so drift is impossible by construction. This follows the existing house pattern: scripts/tests/version-bump-gate-selftest.sh extracts its gate's run: block for the same reason. The extraction refuses to guess — exactly one python3 - <<'PY' block must exist and the extracted body must be non-empty, or it fails loudly with a pointer to fix the target.

All four selftests run, where CI runs each only behind a paths: filter. A PR that refactors a gate without touching its named trigger paths never runs its selftest in CI. Locally there is no filter and no reason for one — they cost ~15 s together, ~15 of which is version-bump-gate (it builds a throwaway git repo per case).

What did NOT go in check, and why

Both are named in the Makefile with the reason at the target, not silently omitted.

gitleakscheck-all. CI installs it per run from a version- and SHA-256-pinned release tarball; it is on no developer machine by default. Making check depend on it would either fail every clean checkout or — worse — get itself quietly skipped, and a credential scan that is skipped is a credential scan that reports clean. check-all therefore hard-fails with an install hint rather than degrading:

$ make check-all
...
==> check: green (gitleaks runs in 'make check-all')
gitleaks is not on PATH — 'make check-all' needs it:
macOS: brew install gitleaks
Linux: install v8.30.1 from the pinned release tarball
(URL and SHA-256 are in .github/workflows/code-quality.yml)
make: *** [guard-gitleaks] Error 1

conformance-gate.yml → neither, and it cannot be. It does not inspect the tree at all: it polls the GitHub API for caller-drift.yml's verdict on the PR head sha. There is no head sha and no run to poll before you push. What it ultimately gates on is local, though, and check runs it: the caller-drift selftest, the half that needs no token. The other half (audit) reads ~20 repos' workflow files through the API, so it is a separate named make audit target — deliberate, not pre-push, and not a check on this tree (its answer can change with no local commit).

The two crons (kanban-reconcile, kanban-archive) and standards-sync's audit half are likewise token-gated live-org jobs, not pre-push checks; the file says so.

black is correctly absent: code-quality-caller.yml does not pass format: true.

Proof: make check is green, measured

$ time make check
ruff check --isolated --select E4,E7,E9,F .
All checks passed!
Shell files to check: 2
shellcheck: clean
./scripts/house-rules.sh --all
house-rules: no findings across 2 file(s).
action-pins: 28 file(s) scanned, 0 finding(s)
actionlint -no-color -oneline -shellcheck shellcheck
actionlint: 0 findings
python3 scripts/tests/caller-drift-selftest.py
[... 4 selftests, all passing ...]
51 passed, 0 failed
==> check: green (gitleaks runs in 'make check-all')
real	0m19.056s
user	0m10.145s
sys	0m6.533s

19.0 s, well inside the 60 s budget. Breakdown: version-bump-gate selftest ~15 s, everything else together under 3 s (ruff 0.16 s, shellcheck 0.32 s, house-rules 0.45 s, action-pins 0.06 s, actionlint 0.68 s, the other three selftests <1 s each).

I also verified the target actually fails rather than passing vacuously: injecting an undefined name into a throwaway .py reddens make ruff (Found 2 errors. / make: *** [ruff] Error 1), and make setup / make install-hooks install the pre-push hook and are idempotent.

One bug caught and fixed before pushing

The pins were first written with trailing inline comments (RUFF_VERSION ?= 0.15.20 # ...). make strips the comment but keeps the whitespace ahead of it, so the version was "0.15.20 " — and version-check then warned that ruff 0.15.20 differed from the pinned "0.15.20 " on a machine that matched exactly. A version guard that cries wolf is one people learn to ignore. Comments moved above the assignments; make version-check is now silent on a matching toolchain. The reason is recorded in the file so it does not come back.

Notes on setup

setup installs nothing — this repo has no venv, lockfile or package to install a pin into. It preflights every tool check needs, warns (does not fail) when a version differs from the pin CI uses, and installs the pre-push hook. A hard version failure would block a contributor with no in-repo way to comply.

Conflict check

Checked both open PRs' file lists first. No overlap — neither touches Makefile:

This PR adds exactly one new file and modifies nothing. Note in particular that .gitignore is untouched (#227 owns it): the extracted action-pins script goes to a mktemp outside the tree, so there is no new build artifact needing an ignore rule.

make check invokes scripts/tests/caller-drift-selftest.py and scripts/caller-drift.py, which #227 changes — but by path, not by content, so whichever merges second simply runs the newer version.

Test plan

  • make check green in 19.0 s on this tree (output above)
  • make ruff goes red on an injected finding — the target is not vacuous
  • make check-all hard-fails with an install hint when gitleaks is absent
  • make setup, make install-hooks install the hook; re-running is idempotent
  • make version-check silent on a matching toolchain
  • make help lists every target, including what is not locally reproducible
  • The new file is invisible to this repo's own CI (not a .py, not shell-classified, not a workflow), so it cannot itself redden the gate

Refs backend#1606.

🤖 Generated with Claude Code


Note

Low Risk
Adds only developer tooling (Makefile and git hook); no workflow, gate logic, or runtime behavior changes in CI.

Overview
Introduces a new root Makefile so this repo matches the org’s standard developer entry points (help, setup, check, check-all) and closes backend#1606’s gap: one local command that should agree with CI.

make check runs lint (ruff, shellcheck with the same tracked-file selection as code-quality.yml, house-rules, action-pins, actionlint) and all four gate selftests unconditionally—unlike CI, where those selftests are often skipped by paths: filters. make check-all adds gitleaks over full git history; make audit is a separate, token/API target for live org caller-drift and is intentionally not pre-push.

Notable fidelity choices: action-pins is not duplicated—the target extracts the single python3 - <<'PY' block from code-quality.yml and runs it with SOFT_FAIL=false; tool versions are pinned to match workflows with warn-onlyversion-check**; **make setup** preflights PATH tools and installs a **pre-push hook** that runs make check(skips branch deletes, missingmake`, and non-owned existing hooks).

conformance-gate.yml is documented as not locally reproducible (API poll on pushed SHA); local coverage is the caller-drift selftest instead.

Reviewed by Cursor Bugbot for commit 317a8e9. Bugbot is set up for automated code reviews on this repo. Configure here.

tracebloc/.github runs real CI — actionlint.yml, the org code-quality
suite against itself, four gate selftests and conformance-gate.yml — and
had no Makefile, so there was no single local command that predicts it.
That is the gap backend#1606 exists to close.
`make check` (~19 s measured, green) runs the fast, offline subset with
the same tools and the same flags as the workflows that own them:
ruff --isolated --select E4,E7,E9,F . (code-quality.yml's
no-repo-config fallback, reproduced not approximated)
shellcheck --severity=error --format=gcc --exclude=SC1091, over the
gate's OWN file selection (extension or shebang, .bats/.ps1
skipped) rather than a looser local glob
house-rules ./scripts/house-rules.sh --all
action-pins EXTRACTED from code-quality.yml's own python heredoc, so a
supply-chain gate cannot have a second copy that disagrees
with the one gating merges
actionlint -no-color -oneline -shellcheck shellcheck
selftests caller-drift, blocked-marker, standards-sync,
version-bump-gate — all four, which CI runs only behind
paths: filters
Two CI steps are deliberately NOT in `check`, each named in the file with
the reason: gitleaks is in `check-all` (CI installs it per run from a
pinned tarball; it is on no dev machine by default, and a credential scan
that gets quietly skipped reports clean), and conformance-gate.yml is in
neither because it polls the API for a verdict on a pushed head sha —
there is no sha before you push. `make audit` runs caller-drift's
token-needing half on demand.
`setup` preflights the tools and installs a pre-push hook that runs
`make check`; it installs nothing, because this repo has no venv or
lockfile to install a pin into.
Refs backend#1606.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@LukasWodkaLukasWodka self-assigned this Aug 12, 2026
Comment threadMakefile
LukasWodkaand others added 2 commits August 12, 2026 18:31
The audit target invoked `caller-drift.py` bare. Both --inventory and
--source-dir default to exactly what caller-drift.yml passes, so it was
equivalent today — but this file's claim is that its commands are COPIED
from the workflow, not that they happen to agree with it. A default is
precisely the kind of thing that moves under you, and the drift would be
silent.
Refs backend#1606.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rs (backend#1606)
The shellcheck recipe piped `git ls-files` into a `while` loop under
`set -e`. A pipe's exit status is the last stage's (the while loop),
so a failed `git ls-files` was swallowed: the selection file stayed
empty, the recipe printed 'no shell files in scope' and exited 0 — a
clean report with nothing scanned. Materialize the listing to a temp
file first (as code-quality.yml does), so a failed listing aborts the
recipe non-zero instead of reporting a false clean.
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

Comment threadMakefile
…e check (backend#1606)
The Makefile target ran only blocked-marker-selftest.py, but the CI
selftest job (blocked-gate-selftest.yml) also runs blocked-marker.py
against a title containing the gate's own filenames — the only coverage
for a self-reference bug that matches the word 'blocked' inside a path
rather than in prose. Without it, make check could report green on a
matcher change that would fail the gate's selftest in CI. Mirror the
second step so local checks predict CI.
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

Comment threadMakefile
…(backend#1606)
version-check compared actionlint -version's first line verbatim to
ACTIONLINT_VERSION (1.7.12). Release and Homebrew builds print '1.7.12',
but 'go install' — the Linux hint on guard-actionlint — prints 'v1.7.12'
from Go build metadata, so a matching toolchain still warned. Strip a
leading v before comparing, the same cries-wolf class already handled for
ruff. Verified: make version-check is clean with 1.7.12 on PATH, and
v1.7.12 normalizes to 1.7.12.
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 317a8e9. Configure here.

@LukasWodka
LukasWodka merged commit e68a3c2 into developAug 12, 2026
9 checks passed
@LukasWodka
LukasWodka deleted the chore/1606-makefile branch August 14, 2026 13:53
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@LukasWodka