From 52d2fc68c3a5ae0d3faa8f3b0f13e528498b4edf Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sat, 13 Jun 2026 02:35:29 +0000 Subject: [PATCH] ci: move image build off-infra to GHA -> ghcr (ADR-0002) Generated by infra/scripts/offinfra-onboard: GHA builds+tests on the GitHub mirror, pushes ghcr.io/viktorbarzin/claude-memory-mcp, then triggers the Woodpecker deploy (repo 78). Old in-cluster build pipeline removed: .woodpecker/build.yml .woodpecker/build-fallback.yml Co-Authored-By: Claude Fable 5 --- .github/workflows/build.yml | 122 +++++++++++++++++++++++++++++++++ .woodpecker/build-fallback.yml | 42 ------------ .woodpecker/build.yml | 84 ----------------------- .woodpecker/deploy.yml | 17 ++--- 4 files changed, 131 insertions(+), 134 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 .woodpecker/build-fallback.yml delete mode 100644 .woodpecker/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5b4b20c --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,122 @@ +name: Build and Push + +# Off-infra build (ADR-0002). Canonical repo is Forgejo viktor/claude-memory-mcp, which +# push-mirrors here; this workflow builds on GitHub-hosted runners, pushes the +# image to GHCR, then signals the Woodpecker deploy pipeline (repo 78) +# to roll the cluster — the homelab never sees build IO or registry pushes. +# +# Committed on the FORGEJO side (the mirror is one-way; commits made on GitHub +# are overwritten by the next sync). Generated by infra/scripts/offinfra-onboard. +on: + push: + branches: [master] + workflow_dispatch: {} + +permissions: + contents: read + packages: write + +jobs: + lint-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Install uv + uses: astral-sh/setup-uv@v5 + - name: Lint + type-check + test + run: | + uv sync --all-extras + uv run ruff check src/ tests/ + uv run mypy src/claude_memory/ + uv run pytest tests/ -v --tb=short + + build: + needs: lint-and-test + runs-on: ubuntu-latest + outputs: + image_tag: ${{ steps.meta.outputs.sha }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 # full history + tags so svu sees the last vX.Y.Z + fetch-tags: true + # Auto-semver (svu): tag-only, pushed to CANONICAL Forgejo (GitHub tags + # would be wiped by the next mirror sync). Best-effort: never blocks the build. + - name: Compute + tag semver (svu) + env: + FORGEJO_GIT_TOKEN: ${{ secrets.FORGEJO_GIT_TOKEN }} + run: | + set +e + git config user.email "ci@viktorbarzin.me" + git config user.name "claude-memory-mcp-ci" + git config --global --add safe.directory "$GITHUB_WORKSPACE" + curl -sSL https://github.com/caarlos0/svu/releases/download/v3.4.1/svu_3.4.1_linux_amd64.tar.gz | tar -xz svu + CUR=$(./svu current 2>/dev/null) + NEXT=$(./svu next 2>/dev/null) + echo "svu current=[$CUR] next=[$NEXT]" + if [ -n "$NEXT" ] && [ "$NEXT" != "$CUR" ]; then + git tag "$NEXT" 2>/dev/null + git push "https://viktor:${FORGEJO_GIT_TOKEN}@forgejo.viktorbarzin.me/viktor/claude-memory-mcp.git" "$NEXT" && echo "pushed tag $NEXT to forgejo" || echo "tag push failed (non-blocking)" + fi + exit 0 + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - id: meta + run: echo "sha=$(echo ${{ github.sha }} | cut -c1-8)" >> "$GITHUB_OUTPUT" + - uses: docker/build-push-action@v7 + with: + context: . + push: true + platforms: linux/amd64 + # Single-manifest images (no provenance/SBOM attestation children) so + # registry retention can never orphan index children (ADR-0002). + provenance: false + tags: | + ghcr.io/viktorbarzin/claude-memory-mcp:${{ steps.meta.outputs.sha }} + ghcr.io/viktorbarzin/claude-memory-mcp:latest + cache-from: type=gha + cache-to: type=gha,mode=max + # Keep the newest ~10 versions on ghcr (latest rides the newest one). + - name: ghcr retention (keep 10) + uses: actions/delete-package-versions@v5 + continue-on-error: true + with: + package-name: claude-memory-mcp + package-type: container + min-versions-to-keep: 10 + + deploy: + needs: build + runs-on: ubuntu-latest + steps: + # Signal Woodpecker (repo 78 = ViktorBarzin/claude-memory-mcp mirror) to run + # .woodpecker/deploy.yml — kubectl set image in-cluster (agent SA is cluster-admin). + - name: Trigger Woodpecker deploy + run: | + for attempt in 1 2 3; do + STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + "https://ci.viktorbarzin.me/api/repos/78/pipelines" \ + -H "Authorization: Bearer ${{ secrets.WOODPECKER_TOKEN }}" \ + -H "Content-Type: application/json" \ + -d "{\"branch\":\"master\",\"variables\":{\"IMAGE_TAG\":\"${{ needs.build.outputs.image_tag }}\",\"IMAGE_NAME\":\"ghcr.io/viktorbarzin/claude-memory-mcp\"}}") + if [ "$STATUS" -ge 200 ] && [ "$STATUS" -lt 300 ]; then + echo "Woodpecker deploy triggered (HTTP $STATUS)"; exit 0 + fi + echo "Attempt $attempt failed (HTTP $STATUS), retrying in 30s..."; sleep 30 + done + echo "Failed to trigger Woodpecker deploy after 3 attempts"; exit 1 + + notify-failure: + needs: [lint-and-test, build, deploy] + if: failure() + runs-on: ubuntu-latest + steps: + - name: Slack notify + run: | + curl -sf -X POST -H 'Content-Type: application/json' \ + -d "{\"text\":\":rotating_light: claude-memory-mcp off-infra build FAILED: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \ + "${{ secrets.SLACK_WEBHOOK }}" || true diff --git a/.woodpecker/build-fallback.yml b/.woodpecker/build-fallback.yml deleted file mode 100644 index 6b06c8e..0000000 --- a/.woodpecker/build-fallback.yml +++ /dev/null @@ -1,42 +0,0 @@ -when: - - event: deployment - -clone: - git: - image: woodpeckerci/plugin-git - settings: - attempts: 5 - backoff: 10s - -steps: - - name: test - image: python:3.12-slim - commands: - - pip install -e ".[api,dev]" - - ruff check src/ tests/ - - pytest tests/ -v --tb=short - - - name: build-and-push - image: woodpeckerci/plugin-docker-buildx - depends_on: - - test - settings: - username: viktorbarzin - password: - from_secret: dockerhub-token - repo: viktorbarzin/claude-memory-mcp - dockerfile: docker/Dockerfile - context: . - platforms: - - linux/amd64 - tags: - - "${CI_PIPELINE_NUMBER}" - - latest - - - name: deploy - image: bitnami/kubectl:latest - depends_on: - - build-and-push - commands: - - kubectl set image deployment/claude-memory claude-memory=viktorbarzin/claude-memory-mcp:${CI_PIPELINE_NUMBER} -n claude-memory - - kubectl rollout status deployment/claude-memory -n claude-memory --timeout=120s diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml deleted file mode 100644 index a675f4a..0000000 --- a/.woodpecker/build.yml +++ /dev/null @@ -1,84 +0,0 @@ -when: - event: push - branch: [main, master] - -clone: - git: - image: woodpeckerci/plugin-git - settings: - attempts: 5 - backoff: 10s - -steps: - - name: test - image: python:3.12-slim - # The woodpecker ns LimitRange defaults containers to a 256Mi memory limit. - # `uv sync` + mypy over fastapi/pydantic/sqlalchemy needs far more, so the - # step was OOM-killed (exit 137) on every run since the 2026-05-07 Forgejo - # switch — repo never built. Pin explicit memory so it never OOMs again. - backend_options: - kubernetes: - resources: - requests: - cpu: 500m - memory: 1Gi - limits: - memory: 2Gi - commands: - - pip install --no-cache-dir uv - - uv sync --all-extras - - uv run ruff check src/ tests/ - - uv run mypy src/claude_memory/ - - uv run pytest tests/ -v --tb=short - - - name: build-and-push - image: woodpeckerci/plugin-docker-buildx - depends_on: - - test - # buildx + image export also exceeds the 256Mi ns default; give it room. - backend_options: - kubernetes: - resources: - requests: - cpu: 500m - memory: 1Gi - limits: - memory: 2Gi - settings: - # Phase 4 of forgejo-registry-consolidation 2026-05-07 — Forgejo only. - # The DockerHub mirror stays as the public-facing release target via - # the GitHub `release.yml` workflow (still enabled), but the cluster - # pulls from Forgejo (infra/stacks/claude-memory/main.tf flipped 2026-05-07). - repo: - - forgejo.viktorbarzin.me/viktor/claude-memory-mcp - logins: - - registry: forgejo.viktorbarzin.me - username: - from_secret: forgejo_user - password: - from_secret: forgejo_push_token - dockerfile: docker/Dockerfile - context: . - # Tag :latest AND the 8-char commit SHA. The SHA tag is what the deploy - # step pins — a unique tag forces a fresh pull under the deployment's - # imagePullPolicy: IfNotPresent (a re-pushed :latest would not). - tags: - - "latest" - - "${CI_COMMIT_SHA:0:8}" - platforms: - - linux/amd64 - - - name: deploy - image: bitnami/kubectl:latest - depends_on: - - build-and-push - when: - branch: [main, master] - event: [push, manual] - # Owned-app deploy model (infra CLAUDE.md): the build pipeline drives the - # rollout, so a push self-deploys — no manual `kubectl set image`. The - # woodpecker-agent SA is cluster-admin, so the in-cluster kubectl needs no - # kubeconfig. Keel stays enrolled as a redundant net. - commands: - - "kubectl set image deployment/claude-memory claude-memory=forgejo.viktorbarzin.me/viktor/claude-memory-mcp:${CI_COMMIT_SHA:0:8} -n claude-memory" - - "kubectl rollout status deployment/claude-memory -n claude-memory --timeout=300s" diff --git a/.woodpecker/deploy.yml b/.woodpecker/deploy.yml index 0b32588..0d9c58b 100644 --- a/.woodpecker/deploy.yml +++ b/.woodpecker/deploy.yml @@ -1,7 +1,9 @@ -# Manual-only targeted deploy of a specific tag (set IMAGE_NAME + IMAGE_TAG). -# Push-driven deploys are handled by build.yml's deploy step now; this no longer -# fires on push (its IMAGE_TAG-absent exit-78 used to red every push pipeline, -# since build.yml + deploy.yml are workflows in the same pipeline run). +# Auto-deploy, triggered ONLY by the GitHub Actions build POSTing to the +# Woodpecker API (manual event, with IMAGE_TAG + IMAGE_NAME) after a successful +# off-infra build+push to GHCR (ADR-0002). event:[manual] (NOT push) so the +# Forgejo->GitHub mirror's raw pushes don't fire a spurious deploy. +# The woodpecker-agent SA is cluster-admin — no kubeconfig needed. +# Generated by infra/scripts/offinfra-onboard. when: - event: manual @@ -9,11 +11,10 @@ steps: - name: check-vars image: alpine commands: - - "[ -n \"$IMAGE_TAG\" ] || (echo 'IMAGE_TAG not set, skipping deploy'; exit 78)" + - "[ -n \"$IMAGE_TAG\" ] || (echo 'IMAGE_TAG not set — refusing to deploy'; exit 1)" - name: deploy image: bitnami/kubectl:latest commands: - - "kubectl set image deployment/claude-memory claude-memory=${IMAGE_NAME}:${IMAGE_TAG} -n claude-memory" - - "kubectl rollout status deployment/claude-memory -n claude-memory --timeout=300s" - + - "kubectl -n claude-memory set image deployment/claude-memory claude-memory=${IMAGE_NAME}:${IMAGE_TAG}" + - "kubectl -n claude-memory rollout status deployment/claude-memory --timeout=300s"