Template
fix(template): children were red because ./... cannot see the entrypoint
Closes #1. Closes #2. A repo generated from this template failed `task check` on errcheck from its first commit, while the template's own `task check` was green. The reason is not in the code: Go ignores directories whose names begin with "_" or "." when expanding package patterns like ./... The entrypoint lives in cmd/__PROJECT_NAME__. So `go build ./...`, `go vet ./...`, `go test ./...` and `golangci-lint run ./...` ALL silently skip the one package every generated repo is built around: $ 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 defect appears. Template green, children red, by construction -- and invisible to anyone reading either repo's CI. Fixes the one live defect: `defer shutdown(ctx)` dropped an error, and SetupTelemetry returns func(context.Context) error. Two of the three defects these issues describe were already fixed and neither issue knew: go.sum has been committed for some time, and resp.Body.Close is already wrapped. Verified before touching anything rather than trusted from the issue text. Adds scripts/tests/generated-child_test.sh, wired into `task check`: it substitutes the tracked tree into a throwaway child and gates go list / build / vet / golangci-lint on the RESULT. Without it the next entrypoint defect rots exactly the same way. Not done by adding ./cmd/__PROJECT_NAME__ to the lint command, which does work -- naming a path directly bypasses the pattern exclusion -- because it reports a gofumpt violation on the import block that does not exist in any child. The formatter's import grouping depends on the module path and the template's is a placeholder, so that route turns the gate red over an artifact of being a template. The harness reads tracked files from the working tree rather than `git archive HEAD`: it runs inside `task check`, so it must gate what is about to be committed. Archiving HEAD reported a stale pass against an unstaged fix on first use here. Proven to fail when it should: red on the unfixed entrypoint, and red again on the stale-HEAD read, before going green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,3 +23,8 @@ tasks:
|
|||||||
- golangci-lint run ./...
|
- golangci-lint run ./...
|
||||||
- go vet ./...
|
- go vet ./...
|
||||||
- go test ./... -race -count=1
|
- 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
|
||||||
|
|||||||
@@ -22,7 +22,13 @@ func main() {
|
|||||||
fmt.Fprintf(os.Stderr, "telemetry: %v\n", err)
|
fmt.Fprintf(os.Stderr, "telemetry: %v\n", err)
|
||||||
os.Exit(1)
|
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(
|
llm := litellm.New(
|
||||||
env("LITELLM_MODEL", "berget/llama-3.3-70b"),
|
env("LITELLM_MODEL", "berget/llama-3.3-70b"),
|
||||||
|
|||||||
Executable
+109
@@ -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 ]
|
||||||
Reference in New Issue
Block a user