fix: green baseline, and let the template test itself #13

Merged
mathias merged 3 commits from fix/green-baseline-and-self-test into main 2026-09-17 16:16:38 +00:00
6 changed files with 144 additions and 6 deletions
+17 -2
View File
@@ -34,8 +34,16 @@ jobs:
check:
name: Lint / Test / Vet
# Deliberately NOT gated on the guard (#12). build and deploy are, because
# they genuinely cannot work here -- placeholder module path, no image name,
# no namespace. But linting and testing the template is meaningful, and
# gating it meant this repo had no working gate at all: #8 sat here with CI
# green throughout, and was found by a child rather than by us.
#
# `task check` includes scripts/tests/generated-child_test.sh, which covers
# the entrypoint that ./... cannot see -- ungating without that would run a
# gate that still skips the package every child is built around.
needs: guard
if: needs.guard.outputs.is_template != 'true'
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
@@ -48,7 +56,14 @@ jobs:
- name: Install toolchain
run: |
go version
go install github.com/a-h/templ/cmd/templ@latest
# Pin the GENERATOR to the same version as the templ LIBRARY in
# go.mod. Installing @latest let them drift: the generator emitted
# calls (templruntime.WriteString) that the pinned library did not
# have, which is template-go-web#8. Nothing caught it because the
# guard skipped `check` in this repo entirely (#12).
templ_version="$(go list -m -f '{{.Version}}' github.com/a-h/templ)"
echo "templ library is $templ_version — installing the matching generator"
go install "github.com/a-h/templ/cmd/templ@$templ_version"
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh \
| sh -s -- -b "$(go env GOPATH)/bin" v2.11.4
+4 -1
View File
@@ -27,4 +27,7 @@ go.work.sum
# Project-specific
bin/
*.templ.go
# templ writes <name>_templ.go -- this was '*.templ.go' (a dot, not an
# underscore) and therefore matched nothing, so generated files were only ever
# kept out of git by luck. template-go-web#8.
*_templ.go
+4
View File
@@ -27,6 +27,10 @@ tasks:
# The template guard decides whether CI runs at all in every child repo,
# so its regression test belongs in the same gate as the Go tests.
- bash scripts/tests/is-template_test.sh
# The three Go commands above cannot see cmd/__PROJECT_NAME__ -- Go's
# ./... expansion skips directories whose names start with an underscore.
# This gates the substituted result, which is what a child actually runs.
- bash scripts/tests/generated-child_test.sh
context:sync:
desc: Regenerate all harness-specific context files
+1 -3
View File
@@ -2,6 +2,4 @@ module __MODULE_PATH__
go 1.26
require (
github.com/a-h/templ v0.2.778
)
require github.com/a-h/templ v0.3.1020
+4
View File
@@ -0,0 +1,4 @@
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+114
View File
@@ -0,0 +1,114 @@
#!/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 ]