This repository has been archived on 2026-09-06. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
cad-atlas/.gitea/workflows/cd.yml
T
mathiasandClaude Sonnet 5 62955c998d
CD / Deploy via GitOps (push) Successful in 1s
CD / Detect unsubstituted template (push) Successful in 1s
CD / Lint / Test / Vet (push) Successful in 6s
CD / var-go/oath (push) Skipped
CD / Build & Import (push) Successful in 7s
fix(ci): authenticate go mod download against the private cad module
Build & Import (run #68) failed: mathias/cad is PRIVATE (unlike
mcp-chassis, the only other internal module a cad-* Dockerfile has fetched
so far, which is public) — 'go mod download' inside the Docker build had
no credential and died on 'terminal prompts disabled'.

Fix mirrors this repo's own existing GIT_ASKPASS pattern (cd.yml's
oathcandidate step, for the private swedsl dependency): a BuildKit secret
mount feeds a GIT_ASKPASS helper for the one RUN that needs it. The token
is never written to a persisted layer — the askpass script and its
reference to /run/secrets/gitea_token are created and removed within the
same RUN instruction. cd.yml writes the token to a file only for
build-image.sh's subprocess and passes it via BUILD_SECRETS (the script's
existing mechanism — 'the caller writes the secret to PATH itself').

Verified by running build-image.sh's actual buildctl invocation locally
against the real koala buildkitd (same daemon CI uses) with a real token —
'go mod download' completes in 1.9s, full build succeeds. Reproduced the
CI failure log before writing this fix, per house discipline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 13:31:59 +02:00

