generated from mathias/template-go-web
cad-atlas is the eighth repo to hit this wall, and it only surfaced now because the build job had been unreachable behind a failing test job: error running container: from creating container for [/bin/sh -c apk add --no-cache git]: : exec: no command Root cause is the gitea-runner pod's securityContext, not this repo: it drops ALL capabilities and sets allowPrivilegeEscalation:false, so newuidmap and newgidmap cannot remap uids and rootless buildah gets a single non-remapped mapping — not enough to set up a RUN step's user namespace. buildctl talks to koala's host buildkitd over the socket the runner pod already mounts, so the privileged layer work happens on the host and the pod keeps its minimal posture. Same migration dispatch, tapir, brain-ingest-ntfy, parallax, macro-research, cobalt-dingo, agentsquad (infra#233) and segment-embedder (infra#243) have already made. Note --opt build-arg:VERSION replaces buildah's --build-arg; the version label this repo stamps into its binary depends on it, and dropping it silently would have produced an image reporting the wrong version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PL5XcG68SvQqz3vGVbWbf
231 lines
9.2 KiB
YAML
231 lines
9.2 KiB
YAML
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
|
||
}
|