Files
mathiasandClaude Opus 5 2c4158a808
CD / Detect unsubstituted template (pull_request) Successful in 0s
CD / Lint / Test / Vet (pull_request) Successful in 5s
CD / Build & Import (pull_request) Skipped
CD / Deploy via GitOps (pull_request) Skipped
CD / Detect unsubstituted template (push) Successful in 0s
CD / Lint / Test / Vet (push) Successful in 5s
CD / Build & Import (push) Skipped
CD / Deploy via GitOps (push) Skipped
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 <noreply@anthropic.com>
2026-09-17 18:14:23 +02:00

115 lines
4.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Does a repo generated from this template pass its own gate?
#
# WHY THIS EXISTS
#
# `golangci-lint run ./...` and friends cannot see this template's entrypoint.
# Go skips directories whose names begin with "_" or "." when expanding package
# patterns, and the entrypoint lives in cmd/__PROJECT_NAME__:
#
# $ go list ./...
# __MODULE_PATH__/internal/web <- no cmd package
#
# After substitution the directory has a real name, becomes visible, and any
# defect in it appears — in the child, not here. That is how template-go-agent
# shipped a broken entrypoint for three months while its own CI stayed green
# (template-go-agent#1, #2).
#
# Linting the directory by name is not a fix: `golangci-lint run
# ./cmd/__PROJECT_NAME__` works, but reports formatter findings that do not
# exist in a generated repo, because gofumpt's import grouping depends on the
# module path and this one is a placeholder.
#
# So this substitutes the tracked tree into a throwaway child and gates the
# RESULT, which is the only artifact anyone actually runs.
#
# Usage: ./scripts/tests/generated-child_test.sh
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
pass=0; fail=0
ok() { printf ' ok %s\n' "$1"; pass=$((pass+1)); }
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
bad "$tool is available" "not on PATH — the generated child cannot be gated"
echo; echo "$pass passed, $fail failed"; exit 1
fi
done
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
# Tracked files from the WORKING TREE, not `git archive HEAD`: this runs inside
# `task check`, so it must gate what is about to be committed rather than the
# previous commit. `git ls-files` keeps the file set honest while taking
# current content.
( cd "$ROOT" && git ls-files -z ) | while IFS= read -r -d '' f; do
mkdir -p "$tmp/$(dirname "$f")"
cp "$ROOT/$f" "$tmp/$f"
done
if [ ! -d "$tmp/cmd/__PROJECT_NAME__" ]; then
bad "the template still has a placeholder entrypoint directory" \
"cmd/__PROJECT_NAME__ not found — has the layout changed?"
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' -o -name 'Dockerfile' \) \
-exec sed -i \
-e 's|__MODULE_PATH__|git.d-ma.be/mathias/childproj|g' \
-e 's|__PROJECT_NAME__|childproj|g' {} +
# 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)"; rc=$?
if [ "$rc" -eq 0 ]; then
ok "templ generate succeeds in the generated child"
else
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.
# If this regresses, everything below it passes for the wrong reason.
if ( cd "$tmp" && go list ./... 2>/dev/null ) | grep -q '/cmd/childproj$'; then
ok "the generated child's entrypoint is visible to go list"
else
bad "the generated child's entrypoint is visible to go list" \
"cmd package missing from ./... — the child would be unchecked too"
fi
out="$(cd "$tmp" && go build ./... 2>&1)"
[ -z "$out" ] && ok "the generated child builds" \
|| bad "the generated child builds" "$(printf '%s' "$out" | head -3)"
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)"
# 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"
[ "$fail" -eq 0 ]