Gabriel Koerich › Orch

Senior Engineer · AI Workflows · Solana / DeFi

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 APPROVE or REQUEST_CHANGES reviews from the App's bot account, with each issue on a changed line as an inline comment, and an orch/review check 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.

FieldValue
NameAny, for example the-orch. The bot account is <slug>[bot]
Homepage URLAny
WebhookOff for reviews only. For events, the relay URL and secret, see Events
Where can it be installedOnly on this account, unless organizations need it

Repository permissions:

PermissionAccessWhy
Pull requestsRead and writeSubmit the review and its inline comments
ChecksRead and writePost the orch/review check run
MetadataReadRequired by GitHub
IssuesReadOnly for events: issues and issue_comment
ContentsReadOnly for events: push
ActionsReadOnly for events: workflow_run

Then:

  1. Generate a private key on the App page. GitHub downloads a .pem file.
  2. 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.
  3. 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-pem

Then 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.yml

private_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=123456

GitHub 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 slug
git:
  name: the-orch[bot]
  email: <bot user id>+the-orch[bot]@users.noreply.github.com

You 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:

EndpointCallerAuthDoes
POST /githubGitHub, the App's webhook URLthe webhook signature, X-Hub-Signature-256Checks the signature on the raw body, stores the event, answers 202
GET /wsorchAuthorization: Bearer <RELAY_TOKEN>Websocket: replays events after orch's cursor, then streams new ones
GET /events?after=<seq>orchthe same bearer tokenThe same events as JSON, the fallback when the websocket is down
GET /healthanyonenone200 ok
  1. 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-secret and orch/relay-token:

    openssl rand -hex 32 | passbox add orch/github-webhook-secret
    openssl rand -hex 32 | passbox add orch/relay-token
  2. Point the Worker at your domain. Clone orch-relay and set the route in wrangler.toml to a host on a zone in your Cloudflare account:

    routes = [{ pattern = "relay.example.com", custom_domain = true }]
  3. 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    # ok

    The values go from passbox to Cloudflare and are never printed. orch-relay's just secret and just deploy do the same.

  4. Send the App's webhooks to it. On the App page, turn Webhook on, set the URL to https://relay.example.com/github and the secret to the webhook secret (passbox copy orch/github-webhook-secret puts 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 show 202 answers.

  5. 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:

Eventorch
Any eventStarts a sync at once, for every project
issues closedAlso 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.