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: | REGISTRY="localhost:5000" REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}" buildah build \ --build-arg VERSION="${{ steps.meta.outputs.version }}" \ --label "org.opencontainers.image.revision=${{ github.sha }}" \ -t ${REF} \ -t ${REGISTRY}/${{ env.IMAGE }}:latest \ . buildah push --tls-verify=false ${REF} buildah push --tls-verify=false ${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 }