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 run: | set -euo pipefail # infra#241: rootless buildah cannot build this Dockerfile in the # gitea-runner pod — it drops ALL capabilities and sets # allowPrivilegeEscalation:false (deliberate, it holds CI secrets), so # newuidmap/newgidmap cannot remap uids and `RUN apk add` dies with # "exec: no command". Every other repo on this runner has already # moved: dispatch, tapir, brain-ingest-ntfy, parallax, macro-research, # cobalt-dingo, agentsquad (infra#233), segment-embedder (infra#243). # # buildctl talks to koala's host buildkitd over the socket the runner # pod already mounts, so the privileged work happens on the host and # this pod's posture is irrelevant to whether the build succeeds. # buildctl cannot push, so it writes an OCI archive that skopeo copies. BUILDKIT_HOST="unix:///run/buildkit/buildkitd.sock" REGISTRY="localhost:5000" REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}" TAR_FILE="$(mktemp)" trap 'rm -f "$TAR_FILE"' EXIT buildctl --addr "$BUILDKIT_HOST" build \ --frontend dockerfile.v0 \ --local context=. \ --local dockerfile=. \ --opt build-arg:VERSION="${{ steps.meta.outputs.version }}" \ --opt label:org.opencontainers.image.revision="${{ github.sha }}" \ --output type=oci,dest="$TAR_FILE" skopeo copy --dest-tls-verify=false \ oci-archive:"$TAR_FILE" docker://"${REF}" skopeo copy --dest-tls-verify=false \ oci-archive:"$TAR_FILE" docker://"${REGISTRY}/${{ env.IMAGE }}:latest" echo "✓ Image pushed to ${REF}" 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 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 }