Closes#9. Closes#10. PR rather than a push to main because #9 is risk/high.
What has been happening
Every child repo born from this template has had fake-green CI. cd.yml's guard grepped a literal placeholder, and both scaffolders substitute every file in the tree — including the guard. So in a generated repo the check became:
grep -q 'git.d-ma.be/mathias/<name>' go.mod
against a go.mod whose first line is module git.d-ma.be/mathias/<name>. It matched forever → is_template=true → check, build and deploy all skipped → the run reported success having built, tested and deployed nothing.
Confirmed twice in the wild: cad-atlas (#9) and openbanking-api (#10, where the pod sat in ImagePullBackOff because no image ever reached the registry).
Why it shipped twice
The second occurrence landed directly underneath a comment warning against exactly this:
# Grep the PLACEHOLDER, never the value substitution writes. A guard# that greps its own substituted module path matches forever …if grep -q '__MODULE_PATH__' go.mod; then
That is the diagnosis, not an aside. An inline shell block in a workflow cannot be run by a test, so a fix for it can only be reviewed by reading — and reading is what missed it twice. The comment was doing the work a test should have been doing.
The change
Detection moves to scripts/is-template.sh and gets a test.
Greps the shape of a placeholder, not any literal token — nothing for a scaffolder to rewrite, and it also catches leftovers the scaffolders know nothing about (a half-substituted tree is not a working repo either).
A missing target now exits non-zero instead of answering false. Failing open there would skip CI on a repo that is not a template at all — the same fake-green failure by another road.
scripts/tests/is-template_test.sh runs a scaffolder-style substitution over a copy of the guard and asserts it still answers correctly. Both shipped bugs fail that test.
The test had to survive substitution too
The test assembles placeholder tokens at runtime rather than writing them whole. A fixture containing a literal token gets substituted before the test runs, so the suite would pass here and fail in every generated repo — shipping a template whose children fail task check from birth, which is the same defect class in a different file.
This is not hypothetical: the guard's first draft was caught by its own test, because the explanatory comment named a token literally.
Verified by simulation, not by reading
Whole tree substituted the way create_project_from_template does it:
child go.mod module git.d-ma.be/mathias/childproj
guard in child -> false (CI runs)
guard in template -> true (template still skips its own build)
suite in child -> 8 passed, 0 failed
suite in template -> 8 passed, 0 failed
Both directions matter: the fix is only correct if the template still refuses to build itself.
Also
Wired into task check, so the guard that decides whether CI runs at all in every child repo is covered by the same gate as the Go tests.
Worth doing separately
#10 notes it: audit repos generated since the guard was introduced. Any with an all-skipped green run has never been built by CI. This PR stops new children being born broken; it does not find the existing ones.
Closes #9. Closes #10. PR rather than a push to main because #9 is `risk/high`.
## What has been happening
Every child repo born from this template has had fake-green CI. `cd.yml`'s guard grepped a **literal placeholder**, and both scaffolders substitute every file in the tree — including the guard. So in a generated repo the check became:
```
grep -q 'git.d-ma.be/mathias/<name>' go.mod
```
against a `go.mod` whose first line is `module git.d-ma.be/mathias/<name>`. It matched forever → `is_template=true` → `check`, `build` and `deploy` all skipped → the run reported **success** having built, tested and deployed nothing.
Confirmed twice in the wild: `cad-atlas` (#9) and `openbanking-api` (#10, where the pod sat in ImagePullBackOff because no image ever reached the registry).
## Why it shipped twice
The second occurrence landed **directly underneath a comment warning against exactly this**:
```yaml
# Grep the PLACEHOLDER, never the value substitution writes. A guard
# that greps its own substituted module path matches forever …
if grep -q '__MODULE_PATH__' go.mod; then
```
That is the diagnosis, not an aside. An inline shell block in a workflow **cannot be run by a test**, so a fix for it can only be reviewed by reading — and reading is what missed it twice. The comment was doing the work a test should have been doing.
## The change
Detection moves to `scripts/is-template.sh` and gets a test.
- Greps the **shape** of a placeholder, not any literal token — nothing for a scaffolder to rewrite, and it also catches leftovers the scaffolders know nothing about (a half-substituted tree is not a working repo either).
- A **missing target now exits non-zero** instead of answering `false`. Failing open there would skip CI on a repo that is not a template at all — the same fake-green failure by another road.
`scripts/tests/is-template_test.sh` runs a scaffolder-style substitution over a copy of the guard and asserts it still answers correctly. **Both shipped bugs fail that test.**
## The test had to survive substitution too
The test assembles placeholder tokens at runtime rather than writing them whole. A fixture containing a literal token gets substituted before the test runs, so the suite would pass here and fail in every generated repo — shipping a template whose children fail `task check` from birth, which is the same defect class in a different file.
This is not hypothetical: the guard's **first draft was caught by its own test**, because the explanatory comment named a token literally.
## Verified by simulation, not by reading
Whole tree substituted the way `create_project_from_template` does it:
```
child go.mod module git.d-ma.be/mathias/childproj
guard in child -> false (CI runs)
guard in template -> true (template still skips its own build)
suite in child -> 8 passed, 0 failed
suite in template -> 8 passed, 0 failed
```
Both directions matter: the fix is only correct if the template still refuses to build itself.
## Also
Wired into `task check`, so the guard that decides whether CI runs *at all* in every child repo is covered by the same gate as the Go tests.
## Worth doing separately
#10 notes it: **audit repos generated since the guard was introduced.** Any with an all-skipped green run has never been built by CI. This PR 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#9. Closes#10.
Every child repo born from this template has had fake-green CI: cd.yml's
guard grepped a literal placeholder, and both scaffolders substitute every
file in the tree -- including the guard. In a generated repo the check
became `grep -q 'git.d-ma.be/mathias/<name>' go.mod` against a go.mod whose
first line is `module git.d-ma.be/mathias/<name>`, so it matched forever,
check/build/deploy all skipped, and the run reported success having built
nothing. Confirmed in cad-atlas (#9) and openbanking-api (#10).
The second occurrence landed directly underneath a comment warning against
exactly this. That is the tell: an inline shell block in a workflow cannot
be tested, so a fix for it can only be reviewed by reading, and reading is
what missed it twice.
So the detection moves to scripts/is-template.sh and gets a test.
It greps the SHAPE of a placeholder rather than any literal token, which
also catches leftovers the scaffolders know nothing about, and a missing
target now exits non-zero instead of answering "false" -- failing open
there would skip CI on a repo that is not a template at all, which is the
same fake-green failure by another road.
scripts/tests/is-template_test.sh runs a scaffolder-style substitution over
a copy of the guard and asserts it still answers correctly. Both shipped
bugs fail that test.
The test assembles placeholder tokens at runtime rather than writing them
whole, for the same reason the guard does: a fixture containing a literal
token is substituted before the test runs, and the suite would then fail in
every generated repo while passing here. The first draft of the guard was
itself caught by this -- its explanatory comment named a token literally.
Verified by simulating a generated child (whole tree substituted):
child go.mod module git.d-ma.be/mathias/childproj
guard child -> false (CI runs)
guard template -> true (template still skips its own build)
suite in child -> 8 passed, 0 failed
suite in template -> 8 passed, 0 failed
Wired into `task check`, so the guard that decides whether CI runs at all
in every child is covered by the same gate as the Go tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 #9. Closes #10. PR rather than a push to main because #9 is
risk/high.What has been happening
Every child repo born from this template has had fake-green CI.
cd.yml's guard grepped a literal placeholder, and both scaffolders substitute every file in the tree — including the guard. So in a generated repo the check became:against a
go.modwhose first line ismodule git.d-ma.be/mathias/<name>. It matched forever →is_template=true→check,buildanddeployall skipped → the run reported success having built, tested and deployed nothing.Confirmed twice in the wild:
cad-atlas(#9) andopenbanking-api(#10, where the pod sat in ImagePullBackOff because no image ever reached the registry).Why it shipped twice
The second occurrence landed directly underneath a comment warning against exactly this:
That is the diagnosis, not an aside. An inline shell block in a workflow cannot be run by a test, so a fix for it can only be reviewed by reading — and reading is what missed it twice. The comment was doing the work a test should have been doing.
The change
Detection moves to
scripts/is-template.shand gets a test.false. Failing open there would skip CI on a repo that is not a template at all — the same fake-green failure by another road.scripts/tests/is-template_test.shruns a scaffolder-style substitution over a copy of the guard and asserts it still answers correctly. Both shipped bugs fail that test.The test had to survive substitution too
The test assembles placeholder tokens at runtime rather than writing them whole. A fixture containing a literal token gets substituted before the test runs, so the suite would pass here and fail in every generated repo — shipping a template whose children fail
task checkfrom birth, which is the same defect class in a different file.This is not hypothetical: the guard's first draft was caught by its own test, because the explanatory comment named a token literally.
Verified by simulation, not by reading
Whole tree substituted the way
create_project_from_templatedoes it:Both directions matter: the fix is only correct if the template still refuses to build itself.
Also
Wired into
task check, so the guard that decides whether CI runs at all in every child repo is covered by the same gate as the Go tests.Worth doing separately
#10 notes it: audit repos generated since the guard was introduced. Any with an all-skipped green run has never been built by CI. This PR 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