Files
gitops-automation/README.md

7.4 KiB

gitops-automation

Claude Code automation wired into Gitea + Portainer + Matrix on this VPS. See ~/.claude/plans/cozy-honking-lantern.md on the host for the full design rationale.

Three things this gives you:

  • PR review: opening/updating a PR in a watched Gitea repo gets a Claude-authored review comment.
  • GitOps redeploy: pushing to main on this repo rebuilds the claude-agent image (Gitea Actions) and redeploys the stack (Portainer webhook).
  • Chat-driven coding agent: !claude owner/repo <instruction> in the Matrix control room clones the repo, runs Claude Code, and opens a PR with the result.
  • Ask other models: !ai <prompt> (default model) or !ai provider/model <prompt> (e.g. !ai google/gemini-2.0-flash-001 explain this error) queries any model on OpenRouter and replies in the room. No repo/file access — just a chat reply, unlike !claude which is the only command that can edit files and open PRs.

Nothing here auto-merges. Every path stops at a comment or an open PR — a human clicks merge.

Prerequisites (one-time, on the VPS)

sudo usermod -aG docker william   # then start a new shell/session
docker network create web

Bring-up order

  1. DNS — confirm these resolve to 217.160.66.143: gitea.apps.williamturner.eu, portainer.apps.williamturner.eu, matrix.apps.williamturner.eu, agent.apps.williamturner.eu.

  2. Front Gitea with Traefik: edit ~/gitea/docker-compose.yml — add it to the web network and Traefik labels (mirror the matrix-homeserver block in this repo's docker-compose.yml, using port 3000 as the service port and rule Host(`gitea.apps.williamturner.eu`)). Update ~/gitea/data/gitea/conf/app.ini: DOMAIN and ROOT_URLhttps://gitea.apps.williamturner.eu/. Recreate the container, confirm the HTTPS URL works, then remove 3000:3000 from the port mapping.

  3. Front Portainer with Traefik: same pattern on ~/portainer-compose.yaml, service port 9443 (Portainer serves TLS itself on that port — either terminate TLS at Traefik with traefik.http.services.portainer.loadbalancer.server.scheme=https and serversTransport with insecure skip-verify, or simplest: also expose Portainer's plain HTTP port internally and point Traefik at that instead). Confirm https://portainer.apps.williamturner.eu works before removing 9443:9443.

  4. Create the gitops-automation repo in Gitea (via http://217.160.66.143:3000 if step 2 isn't done yet, otherwise the HTTPS URL), push this directory to it.

  5. Gitea API token: user Settings → Applications → generate a token with repo + webhook scopes. Put it in .env as GITEA_TOKEN.

  6. Gitea Actions runner: admin Settings → Actions → Runners → create registration token → .env as ACT_RUNNER_REGISTRATION_TOKEN. Also set repo-level Actions variables GITEA_HOST (e.g. gitea.apps.williamturner.eu) and secret GITEA_TOKEN (Settings → Actions → Variables/Secrets on the repo) — the workflow in .gitea/workflows/build.yml reads those.

  7. First image build (registry is empty until Actions runs once):

    cp .env.example .env   # fill in values as you go
    docker build -t "$(grep GITEA_REGISTRY_IMAGE .env | cut -d= -f2)" ./agent
    docker login <gitea-host> -u <your-username>
    docker push "$(grep GITEA_REGISTRY_IMAGE .env | cut -d= -f2)"
    
  8. Bring up the stack, ideally as a Portainer "Repository" stack pointed at this repo (so it's also the GitOps redeploy target) — or directly:

    docker compose up -d
    
  9. First boot: Matrix bot account — with MATRIX_ALLOW_REGISTRATION=true in .env, redeploy matrix-homeserver, then register the bot:

    curl -s https://matrix.apps.williamturner.eu/_matrix/client/v3/register \
      -H 'Content-Type: application/json' \
      -d '{"username":"claude-bot","password":"<pick one>","auth":{"type":"m.login.dummy"}}'
    

    This returns an access_token — put it in .env as MATRIX_BOT_TOKEN. Then set MATRIX_ALLOW_REGISTRATION=false and redeploy matrix-homeserver again.

  10. Control room: from any Matrix client logged in as yourself on this homeserver, create a room, invite @claude-bot:matrix.apps.williamturner.eu, copy the room ID into .env as MATRIX_CONTROL_ROOM_ID. Redeploy claude-agent.

  11. Portainer stack webhook: in the stack's settings, enable the webhook, copy the URL into .env/repo secrets as PORTAINER_STACK_WEBHOOK_URL.

  12. Gitea webhooks on each repo you want automation for:

    • push → mainPORTAINER_STACK_WEBHOOK_URL (only needed on this repo, for GitOps redeploy of the automation stack itself)
    • pull request (opened, synchronized) → https://agent.apps.williamturner.eu/webhooks/gitea, secret = GITEA_WEBHOOK_SECRET, on every repo you want auto-reviewed.
  13. Firewall: sudo ufw allow 80/tcp 443/tcp; once the HTTPS routes above are all confirmed working, sudo ufw delete allow 3000/tcp and sudo ufw delete allow 9443/tcp.

  14. Branch protection on main, on every repo you want the bot working on (so it's structurally limited to opening PRs, never merging or pushing directly) — via the repo's Settings → Branches → Add Rule, or the API:

    curl -X POST -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
      "$GITEA_URL/api/v1/repos/<owner>/<repo>/branch_protections" \
      -d '{
        "rule_name": "main",
        "enable_push": false,
        "enable_merge_whitelist": true,
        "merge_whitelist_usernames": ["<your-human-username>"]
      }'
    

    This blocks all direct pushes to main (everyone goes through a PR) and restricts merging to the human usernames listed — claude-bot (see below) is never in that list, so it structurally cannot merge, only open PRs, regardless of what its token can do.

  15. Dedicated bot account (recommended over using your own account/token for the agent): create a separate Gitea user (e.g. claude-bot) via Site Administration → User Accounts, generate its own token (scopes: repository, issue, package — no admin, organization, or user needed), add it as a repo Collaborator with Write permission on each automated repo, then use its token as GITEA_TOKEN (agent) and REGISTRY_TOKEN (CI secret) instead of your own. This makes every PR comment, branch, and PR clearly attributed to the bot instead of you, and its access is easy to revoke independently.

Smoke test

  • Open a throwaway PR on a repo with the PR webhook set → expect a Claude review comment.
  • In the Matrix control room: !claude owner/repo add a comment to the README → expect a "working on it" reply, then a PR link.
  • git push to main on this repo → expect a Gitea Actions run, then a Portainer redeploy.

Notes

  • agent/src/runner.js is the only thing that ever runs git commit/git push/git checkout — Claude Code itself is explicitly denied those tools (--disallowedTools), so even a misbehaving prompt can't push directly or touch main.
  • The Matrix bot only reacts inside MATRIX_CONTROL_ROOM_ID; keep that room invite-only.
  • .gitea/workflows/build.yml assumes the act_runner label docker — check GITEA_RUNNER_LABELS in docker-compose.yml matches what you actually registered.