Files
cad-atlas/.gitea/workflows/cd.yml
T
mathiasandClaude Sonnet 5 805b76d7c3
CD / Detect unsubstituted template (push) Successful in 0s
CD / Lint / Test / Vet (push) Successful in 5s
CD / var-go/oath (push) Has been skipped
CD / Build & Import (push) Successful in 14s
CD / Deploy via GitOps (push) Successful in 1s
feat(oath): gate cad-atlas's own real candidate, not swedsl's toy stub (#8)
oathcandidate/ is a separate Go module (mirrors swedsl's own
oath/testdata/selfcandidate pattern, keeping var-go's transitive deps
out of the deployed atlas binary) whose Build() parses the committed
.gitea/workflows/cd.yml and checks the "oath" job exists and invokes
cmd/vargo-gate. TDD: passes against the real file, fails closed on a
fixture missing the job.

Rewires the oath CI job to go-run vargo-gate from its real module path
(git.d-ma.be/mathias/swedsl/oath/cmd/vargo-gate@oath/v0.28.0, unblocked
by swedsl#35/#38) against VARGO_CANDIDATE_DIR=oathcandidate, instead of
checking out swedsl and gating its hardcoded toy fixture. Private-module
auth via a short-lived GIT_ASKPASS script (token never in argv, never
written to git config, matches act_runner's env:-block-with-secrets
gotcha).

Discovered along the way: var-go's parser needs single-line,
period-separated oath sentences with no Given/When/Then/And keyword
stripping — this repo's older oaths (incl. #1) used an unverified
multi-line keyword-prefixed style. #8's oath uses the proven format.

Still not required by branch protection pending a real-PR confirmation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 14:33:17 +02:00

210 lines
8.0 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
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
}