232 lines
9.4 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: CD
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
env:
IMAGE: cad-atlas
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
# Detect an UNSUBSTITUTED template by the leftover __PLACEHOLDER__ tokens.
# Must not grep for the substituted module path — that pattern is itself
# substituted at generate time, so it would match every real child repo's
# own go.mod and skip all CI forever (template-go-web bug, see repo #).
run: |
if grep -qE '__[A-Z_]+__' go.mod; then
echo "is_template=true" >> "$GITHUB_OUTPUT"
else
echo "is_template=false" >> "$GITHUB_OUTPUT"
fi
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
- name: oathcandidate module — vet + test (private dep, short-lived askpass)
working-directory: oathcandidate
run: |
set -euo pipefail
export DMABE_GITEA_API_TOKEN='${{ secrets.DMABE_GITEA_API_TOKEN }}'
ASKPASS=$(mktemp)
{ echo '#!/bin/sh'
echo 'case "$1" in'
echo ' *Username*) echo oauth2 ;;'
echo ' *) echo "$DMABE_GITEA_API_TOKEN" ;;'
echo 'esac'
} > "$ASKPASS"
chmod 700 "$ASKPASS"
export GIT_ASKPASS="$ASKPASS" GIT_TERMINAL_PROMPT=0 GOPRIVATE=git.d-ma.be
go vet ./...
go test ./...
rm -f "$ASKPASS"
oath:
name: var-go/oath
needs: guard
# cad-atlas's own real candidate (#8): oathcandidate/ parses the committed
# .gitea/workflows/cd.yml and gates it against cad-atlas#8's oath — replacing the
# earlier wiring-only proof (#1) that always gated swedsl's toy self-test fixture
# and always failed closed. cmd/vargo-gate (swedsl#35/#37/#38) now go-installs
# cleanly from its real module path and runs the candidate module in a sandboxed
# subprocess (SubprocessGate, ADR-0003) — a green status here means "the committed
# CI config satisfies its oath", not merely "the wiring ran". Still NOT required by
# branch protection (#8) until proven green on a real PR.
if: needs.guard.outputs.is_template != 'true' && github.event_name == 'pull_request'
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: oathcandidate/go.mod
cache: false
- name: Run vargo-gate (fetch linked oath -> sandboxed-gate the real candidate -> post status)
env:
VARGO_GITEA_BASEURL: ${{ github.server_url }}
VARGO_GITEA_OWNER: ${{ github.repository_owner }}
VARGO_GITEA_REPO: cad-atlas
# Oath issue resolution (swedsl#38): a "Closes #NN" reference in the PR body
# picks the linked oath issue; VARGO_GITEA_ISSUE is the fallback (PR's own
# number, correct only for a PR filed directly against its oath issue).
VARGO_PR_BODY: ${{ github.event.pull_request.body }}
VARGO_GITEA_ISSUE: ${{ github.event.pull_request.number }}
VARGO_GITEA_SHA: ${{ github.event.pull_request.head.sha }}
VARGO_CANDIDATE_DIR: oathcandidate
# Sandbox is ON by default (untrusted PR code runs in a fresh user+net
# namespace, swedsl#37); no need to set VARGO_SANDBOX here.
run: |
set -euo pipefail
export DMABE_GITEA_API_TOKEN='${{ secrets.DMABE_GITEA_API_TOKEN }}'
ASKPASS=$(mktemp)
{ echo '#!/bin/sh'
echo 'case "$1" in'
echo ' *Username*) echo oauth2 ;;'
echo ' *) echo "$DMABE_GITEA_API_TOKEN" ;;'
echo 'esac'
} > "$ASKPASS"
chmod 700 "$ASKPASS"
export GIT_ASKPASS="$ASKPASS" GIT_TERMINAL_PROMPT=0 GOPRIVATE=git.d-ma.be
go run git.d-ma.be/mathias/swedsl/oath/cmd/vargo-gate@oath/v0.28.0
rm -f "$ASKPASS"
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
with:
fetch-depth: 0 # full history + tags so `git describe` sees the SemVer tag
- name: Derive image tags
id: meta
run: |
SHA=$(git rev-parse --short HEAD)
VERSION=$(git describe --tags --always --dirty)
echo "sha-tag=${SHA}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- name: Build and push to local registry
# infra#309: 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 names any repo still carrying an old copy.
#
# cad-atlas#11: the build now fetches git.d-ma.be/mathias/cad, a
# PRIVATE module (unlike mcp-chassis, the only other internal module
# any cad-* repo's Dockerfile has fetched so far, which is public) --
# `go mod download` inside the Dockerfile needs to authenticate. The
# token is written to a file ONLY for the build-image.sh subprocess and
# passed as a BuildKit secret (build-image.sh's existing BUILD_SECRETS
# mechanism: "the caller writes the secret to PATH itself... a secret
# VALUE must never be passed here"), never as a build-arg or env var
# that would land in the image or this log.
env:
SHA_TAG: ${{ steps.meta.outputs.sha-tag }}
GIT_SHA: ${{ github.sha }}
BUILD_ARGS: VERSION=${{ steps.meta.outputs.version }}
DMABE_GITEA_API_TOKEN: ${{ secrets.DMABE_GITEA_API_TOKEN }}
run: |
set -euo pipefail
TOKEN_FILE=$(mktemp)
printf '%s' "$DMABE_GITEA_API_TOKEN" > "$TOKEN_FILE"
chmod 600 "$TOKEN_FILE"
export BUILD_SECRETS="id=gitea_token,src=${TOKEN_FILE}"
bash .gitea/build-image.sh
shred -u "$TOKEN_FILE"
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/cad-atlas/deployment.yaml"
sed -i "s|image: localhost:5000/cad-atlas:.*|image: localhost:5000/cad-atlas:${IMAGE_TAG}|" "$DEPLOYMENT"
grep -q "localhost:5000/cad-atlas:${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="cad-atlas CI" \
-c user.email="ci@cad-atlas.local" \
commit -m "chore(deploy): cad-atlas → ${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/cad-atlas \
--namespace cad-atlas \
--timeout=120s \
|| {
kubectl get pods -n cad-atlas -o wide
kubectl get events -n cad-atlas --sort-by='.lastTimestamp' | tail -20
exit 1
}