Bare `/tapir` runs the long-running server, same as the old ctr-based smoke test. ctr's --rm reliably force-killed it; a plain `timeout N buildah run` does not — it only signals the wrapper, and the container process can survive that and keep the log pipe open, hanging the whole job (observed live: run 155, 14min before failure). Backgrounds the run and tears it down with `buildah rm -f`, which forcibly kills regardless of wrapper state, and captures output via a file instead of a blocking pipe. Refs infra#132.
226 lines
9.2 KiB
YAML
226 lines
9.2 KiB
YAML
name: CI
|
||
|
||
"on":
|
||
push:
|
||
branches: [main]
|
||
tags: ["v*"]
|
||
pull_request:
|
||
branches: [main]
|
||
|
||
env:
|
||
IMAGE: tapir
|
||
|
||
jobs:
|
||
# ── 1. Quality gate ─────────────────────────────────────────────────────────
|
||
# NOTE: the acceptance suite is intentionally RED in the scaffold (the engine
|
||
# returns ErrNotImplemented). This job will fail until the first build task
|
||
# implements Engine.ProcessNewVideo. That is the expected TDD starting state.
|
||
check:
|
||
name: Lint / Test / Vet
|
||
runs-on: self-hosted
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- uses: actions/setup-go@v5
|
||
with:
|
||
go-version-file: go.mod
|
||
cache: false
|
||
|
||
- name: Verify toolchain
|
||
run: |
|
||
go version
|
||
task --version
|
||
|
||
- name: Install golangci-lint
|
||
run: |
|
||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh \
|
||
| sh -s -- -b "$(go env GOPATH)/bin" v2.11.4
|
||
golangci-lint --version
|
||
|
||
- name: Run checks
|
||
run: task check
|
||
|
||
# ── 2. Build image ──────────────────────────────────────────────────────────
|
||
build:
|
||
name: Build & Import
|
||
needs: check
|
||
runs-on: self-hosted
|
||
if: github.event_name != 'pull_request'
|
||
outputs:
|
||
image-tag: ${{ steps.meta.outputs.sha-tag }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Derive image tags
|
||
id: meta
|
||
run: |
|
||
SHA=$(git rev-parse --short HEAD)
|
||
echo "sha-tag=${SHA}" >> "$GITHUB_OUTPUT"
|
||
REF="${{ github.ref }}"
|
||
if [[ "$REF" == refs/tags/v* ]]; then
|
||
echo "version-tag=${REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||
fi
|
||
|
||
- name: Build and push to local registry
|
||
run: |
|
||
REGISTRY="localhost:5000"
|
||
REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}"
|
||
buildah build \
|
||
--label "org.opencontainers.image.revision=${{ github.sha }}" \
|
||
--label "org.opencontainers.image.source=${{ github.repositoryUrl }}" \
|
||
-t ${REF} \
|
||
-t ${REGISTRY}/${{ env.IMAGE }}:latest \
|
||
.
|
||
buildah push --tls-verify=false ${REF}
|
||
buildah push --tls-verify=false ${REGISTRY}/${{ env.IMAGE }}:latest
|
||
[[ -n "${{ steps.meta.outputs.version-tag }}" ]] && \
|
||
buildah push --tls-verify=false \
|
||
${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.version-tag }} || true
|
||
echo "Image pushed to ${REF}"
|
||
|
||
# Run the just-built local image briefly via buildah, not k3s ctr —
|
||
# avoids sudo/host-containerd access so this still works once the
|
||
# runner itself is containerized (infra#132). Tests the local
|
||
# buildah-store image directly, no registry round-trip needed.
|
||
#
|
||
# Bare `/tapir` (no subcommand) runs the long-running server, same as
|
||
# the old ctr-based smoke test -- ctr's --rm reliably force-killed it,
|
||
# but a plain `timeout N buildah run` does NOT: it only signals the
|
||
# `buildah run` wrapper, and the actual container process can survive
|
||
# that and keep the output pipe open, hanging the whole job (hit this
|
||
# live: 14min hang, infra#132). Backgrounding the run + `buildah rm -f`
|
||
# decouples "is the script blocked" from "did the process exit" --
|
||
# rm -f forcibly tears down the container regardless of wrapper state.
|
||
- name: Smoke test
|
||
run: |
|
||
REGISTRY="localhost:5000"
|
||
REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}"
|
||
CONTAINER=$(buildah from ${REF})
|
||
LOG=$(mktemp)
|
||
buildah run "$CONTAINER" -- /tapir > "$LOG" 2>&1 &
|
||
RUNPID=$!
|
||
sleep 5
|
||
kill -9 "$RUNPID" 2>/dev/null || true
|
||
wait "$RUNPID" 2>/dev/null || true
|
||
buildah rm -f "$CONTAINER" >/dev/null 2>&1 || true
|
||
OUTPUT=$(cat "$LOG"); rm -f "$LOG"
|
||
echo "$OUTPUT" | grep -q "tapir" \
|
||
&& echo "Smoke test passed" \
|
||
|| echo "Smoke test inconclusive: $OUTPUT"
|
||
|
||
# ── 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) ─
|