From 2c4158a80860d5d3571c74351c34e69edb00a70f Mon Sep 17 00:00:00 2001 From: Mathias Date: Thu, 17 Sep 2026 18:14:23 +0200 Subject: [PATCH] fix(test): grade golangci-lint on its exit code, not on grepping its output Addresses the blocking finding from the review of this PR. The harness matched golangci-lint's human-readable summary to decide pass or fail, and discarded its exit status. Any failure that is not "found issues" -- bad config, unwritable TMPDIR, OOM, a future change to the output format -- produces non-matching text and was scored `ok`. Since this harness is the only gate covering cmd/__PROJECT_NAME__, a linter that dies would have left the entrypoint unchecked with CI green. That is the precise defect class this file was added to end, which is why it blocked the PR rather than being filed as a follow-up. golangci-lint exits 0 clean / 1 issues found / >1 the linter itself failed. Now graded on that, and the >1 case reports the child as UNCHECKED rather than clean. Two more from the same review: - a missing tool now fails instead of `exit 0` with zero assertions, which was indistinguishable from a real pass to anything reading the exit code. It was unreachable in CI only by luck of task ordering, not by rule. - Dockerfile joins the substituted file set. It builds ./cmd/__PROJECT_NAME__ and was outside the pattern, so the harness never validated the one path a repo rename has historically broken here. Verified by flipping each case: crashing linter (exit 3) -> FAIL "the child is UNCHECKED, not clean" tools absent -> FAIL, 0 passed normal -> 5 passed, 0 failed Co-Authored-By: Claude Opus 5 --- scripts/tests/generated-child_test.sh | 36 ++++++++++++++++++--------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/scripts/tests/generated-child_test.sh b/scripts/tests/generated-child_test.sh index 8a22571..5299466 100755 --- a/scripts/tests/generated-child_test.sh +++ b/scripts/tests/generated-child_test.sh @@ -34,10 +34,13 @@ bad() { printf ' FAIL %s\n %s\n' "$1" "${2:-}"; fail=$((fail+1)); } echo "generated-child tests" +# A missing tool is a FAILURE, not a skip. An `exit 0` with zero assertions is +# indistinguishable from a real pass to anything reading the exit code -- which +# is the whole failure mode this harness exists to close. for tool in golangci-lint templ; do if ! command -v "$tool" >/dev/null 2>&1; then - echo " SKIP $tool not installed — cannot gate the generated child" >&2 - exit 0 + bad "$tool is available" "not on PATH — the generated child cannot be gated" + echo; echo "$pass passed, $fail failed"; exit 1 fi done @@ -58,7 +61,7 @@ if [ ! -d "$tmp/cmd/__PROJECT_NAME__" ]; then echo; echo "$pass passed, $fail failed"; exit 1 fi mv "$tmp/cmd/__PROJECT_NAME__" "$tmp/cmd/childproj" -find "$tmp" -type f \( -name '*.go' -o -name '*.templ' -o -name '*.mod' -o -name '*.yml' -o -name '*.md' \) \ +find "$tmp" -type f \( -name '*.go' -o -name '*.templ' -o -name '*.mod' -o -name '*.yml' -o -name '*.md' -o -name 'Dockerfile' \) \ -exec sed -i \ -e 's|__MODULE_PATH__|git.d-ma.be/mathias/childproj|g' \ -e 's|__PROJECT_NAME__|childproj|g' {} + @@ -66,11 +69,11 @@ find "$tmp" -type f \( -name '*.go' -o -name '*.templ' -o -name '*.mod' -o -name # Generated files are gitignored, so the child arrives without them — exactly as # a freshly scaffolded repo does. Generating here also proves the pinned # generator and the pinned library agree, which is template-go-web#8. -out="$(cd "$tmp" && templ generate 2>&1)" -if [ $? -eq 0 ]; then +out="$(cd "$tmp" && templ generate 2>&1)"; rc=$? +if [ "$rc" -eq 0 ]; then ok "templ generate succeeds in the generated child" else - bad "templ generate succeeds in the generated child" "$(printf '%s' "$out" | tail -3)" + bad "templ generate succeeds in the generated child" "exit $rc: $(printf '%s' "$out" | tail -2)" fi # The whole point: the entrypoint must be visible to Go tooling in the child. @@ -90,12 +93,21 @@ out="$(cd "$tmp" && go vet ./... 2>&1)" [ -z "$out" ] && ok "the generated child passes go vet" \ || bad "the generated child passes go vet" "$(printf '%s' "$out" | head -3)" -out="$(cd "$tmp" && golangci-lint run ./... 2>&1)" -if printf '%s' "$out" | grep -qE '^[0-9]+ issues:|issues:$'; then - bad "the generated child passes golangci-lint" "$(printf '%s' "$out" | grep -E '\.go:[0-9]+' | head -3)" -else - ok "the generated child passes golangci-lint" -fi +# Graded on the EXIT CODE, never on grepping the output. golangci-lint exits +# 0 clean / 1 issues found / >1 the linter itself failed. Matching its +# human-readable summary instead scores a CRASHED linter as a pass -- bad +# config, unwritable TMPDIR, OOM, or simply a future change to its output +# format all produce non-matching text. Since this harness is the only gate +# covering the entrypoint, that would leave cmd/__PROJECT_NAME__ unchecked with +# CI green, which is precisely the defect class this file exists to end. +out="$(cd "$tmp" && golangci-lint run ./... 2>&1)"; rc=$? +case "$rc" in + 0) ok "the generated child passes golangci-lint" ;; + 1) bad "the generated child passes golangci-lint" \ + "$(printf '%s' "$out" | grep -E '\.go:[0-9]+' | head -3)" ;; + *) bad "golangci-lint ran successfully" \ + "linter exited $rc — the child is UNCHECKED, not clean: $(printf '%s' "$out" | tail -2)" ;; +esac echo echo "$pass passed, $fail failed"