arizuko › components › gitd
gitd
gitd lets a GitHub repository talk to the agent in the same way as a Slack channel. It takes the signed webhook deliveries of GitHub, turns each one into an ordinary inbound message and gives it to routd. After that, a pull request is one more conversation.
gitd is a channel adapter for GitHub webhooks
gitd is a channel adapter with the same shape as slakd or teled. It checks the signed webhook deliveries and posts chanlib.InboundMsg rows to routd. A completed CI delivery can produce one row for each associated pull request. The actions of the agent on the repository, such as merge, label, review or close, go through the GitHub MCP connector. They do not use channel verbs, because they are repository actions, not chat.
gitd lets the agent hear from a repository
Without gitd, the agent can be told about a repository but cannot hear from it. Someone has to paste the pull request, and the agent answers about a snapshot that has already changed. With gitd, the repository takes part. The comment arrives as a message, the agent reads its thread, and the reply lands on the same pull request.
gitd only receives, by design. Merging, labelling and reviewing are repository actions with their own permissions. To route them through chat verbs would need a second permission model next to the one of GitHub. They go through the MCP connector instead, where the scope of the token is the limit.
gitd sits between GitHub and routd
send_text, which posts a comment when GITHUB_TOKEN is set.gitd depends on routd for delivery and on the github_sources table of routd for the secrets. Nothing depends on gitd: without it, the repository stops talking to the agent.
A source is one webhook secret for one folder
A source is one webhook secret registered for a folder. GitHub sends the same signed post for all three kinds, and one receiver handles them:
| kind | name | paste the URL and secret into |
|---|---|---|
repo | <owner>/<repo> | the Webhooks page in the settings of the repository |
org | <owner> | the Webhooks page in the settings of the organization, for every repository it owns |
app | <slug> | the webhook settings of the GitHub App, for every installation |
arizuko github -i <inst> add app arizuko-krons eng # prints url + secret ONCE
arizuko github -i <inst> list # secrets never print again
arizuko github -i <inst> rm app/arizuko-krons eng # 404s the URL, drops the routes
routd seals the secret under SECRETS_KEY in its github_sources table, and gitd unseals it for each delivery. The URL path /github/<kind>/<name> selects the secret that verifies the signature, so a payload never chooses its own verifier. A repository or organization source is also bound to its repository or organization. gitd refuses a payload that names another repository with 400, even when the signature is valid.
The JID names the owner, the repository and the thread
github:<owner> account or org event, no repository
github:<owner>/<repo> repo event — push, release, …
github:<owner>/<repo>/pr/<n> one pull request, topic=pr/<n>
github:<owner>/<repo>/issue/<n> one issue, topic=issue/<n>
The owner and the repository are in the JID, so one App secret for fifty repositories gives fifty JIDs. The ordinary route table maps them. The sender is github:<login>, the same principal that authd creates for a GitHub sign-in. So the dashboard login of a person and the comment of that person on a pull request are one identity.
Requests to the bot start a turn and the rest is observed
A new source gets seeded route rows. A pull request mention, a review request or assignment to the bot, or a direct reply to a bot comment starts a turn. Pushes, review states and CI results never start a turn, even on a pull request that the bot reviews. Human comments and reviews with a body on a bot-authored pull request or issue also map to mention. Other activity lands as <folder>#observe. After the bot replies, a human comment on an engaged pull request or issue starts a turn without a new mention. Only a message or mention rides that engagement. To route other CI rows while a conversation is engaged, add a route above the fallback, for example chat_jid=github:o/r/pr/* verb=check_run to team/reviewer#engaged.
gitd makes no GitHub API call to classify an event. For CI, subscribe to check_run, check_suite and workflow_run. A completed delivery adds one observation row for each associated pull request. One CI delivery allows ten distinct pull requests; more are refused with 400.
Repository rows use seq 10 and 100, organization rows 20 and 200, and App rows 30 and 300, so a narrower registration wins. To change the split, edit the rows on /dash/routd/ or with arizuko route.
Every message opens with a location header
gitd puts one header line before the text of each message. For an inline review comment it names the event, the message id, the thread, the review, the file, the lines and the side, and it adds the last lines of the diff hunk in a code block:
[github kind=pull_request_review_comment id=gh-rc5678901234 thread=gh-rc5678901200 review=gh-r6789012345 path=gitd/verify.go lines=10-12 side=RIGHT url=https://github.com/kronael/arizuko/pull/12#discussion_r5678901234]
Every other event carries the keys it has, such as kind, id, url and the review state. The agent reads the place of a comment from the header and needs no extra API call.
gitd posts replies as comments
send_text, send_file and typing need GITHUB_TOKEN. A reply goes to the pull request or issue that the JID names. The agent must pass replyToId, the id from the header of the message it answers. A gh-rc id keeps the reply in that review thread, and a gh-c id answers in the conversation. gitd does not edit comments, so a pull request gets no progress comment. The eyes reaction on the comment that started the turn is the only sign of work. gitd drops the own comments of the token and the comments of other bots, so the agent cannot answer itself. Typed pull request state events and CI events from automation stay eligible. Without a token, gitd only receives.
Five environment variables configure gitd
| var | default | note |
|---|---|---|
ROUTER_URL | required | |
SECRETS_KEY | unset | the keyring that seals github_sources. gitd needs it to unseal the secrets |
GITHUB_TOKEN | unset | enables send_text, send_file and typing |
GITHUB_API_URL | https://api.github.com | |
LISTEN_ADDR | :8080 |
The answer to a delivery shows what happened
The “Recent deliveries” tab on GitHub shows the answer of gitd, which is the fastest way to see what happened:
| answer | meaning |
|---|---|
200 {"ok":true,…} | delivered to routd |
202 {"retained":true} | routd was down, and the row waits in the tables of gitd for delivery |
200 {"dropped":"bot"|"self"} | ignored on purpose |
401 | bad signature |
404 | unknown source |
400 | a payload outside the source, or routd refused it |
503 | neither routd nor retention accepted the message |
GET /dash/gitd/ lists the last accepted delivery for each source. It is the shared adapter page, rendered by the same code as for every other adapter.
gitd runs alone with routd and the store
gitd runs alone. It needs routd to deliver to and a store to read its sources from, and nothing more:
export ROUTER_URL=http://routd:8080
export SECRETS_KEY=$(grep ^SECRETS_KEY .env | cut -d= -f2)
export DYNAMODB_URL=https://extenddb:18443
export AWS_ACCESS_KEY_ID=<key id> AWS_SECRET_ACCESS_KEY=<secret> AWS_REGION=us-east-1
export GITHUB_TOKEN=<token> # optional; enables send_text
export LISTEN_ADDR=:8080
./gitd
Point a webhook at http://<host>:8080/github/<kind>/<name> with the secret that arizuko github add printed. Any program that speaks POST /v1/messages like routd can replace routd, and the end-to-end tests run gitd in that way.
[gitd/README.md] has the full detail.