GitHub side
Create the GitHub App, set the right permissions and webhook URL, generate a private key, install on repos.
The tool authenticates to GitHub as a GitHub App — not a personal access token. One App covers all repos you want to
manage runners for; each repo Installs the App.
PAT auth is not supported: minting JIT runner configs requires an installation token, which only Apps can produce.
1. Create the App
- Personal account: https://github.com/settings/apps/new
- Organization:
https://github.com/organizations/<org>/settings/apps/new
Fill in:
| Field | Value |
|---|---|
| GitHub App name | anything unique, e.g. pacer-<your-org> |
| Homepage URL | anything (e.g. your server.public_url) |
| Webhook URL | <server.public_url>/api/webhook |
| Webhook secret | a random hex string — save it, you’ll paste it into github.webhook_secret |
| Where can this App be installed? | Only on this account (recommended for self-hosted) |
Generate the webhook secret with:
openssl rand -hex 32
Uncheck Active under “User authorization (OAuth)” — we don’t use OAuth.
2. Permissions
Set ONLY the permissions the tool actually uses. The set you need depends on whether you’ll run repo-scoped projects (one project per repo binding), org-scoped projects (one project per GitHub org), or both.
Repository permissions (always required)
| Permission | Access | Why |
|---|---|---|
| Actions | Read-only | read the workflow_job event payload |
| Administration | Read and write | required by POST /repos/.../actions/runners/generate-jitconfig (repo-scoped JIT config) |
| Metadata | Read-only | mandatory default |
Organization permissions (only for org-scoped projects)
| Permission | Access | Why |
|---|---|---|
| Self-hosted runners | Read and write | required by POST /orgs/<org>/actions/runners/generate-jitconfig (org-scoped JIT config) |
Leave every other permission at No access.
Without
administration:writeyou’ll seejit config: 403 Forbiddenfor repo-scoped projects. Withoutorganization_self_hosted_runners:writeyou’ll see the same for org-scoped ones. Both keep working independently — pick the permission that matches the scope you actually use.
3. Subscribe to events
Under “Subscribe to events”, check only:
- ✅ Workflow job
Nothing else. The webhook handler ignores other events (logged-and-dropped at 200) but receiving them wastes deliveries on both sides.
4. Create the App + private key
Click Create GitHub App. On the resulting settings page:
- Note the App ID at the top (a 6–7 digit number) → goes into
github.app_id. - Scroll to Private keys → Generate a private key → save the downloaded
.pemfile somewhere on the server ( e.g./etc/pacer/gh-app.pem, mode0400, owned by the user that runs the tool) → path goes intogithub.private_key_path. - The webhook secret you set in step 1 → goes into
github.webhook_secret.
5. Install the App
In the App settings sidebar → Install App → pick the account.
- Repo-scoped projects only: choose Selected repositories and pick the repos you want runners for. Safer than
All repositories — the tool only sees
workflow_jobevents from repos that explicitly trust it. - Org-scoped projects: install at the organization level (when “Where can this App be installed?” is set to Any
account or the App lives in the org’s account). The org-level install is what makes
/orgs/<org>/actions/runners/generate-jitconfigwork; without it the org JIT call returns 404. All repositories + org install is the typical org-scoped posture.
No per-repo webhook configuration. GitHub Apps centralize webhooks: the App has one webhook URL (the one you set in step 1), and events from every installed repo flow through that single URL. Do not add a webhook under each repo’s Settings → Webhooks; those are for non-App integrations and would result in duplicate / unsigned deliveries.
GitHub assigns each install an installation_id; the tool reads it from each workflow_job webhook payload, so you
don’t need to record it manually.
6. Verify deliveries
Once the server is running and reachable at server.public_url:
- GitHub App settings → Advanced → Recent Deliveries → trigger a
ping(Redeliver). The tool returns 200. - Trigger a real workflow run on a bound repo with
runs-on: [self-hosted, <project>]. The tool’s Jobs page should show it transitionqueued → claimed → starting → running → completed.
Common gotchas
- HMAC verification failures in logs — the webhook secret on GitHub doesn’t match
github.webhook_secretin YAML. Re-paste both. - Job stuck at
queuedforever — repo not bound, or no pool’s labels match the workflow’sruns-on. Check Recent Deliveries on the App and look foraudit.job.no_pool_matchin the tool’s logs. - Workflow runs but never appears in the Jobs page — the workflow’s
runs-ondoesn’t includeself-hosted(e.g.runs-on: ubuntu-latest). Github-hosted workflows are silently ignored at the webhook gate; they don’t enter pacer’s queue and don’t write an audit row. Addself-hostedplus your project / pool labels toruns-onto route the job through pacer. config invalid: github.private_key_path required— the tool can’t find the PEM at the configured path; check perms (the running user must have read access).jit config: 403 Forbidden— the App is missingadministration:write. Re-edit the App’s permissions; existing installs auto-pick up the new permission set on the next webhook (no reinstall needed).