Closes#8. Closes#12. PR because #12 is risk/medium.
Two commits, one causal chain: this template could not test itself, so it accumulated defects that only its children ever saw.
What task check actually did on a clean checkout
It failed. Three defects, each hiding the next:
No go.sum at all — go build ./... died on missing sum entries, so nothing downstream ran.
With that fixed, the real skew appeared: CI installs the templ generator as @latest while go.mod pinned the templ library at v0.2.778. The v0.3 generator emits templruntime.WriteString, which v0.2.778 does not have. Generated code could not compile against the pinned runtime.
.gitignore carried *.templ.go. templ writes <name>_templ.go — underscore, not dot — so the pattern matched nothing, and generated files stayed out of git by luck rather than by rule.
Fixed: library at v0.3.1020, and the CI install now derives its version from go.mod so generator and library cannot drift apart again. go build ./... clean, task check exit 0.
Why none of it was visible
The guard job gated check as well as build and deploy, and is_template is always true here. The repo that mints every other repo had no working gate.#8 sat in it with CI green throughout and was found by a child.
check is now ungated. build and deploy stay gated — they genuinely cannot work against a placeholder module path with no image name and no namespace.
Ungating alone would have been a false fix
go build ./..., go vet ./..., go test ./... and golangci-lint run ./...cannot see this template's entrypoint. Go skips directories whose names begin with _ when expanding package patterns, and it lives in cmd/__PROJECT_NAME__:
$ go list ./...
__MODULE_PATH__/internal/web <- no cmd package
So a freshly-ungated check would still have skipped the one package every generated repo is built around. That is exactly how template-go-agent shipped a broken entrypoint for three months with its own CI green (template-go-agent#1, #2, fixed today in 2b3851a).
Linting the directory by name is not the answer either: golangci-lint run ./cmd/__PROJECT_NAME__ works, but reports gofumpt findings that do not exist in a child, because import grouping depends on the module path and this one is a placeholder. That route turns the gate red over an artifact of being a template.
The fix: gate the thing that actually ships
scripts/tests/generated-child_test.sh substitutes the tracked tree into a throwaway child and gates the result — templ generate, go list, build, vet, golangci-lint. Ported from template-go-agent with a templ step, which also proves the pinned generator and library agree.
It reads tracked files from the working tree, not git archive HEAD: it runs inside task check, so it must gate what is about to be committed. The agent version reported a stale pass against an unstaged fix on first use, which is how that was found.
Proven to fail when it should
With a deliberate errcheck violation added to the entrypoint:
The existing gate calls the same broken code clean. That contrast is the whole argument for this harness over a wider lint invocation.
Verification
task check exit 0, zero FAIL lines, both harnesses running: is-template 8/8, generated-child 5/5.
Not in scope
local-dev#33 — repos generated before today still carry the old guard and their green runs never built anything. This stops new children being born broken; it does not find the existing ones.
Closes #8. Closes #12. PR because #12 is `risk/medium`.
Two commits, one causal chain: this template could not test itself, so it accumulated defects that only its children ever saw.
## What `task check` actually did on a clean checkout
It failed. Three defects, each hiding the next:
1. **No `go.sum` at all** — `go build ./...` died on missing sum entries, so nothing downstream ran.
2. With that fixed, the real skew appeared: CI installs the templ **generator** as `@latest` while `go.mod` pinned the templ **library** at `v0.2.778`. The v0.3 generator emits `templruntime.WriteString`, which v0.2.778 does not have. Generated code could not compile against the pinned runtime.
3. `.gitignore` carried `*.templ.go`. templ writes `<name>_templ.go` — **underscore, not dot** — so the pattern matched nothing, and generated files stayed out of git by luck rather than by rule.
Fixed: library at `v0.3.1020`, and the CI install now derives its version from `go.mod` so generator and library cannot drift apart again. `go build ./...` clean, `task check` exit 0.
## Why none of it was visible
The guard job gated `check` as well as `build` and `deploy`, and `is_template` is always true here. **The repo that mints every other repo had no working gate.** #8 sat in it with CI green throughout and was found by a child.
`check` is now ungated. `build` and `deploy` stay gated — they genuinely cannot work against a placeholder module path with no image name and no namespace.
## Ungating alone would have been a false fix
`go build ./...`, `go vet ./...`, `go test ./...` and `golangci-lint run ./...` **cannot see this template's entrypoint**. Go skips directories whose names begin with `_` when expanding package patterns, and it lives in `cmd/__PROJECT_NAME__`:
```
$ go list ./...
__MODULE_PATH__/internal/web <- no cmd package
```
So a freshly-ungated `check` would still have skipped the one package every generated repo is built around. That is exactly how `template-go-agent` shipped a broken entrypoint for three months with its own CI green (`template-go-agent#1`, `#2`, fixed today in `2b3851a`).
Linting the directory by name is not the answer either: `golangci-lint run ./cmd/__PROJECT_NAME__` works, but reports gofumpt findings that do not exist in a child, because import grouping depends on the module path and this one is a placeholder. That route turns the gate red over an artifact of being a template.
## The fix: gate the thing that actually ships
`scripts/tests/generated-child_test.sh` substitutes the tracked tree into a throwaway child and gates the result — `templ generate`, `go list`, `build`, `vet`, `golangci-lint`. Ported from `template-go-agent` with a templ step, which also proves the pinned generator and library agree.
It reads tracked files from the **working tree**, not `git archive HEAD`: it runs inside `task check`, so it must gate what is about to be committed. The agent version reported a stale pass against an unstaged fix on first use, which is how that was found.
## Proven to fail when it should
With a deliberate `errcheck` violation added to the entrypoint:
```
generated-child harness -> FAIL cmd/childproj/main.go:12:26 errcheck
golangci-lint run ./... -> "0 issues."
```
The existing gate calls the same broken code clean. That contrast is the whole argument for this harness over a wider lint invocation.
## Verification
`task check` exit 0, zero FAIL lines, both harnesses running: `is-template` 8/8, `generated-child` 5/5.
## Not in scope
`local-dev#33` — repos generated before today still carry the old guard and their green runs never built anything. This stops new children being born broken; it does not find the existing ones.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---
_Created via git-mcp on behalf of @mathias_
Closes#8.
`task check` did not pass on a clean checkout of this template, and had not
for some time. Three defects in a chain, each hiding the next:
1. No go.sum at all. `go build ./...` failed on missing sum entries, so
nothing downstream of it ever ran.
2. With that fixed, the real skew appeared: CI installs the templ GENERATOR
as @latest while go.mod pinned the templ LIBRARY at v0.2.778. The v0.3
generator emits calls to templruntime.WriteString, which v0.2.778 does not
have -- so generated code could not compile against the pinned runtime.
Library moved to v0.3.1020 and the CI install now derives its version from
go.mod, so generator and library cannot drift apart again.
3. .gitignore carried `*.templ.go`. templ writes `<name>_templ.go` -- an
underscore, not a dot -- so the pattern matched nothing. Generated files
were kept out of git by luck rather than by the ignore rule.
None of this was visible from this repo's CI, which has been green
throughout, because the guard job skips `check` here entirely (#12). A
template whose gate cannot run is a template whose defects are found by its
children.
Verified on a clean checkout: `go build ./...` clean, `task check` exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes#12.
The guard that stops this template building itself also stopped it TESTING
itself: check, build and deploy were all gated on is_template, which is always
true here. So the repo that mints every other repo was the one repo with no
working gate — and #8 sat in it for months with CI green throughout, found
eventually by a child rather than by us.
check is now ungated. build and deploy stay gated, because they genuinely
cannot work here: placeholder module path, no image name, no namespace.
Ungating alone would have been a false fix. `go build ./...`, `go vet ./...`,
`go test ./...` and `golangci-lint run ./...` cannot see cmd/__PROJECT_NAME__
at all — Go skips directories whose names begin with an underscore when
expanding package patterns:
$ go list ./...
__MODULE_PATH__/internal/web <- no cmd package
So a newly-ungated check would still have skipped the package every generated
repo is built around.
scripts/tests/generated-child_test.sh substitutes the tracked tree into a
throwaway child and gates the result: templ generate, go list, build, vet,
golangci-lint. Ported from template-go-agent, plus a templ step, which also
proves the pinned generator and pinned library agree.
Proven to fail when it should. With a deliberate errcheck violation added to
the entrypoint:
generated-child harness -> FAIL cmd/childproj/main.go:12:26 errcheck
golangci-lint run ./... -> "0 issues."
The existing gate calls the same broken code clean. That contrast is the
reason this harness exists rather than a wider lint invocation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review: blocked on one finding — the new harness grades a crashed linter as a pass
I verified every claim in the body independently, and the substance holds up: go list ./... really is blind to cmd/__PROJECT_NAME__, the wide lint really does say 0 issues. on a tree with a live errcheck violation, the templ generator really is derived from go.mod, and check really did run for the first time (run 7511, job 11201, Lint / Test / Vet → success; Build & Import and Deploy via GitOps → skipped). Details at the bottom.
But I flipped the new harness five ways, and one flip it should have caught, it passed.
The golangci-lint result is graded by grepping the tool's stdout prose; its exit code is discarded. Any golangci-lint invocation that dies for a reason other than finding issues — bad config, cache/$TMPDIR permission error, OOM, a future output-format change — produces output that does not match '^[0-9]+ issues:|issues:$', and the harness therefore calls it ok.
Reproduced by putting a shim on PATH that exits 3 and prints a fatal error instead of a findings list:
$ PATH=/tmp/shimA:$PATH bash scripts/tests/generated-child_test.sh
generated-child tests
ok templ generate succeeds in the generated child
ok the generated child's entrypoint is visible to go list
ok the generated child builds
ok the generated child passes go vet
ok the generated child passes golangci-lint <-- linter exited 3, "FATAL: golangci-lint failed to run"
5 passed, 0 failed
HARNESS EXIT=0
This matters more here than it normally would, because this harness is the only gate that covers the entrypoint. If it rubber-stamps, the package every child repo is built around goes unchecked again — and the check job stays green while doing it. That is the same shape as the defect this PR exists to remove: a gate that passes without proving anything.
golangci-lint exits 0 clean / 1 on issues / >1 on error, so the correction is to assert on the exit status rather than the message:
out="$(cd"$tmp"&& golangci-lint run ./... 2>&1)";rc=$?if["$rc" -eq 0];then
ok "the generated child passes golangci-lint"else
bad "the generated child passes golangci-lint""$(printf'%s'"$out"| grep -E '\.go:[0-9]+'| head -3)"fi
Second finding (non-blocking, but please fix in the same push) — :37-42
Missing tooling is a silent success:
$ env PATH=/tmp/empty:/usr/bin:/bin bash scripts/tests/generated-child_test.sh
generated-child tests
SKIP golangci-lint not installed — cannot gate the generated child
HARNESS EXIT=0
Zero assertions, exit 0, and the SKIP goes to stderr so the body's own success criterion ("exit 0, zero FAIL lines") cannot distinguish it from a real 5/5.
In the CI path this is currently unreachable — task check runs templ generate and golangci-lint run ./...before the harnesses, so a missing tool fails earlier. But it is unreachable by luck, not by rule, and the Install toolchain step has a matching hole: curl -sSfL ... | sh -s -- -b ... v2.11.4 runs without pipefail, so if the curl fails, sh reads empty input and the step still succeeds having installed nothing. A missing tool should be bad + nonzero, not exit 0.
Third (note only, no action needed in this PR) — :61-64
The substitution covers *.go *.templ *.mod *.yml *.md. Three tracked files carry placeholders and are outside that set: Dockerfile, .context/system-prompt.txt, and the harness itself. Dockerfile:8 builds ./cmd/__PROJECT_NAME__, so the harness does not validate the one path that a repo rename has historically broken in this estate. The real scaffolder substitutes every file, so children are fine — this is a coverage gap in the gate, not a live bug. Worth its own issue.
What I verified, and how
Claim 1 — go.sum + templ pin. Confirmed derived, not @latest. CI log, job 11201:
templ library is v0.3.1020 — installing the matching generator
Reintroducing the skew (go.mod → v0.2.778) turns the harness red: 1 passed, 4 failed. Deleting go.sum likewise: 1 passed, 4 failed. The .gitignore fix is real too — after templ generate, git status --porcelain is empty, so *_templ.go is now ignored by rule rather than by luck.
Claim 2 — ungating. Run 7511 on head 39b8e86, the only run for that SHA:
Detect unsubstituted template -> success
Lint / Test / Vet -> success <-- first time this repo gated itself
Build & Import -> skipped
Deploy via GitOps -> skipped
No SKIP line anywhere in the log; both harnesses genuinely executed (is-template 8/8, generated-child 5/5). build/deploy remain correctly gated on is_template != 'true'.
Claim 3 — the entrypoint blind spot. Confirmed on the branch:
$ go list ./...
__MODULE_PATH__/internal/web # no cmd package
With _, _ = w.Write(...) changed to w.Write(...) in the entrypoint, and templ generated:
$ bash scripts/tests/generated-child_test.sh
FAIL the generated child passes golangci-lint
cmd/childproj/main.go:17:10: Error return value of `w.Write` is not checked (errcheck)
4 passed, 1 failed (exit 1)
$ golangci-lint run ./...
0 issues. (exit 0)
The contrast in the body reproduces exactly. (Note it only reproduces aftertempl generate; on a tree without generated files the wide lint fails on typecheck instead.)
#12's done-when clauses. Clause 1, demonstrated by breaking rather than reading — I narrowed the guard's regex to a literal placeholder token and scripts/tests/is-template_test.sh went 4 passed, 4 failed, exit 1; since check is now ungated and runs that harness, this would now fail the repo's CI. Clause 2, satisfied per run 7511 above. Clause 3, I substituted the tracked tree into a real child (mysvc): guard answers false, go list ./... now shows cmd/mysvc, and the harness still passes 5/5 there.
#8. Both halves fixed and both regression-tested. Verified empirically, not by reading the diff.
No secrets in the diff. task check locally reproduces CI exactly: 0 issues., 8/8, 5/5.
Everything except the first finding is in good shape, and this is a clear improvement on main (which does not build). I am blocking only on :93-98, because a gate that reports success when its checker crashed is the specific failure this PR is meant to end, and the fix is three lines rather than a design decision. Push the exit-code assertion and I will re-run the flip and re-gate.
## Review: blocked on one finding — the new harness grades a *crashed* linter as a pass
I verified every claim in the body independently, and the substance holds up: `go list ./...` really is blind to `cmd/__PROJECT_NAME__`, the wide lint really does say `0 issues.` on a tree with a live `errcheck` violation, the templ generator really is derived from `go.mod`, and `check` really did run for the first time (run **7511**, job **11201**, `Lint / Test / Vet` → success; `Build & Import` and `Deploy via GitOps` → skipped). Details at the bottom.
But I flipped the new harness five ways, and **one flip it should have caught, it passed.**
### Finding (blocking) — `scripts/tests/generated-child_test.sh:93-98`
The golangci-lint result is graded by grepping the tool's **stdout prose**; its **exit code is discarded**. Any golangci-lint invocation that dies for a reason other than finding issues — bad config, cache/`$TMPDIR` permission error, OOM, a future output-format change — produces output that does not match `'^[0-9]+ issues:|issues:$'`, and the harness therefore calls it `ok`.
Reproduced by putting a shim on `PATH` that exits 3 and prints a fatal error instead of a findings list:
```
$ PATH=/tmp/shimA:$PATH bash scripts/tests/generated-child_test.sh
generated-child tests
ok templ generate succeeds in the generated child
ok the generated child's entrypoint is visible to go list
ok the generated child builds
ok the generated child passes go vet
ok the generated child passes golangci-lint <-- linter exited 3, "FATAL: golangci-lint failed to run"
5 passed, 0 failed
HARNESS EXIT=0
```
This matters more here than it normally would, because this harness is the **only** gate that covers the entrypoint. If it rubber-stamps, the package every child repo is built around goes unchecked again — and the `check` job stays green while doing it. That is the same shape as the defect this PR exists to remove: a gate that passes without proving anything.
`golangci-lint` exits 0 clean / 1 on issues / >1 on error, so the correction is to assert on the exit status rather than the message:
```bash
out="$(cd "$tmp" && golangci-lint run ./... 2>&1)"; rc=$?
if [ "$rc" -eq 0 ]; then
ok "the generated child passes golangci-lint"
else
bad "the generated child passes golangci-lint" "$(printf '%s' "$out" | grep -E '\.go:[0-9]+' | head -3)"
fi
```
### Second finding (non-blocking, but please fix in the same push) — `:37-42`
Missing tooling is a silent success:
```
$ env PATH=/tmp/empty:/usr/bin:/bin bash scripts/tests/generated-child_test.sh
generated-child tests
SKIP golangci-lint not installed — cannot gate the generated child
HARNESS EXIT=0
```
Zero assertions, exit 0, and the SKIP goes to **stderr** so the body's own success criterion ("exit 0, zero FAIL lines") cannot distinguish it from a real 5/5.
In the CI path this is currently unreachable — `task check` runs `templ generate` and `golangci-lint run ./...` *before* the harnesses, so a missing tool fails earlier. But it is unreachable by luck, not by rule, and the `Install toolchain` step has a matching hole: `curl -sSfL ... | sh -s -- -b ... v2.11.4` runs without `pipefail`, so if the curl fails, `sh` reads empty input and the step still succeeds having installed nothing. A missing tool should be `bad` + nonzero, not `exit 0`.
### Third (note only, no action needed in this PR) — `:61-64`
The substitution covers `*.go *.templ *.mod *.yml *.md`. Three tracked files carry placeholders and are outside that set: `Dockerfile`, `.context/system-prompt.txt`, and the harness itself. `Dockerfile:8` builds `./cmd/__PROJECT_NAME__`, so the harness does not validate the one path that a repo rename has historically broken in this estate. The real scaffolder substitutes every file, so children are fine — this is a coverage gap in the gate, not a live bug. Worth its own issue.
---
### What I verified, and how
**Claim 1 — go.sum + templ pin.** Confirmed derived, not `@latest`. CI log, job 11201:
```
templ library is v0.3.1020 — installing the matching generator
```
Reintroducing the skew (`go.mod` → `v0.2.778`) turns the harness red: `1 passed, 4 failed`. Deleting `go.sum` likewise: `1 passed, 4 failed`. The `.gitignore` fix is real too — after `templ generate`, `git status --porcelain` is empty, so `*_templ.go` is now ignored by rule rather than by luck.
**Claim 2 — ungating.** Run 7511 on head `39b8e86`, the only run for that SHA:
```
Detect unsubstituted template -> success
Lint / Test / Vet -> success <-- first time this repo gated itself
Build & Import -> skipped
Deploy via GitOps -> skipped
```
No `SKIP` line anywhere in the log; both harnesses genuinely executed (`is-template` 8/8, `generated-child` 5/5). `build`/`deploy` remain correctly gated on `is_template != 'true'`.
**Claim 3 — the entrypoint blind spot.** Confirmed on the branch:
```
$ go list ./...
__MODULE_PATH__/internal/web # no cmd package
```
With `_, _ = w.Write(...)` changed to `w.Write(...)` in the entrypoint, and templ generated:
```
$ bash scripts/tests/generated-child_test.sh
FAIL the generated child passes golangci-lint
cmd/childproj/main.go:17:10: Error return value of `w.Write` is not checked (errcheck)
4 passed, 1 failed (exit 1)
$ golangci-lint run ./...
0 issues. (exit 0)
```
The contrast in the body reproduces exactly. (Note it only reproduces *after* `templ generate`; on a tree without generated files the wide lint fails on typecheck instead.)
**#12's done-when clauses.** Clause 1, demonstrated by breaking rather than reading — I narrowed the guard's regex to a literal placeholder token and `scripts/tests/is-template_test.sh` went `4 passed, 4 failed`, exit 1; since `check` is now ungated and runs that harness, this would now fail the repo's CI. Clause 2, satisfied per run 7511 above. Clause 3, I substituted the tracked tree into a real child (`mysvc`): guard answers `false`, `go list ./...` now shows `cmd/mysvc`, and the harness still passes 5/5 there.
**#8.** Both halves fixed and both regression-tested. Verified empirically, not by reading the diff.
No secrets in the diff. `task check` locally reproduces CI exactly: `0 issues.`, 8/8, 5/5.
---
Everything except the first finding is in good shape, and this is a clear improvement on `main` (which does not build). I am blocking only on `:93-98`, because a gate that reports success when its checker crashed is the specific failure this PR is meant to end, and the fix is three lines rather than a design decision. Push the exit-code assertion and I will re-run the flip and re-gate.
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>
mathias
merged commit 2c4158a808 into main2026-09-17 16:16:38 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #8. Closes #12. PR because #12 is
risk/medium.Two commits, one causal chain: this template could not test itself, so it accumulated defects that only its children ever saw.
What
task checkactually did on a clean checkoutIt failed. Three defects, each hiding the next:
go.sumat all —go build ./...died on missing sum entries, so nothing downstream ran.@latestwhilego.modpinned the templ library atv0.2.778. The v0.3 generator emitstemplruntime.WriteString, which v0.2.778 does not have. Generated code could not compile against the pinned runtime..gitignorecarried*.templ.go. templ writes<name>_templ.go— underscore, not dot — so the pattern matched nothing, and generated files stayed out of git by luck rather than by rule.Fixed: library at
v0.3.1020, and the CI install now derives its version fromgo.modso generator and library cannot drift apart again.go build ./...clean,task checkexit 0.Why none of it was visible
The guard job gated
checkas well asbuildanddeploy, andis_templateis always true here. The repo that mints every other repo had no working gate. #8 sat in it with CI green throughout and was found by a child.checkis now ungated.buildanddeploystay gated — they genuinely cannot work against a placeholder module path with no image name and no namespace.Ungating alone would have been a false fix
go build ./...,go vet ./...,go test ./...andgolangci-lint run ./...cannot see this template's entrypoint. Go skips directories whose names begin with_when expanding package patterns, and it lives incmd/__PROJECT_NAME__:So a freshly-ungated
checkwould still have skipped the one package every generated repo is built around. That is exactly howtemplate-go-agentshipped a broken entrypoint for three months with its own CI green (template-go-agent#1,#2, fixed today in2b3851a).Linting the directory by name is not the answer either:
golangci-lint run ./cmd/__PROJECT_NAME__works, but reports gofumpt findings that do not exist in a child, because import grouping depends on the module path and this one is a placeholder. That route turns the gate red over an artifact of being a template.The fix: gate the thing that actually ships
scripts/tests/generated-child_test.shsubstitutes the tracked tree into a throwaway child and gates the result —templ generate,go list,build,vet,golangci-lint. Ported fromtemplate-go-agentwith a templ step, which also proves the pinned generator and library agree.It reads tracked files from the working tree, not
git archive HEAD: it runs insidetask check, so it must gate what is about to be committed. The agent version reported a stale pass against an unstaged fix on first use, which is how that was found.Proven to fail when it should
With a deliberate
errcheckviolation added to the entrypoint:The existing gate calls the same broken code clean. That contrast is the whole argument for this harness over a wider lint invocation.
Verification
task checkexit 0, zero FAIL lines, both harnesses running:is-template8/8,generated-child5/5.Not in scope
local-dev#33— repos generated before today still carry the old guard and their green runs never built anything. This stops new children being born broken; it does not find the existing ones.🤖 Generated with Claude Code
Created via git-mcp on behalf of @mathias
Review: blocked on one finding — the new harness grades a crashed linter as a pass
I verified every claim in the body independently, and the substance holds up:
go list ./...really is blind tocmd/__PROJECT_NAME__, the wide lint really does say0 issues.on a tree with a liveerrcheckviolation, the templ generator really is derived fromgo.mod, andcheckreally did run for the first time (run 7511, job 11201,Lint / Test / Vet→ success;Build & ImportandDeploy via GitOps→ skipped). Details at the bottom.But I flipped the new harness five ways, and one flip it should have caught, it passed.
Finding (blocking) —
scripts/tests/generated-child_test.sh:93-98The golangci-lint result is graded by grepping the tool's stdout prose; its exit code is discarded. Any golangci-lint invocation that dies for a reason other than finding issues — bad config, cache/
$TMPDIRpermission error, OOM, a future output-format change — produces output that does not match'^[0-9]+ issues:|issues:$', and the harness therefore calls itok.Reproduced by putting a shim on
PATHthat exits 3 and prints a fatal error instead of a findings list:This matters more here than it normally would, because this harness is the only gate that covers the entrypoint. If it rubber-stamps, the package every child repo is built around goes unchecked again — and the
checkjob stays green while doing it. That is the same shape as the defect this PR exists to remove: a gate that passes without proving anything.golangci-lintexits 0 clean / 1 on issues / >1 on error, so the correction is to assert on the exit status rather than the message:Second finding (non-blocking, but please fix in the same push) —
:37-42Missing tooling is a silent success:
Zero assertions, exit 0, and the SKIP goes to stderr so the body's own success criterion ("exit 0, zero FAIL lines") cannot distinguish it from a real 5/5.
In the CI path this is currently unreachable —
task checkrunstempl generateandgolangci-lint run ./...before the harnesses, so a missing tool fails earlier. But it is unreachable by luck, not by rule, and theInstall toolchainstep has a matching hole:curl -sSfL ... | sh -s -- -b ... v2.11.4runs withoutpipefail, so if the curl fails,shreads empty input and the step still succeeds having installed nothing. A missing tool should bebad+ nonzero, notexit 0.Third (note only, no action needed in this PR) —
:61-64The substitution covers
*.go *.templ *.mod *.yml *.md. Three tracked files carry placeholders and are outside that set:Dockerfile,.context/system-prompt.txt, and the harness itself.Dockerfile:8builds./cmd/__PROJECT_NAME__, so the harness does not validate the one path that a repo rename has historically broken in this estate. The real scaffolder substitutes every file, so children are fine — this is a coverage gap in the gate, not a live bug. Worth its own issue.What I verified, and how
Claim 1 — go.sum + templ pin. Confirmed derived, not
@latest. CI log, job 11201:Reintroducing the skew (
go.mod→v0.2.778) turns the harness red:1 passed, 4 failed. Deletinggo.sumlikewise:1 passed, 4 failed. The.gitignorefix is real too — aftertempl generate,git status --porcelainis empty, so*_templ.gois now ignored by rule rather than by luck.Claim 2 — ungating. Run 7511 on head
39b8e86, the only run for that SHA:No
SKIPline anywhere in the log; both harnesses genuinely executed (is-template8/8,generated-child5/5).build/deployremain correctly gated onis_template != 'true'.Claim 3 — the entrypoint blind spot. Confirmed on the branch:
With
_, _ = w.Write(...)changed tow.Write(...)in the entrypoint, and templ generated:The contrast in the body reproduces exactly. (Note it only reproduces after
templ generate; on a tree without generated files the wide lint fails on typecheck instead.)#12's done-when clauses. Clause 1, demonstrated by breaking rather than reading — I narrowed the guard's regex to a literal placeholder token and
scripts/tests/is-template_test.shwent4 passed, 4 failed, exit 1; sincecheckis now ungated and runs that harness, this would now fail the repo's CI. Clause 2, satisfied per run 7511 above. Clause 3, I substituted the tracked tree into a real child (mysvc): guard answersfalse,go list ./...now showscmd/mysvc, and the harness still passes 5/5 there.#8. Both halves fixed and both regression-tested. Verified empirically, not by reading the diff.
No secrets in the diff.
task checklocally reproduces CI exactly:0 issues., 8/8, 5/5.Everything except the first finding is in good shape, and this is a clear improvement on
main(which does not build). I am blocking only on:93-98, because a gate that reports success when its checker crashed is the specific failure this PR is meant to end, and the fix is three lines rather than a design decision. Push the exit-code assertion and I will re-run the flip and re-gate.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>