# 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 ` in the Matrix control room clones the repo, runs Claude Code, and opens a PR with the result. - **Ask other models**: `!ai ` (default model) or `!ai provider/model ` (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) ```bash 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_URL` → `https://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): ```bash cp .env.example .env # fill in values as you go docker build -t "$(grep GITEA_REGISTRY_IMAGE .env | cut -d= -f2)" ./agent docker login -u 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: ```bash docker compose up -d ``` 9. **First boot: Matrix bot account** — with `MATRIX_ALLOW_REGISTRATION=true` in `.env`, redeploy `matrix-homeserver`, then register the bot: ```bash curl -s https://matrix.apps.williamturner.eu/_matrix/client/v3/register \ -H 'Content-Type: application/json' \ -d '{"username":"claude-bot","password":"","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 → `main` → `PORTAINER_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: ```bash curl -X POST -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \ "$GITEA_URL/api/v1/repos///branch_protections" \ -d '{ "rule_name": "main", "enable_push": false, "enable_merge_whitelist": true, "merge_whitelist_usernames": [""] }' ``` 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.