Skip to content

feat(code-quality): opt-in black --check format job (diff-scoped) - #115

Merged
LukasWodka merged 2 commits into
developfrom
feat/code-quality-format-job
Jul 31, 2026
Merged

feat(code-quality): opt-in black --check format job (diff-scoped)#115
LukasWodka merged 2 commits into
developfrom
feat/code-quality-format-job

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What

Adds an opt-in format job (black --check) to the shared code-quality.yml. Nothing changes for any repo until a caller sets format: true — no caller does in this PR.

Why (backend#1303, the epic's last item)

Black is CI-enforced in backend only (its own lint.yml, pinned from requirements-dev.txt). Everywhere else, the pre-commit hooks from #1307 are the only formatting signal, and those are opt-in per developer.

The design decision: diff-scoped, not repo-wide

Measured today (black 26.3.1, zero parse errors, so these are real verdicts):

repodirty files
backend0 (its gate works)
tracebloc-engine157
tracebloc-py-package168
data-ingestors78
averaging-service71
client-runtime15

So there is a genuine ~470-file backlog outside backend. A repo-wide black . sweep right now would collide with the 33 open Code-review PRs and the 23 items sitting in Ready for prod, and would damage git blame across five repos.

Instead the job is diff-scoped like ruff: only the .py files a PR changes must be black-clean (all files in all-files mode). A repo can adopt the gate at zero churn and the tree converges as files are touched.

Inputs

  • format (default false) — opt in.
  • black-version (default "23.1.0") — pin per caller. black's stable style changes between releases; a mismatched version reports the whole repo as unformatted.
  • format-soft-fail (default false) — adopt advisory-first while ruff/gitleaks stay blocking.

Correctness notes

  • No --isolated: black reads the repo's own [tool.black] (line-length/target-version/exclude), so CI matches what contributors run locally.
  • RC 123 fails closed — an unparsable file is an error, not a formatting verdict, so it is never swallowed by soft-fail.
  • NUL-delimited file list + --no-run-if-empty — paths with spaces survive, and an empty changed-file list never silently checks the whole repo.
  • Same three-dot diff and fail-open-to-all-files contract as the ruff job.

Test plan

  • actionlint clean.
  • Logic validated locally against a real clone: clean-file path exits 0; a badly formatted file exits 1 and is counted; a filename containing a space is handled; an empty list is a no-op; an unparsable file returns 123.

Part of tracebloc/backend#1303

🤖 Generated with Claude Code


Note

Low Risk
Behavior is gated behind format: false by default; changes only affect repos that explicitly opt in to the new CI job.

Overview
Extends the reusable code-quality.yml with an opt-in format job (black --check) that is off by default until a caller sets format: true.

New inputs: format, format-soft-fail (format-only advisory mode), and black-version (default 23.1.0 to match the fleet). Scope mirrors ruff: changed .py files on PRs, or the whole tree when all-files is set, with the same three-dot diff and fail-open-to-all-files behavior.

The job installs black via pipx, respects the repo’s [tool.black] (no --isolated), and writes a step summary. Pass/fail is driven by black’s output, not raw exit codes—so GNU xargs remapping reformat failures to 123 does not break advisory adopters, parse/processing errors are fail-closed and not treated as “needs formatting,” and soft-fail / format-soft-fail only soften real reformat findings.

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

Closes the last Layer-0/1 gap on backend#1303: black is CI-enforced only in
backend (its own lint.yml). This adds a shared, opt-in 'format' job so any
repo can enforce formatting without standing up its own workflow.
Diff-scoped like ruff: only the .py files a PR changes must be black-clean
(all files in all-files mode). That matters because every Python repo except
backend has a real formatting backlog (measured 2026-07-31 with black 26.3.1:
engine 157, py-package 168, data-ingestors 78, averaging 71, client-runtime
15; backend 0). Diff scoping means a repo can adopt the gate with zero churn
instead of a repo-wide reformat that would collide with in-flight work.
'format-soft-fail' lets a repo adopt advisory-first while its lint and
credential gates stay blocking. 'black-version' is pinned per caller because
black's stable style changes between releases; no repo enables this job by
default, so nothing changes until a caller opts in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

👋 Heads-up — Code review queue is at 35 / 30

Above the WIP limit. The team convention is to review existing PRs before opening new work.

Open PRs currently in Code review (oldest first):

Pull from review before opening new work. (This is a nudge from the kanban WIP check, not a block.)

@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

Comment thread.github/workflows/code-quality.yml Outdated
@aptracebloc

Copy link
Copy Markdown
Contributor

Confirmed the Bugbot "xargs remaps soft-fail exit code" finding — it's a real High, not a false positive. Trace:

Diff mode (L381-383) runs black --check under xargs -0. When black finds files that would reformat it exits 1, but xargs remaps any child exit in 1–125 to its own exit 123 (per man xargs). So RC=123, not 1.

Exit handling (L401-406) then hits the fail-closed branch first:

# RC 123 = internal error (unparsable file), not a formatting verdict.if [ "$RC"!="0" ] && [ "$RC"!="1" ];then# 123 → both trueecho"::error::black exited $RC ... - failing closed."exit"$RC"# exits before the soft-fail checkfiif [ "$RC"!="0" ] && [ "$SOFT_FAIL"="true" ];thenexit 0;fi# never reached in diff mode

So on the diff-scoped path — the job's primary mode — an ordinary "would reformat" always hard-fails, even with soft-fail/format-soft-fail set, and a genuine internal error (unparsable file) becomes indistinguishable from a normal formatting verdict (both surface as 123). The comment on L401 (RC 123 = internal error) is the buggy assumption under xargs. The all-mode path (black --check ., no xargs) is unaffected — black's 1 is captured directly there and soft-fail works.

Since /tmp/black.out already holds the verdict, the smallest fix is to disambiguate by output rather than trusting xargs's exit code (COUNT is already computed just above):

if [ "$RC"!="0" ] && [ "$RC"!="1" ];then# Diff mode runs black under xargs, which remaps black's exit 1 -> 123.# It's a real internal error only if there are no "would reformat" lines.if [ "$COUNT"-gt 0 ];then
RC=1
elseecho"::error::black exited $RC (not a formatting failure) - failing closed."
cat /tmp/black.out
exit"$RC"fifi

That restores advisory behavior on the diff path while still failing closed on genuine internal errors. Given the whole caller rollout is advisory (soft-fail), worth fixing before repos opt into the format job in diff mode — otherwise the first unformatted file hard-blocks them.

Bugbot + @aptracebloc: in diff mode black runs under xargs, and GNU xargs
remaps any child status in 1-125 to its own 123. So black's ordinary 'would
reformat' (exit 1) arrived as 123, hit the fail-closed branch, and hard-failed
before soft-fail could apply -- advisory adoption was impossible on the job's
primary path, and a real internal error looked identical to a formatting nit.
Now the verdict comes from black's OUTPUT: reformat-count vs error-count,
checked errors-first so a genuine internal error is never masked by a
reformat finding in the same run (the residual hole in the suggested
RC=1 rewrite). Exit codes are only a tiebreaker for 'nonzero with no
findings', which still fails closed.
Why local validation missed it: BSD/macOS xargs propagates 1, GNU xargs
remaps to 123 -- verified both. Simulated all 8 outcome combinations
(dirty/unparsable/clean x advisory/blocking x diff/all-files).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

@aptracebloc confirmed and fixed — thank you, that was a real High and your trace was exact.

Verified the platform difference empirically, which is also why my local validation gave a false green: printf 'x\0' | xargs -0 sh -c 'exit 1' returns 1 on BSD/macOS xargs but 123 on GNU xargs (ubuntu-latest). So the diff path — the job's primary mode — would have hard-failed every advisory adopter on the first unformatted file, exactly as you described, while passing on my machine.

Fix (b2ef22c): decide from black's OUTPUT, not its exit code. I took your approach but reordered it to close one residual hole: your version sets RC=1 when COUNT>0, which means a run containing both a reformat finding and a genuine internal error would be downgraded to a formatting verdict and then swallowed by soft-fail. So it now checks errors first:

ERRS=$(grep -cE '^error:|would fail to reformat' /tmp/black.out || true)if [ "${ERRS:-0}"-gt 0 ];then ... fail closed, always ...
elif [ "${COUNT:-0}"-gt 0 ];then ... soft-fail applies here ...
elif [ "$RC"!="0" ];then ... nonzero with no findings -> fail closed ...

Exit codes are now only a tiebreaker for "nonzero but reported nothing".

Simulated all 8 combinations (dirty / unparsable / clean × advisory / blocking × diff / all-files) with rc forced to 123 to mimic GNU xargs: advisory now passes on a dirty diff, blocking fails, an unparsable file fails closed regardless of soft-fail, and error+dirty in one run fails closed. Clean stays clean.

Side note: the errors-vs-verdicts distinction this fix rests on is the same one that produced a bogus "0 files need formatting" in the #1303 sizing (black 23.1.0 can't run on Python 3.12+ — ast.Str removed — so it errored on every file while the reformat count stayed 0). That lesson is now encoded in the job with a comment, so it can't silently recur.

@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 affc564. Configure here.

@LukasWodka
LukasWodka merged commit 772cea8 into developJul 31, 2026
2 checks passed
@LukasWodka
LukasWodka deleted the feat/code-quality-format-job branch August 1, 2026 21:45
LukasWodka added a commit that referenced this pull request Aug 24, 2026
…#314)
* ci(2364): a PR whose title names a ticket must link it (backend#2364)
Merging a fix never closed or advanced its ticket. `closingIssuesReferences`
was 0 on 7 of 7 sampled merged PRs (release-train#109/#108, .github#304/#300,
backend#2266, client#774, docs#131). The house convention puts the ticket in
the PR TITLE; GitHub creates a closing link ONLY from a keyword in the BODY, so
a title reference is inert. `kanban-closure-router.yml` fires, finds no linked
issue, and correctly does nothing -- every kanban workflow green, every card
unmoved.
Adds a `closing-ref` job to the EXISTING `set-pr-status.yml` reusable: parse the
real title, assert the real `closingIssuesReferences` contains what it names.
Derived, not restated (rule 1): two live reads, no list of tickets, repos or
authors. The four title forms are measured, not imagined. A bare `#N` outside
parentheses is deliberately NOT read as a ticket -- backend#2309's `#2271` is
prose about a PR, and scanning loose `#N` would redden a compliant PR.
Fails closed (rule 3): a blank title, a GraphQL error, `pullRequest: null`, a
null/ownerless node, or `totalCount > len(nodes)` all exit 2 as "cannot tell",
never a pass and never a finding against the author. The truncation test is
load-bearing beyond pagination -- a link to an issue the token cannot read comes
back missing from `nodes` while `totalCount` still counts it, which is
indistinguishable from "not linked".
The cross-repo trap is its own verdict: `WRONG_REPO` is reported apart from
`MISSING` because the remedies differ -- a bare `Closes#304` in `.github` links
`.github#304`, closing the wrong issue on merge, and needs the line rewritten
rather than added.
Fixtures are measured bytes (the backend#2114 lesson), captured with
`gh api graphql` and re-verified against the live API before commit.
Tests: 102 selftest assertions; 34 mutations, 0 stale, 0 uncaught. The mutation
harness edits the real gate and re-runs the real suite -- no inline copy of any
rule (rule 9, .github#114/#115). Every anchor must match exactly once, which is
the assertion that it actually applied. Refusals are asserted by their own
message, never a catch-all (rule 10). The commit-type vocabulary is derived out
of org-standards.md and the derivation fails closed if it finds nothing (rule 6).
Arming: `closing-ref` is a required status check NOWHERE -- measured across 19
repos x develop/staging/main/master x both classic protection and rulesets -- so
a finding blocks no merge (rule 4). Callers trigger on
opened/reopened/ready_for_review/converted_to_draft, not `synchronize`, so the
13 open PRs that would report a finding are not reddened by a push.
Touches no file in `conformance-gate.yml`'s GUARDED list, and needs no
`repo-inventory.yml` row: the inventory tracks callers, one row per reusable,
and `set-pr-status.yml` already has its rows.
Closestracebloc/backend#2364
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(2364): the remedy stops guessing a repo it cannot know (backend#2364)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(2364): the scope pattern admits a leading dot too, so .github stops depending on a coincidence (backend#2364)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

2 participants

@LukasWodka@aptracebloc