name: CD "on": push: branches: [main] tags: ["v*"] # Docs-only pushes don't change the image — skip the build/deploy roll. # (Only applies to branch pushes; tag pushes always trigger.) paths-ignore: - 'docs/**' env: IMAGE: gitea-mcp jobs: # ── 1. Quality gate ───────────────────────────────────────────────────────── 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 # self-hosted runner: Go cache persists on disk between runs - name: Verify toolchain run: | go version task --version govulncheck -version 2>&1 || true - 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 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: | set -e # infra#241: buildctl, not buildah. Rootless buildah in gitea-runner's # hardened securityContext (capabilities drop ALL, # allowPrivilegeEscalation false) cannot remap the UID/GID range # newuidmap/newgidmap need on a fresh base-image pull, and the pod has # no OCI runtime at all -- so this Dockerfile's `RUN apk add` / `RUN go # build` could not execute there either. buildctl is a thin client # that hands the build to the host's buildkitd over a socket, so the # privileged layer work happens on the host and this pod's # securityContext stops being in the picture. Same pattern as # mathias/infra's segment-embedder.yml. BUILDKIT_HOST="unix:///run/buildkit/buildkitd.sock" REGISTRY="localhost:5000" REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}" # Stamp the real build version: the git tag on a v* build, else the short sha. VERSION="${{ steps.meta.outputs.version-tag }}" # `if`, not `[ -z ] &&`: under `set -e` the && form exits the step # whenever VERSION is already set. if [ -z "$VERSION" ]; then VERSION="${{ steps.meta.outputs.sha-tag }}"; fi 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="${VERSION}" \ --opt label:org.opencontainers.image.revision="${{ github.sha }}" \ --opt label:org.opencontainers.image.source="${{ github.repositoryUrl }}" \ --output type=oci,dest="$TAR_FILE" # buildctl has no registry-push equivalent to `buildah push`; skopeo # copies the OCI archive up, once per tag. 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 if [ -n "${{ steps.meta.outputs.version-tag }}" ]; then skopeo copy --dest-tls-verify=false \ oci-archive:"$TAR_FILE" \ docker://${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.version-tag }} fi echo "✓ Image pushed to ${REF}" # No smoke test here. The old one ran `sudo k3s ctr run` against the host's # containerd: written for the Arch host runner, impossible in the # containerized runner (infra#233) which has no sudo, no ctr and no # containerd socket -- it failed with `sudo: command not found` the moment # infra#241's buildctl migration let the build reach it. The deploy job's # `Verify rollout` + `Confirm pod running new image` cover the same ground # more strongly, which is why tapir dropped its smoke test too (infra#241). # ── 3. Deploy via infra repo + Flux ───────────────────────────────────────── 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 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/gitea-mcp/deployment.yaml" sed -i "s|image: localhost:5000/gitea-mcp:.*|image: localhost:5000/gitea-mcp:${IMAGE_TAG}|" "$DEPLOYMENT" grep -q "localhost:5000/gitea-mcp:${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="gitea-mcp CI" \ -c user.email="ci@gitea-mcp.local" \ commit -m "chore(deploy): gitea-mcp → ${IMAGE_TAG}" "$DEPLOYMENT" git push origin main echo "✓ pushed to infra repo" fi shred -u ~/.ssh/id_infra - name: Trigger Flux reconcile (immediate) 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: Wait for Flux to apply new image env: IMAGE_TAG: ${{ needs.build.outputs.image-tag }} run: | EXPECTED="localhost:5000/gitea-mcp:${IMAGE_TAG}" for i in $(seq 1 60); do CURRENT=$(kubectl get deploy gitea-mcp -n gitea-mcp \ -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 kubectl get deploy gitea-mcp -n gitea-mcp \ -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/gitea-mcp \ --namespace gitea-mcp \ --timeout=120s \ || { echo "── pod status ──" kubectl get pods -n gitea-mcp -o wide echo "── events ──" kubectl get events -n gitea-mcp --sort-by='.lastTimestamp' | tail -20 echo "── describe ──" kubectl describe pods -n gitea-mcp -l app=gitea-mcp | tail -40 exit 1 } - name: Confirm pod running new image env: IMAGE_TAG: ${{ needs.build.outputs.image-tag }} run: | kubectl get pods -n gitea-mcp \ -l app=gitea-mcp \ --field-selector=status.phase=Running \ -o jsonpath='{.items[*].spec.containers[0].image}' \ | grep -q "localhost:5000/gitea-mcp:${IMAGE_TAG}" \ && echo "✓ pod running new image" \ || { echo "✗ pod image mismatch"; exit 1; }