diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 2522490..ef58562 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -90,4 +90,118 @@ jobs: && echo "Smoke test passed" \ || echo "Smoke test inconclusive: $OUTPUT" - # ── 3. Mirror to GitHub — skipped for now (SSH key rotation pending) ─ + # ── 3. Deploy via infra repo + Flux ───────────────────────────────────────── + # Flux native image-automation can't scan localhost:5000 from inside k3s pods + # (mathias/infra k3s/flux/flux-system/image-automation.yaml) — this job + # mirrors cobalt-dingo's proven pattern instead: patch the infra repo's + # manifest directly on every push to main, then annotate Flux for a fast + # reconcile. Fixes infra#111 (image built+pushed but manifest bump was + # manual, so merged features silently didn't deploy). + deploy: + name: Deploy via GitOps + needs: build + runs-on: self-hosted + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + environment: staging + 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_DEPLOY_KEY is a Gitea org secret (mathias org), already + # configured per docs/cd-pipeline.md in the infra repo — same key + # cobalt-dingo and brain-gardener use, no new secret needed. + 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/tapir/deployment.yaml" + # In-place update of the image tag. sed (not yq) so we don't + # depend on additional tooling on the runner — same as cobalt-dingo. + sed -i "s|image: localhost:5000/tapir:.*|image: localhost:5000/tapir:${IMAGE_TAG}|" "$DEPLOYMENT" + + # Verify the patch took effect. + grep -q "localhost:5000/tapir:${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="tapir CI" \ + -c user.email="ci@tapir.local" \ + commit -m "chore(deploy): tapir → ${IMAGE_TAG}" "$DEPLOYMENT" + git push origin main + echo "✓ pushed to infra repo" + fi + + shred -u ~/.ssh/id_infra + + - name: Trigger Flux reconcile (immediate) + run: | + # Without these annotations, Flux would still pick up the change + # within 30s (the apps Kustomization interval). The annotations + # cut latency to ~1s. + 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: Wait for Flux to apply new image + env: + IMAGE_TAG: ${{ needs.build.outputs.image-tag }} + run: | + # Poll the Deployment spec until it reflects the new tag. + # Bound to 60s so a stuck Flux doesn't hang CI. + EXPECTED="localhost:5000/tapir:${IMAGE_TAG}" + for i in $(seq 1 60); do + CURRENT=$(kubectl get deploy tapir -n tapir \ + -o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null || echo "") + if [ "$CURRENT" = "$EXPECTED" ]; then + echo "✓ Flux applied new image after ${i}s" + break + fi + sleep 1 + done + # Final assertion (in case the loop exited without matching). + kubectl get deploy tapir -n tapir \ + -o jsonpath='{.spec.template.spec.containers[0].image}' \ + | grep -qx "$EXPECTED" \ + || { echo "✗ Flux did not apply new image within 60s"; exit 1; } + + - name: Verify rollout + run: | + kubectl rollout status deployment/tapir \ + --namespace tapir \ + --timeout=120s \ + || { + echo "── pod status ──" + kubectl get pods -n tapir -o wide + echo "── events ──" + kubectl get events -n tapir --sort-by='.lastTimestamp' | tail -20 + echo "── describe ──" + kubectl describe pods -n tapir -l app=tapir | tail -40 + exit 1 + } + + - name: Confirm pod running new image + env: + IMAGE_TAG: ${{ needs.build.outputs.image-tag }} + run: | + kubectl get pods -n tapir \ + -l app=tapir \ + --field-selector=status.phase=Running \ + -o jsonpath='{.items[*].spec.containers[0].image}' \ + | grep -q "localhost:5000/tapir:${IMAGE_TAG}" \ + && echo "✓ pod running new image" \ + || { echo "✗ pod image mismatch"; exit 1; } + + # ── 4. Mirror to GitHub — skipped for now (SSH key rotation pending) ─