Skip to content
Docs/GitHub App

Deploys

GitHub App

Everything UpButler does on GitHub works with a token and a deploy hook. The App is the upgrade: one install instead of pasted tokens, Check Runs instead of commit statuses, and a place on the pull request where the verdict lives.

What it adds

Without the AppWith the App
Preview checksA commit status, through a fine-grained token you pasteA Check Run with a table of every check next to its production result. No token
Pull requestsNothing on the pull requestOne comment, edited in place: the monitored routes the change touches, a warning when it deletes one or takes out a heartbeat ping, new routes without a monitor, and the deploy guard verdict after merge
IncidentsSuspect deploys, by timeSuspect pull requests: what was merged before the failure, ranked by deploy time and by whether it changes the failing route
Fix with Claude trackingThe deploy is matched by pull request number or commit message. A rebase merge leaves neitherThe merge is seen directly and its commit recorded, so any merge style is recognised
DeploysA deploy hook URL per providerOptionally, GitHub's own deployment_status events
Linked repositoriesFrom upbutler.yaml, init, or typed inThe repositories you give the installation

Install

  1. Settings → Integrations → GitHub → Install the UpButler GitHub App (owners and admins).
  2. GitHub asks which account and which repositories. Pick only the ones you deploy; you can change the selection later on GitHub.
  3. GitHub asks you to authorize. That is how UpButler knows the installation is yours: it asks GitHub whether your account can see it, and only then connects it to the workspace you started from.

The install link works once, for an hour, in the browser session that started it. If an organization owner has to approve the installation, press Install again once they have. One GitHub installation belongs to one workspace; a workspace can connect several.

What it can do on your repositories

PermissionAccessUsed to
checkswritepost the preview verdict as a Check Run
statuseswriteset a commit status next to the Check Run, for branch protection that requires a status by name
pull_requestswriteread the changed files of a pull request and keep one comment on it
contentsreadread upbutler.yaml (the coverage globs) from the base branch. No code is cloned or stored
deploymentsreadreceive deployment_status events
metadatareadrequired by GitHub for every app

Events: pull_request, push, deployment_status, check_suite, plus the installation events GitHub sends to every app. The App cannot push, merge, change settings or read secrets. Fix with Claude still pushes with your workflow's token or your own gh.

Check Runs for preview checks

When a preview run is for a repository the installation covers, UpButler creates a Check Run named UpButler / preview on the head commit as the checks start and concludes it with the verdict:

VerdictConclusion
passsuccess"4/4 checks pass on the preview"
failfailure"1 of 4 checks fail on the preview". Require the check in branch protection and it blocks the merge
errorneutralThe preview could not be tested (deployment protection, no monitor). It does not block

The summary is a table of every check: what the preview answered, what production answers, and the result. Failing checks come first. A check counts as failing only when it passes in production and fails on the preview.

The commit status, without a token

