diff --git a/Taskfile.yml b/Taskfile.yml index d9a0e8c..97424b7 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -23,3 +23,8 @@ tasks: - golangci-lint run ./... - go vet ./... - go test ./... -race -count=1 + # The three commands above cannot see cmd/__PROJECT_NAME__ -- Go's ./... + # expansion skips directories whose names start with an underscore, which + # is why #1 and #2 shipped green here and red in every generated repo. + # This gates the substituted result instead. + - bash scripts/tests/generated-child_test.sh diff --git a/cmd/__PROJECT_NAME__/main.go b/cmd/__PROJECT_NAME__/main.go index 2c3b921..e2edb84 100644 --- a/cmd/__PROJECT_NAME__/main.go +++ b/cmd/__PROJECT_NAME__/main.go @@ -22,7 +22,13 @@ func main() { fmt.Fprintf(os.Stderr, "telemetry: %v\n", err) os.Exit(1) } - defer shutdown(ctx) + // Wrapped rather than bare: errcheck flags a deferred call that drops an + // error, and SetupTelemetry returns func(context.Context) error. The bare + // form shipped for months because this package lives in a directory whose + // name starts with an underscore, which Go's ./... expansion skips -- so + // the template's own lint never saw it while every generated repo, where + // the directory has a real name, inherited the failure. + defer func() { _ = shutdown(ctx) }() llm := litellm.New( env("LITELLM_MODEL", "berget/llama-3.3-70b"), diff --git a/scripts/tests/generated-child_test.sh b/scripts/tests/generated-child_test.sh new file mode 100755 index 0000000..54c06cf --- /dev/null +++ b/scripts/tests/generated-child_test.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# Does a repo generated from this template pass its own gate? +# +# WHY THIS EXISTS +# +# `task check` on the template is green and has been for months, while children +# generated from it were red from their first commit (#1, #2). The template's +# own gate cannot see the defect, for a reason that is invisible unless you go +# looking: +# +# Go ignores directories whose names begin with "_" or "." when expanding +# package patterns like ./... +# +# The entrypoint lives in cmd/__PROJECT_NAME__, which begins with an underscore. +# So `go build ./...`, `go vet ./...`, `go test ./...` AND +# `golangci-lint run ./...` all silently skip the one package every generated +# repo is built around. Confirmed: +# +# $ go list ./... +# __MODULE_PATH__/internal/agent +# __MODULE_PATH__/pkg/litellm <- no cmd package +# +# After substitution the directory has a real name, becomes visible, and the +# defects appear in the child. Template green, children red, by construction. +# +# WHY NOT JUST LINT THE DIRECTORY DIRECTLY +# +# Tried: `golangci-lint run ./cmd/__PROJECT_NAME__` does work (naming a path +# directly bypasses the pattern exclusion). But it reports a gofumpt violation +# on the import block that does NOT exist in a generated repo -- the formatter's +# import grouping depends on the module path, and the template's is a +# placeholder rather than a real path. Wiring that into `check` would turn the +# template's gate red over an artifact of being a template. +# +# So this simulates the substitution and gates on the RESULT, which is the only +# artifact anyone actually runs. Same discipline as template-go-web's +# scripts/tests/is-template_test.sh: a test for transformation-sensitive code +# has to undergo the transformation. +# +# 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" + +if ! command -v golangci-lint >/dev/null 2>&1; then + echo " SKIP golangci-lint not installed — cannot gate the generated child" >&2 + exit 0 +fi + +tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT + +# Build the child exactly as a scaffolder does: the tracked tree, the entrypoint +# directory renamed, every placeholder substituted. +# +# Tracked files from the WORKING TREE, not `git archive HEAD`. This runs inside +# `task check`, so it has to gate what is about to be committed -- archiving +# HEAD tests the previous commit and reports stale results against a fix sitting +# unstaged, which it did on first use here. `git ls-files` keeps the file set +# honest (no build output, no local scratch) 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 '*.mod' -o -name '*.yml' -o -name '*.md' \) \ + -exec sed -i \ + -e 's|__MODULE_PATH__|git.d-ma.be/mathias/childproj|g' \ + -e 's|__PROJECT_NAME__|childproj|g' {} + + +# 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)" + +# The one that caught #1/#2: errcheck on the deferred shutdown, in a package the +# template's own `golangci-lint run ./...` never analysed. +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 + +echo +echo "$pass passed, $fail failed" +[ "$fail" -eq 0 ]