GitHub App
A GitHub App is optional. Without one, orch reviews with an ## Automated Review comment and polls GitHub every 45 seconds. With one:
- Reviews are real
APPROVEorREQUEST_CHANGESreviews from the App's bot account, with each issue on a changed line as an inline comment, and anorch/reviewcheck run. See Review Agent. - Events reach orch as soon as GitHub sends them, through a relay, so orch syncs at once instead of on the next poll.
The App only reviews. Every other call (pushes, pull requests, comments, labels, reads) uses your gh token, and you stay the author of the pull requests. GitHub refuses a review from a pull request's author, so the App must never open the pull requests it reviews.
Create the App
On GitHub: Settings → Developer settings → GitHub Apps → New GitHub App, or the same under an organization.
| Field | Value |
|---|---|
| Name | Any, for example the-orch. The bot account is <slug>[bot] |
| Homepage URL | Any |
| Webhook | Off for reviews only. For events, the relay URL and secret, see Events |
| Where can it be installed | Only on this account, unless organizations need it |
Repository permissions:
| Permission | Access | Why |
|---|---|---|
| Pull requests | Read and write | Submit the review and its inline comments |
| Checks | Read and write | Post the orch/review check run |
| Metadata | Read | Required by GitHub |
| Issues | Read | Only for events: issues and issue_comment |
| Contents | Read | Only for events: push |
| Actions | Read | Only for events: workflow_run |
Then:
- Generate a private key on the App page. GitHub downloads a
.pemfile. - Install the App on the repositories orch works on: the App page → Install App. Your account and each organization are separate installations, and orch finds the right one per repository.
- Upload a logo, if you like. GitHub shows it as a circle, so keep the drawing centered.
Configure orch
Store the key in passbox. passbox keeps only the first line of a value, so put the PEM on one line with a literal \n for each newline. orch turns them back into newlines:
awk 'NF {printf "%s\\n", $0}' your-app.private-key.pem | passbox add orch/github-app-pemThen in ~/.orch/config.yml:
gh:
app:
id: "123456" # the App ID, a number, from the App page
private_key: passbox:orch/github-app-pem
review:
mode: app # or in one project's .orch.ymlprivate_key_path (a path to the .pem file) and private_key_env (a variable holding the PEM) work instead of private_key. See Configuration.
orch reads the key once after it starts, in the background, and keeps it in memory. A passbox read waits on Touch ID, so approve the prompt after a restart. Until the key loads, reviews fall back to the comment. The log confirms it:
GitHub App loaded, App reviews are on app_id=123456GitHub App unavailable on the same line names the reason, for example a key that does not match the App.
Commits as the bot
To show agent commits as the bot, with its logo, use the bot account's no-reply address. GitHub links a commit to an App only through this address, and it needs the bot account's user ID, not the App ID:
gh api "users/the-orch%5Bbot%5D" --jq .id # use your App's sluggit:
name: the-orch[bot]
email: <bot user id>+the-orch[bot]@users.noreply.github.comYou still open the pull requests, so the App can review them.
Events
GitHub sends App webhooks to a URL, and orch has no public endpoint. A relay receives them instead: a small service, for example a Cloudflare Worker, that checks GitHub's signature, stores each event in order, and streams them to orch. orch connects out, so nothing listens on your machine.
Set up the relay
The relay is orch-relay: one Cloudflare Worker with one Durable Object that stores events in SQLite, which the Workers Free plan allows. Its endpoints:
| Endpoint | Caller | Auth | Does |
|---|---|---|---|
POST /github | GitHub, the App's webhook URL | the webhook signature, X-Hub-Signature-256 | Checks the signature on the raw body, stores the event, answers 202 |
GET /ws | orch | Authorization: Bearer <RELAY_TOKEN> | Websocket: replays events after orch's cursor, then streams new ones |
GET /events?after=<seq> | orch | the same bearer token | The same events as JSON, the fallback when the websocket is down |
GET /health | anyone | none | 200 ok |
Make two secrets. A webhook secret for GitHub and a bearer token for orch, both long and random. Keep them in passbox, for example
orch/github-webhook-secretandorch/relay-token:openssl rand -hex 32 | passbox add orch/github-webhook-secret openssl rand -hex 32 | passbox add orch/relay-tokenPoint the Worker at your domain. Clone orch-relay and set the route in
wrangler.tomlto a host on a zone in your Cloudflare account:routes = [{ pattern = "relay.example.com", custom_domain = true }]Store the secrets in the Worker and deploy. With a Cloudflare API token that can edit Workers (
CLOUDFLARE_API_TOKEN):bun install passbox exec --env VALUE=orch/github-webhook-secret -- sh -c 'printf %s "$VALUE" | bunx wrangler secret put GITHUB_WEBHOOK_SECRET' passbox exec --env VALUE=orch/relay-token -- sh -c 'printf %s "$VALUE" | bunx wrangler secret put RELAY_TOKEN' bunx wrangler deploy curl https://relay.example.com/health # okThe values go from passbox to Cloudflare and are never printed. orch-relay's
just secretandjust deploydo the same.Send the App's webhooks to it. On the App page, turn Webhook on, set the URL to
https://relay.example.com/githuband the secret to the webhook secret (passbox copy orch/github-webhook-secretputs it on the clipboard for a few seconds), and subscribe to Issues, Issue comment, Pull request, Pull request review, Check suite, Check run, Workflow run and Push. GitHub's Recent Deliveries tab should show202answers.Point orch at it in
~/.orch/config.yml, then restart orch and approve the Touch ID prompt for the token:relay: url: https://relay.example.com token: passbox:orch/relay-token
What orch does with events
orch opens a websocket to <url>/ws with the token and keeps a cursor, so it resumes where it stopped after a restart. On each event:
| Event | orch |
|---|---|
| Any event | Starts a sync at once, for every project |
issues closed | Also releases tasks waiting on that issue, since a merged orch PR closes its issue |
| A gap (the relay dropped events orch never read) | Syncs every project |
Polling stays on every 45 seconds as the fallback, so a relay that is down delays events but loses nothing. The log shows relay stream connected when the stream is up.