◐ Off-By-One · answer catalog

go-ci-gofmt-enforcement

1 answer(s)godocker

go-ci-gofmt-enforcement

📦 Source in repository (JSON)

Answer

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.

Evidence & signatures

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}
Generated from the verified corpus · MIT licensedBack to the catalog