The App also sets a commit status named UpButler / preview status (a different name, so the pull request's checks list shows no duplicate): pending, then success, failure or error. A branch protection rule can require either the Check Run UpButler / preview or the status UpButler / preview status with the App alone; you do not need to paste a token. If you did save a token under Deploy hooks → Preview checks, the status is set with that token and under the name you chose there. Check Runs and the commit status can each be switched off per installation.

The pull request comment

When a pull request is opened or pushed to, UpButler reads the list of changed files and maps them to URL paths by the conventions of the common frameworks (src/pages/api/checkout.ts, app/orders/[id]/route.ts, src/routes/api/users/+server.ts, a top-level api/). A path that an HTTP monitor of the workspace watches is a hit.

### UpButler

This pull request touches 1 monitored route.

| Route | Monitor | In production now | Changed files |
| --- | --- | --- | --- |
| `/api/checkout` | Checkout | healthy | `src/pages/api/checkout.ts` |

New route without a monitor: `/api/refunds`. Add it to `upbutler.yaml` or run `npx upbutler init`.

**After merge:** ✅ No regression in the 10 minutes after deploy 8c1d2e7: 3 watched monitors stayed healthy. Details
  • One comment. It is created the first time there is something to say and edited after that, also when the same pull request is pushed to twenty times. A pull request that touches nothing monitored gets no comment.
  • Coverage globs. If the repository's upbutler.yaml has coverage.routes (upbutler init writes them), only files matching those globs count as routes. The file is read from the base branch, so a pull request cannot change what is reviewed about itself.
  • After merge. When the merge is deployed and the deploy guard reaches its verdict, the verdict is added to the same comment, where the author will see it.
  • Draft pull requests get the comment when they are marked ready.
# upbutler.yaml, on the default branch
coverage:
  routes:
    - "src/pages/api/**/*.ts"
    - "app/**/route.ts"

When a pull request takes monitoring away

Two changes get a warning at the top of the comment, because after the merge a monitor fails or, worse, goes quiet:

  • A monitored route is removed. The file behind a route that a monitor watches is deleted, or renamed to a different route. Moving the file so that another file of the same pull request serves the same URL (pages/ to app/) is not a removal.
  • A heartbeat ping is removed. The diff deletes the ping URL of one of your heartbeat monitors (the monitor is named; the URL is never printed), or deletes a ping call whose URL comes from the environment (withHeartbeat(…), upbutler heartbeat, UPBUTLER_HEARTBEAT…) without adding one back. A ping that only moves between files is not reported.
### UpButler

⚠️ **This pull request removes a monitored route.** Its monitor will fail once this is deployed:

- `/api/checkout` (deleted `src/pages/api/checkout.ts`), watched by Checkout

If the route is meant to go, delete or pause the monitor in the same change (or take it out of `upbutler.yaml`).

⚠️ **This pull request removes a heartbeat ping.** A job that stops pinging is reported as not running:

- the ping of Nightly export, in `jobs/export.ts`

The diff is read to find these and is not stored; UpButler keeps the list of changed file names. GitHub leaves very large and binary files out of the diff it returns, so a ping removed inside one of those is not seen.

Suspect pull requests on an incident

A deploy marker says when something went live. With the App, an incident also says what: the pull requests merged into the incident's repository in the 24 hours before the first failing check, most suspect first. The ranking uses, in this order:

  1. Deploy proximity. The deploy that carried the pull request (matched by merge commit, else by number): between the last good and the first failing check, within 30 minutes before, or earlier. A pull request whose deploy came after the failure is left out. Without a deploy marker, the merge time stands in and the reason says so.
  2. Files behind the failing routes. Whether the pull request changes (or deletes) a file that maps to the URL of a failing monitor, by the same conventions and coverage.routes globs as the comment.
  3. Size. Between otherwise equal candidates, the larger change ranks higher.

Confidence is high when the pull request was deployed in the gap and changes a failing route, medium for one of the two, low otherwise. The list is in What changed on the incident page, in the handoff packet and GET /incidents/:id/evidence as suspectPrs, and in the prompt a Fix with Claude agent gets.

"suspectPrs": [{
  "rank": 1, "number": 213, "repo": "acme/shop", "title": "Refund rounding", "author": "maya",
  "url": "https://github.com/acme/shop/pull/213",
  "mergedAt": "2026-10-10T13:54:00.000Z", "mergeCommit": "8c1d2e7…",
  "deployId": "dpl_…", "deployedAt": "2026-10-10T13:56:00.000Z", "minutesBeforeFailure": 4,
  "confidence": "high",
  "reason": "deployed between the last good and the first failing check; changes /api/refunds (src/pages/api/refunds.ts), which a failing monitor watches; 47 lines in 2 files",
  "touches": [{ "route": "/api/refunds", "monitor": "Refunds", "files": ["src/pages/api/refunds.ts"] }],
  "size": { "files": 2, "additions": 34, "deletions": 13 }
}]

Merges from before the App was installed are included: the list is read from GitHub when the incident is opened, and the file list of a merged pull request is fetched once.

Merges and Fix with Claude

When a pull request that Fix with Claude opened is merged, GitHub tells UpButler and the incident's timeline says "Pull request #213 was merged by alex (commit 8c1d2e7). Waiting for the deploy". The merge commit is recorded, and the deploy that carries that commit moves the fix to Deployed. This closes a gap of the token-only path, where a rebase merge (no (#213) in any commit message) was never recognised. A fix pull request that is closed without merging is marked as given up, so "Let Claude fix" is offered again.

GitHub deployments as a deploy source

Off by default. Turn on Use GitHub deployments as a deploy source for an installation and its deployment_status events are handled like a deploy hook delivery: a successful production deployment becomes a deploy marker with a guard, a successful deployment of another environment starts preview checks. Most hosts (Vercel, Netlify, Railway) write GitHub deployments and have their own hook; a deploy that another hook reported in the last 20 minutes is not marked a second time. If you already use a host's deploy hook, you do not need this.

Security

  • Webhook deliveries are verified with the app's secret (X-Hub-Signature-256, HMAC SHA-256 of the raw body) before anything is read. Each delivery is handled once.
  • UpButler authenticates as the app with a short-lived signed JWT and works with installation access tokens that GitHub retires after an hour. They are kept in memory only.
  • The app's private key, webhook secret and client secret are stored encrypted in the database. They are never shown again, never logged and never returned by the API.
  • An installation is connected only when the one-time state belongs to your signed-in session and GitHub confirms that your account can see that installation. An installation id in a URL proves nothing on its own.
  • Events for an installation that no workspace connected are acknowledged and ignored.

Disconnect

Disconnect under Settings → Integrations, or uninstall the app on GitHub: either way Check Runs and comments stop. Linked repositories and saved tokens stay, so commit statuses, deploy hooks and Fix with Claude keep working as before.

API

# What is connected
curl https://upbutler.com/api/v1/github -H "Authorization: Bearer $UPBUTLER_API_KEY"

# Settings per installation: checkRuns, commitStatus, prComments, deploySource
# Turn GitHub deployments into deploy markers and guards (off by default)
curl -X PATCH https://upbutler.com/api/v1/github/installations/12345678 \
  -H "Authorization: Bearer $UPBUTLER_API_KEY" -H "Content-Type: application/json" \
  -d '{"deploySource": true}'

# Disconnect (add ?uninstall=true to also remove the app from the GitHub account)
curl -X DELETE https://upbutler.com/api/v1/github/installations/12345678 \
  -H "Authorization: Bearer $UPBUTLER_API_KEY"

Operations: github.status, github.update, github.sync, github.disconnect. Installing needs GitHub's consent screen, so it starts in the browser at /app/settings/github/install.

Running your own UpButler: registering the App

The App is registered once per UpButler installation, by its operator, with GitHub's App Manifest flow. Nothing is typed into GitHub by hand and no secret goes into an env file.

# .env of the UpButler installation (web and worker)
OPERATOR_EMAILS=[email protected]
  1. Set OPERATOR_EMAILS to your sign-in email (comma-separated for several). Empty, the default, means the page below does not exist for anyone.
  2. Signed in with that email, open /app/admin/github-app. To own the app with an organization, type its name; otherwise it belongs to your account.
  3. Press Create on GitHub, confirm the name there. GitHub returns to UpButler with a one-time code, which is traded for the app id, private key, webhook secret and client credentials. They are stored encrypted (with APP_SECRET).