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#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>
On 2026-08-25 a deploy job ran with an empty IMAGE_TAG, wrote
`image: localhost:5000/gitea-mcp:` into the infra repo and pushed it.
That is invalid YAML, so Flux failed the ENTIRE apps Kustomization --
every app stopped reconciling for about an hour, including three
unrelated deploys queued behind it.
This job's existing check could not catch it. It greps for
`image: <repo>:${IMAGE_TAG}` after the sed, and with IMAGE_TAG empty that
pattern matches the corrupted line itself, so the verification passes
because of the bug rather than despite it.
Asserting the input up front is the fix: an empty tag now fails the job
before anything is written, let alone committed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011PL5XcG68SvQqz3vGVbWbf
Canonical gained two inputs so the repos still to migrate can adopt
without forking the script again: TARGET for multi-stage builds (cad,
agentsquad) and BUILD_SECRETS for buildkit secret mounts (agentsquad).
BUILD_SECRETS takes the SPEC only (`id=NAME,src=PATH`) -- the caller
writes the secret to PATH itself, so no secret value ever reaches this
script's argv or the CI log.
Unused here; this repo's copy just stays byte-identical, which is the
whole point. Written by `task ci:sync`, not by hand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011PL5XcG68SvQqz3vGVbWbf
Same change as template-go-agent. A template that carries the build LOGIC
manufactures a private fork of it in every generated repo; one that
carries a CALL manufactures nothing. That distinction is what made
infra#241 a nine-repo sprint plus two rediscoveries in the templates.
Invoked as `bash .gitea/build-image.sh` -- files written through the
Gitea API land 0644, and CI that depends on a file mode is a trap for
every repo generated from here.
Byte-identical copy of mathias/infra's ci-templates/build-image.sh, so
repos generated from this template inherit a managed copy of one file
rather than a private fork of the build logic.
Rootless buildah in gitea-runner's hardened securityContext (capabilities:
drop [ALL], allowPrivilegeEscalation: false) cannot remap the UID/GID range
newuidmap/newgidmap need on a fresh base-image pull, and the pod has no OCI
runtime at all. buildctl talks to the host's buildkitd over a socket, so the
privileged layer extraction happens on the host and the pod's securityContext
stops being in the picture. Same pattern as mathias/infra's segment-embedder.yml.
A template is the highest-leverage place to fix this: every repo generated from
here inherits its CI, so a buildah-based template does not merely carry the bug,
it manufactures new instances. The failure is latent — warm base-image layers
mask it — so a generated repo builds fine until its first genuinely cold pull,
which is precisely the situation a new repo is in.
Verification is PARTIAL here, and the limit is worth stating rather than
implying a green build. Running the exact command against this repo's real
context reaches step 8 and fails inside the Dockerfile:
RUN templ generate && go build ... -o /out/app ./cmd/__PROJECT_NAME__
error: failed to solve: ... exit code: 1
That failure is the template's own placeholder path, which cannot resolve on the
raw template — the same reason the guard job gates check/build/deploy off here
(is_template detects __MODULE_PATH__ in go.mod). What it does prove is that
buildctl resolved the frontend, the context and the Dockerfile and began
executing stages; every part of the buildctl integration worked, and only the
template's unsubstituted content did not.
template-go-agent's Dockerfile has no placeholder in its build path, so there the
same command produced a complete 6.8MB OCI archive. The first generated repo from
this template is where this one gets its end-to-end proof.
skopeo copy replaces `buildah push` — buildctl has no registry-push equivalent —
and runs twice to preserve the :sha and :latest tags.
template-go-web was seeded from the real `hostexecutor` project and never
parameterized: go.mod hardcoded `module gitea.d-ma.be/mathias/hostexecutor` and
`hostexecutor` appeared throughout (cmd dir, Dockerfile, Taskfile, CI, context
docs). create_project_from_template therefore substituted nothing and returned
a "no placeholders" partial_failure on the zero-config default path.
Replaced the full module path with __MODULE_PATH__ and the bare project name
with __PROJECT_NAME__ (incl. renaming cmd/hostexecutor → cmd/__PROJECT_NAME__),
matching the substitution the tool performs. Also drops the stale gitea.d-ma.be
host (the placeholder resolves to git.d-ma.be/<owner>/<name>).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>