Release Bot: GitHub App provisioning
The koryph release pipeline uses a GitHub App (the "release bot") to open and update Release PRs. This document explains why an App is required, how to perform the one-time setup, and how to replicate it across projects and organisations.
All provisioning is done with the koryph bot command family — no repo
clone, no Python, and no separate scripts required.
Why a GitHub App, not a PAT?
The release pipeline uses release-please to maintain a Release PR — a pull request that accumulates the changelog and version bump for the next release.
When a human-authored PAT opens a pull request, the token's owner becomes the PR author. GitHub's branch-protection rules prevent authors from approving their own pull requests. This means:
- The operator who owns the PAT cannot approve the Release PR.
- Approvals must come from a different account, adding friction.
- If the repo enforces required reviewers (which koryph-managed repos do), the Release PR is permanently blocked unless a second human dismisses the author restriction.
A GitHub App solves this cleanly:
| Mechanism | PR author | Operator can approve? |
|---|---|---|
| PAT | The PAT owner | No — author cannot self-approve |
| GitHub App | The App (bot identity) | Yes — operator is not the author |
The App needs only two permissions (Contents: write, Pull requests: write)
and has no webhook — it is a narrow-scope, no-inbound-listener identity.
Configure your vault once
koryph bot create stores the App's private key in a vault. To avoid
passing --vault-provider on every invocation, configure a default in
~/.koryph/config.json (machine-wide) or koryph.project.json (per project):
Machine-wide (~/.koryph/config.json):
{
"vault": {
"provider": "protonpass",
"container": "Engineering"
}
}
Per-project (koryph.project.json):
{
"vault": {
"provider": "onepassword",
"container": "Personal"
}
}
With either block in place, koryph bot create picks up the provider
automatically and you can skip --vault-provider.
See Commit & Artifact Signing — Configure your vault once
for the full resolution order and per-provider container semantics.
Prerequisites
koryphinstalled (brew install koryph/tap/koryphor from source).gh(GitHub CLI) installed and authenticated (gh auth status).- Browser access to github.com from the machine
running the command (only needed for
bot create; use--headlesson remote machines).
Three replication scenarios
Choose the scenario that matches your ownership model:
| Scenario | Command | Who can install |
|---|---|---|
| Personal account (default) | koryph bot create |
Owning account only |
| Guest org (repo admin, not org owner) | koryph bot create --public |
Any repo admin (scoped) |
| Org you own | koryph bot create --org <org> |
Within the org |
Scenario A — Personal account (private bot)
One bot, one account. The operator who creates the App can install it on any repo they own.
Step 1 — Create the App (one browser click)
koryph bot create --name mylogin-release-bot
What happens:
koryph bot createstarts a local redirect catcher on a random port.- Your browser opens with a form pre-filled with the App manifest.
- The form auto-submits to
github.com/settings/apps/new. - Click "Create GitHub App" — this is the single human action.
- GitHub redirects back to the local server with a one-time code.
- koryph exchanges the code for the App ID and private key PEM.
- Credentials are saved to
~/.koryph/bots/mylogin-release-bot.json(mode 0600).
The PEM is never printed to the terminal.
Headless machines: if the browser cannot open automatically, pass
--headlessand visit the URL printed to the terminal from another device.koryph bot create --name mylogin-release-bot --headless
Step 2 — Install the App (one browser click)
koryph bot install --name mylogin-release-bot
This prints the installation URL and opens it in your browser. Select the repositories you want to grant access to, then click Install.
Step 3 — Attach the bot to a repository
koryph bot attach --name mylogin-release-bot --repo OWNER/REPO
This performs all wiring in a single idempotent command:
- Mints a short-lived JWT from the stored PEM (no
ghdependency for App-auth calls). - Resolves the App installation that covers
OWNERvia the GitHub App API. - Adds
OWNER/REPOto the installation (idempotent). - Sets
RELEASE_BOT_APP_IDandRELEASE_BOT_PRIVATE_KEYas per-repo Actions secrets. - Enables the
can_approve_pull_request_reviewstoggle on the repository.
For org-level secrets (shared across repos in the same org), pass
--org-secrets:
koryph bot attach --name mylogin-release-bot --repo OWNER/REPO --org-secrets
Step 4 — Wire the bot to a project
koryph release setup --project <id>
Step 5 — Verify
# Full validator chain: JWT validity, installation, secrets, toggle, workflow
koryph bot check --name mylogin-release-bot --repo OWNER/REPO
# Per-project doctor check (also checks bot credentials offline)
koryph doctor --project <id>
Scenario B — Guest org (public bot, repo-admin installs)
You administer repos in an organisation you do not own. A public bot can be installed by any repo admin in any org — scoped to the repos they select.
# Create a public bot (any repo admin can install it)
koryph bot create --name mylogin-release-bot --public
# Install: visit the URL and select the guest-org repos you administer
koryph bot install --name mylogin-release-bot
# Wire the specific repo
koryph bot attach --name mylogin-release-bot --repo GUEST-ORG/MY-REPO
On the installation page, select the guest organisation from the account dropdown, then choose the specific repos you administer. GitHub creates a repo-scoped installation — it does not grant access to the entire org.
Approval-request behaviour
If the guest org has a policy that requires admin approval for third-party app installs, your install click sends an approval request to the org owner. The bot activates only after the org owner approves. Check with the org owner if the install appears to be pending.
Scenario C — Org you own (org-private bot)
# Create an org-owned private bot
koryph bot create --name myorg-release-bot --org myorg
# Install within the org
koryph bot install --name myorg-release-bot
# Wire a specific repo (sets per-repo secrets by default;
# use --org-secrets to set org-level secrets shared across repos)
koryph bot attach --name myorg-release-bot --repo myorg/my-repo
The bot is owned by the org and can only be installed within that org.
Listing bots
koryph bot list
mylogin-release-bot app_id=12345 owner=mylogin private
myorg-release-bot app_id=67890 owner=myorg private
Add --check for a quick offline PEM validity check:
koryph bot list --check
For a full live identity check against the GitHub API, use koryph bot check:
koryph bot check --name mylogin-release-bot
Checking bot health
koryph bot check runs a validator chain with precise remediation per failure:
| Check | What it validates |
|---|---|
jwt-valid |
PEM parses; JWT minted; GET /app confirms app_id match |
installation-exists |
At least one installation found for the app |
installation-covers |
Installation covers the target OWNER (with --repo) |
secrets-present |
RELEASE_BOT_APP_ID + RELEASE_BOT_PRIVATE_KEY present |
toggle-on |
can_approve_pull_request_reviews is enabled |
caller-workflow |
.github/workflows/release.yml present in the repo |
# Identity check only (no --repo):
koryph bot check --name mylogin-release-bot
# Full check against a specific repo:
koryph bot check --name mylogin-release-bot --repo OWNER/REPO
Exit codes: 0 all ok / 1 warnings / 2 failures.
koryph doctor integration
koryph doctor --project <id> automatically checks bot health when the project
has a release block configured:
release-bot-secrets: verifiesRELEASE_BOT_APP_IDandRELEASE_BOT_PRIVATE_KEYare present on the project's GitHub repo.actions-approval: verifiescan_approve_pull_request_reviewsis enabled.bot-credentials: offline PEM validity check for all stored bots in~/.koryph/bots/.
The bot-credentials check never makes a network call — it surfaces corrupted credential files before the operator tries to use them.
Credential storage
Credentials are stored at ~/.koryph/bots/<name>.json (mode 0600):
{
"name": "mylogin-release-bot",
"app_id": 12345,
"slug": "mylogin-release-bot",
"owner": "mylogin",
"public": false,
"pem": "<RSA private key — never printed to terminal>"
}
The PEM field is the only secret — the other fields are safe to inspect or copy. Never commit this file to a repository.
How the workflow uses the App
The release workflow mints a short-lived installation token using
actions/create-github-app-token:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
Fallback: when RELEASE_BOT_APP_ID or RELEASE_BOT_PRIVATE_KEY are
absent, the workflow falls back to GITHUB_TOKEN. Repositories without the
bot still work — they just inherit the PAT self-approval limitation (close/
reopen the Release PR with a different token to trigger checks).
Fallback ladder
When a GitHub App cannot be installed (org policy, personal-account restrictions, or simply not worth the setup for a low-frequency project), the release pipeline degrades gracefully through a ladder of supported alternatives. Choose the rung that fits your constraints.
| Rung | Mechanism | Checks fire automatically? | Operator can self-approve? | Per-release action required |
|---|---|---|---|---|
| 1 — Bot (preferred) | GitHub App (RELEASE_BOT_APP_ID + RELEASE_BOT_PRIVATE_KEY) |
Yes — App is not GITHUB_TOKEN | Yes — operator is not the PR author | None |
| 2 — Bot-less + kick | GITHUB_TOKEN fallback | No — use koryph release kick |
Yes | koryph release kick --repo OWNER/REPO |
| 3 — PAT as token | Fine-grained PAT in token: action input |
Yes — PAT is a real actor | No — PAT owner becomes PR author; cannot self-approve | None, but needs a second reviewer (or no required-approval ruleset) |
| 4 — Admin-merge | Admin bypass of branch-protection on merge | N/A — admin bypasses check requirement | N/A | Admin merge via GitHub UI or gh pr merge --admin |
Rung 1 — GitHub App (preferred)
See the sections above. Run koryph bot create + koryph bot attach then
koryph release setup --bot. The Release PR is authored by the App identity;
checks fire on every push; the operator approves and merges normally.
Rung 2 — Bot-less with koryph release kick
No bot installed. The caller workflow falls back to GITHUB_TOKEN — release-please
opens and updates the Release PR normally, but GitHub's platform rule prevents
GITHUB_TOKEN-triggered events from firing workflows. Close+reopen under a
real actor's token to unblock:
koryph release kick --repo OWNER/REPO
# auto-detects the PR by the "autorelease: pending" label
koryph release kick --repo OWNER/REPO --pr 42
# explicit PR number (guard relaxed to a warning)
koryph release kick --repo OWNER/REPO --wait
# blocks until all check conclusions arrive (default timeout: 10 min)
koryph doctor --project ID reports "bot-less: kick required per release"
when secrets are absent, so the operator knows exactly what to do.
Trade-off: one manual step per release cycle. Suitable for infrequent releases where bot setup overhead is not justified.
Rung 3 — Fine-grained PAT as the action token
Set a fine-grained PAT (with Contents: write + Pull requests: write
permissions on the repo) as a repository secret, then pass it as the token:
input to actions/create-github-app-token — or, more directly, supply it as
the workflow GITHUB_TOKEN override via environment:
# In the caller workflow (manual customization):
jobs:
release:
uses: koryph/koryph/.github/workflows/release-train.yml@main
with:
...
secrets:
token: ${{ secrets.MY_PAT_SECRET }}
Checks fire automatically because the PAT is a real actor. However: the PAT owner becomes the PR author. GitHub's branch-protection rules prevent an author from approving their own PR — so:
- Without a required-approval ruleset → fine (no approver needed).
- With a required reviewer count ≥ 1 → a second reviewer must approve, or the approval requirement must be waived for release-bot PRs.
Trade-off: no per-release manual step, but the self-approval limitation means either a second reviewer is required or the required-approval rule must be relaxed/bypassed for release-bot PRs. The PAT must also be rotated periodically (fine-grained PATs expire).
Rung 4 — Admin-merge escape hatch
An admin (anyone with the "Bypass pull request requirements" permission on the repository) can merge the Release PR without satisfying checks or approval rules. This is an emergency escape hatch, not a workflow:
gh pr merge --repo OWNER/REPO 42 --admin --squash
Trade-off: bypasses all branch-protection requirements. Only appropriate when a release is time-critical and no other rung is available.
Troubleshooting
"Timed out waiting for the GitHub callback"
koryph bot create waited for the redirect without receiving it (default
timeout: 5 minutes). Causes:
- The browser did not open: use
--headlessand visit the URL manually. - You did not click Create GitHub App on the GitHub page.
Re-run koryph bot create; each run generates a fresh one-time code.
"Code exchange failed (HTTP 404)"
Manifest codes are single-use and expire quickly. Re-run
koryph bot create to get a new code.
"Bot already exists"
A credential file already exists at ~/.koryph/bots/<name>.json. Either:
- Delete the file and re-run to provision a new App, or
- Use
--namewith a different name to create a parallel App.
koryph bot check reports "no installation found"
Run koryph bot install --name <name> to open the GitHub installation page,
then select the repositories to grant access to. After installing, re-run
koryph bot attach.
koryph bot check reports secrets-present: warn
The check requires repository admin access to read secret names via the GitHub API. If you have admin access and the secrets are still missing, run:
koryph bot attach --name <name> --repo OWNER/REPO
Permission denied during installation
If the org requires admin approval, your install click submits a request that the org owner must approve. The bot will not be active until the org owner approves the request via GitHub.
Security notes
- The private key PEM is stored at mode
0600in~/.koryph/bots/. Do not commit this file to a repository. - Repository secrets (
RELEASE_BOT_APP_ID,RELEASE_BOT_PRIVATE_KEY) are encrypted at rest by GitHub and never exposed in workflow logs. - The App has no webhook — it cannot receive inbound events, minimising its attack surface.
- Installation tokens minted by
actions/create-github-app-tokenare short-lived (≤ 1 hour) and scoped to the repository. - Only
contents: writeandpull_requests: writeare requested — no org permissions, no admin access. - JWTs minted by
koryph bot attachandkoryph bot checkare also short-lived (10-minute expiry), used only for the GitHub App API, and never stored or logged.