go-ci-gofmt-enforcement
Root cause: gofmt -l prints the paths of unformatted files but always exits 0 (verified: exit 0 with unformatted files present, exit 2 only on parse errors). The lint job either skipped gofmt entirely or ran it without failing on its output, so 26 files rotted with CI green.
Fix — three commits:
1. Style-only backlog commit. Run gofmt -w once and commit the whitespace/indentation fixes with no logic changes:
gofmt -w . # fix all 26 rotted files
git diff -w --stat # MUST be empty → proves style-only
git add -A && git commit -m "style: gofmt -w over all packages (style-only)"
Push and confirm existing CI still passes before proceeding.
2. Enforcement commit — add a dedicated Check gofmt step to the lint job (.github/workflows/lint.yml), placed before the other lint steps so it fails fast:
name: lint
on:
push:
branches: [main]
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22"
- name: Check gofmt
run: |
unformatted="$(gofmt -l .)"
if [ -n "$unformatted" ]; then
echo "The following files are not gofmt-formatted:"
echo "$unformatted"
exit 1
fi
echo "All Go files are gofmt-formatted."
- name: Run lints
run: go vet ./...
The step fails on any output, which is the key: gofmt -l . alone cannot gate the build. This catches formatting drift on every push/PR, so the 150-tick decay can never recur. (Optional hardening: append go run golang.org/x/tools/cmd/goimports -l . for import grouping.)
3. Land enforcement as its own commit so CI history shows the style commit passing before the gate exists.
Verified empirically in a scratch repo (`go1.26.0`, git-tracked) with deliberately misformatted files (space-indented, misaligned) plus one clean file: | Check | Result | |---|---| | `gofmt -l .` on dirty tree | Listed `cmd/app/main.go`, `internal/util/math.go` — **exit 0** (the bug) | | Guard `test -z "$(gofmt -l .)"` dirty tree | exit 1 → step fails ✅ | | Guard on clean tree | exit 0 → step passes ✅ | | `gofmt -w .` then `gofmt -l .` | Empty output, all clean ✅ | | Style-only proof | `git diff -w` after `gofmt -w` = 0 lines (only indentation changed) ✅ | | Full CI step simulation | Clean tree → PASS; re-dirtied tree → FAIL listing the file, exit 1 ✅ | | Workflow YAML | Parses with `python yaml.safe_load`; `Check gofmt` step present and ordered before lints ✅ | **Edge cases tested:** - **Non-Go files**: `scripts/build.sh` never listed by `gofmt -l` (grep count 0) — safe in a monorepo with shell files. - **Recursion**: files in nested `internal/util/` subdirs detected by `gofmt -l .` — no per-dir globbing needed. - **Parse errors**: a broken `.go` file makes `gofmt -l` exit **2** with a parser error on stderr, and `gofmt -w` refuses to write it — the step fails hard (and `go vet`/`go build` would too). - **Trailing whitespace / build-tag files**: detected and fixed by `gofmt -w`; `gofmt -d` renders a readable diff for CI logs. - **Empty-output step semantics**: the `if [ -n "$unformatted" ]` guard is POSIX-sh safe across ubuntu-latest's default shell; no `set -o pipefail` dependency.
{"model": "gpt-5.6-sol/openai-codex", "problem_class": "go-ci-gofmt-enforcement", "result": "passed", "tests": 12}