Compare commits

...
Author SHA1 Message Date
william 8db8347df0 Chain Portainer redeploy to the end of the build workflow, not a parallel webhook
The separate Gitea push webhook to Portainer fired in parallel with this
build workflow on the same push event, so it could redeploy before the
new image finished pushing — Portainer would then pull the still-current
:latest tag and silently keep running old code. Removed that webhook;
redeploy now only happens as this workflow's last step, after the image
push actually completes.
2026-08-23 15:11:31 +00:00
william 34c2f7d634 Merge pull request 'Fix Matrix bot autojoin 404 on startup' (#5) from fix/matrix-autojoin-404 into main
build-agent / build-and-push (push) Successful in 10s
Reviewed-on: #5
2026-08-23 15:08:37 +00:00
william fc53251285 Replace AutojoinRoomsMixin with a lightweight invite handler
Same root cause as the earlier whoami fix: the mixin's initial
/joined_rooms scan 404s against Continuwuity even though the endpoint
works fine when called directly. Only auto-join-on-invite was actually
needed, so a plain room.invite listener replaces the whole mixin.
2026-08-23 15:06:57 +00:00
william 6d0cb3bba3 Document branch protection + dedicated bot account so the bot can only open PRs, never merge 2026-08-23 14:51:13 +00:00
3 changed files with 47 additions and 2 deletions
+7
View File
@@ -31,3 +31,10 @@ jobs:
IMAGE="${{ vars.REGISTRY_HOST }}/${{ gitea.repository }}/claude-agent:latest" IMAGE="${{ vars.REGISTRY_HOST }}/${{ gitea.repository }}/claude-agent:latest"
docker build -t "$IMAGE" ./agent docker build -t "$IMAGE" ./agent
docker push "$IMAGE" docker push "$IMAGE"
- name: Trigger Portainer redeploy
# Deliberately NOT a separate Gitea repo webhook firing in parallel on the same
# push — that raced with this build and could redeploy before the new image was
# actually pushed, silently keeping the old code running. Chaining it here as the
# last step guarantees the image exists before Portainer goes to pull it.
run: curl -f -X POST "${{ secrets.PORTAINER_WEBHOOK_URL }}"
+26
View File
@@ -96,6 +96,32 @@ docker network create web
13. **Firewall**: `sudo ufw allow 80/tcp 443/tcp`; once the HTTPS routes above are all 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`. 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/<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 ## Smoke test
- Open a throwaway PR on a repo with the PR webhook set → expect a Claude review comment. - Open a throwaway PR on a repo with the PR webhook set → expect a Claude review comment.
+14 -2
View File
@@ -1,4 +1,4 @@
import { MatrixClient, SimpleFsStorageProvider, AutojoinRoomsMixin } from "matrix-bot-sdk"; import { MatrixClient, SimpleFsStorageProvider } from "matrix-bot-sdk";
import { runChatTask } from "./runner.js"; import { runChatTask } from "./runner.js";
import { askOpenRouter, DEFAULT_MODEL } from "./openrouter.js"; import { askOpenRouter, DEFAULT_MODEL } from "./openrouter.js";
@@ -45,7 +45,19 @@ export async function startMatrixBot() {
const storage = new SimpleFsStorageProvider("/workspace/matrix-bot-storage.json"); const storage = new SimpleFsStorageProvider("/workspace/matrix-bot-storage.json");
const client = new MatrixClient(HOMESERVER_URL, ACCESS_TOKEN, storage); const client = new MatrixClient(HOMESERVER_URL, ACCESS_TOKEN, storage);
AutojoinRoomsMixin.setupOnClient(client);
// Not using AutojoinRoomsMixin: it calls /joined_rooms at startup to build its initial
// state, which — like the /whoami call removed earlier — 404s against Continuwuity for
// reasons unrelated to the endpoint itself (curling it directly works fine). This
// simpler handler does the one thing we actually need — auto-join on invite — without
// that startup scan.
client.on("room.invite", async (roomId) => {
try {
await client.joinRoom(roomId);
} catch (err) {
console.error("failed to join invited room", roomId, err.message);
}
});
client.on("room.message", async (roomId, event) => { client.on("room.message", async (roomId, event) => {
// Invite-only control room enforces who can reach the bot at all; this just scopes // Invite-only control room enforces who can reach the bot at all; this just scopes