ci(deploy): add Flux GitOps deploy job, mirroring cobalt-dingo's pattern
Fixes silent stale-deploy gap: CI built+pushed images but nothing bumped k3s/apps/tapir/deployment.yaml, so merged features sat CI-green with zero production effect (infra#111, infra#168). Flux native image-automation can't scan localhost:5000 from inside k3s pods, so this patches the infra repo directly via the existing INFRA_DEPLOY_KEY org secret (same key cobalt-dingo and brain-gardener already use) on every push to main. Refs infra#111.
This commit is contained in:
+115
-1
@@ -90,4 +90,118 @@ jobs:
|
|||||||
&& echo "Smoke test passed" \
|
&& echo "Smoke test passed" \
|
||||||
|| echo "Smoke test inconclusive: $OUTPUT"
|
|| 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) ─
|
||||||
|
|||||||
Reference in New Issue
Block a user