Template
CD / Detect unsubstituted template (pull_request) Successful in 1s
CD / Lint / Test / Vet (pull_request) Skipped
CD / Build & Import (pull_request) Skipped
CD / Deploy via GitOps (pull_request) Skipped
CD / Detect unsubstituted template (push) Successful in 1s
CD / Lint / Test / Vet (push) Skipped
CD / Build & Import (push) Skipped
CD / Deploy via GitOps (push) Skipped
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>
157 lines
6.2 KiB
YAML
157 lines
6.2 KiB
YAML
name: CD
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
tags: ["v*"]
|
||
pull_request:
|
||
branches: [main]
|
||
|
||
env:
|
||
IMAGE: __PROJECT_NAME__
|
||
|
||
jobs:
|
||
guard:
|
||
name: Detect unsubstituted template
|
||
runs-on: self-hosted
|
||
outputs:
|
||
is_template: ${{ steps.detect.outputs.is_template }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- id: detect
|
||
# The detection lives in scripts/is-template.sh so it can be TESTED.
|
||
# It was inline here twice and wrong both times (#9, #10): the grep
|
||
# named a placeholder literally, and the scaffolder substitutes every
|
||
# file including this one, so in a generated repo the guard matched
|
||
# forever and check/build/deploy all skipped while the run went green.
|
||
#
|
||
# An inline shell block in a workflow cannot be run by a test, which is
|
||
# why the second fix repeated the first bug under a comment warning
|
||
# about it. scripts/tests/is-template_test.sh now substitutes the guard
|
||
# and asserts it still answers correctly.
|
||
run: |
|
||
echo "is_template=$(bash scripts/is-template.sh go.mod)" >> "$GITHUB_OUTPUT"
|
||
|
||
check:
|
||
name: Lint / Test / Vet
|
||
needs: guard
|
||
if: needs.guard.outputs.is_template != 'true'
|
||
runs-on: self-hosted
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- uses: actions/setup-go@v5
|
||
with:
|
||
go-version-file: go.mod
|
||
cache: false
|
||
|
||
- name: Install toolchain
|
||
run: |
|
||
go version
|
||
go install github.com/a-h/templ/cmd/templ@latest
|
||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh \
|
||
| sh -s -- -b "$(go env GOPATH)/bin" v2.11.4
|
||
|
||
- name: Run checks
|
||
run: task check
|
||
|
||
build:
|
||
name: Build & Import
|
||
needs: [guard, check]
|
||
if: needs.guard.outputs.is_template != 'true' && github.event_name != 'pull_request'
|
||
runs-on: self-hosted
|
||
outputs:
|
||
image-tag: ${{ steps.meta.outputs.sha-tag }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Derive image tags
|
||
id: meta
|
||
run: |
|
||
SHA=$(git rev-parse --short HEAD)
|
||
echo "sha-tag=${SHA}" >> "$GITHUB_OUTPUT"
|
||
|
||
# infra#309: the implementation lives in .gitea/build-image.sh, a
|
||
# byte-identical copy of mathias/infra's ci-templates/build-image.sh.
|
||
# Do not edit it here -- edit the canonical file, run `task ci:sync`,
|
||
# and infra's drift gate will name any repo still carrying an old copy.
|
||
#
|
||
# This matters more in a template than anywhere else: every repo
|
||
# generated from here inherits its CI, so a template that carries the
|
||
# build LOGIC manufactures a new copy of every bug in it. That is
|
||
# precisely how rootless buildah (infra#241) reached a dozen repos, and
|
||
# why the fix then had to be rediscovered here weeks later. A template
|
||
# that carries a CALL manufactures nothing.
|
||
#
|
||
# NEW REPOS: add yourself to mathias/infra's ci-templates/adopters.txt so
|
||
# the gate keeps this copy current. Until you do, the copy is unmanaged
|
||
# and will silently fall behind.
|
||
#
|
||
# `bash <script>` rather than executing it directly: files written via
|
||
# the Gitea API land 0644, and CI that depends on a file mode is a trap.
|
||
- name: Build and push to local registry
|
||
env:
|
||
SHA_TAG: ${{ steps.meta.outputs.sha-tag }}
|
||
GIT_SHA: ${{ github.sha }}
|
||
run: bash .gitea/build-image.sh
|
||
|
||
deploy:
|
||
name: Deploy via GitOps
|
||
needs: [guard, build]
|
||
if: needs.guard.outputs.is_template != 'true' && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||
runs-on: self-hosted
|
||
steps:
|
||
- name: Update image tag in infra repo
|
||
env:
|
||
IMAGE_TAG: ${{ needs.build.outputs.image-tag }}
|
||
DEPLOY_KEY: ${{ secrets.INFRA_DEPLOY_KEY }}
|
||
run: |
|
||
set -euo pipefail
|
||
# infra#311: fail on an empty tag BEFORE patching anything. The
|
||
# grep verification further down CANNOT catch it -- with IMAGE_TAG
|
||
# empty its pattern matches the very corruption it exists to reject.
|
||
# The result is `image: <repo>:` in the manifest, which is invalid
|
||
# YAML, so Flux fails the ENTIRE apps Kustomization and every app
|
||
# stops reconciling, not just this one. That happened on 2026-08-25.
|
||
[ -n "${IMAGE_TAG:-}" ] || { echo "IMAGE_TAG is empty - refusing to patch"; exit 1; }
|
||
mkdir -p ~/.ssh
|
||
echo "$DEPLOY_KEY" > ~/.ssh/id_infra
|
||
chmod 600 ~/.ssh/id_infra
|
||
ssh-keyscan -p 30022 10.0.1.20 >> ~/.ssh/known_hosts 2>/dev/null
|
||
export GIT_SSH_COMMAND="ssh -i ~/.ssh/id_infra -o IdentitiesOnly=yes"
|
||
rm -rf /tmp/infra
|
||
git clone -b main ssh://git@10.0.1.20:30022/mathias/infra.git /tmp/infra
|
||
cd /tmp/infra
|
||
DEPLOYMENT="k3s/apps/__PROJECT_NAME__/deployment.yaml"
|
||
sed -i "s|image: localhost:5000/__PROJECT_NAME__:.*|image: localhost:5000/__PROJECT_NAME__:${IMAGE_TAG}|" "$DEPLOYMENT"
|
||
grep -q "localhost:5000/__PROJECT_NAME__:${IMAGE_TAG}" "$DEPLOYMENT" \
|
||
|| { echo "✗ image tag patch failed"; exit 1; }
|
||
if git diff --quiet "$DEPLOYMENT"; then
|
||
echo "ℹ image tag unchanged — skipping push"
|
||
else
|
||
git -c user.name="__PROJECT_NAME__ CI" \
|
||
-c user.email="ci@__PROJECT_NAME__.local" \
|
||
commit -m "chore(deploy): __PROJECT_NAME__ → ${IMAGE_TAG}" "$DEPLOYMENT"
|
||
git push origin main
|
||
echo "✓ pushed to infra repo"
|
||
fi
|
||
shred -u ~/.ssh/id_infra
|
||
|
||
- name: Trigger Flux reconcile
|
||
run: |
|
||
kubectl -n flux-system annotate gitrepository flux-system \
|
||
reconcile.fluxcd.io/requestedAt="$(date +%s)" --overwrite
|
||
kubectl -n flux-system annotate kustomization apps \
|
||
reconcile.fluxcd.io/requestedAt="$(date +%s)" --overwrite
|
||
|
||
- name: Verify rollout
|
||
run: |
|
||
kubectl rollout status deployment/__PROJECT_NAME__ \
|
||
--namespace __PROJECT_NAME__ \
|
||
--timeout=120s \
|
||
|| {
|
||
kubectl get pods -n __PROJECT_NAME__ -o wide
|
||
kubectl get events -n __PROJECT_NAME__ --sort-by='.lastTimestamp' | tail -20
|
||
exit 1
|
||
}
|