207 lines
8.6 KiB
YAML
207 lines
8.6 KiB
YAML
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/**'
|
||
# gitea-mcp#68: without this a PR never got a CI run, so the risk/medium+
|
||
# PR lane merged on local `task check` alone. Only the check job runs for a
|
||
# PR -- build and deploy stay push/tag-only (no images, no infra writes,
|
||
# no secrets from a PR).
|
||
pull_request:
|
||
branches: [main]
|
||
|
||
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
|
||
if: github.event_name == 'push'
|
||
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"
|
||
echo "version=${REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||
else
|
||
# The ldflags version stamp: git tag on a v* build, else short
|
||
# sha. Computed in shell rather than as a fallback expression in
|
||
# YAML, so it cannot depend on how the runner treats an empty step
|
||
# output -- a wrong VERSION would be baked into the binary silently.
|
||
#
|
||
# NOTE: no expression syntax in this comment, deliberately. act
|
||
# interpolates a run: block INCLUDING its comments, so prose
|
||
# containing that syntax is evaluated as an expression -- it failed
|
||
# with "Unavailable context: a", the step wrote no outputs, and the
|
||
# deploy job committed an empty image tag that broke Flux
|
||
# estate-wide (infra 51ceb7c).
|
||
echo "version=${SHA}" >> "$GITHUB_OUTPUT"
|
||
fi
|
||
|
||
- name: Build and push to local registry
|
||
# infra#309: implementation lives in .gitea/build-image.sh, a
|
||
# byte-identical copy of mathias/infra's ci-templates/build-image.sh.
|
||
# Do not edit it here -- edit the canonical file, run `task ci:sync`,
|
||
# and infra's drift gate names any repo still carrying an old copy.
|
||
env:
|
||
SHA_TAG: ${{ steps.meta.outputs.sha-tag }}
|
||
VERSION_TAG: ${{ steps.meta.outputs.version-tag }}
|
||
GIT_SHA: ${{ github.sha }}
|
||
IMAGE_SOURCE: ${{ github.repositoryUrl }}
|
||
BUILD_ARGS: VERSION=${{ steps.meta.outputs.version }}
|
||
run: bash .gitea/build-image.sh
|
||
|
||
# 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
|
||
# infra#311: fail on an empty tag BEFORE patching anything. The
|
||
# grep verification further down CANNOT catch it -- with IMAGE_TAG
|
||
# empty its pattern matches the very corruption it exists to reject.
|
||
# The result is `image: <repo>:` in the manifest, which is invalid
|
||
# YAML, so Flux fails the ENTIRE apps Kustomization and every app
|
||
# stops reconciling, not just this one. That happened on 2026-08-25.
|
||
[ -n "${IMAGE_TAG:-}" ] || { echo "IMAGE_TAG is empty - refusing to patch"; exit 1; }
|
||
|
||
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; }
|