GitHub App setup
This is the operational runbook for creating the GitHub App that the Cloudflare Worker uses to authenticate against GitHub. App auth is live: the migration epic has landed and the Worker routes every GitHub call through the App when its credentials are configured.
Before App auth, the Worker read GITHUB_ADMIN_PAT, a user PAT tied to an
individual maintainer. Every Worker call to GitHub competed against that
same maintainer’s interactive gh CLI usage in one shared 5,000/hr core
bucket, so a large publication batch could starve the queue.
A GitHub App installation gets its own rate-limit pool per installed
organization, independent of any human user. With the App configured, the
Worker mints short-lived installation tokens (getInstallationToken in
backend/src/services/github-auth.ts, cached per installation) and routes
through them via getDatasetsAuth / getDefaultGitHubAuth. The user PAT
remains only as a fallback: getDefaultGitHubAuth falls back to
GITHUB_ADMIN_PAT when any App secret is missing, so dev environments
without App secrets keep working.
This runbook covers creating the App and verifying it works.
1. Create the App under nemarOrg
Section titled “1. Create the App under nemarOrg”Owner must be the organization, not a personal account, so the App survives any individual maintainer leaving.
- Open https://github.com/organizations/nemarOrg/settings/apps/new while signed in as an org owner.
- Fill in:
- GitHub App name:
nemar-publish-bot - Homepage URL:
https://github.com/nemarOrg/nemar-cli - Webhook: check Active.
- Webhook URL:
https://api.nemar.org/webhooks/github - Webhook secret: a random value (e.g.,
openssl rand -hex 32). Store this exact value as the Cloudflare Worker secretGITHUB_WEBHOOK_SECRET(wrangler secret put GITHUB_WEBHOOK_SECRET). The Worker uses it as the HMAC secret for verifyingX-Hub-Signature-256on push deliveries. - Under Subscribe to events (further down the form), tick
Push. This is the only event the Worker acts on today;
shouldDispatchEnrichment/shouldDispatchVersionDoiinbackend/src/routes/webhooks.tsgate on it. The App receiver was added in Phase 1 of centralization epic #601 — without Active webhook + Push event subscription, centralrun-version-doi.ymlandrun-enrichment.ymlwill never receive theirrepository_dispatchfrom tag and main pushes, and DOIs silently never mint.
- Webhook URL:
- Repository permissions:
- Contents: Read & write
- Actions: Read & write (Read for orchestrator CI checks;
Write so the central workflows on
nemarDatasets/.githubcanrepository_dispatchto each other — e.g.run-version-doi.ymldispatchinggenerate-archiveagainstnemarDatasets/.github. Phase 3 of centralization epic #601 moved these dispatches off the dataset repos onto.github.) - Administration: Read & write (needed for branch / tag protection rulesets and visibility flips)
- Issues: Read & write (BIDS-validation issue creation flow)
- Checks: Read & write (Phase 4 of #601: central
run-bids-validation.ymlon nemarDatasets/.github postscheck-runsto each dataset repo’s PR via the GitHub Checks API. Without this, the central workflow’sgh api ... /check-runscalls return 403 and the PR loses its validation check.) - Metadata: Read-only (always required)
- Pull requests: Read & write
- Workflows: Read & write (CI workflow deploy)
- Organization permissions: leave all at
No accessfor now. - Where can this GitHub App be installed?: Any account. The
App is owned by
nemarOrgbut needs to be installable onnemarDatasetstoo. “Only on this account” locks the App to its owner and blocks the second installation.
- GitHub App name:
- Click Create GitHub App.
- On the new App’s settings page, note the numeric App ID near the
top. Record it; it is set as the Worker secret
GITHUB_APP_ID, whichgetGitHubAppConfigreads to decide whether App auth is available.
2. Generate and download the private key
Section titled “2. Generate and download the private key”- Still on the App settings page, scroll to Private keys and click
Generate a private key. The browser downloads a
.pemfile. - Open the file in a text editor and confirm the header reads
-----BEGIN RSA PRIVATE KEY-----. Convert to PKCS#8 if needed (GitHub ships PKCS#1; both the verify script and the Worker’s JWT signersignAppJwt/pkcs8PemToDerimport keys only as PKCS#8, and the latter throws a clear error if handed a PKCS#1 or encrypted PEM):Terminal window openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt \-in nemar-publish-bot.<date>.private-key.pem \-out /tmp/nemar-app.pem - Store the original download AND the PKCS#8 copy in 1Password under a new item titled NEMAR / GitHub App along with the App ID. Share the 1Password item with at least one other maintainer so rotation isn’t single-pointed.
- Delete the local downloads:
The PKCS#8 copy in
Terminal window rm ~/Downloads/nemar-publish-bot.*.private-key.pem/tmpis fine to keep for the next step; remove it once verification passes.
3. Install on both organizations
Section titled “3. Install on both organizations”The App is owned by nemarOrg and needs two installations:
nemarDatasets— required. All dataset-repo writes happen here.nemarOrg— optional today; covers any future feature that writes to tooling repos. Install it too to stay symmetric.
Repository visibility in each install scope determines which repos the Worker can access.
- From the App settings page, click Install App in the left sidebar.
- Click Install next to nemarDatasets first (this is the
installation the Worker actively uses —
getDatasetsAuthresolves thenemarDatasetsinstallation ID viaGITHUB_APP_INSTALLATION_ID_NEMAR_DATASETS). - Choose All repositories and confirm. Note the installation ID
from the resulting URL:
https://github.com/organizations/nemarDatasets/settings/installations/<INSTALL_ID>. Record this asGITHUB_APP_INSTALLATION_ID_NEMAR_DATASETS. - Back on the install screen, click Install next to nemarOrg.
- Choose All repositories and confirm. Note the installation ID.
Record this as
GITHUB_APP_INSTALLATION_ID_NEMAR_ORG.
4. Verify
Section titled “4. Verify”From the repo root:
bun run scripts/verify-github-app.ts \ --app-id <APP_ID> \ --private-key /tmp/nemar-app.pemExpected output:
Listing installations... installation_id=12345678 account=nemarOrg target_type=Organization installation_id=12345679 account=nemarDatasets target_type=Organization
Minting installation tokens and listing repositories... installation_id=12345678 account=nemarOrg repos=4 first_repo=nemar-cli installation_id=12345679 account=nemarDatasets repos=156 first_repo=nm000103
OK: both expected installations validated.If the script reports a missing installation, revisit step 3 and confirm the App is installed on both orgs. If it reports a non-zero repo count mismatch, confirm the install scope is All repositories in both orgs.
After a clean verify run, remove the PKCS#8 PEM:
rm /tmp/nemar-app.pemThe canonical copy lives in 1Password.
App-creation acceptance checklist
Section titled “App-creation acceptance checklist”- App
nemar-publish-botexists undernemarOrg. - App permissions match the list in step 1 (no extras, no missing grants).
- App installed on
nemarOrgandnemarDatasets, both scoped to All repositories. - App ID, two installation IDs, and the private key (PKCS#8) stored in 1Password.
-
bun run scripts/verify-github-app.ts ...returns OK. - Local copy of the private key removed from disk.
Once these credentials are set as Worker secrets (GITHUB_APP_ID,
GITHUB_APP_PRIVATE_KEY, and the two GITHUB_APP_INSTALLATION_ID_*
values), getGitHubAppConfig detects them and the Worker mints
installation tokens instead of using the PAT.
Do NOT remove
GITHUB_ADMIN_PATfrom the Worker. The routing helpergetDefaultGitHubAuthfalls back to the PAT when any App secret is missing; if both are absent the helper throws “No GitHub auth configured” and every orchestrator call 500s. The PAT stays as a safety net (and keeps dev environments without App secrets working) until a dedicated cleanup removes it after a soak period confirms App auth is healthy ingithub-rllogs.
Dataset-repo CI uses the same App
Section titled “Dataset-repo CI uses the same App”Workflow templates in dataset repos mint short-lived installation tokens
via actions/create-github-app-token@v1 so all CI writes carry the
nemar-publish-bot[bot] identity. Two org-level secrets must be set on
nemarDatasets for the templates to work.
Ops steps
Section titled “Ops steps”-
Accept the updated App permissions.
This step raises Actions from Read-only to Read & write. If the App was created against the original list, GitHub holds the new permission request as “pending approval” until an org owner accepts it.
- Visit the App settings page:
https://github.com/organizations/nemarOrg/settings/apps/nemar-publish-bot/permissions. - Bump Actions to Read & write if not already set.
- Save. GitHub emails the installations.
- In each org’s Installed GitHub Apps page (one for nemarOrg, one for nemarDatasets), click the App and accept the new permissions.
- Visit the App settings page:
-
Set org-level secrets on
nemarDatasets.Visit
https://github.com/organizations/nemarDatasets/settings/secrets/actions. Add (Repository access: All repositories):NEMAR_APP_ID— the same numeric App ID stored in 1Password.NEMAR_APP_PRIVATE_KEY— the PKCS#8 PEM, pasted in full (BEGIN/END lines included).NEMAR_WEBHOOK_TOKEN— bearer token sent asX-Webhook-Tokenbyrun-version-doi.yml(calling/webhooks/publish-version-doi) andrun-enrichment.yml(calling/webhooks/llm-enrich). Set the same value as the Cloudflare Worker secretNEMAR_WEBHOOK_TOKEN(wrangler secret put NEMAR_WEBHOOK_TOKEN).
Webhook secrets — the two-secret model
Section titled “Webhook secrets — the two-secret model”The Worker uses two separate secrets, each with a distinct purpose. They MAY hold the same value but no longer must (since the secret-untangle landing on this PR); rotate independently going forward.
Secret Stored on Purpose GITHUB_WEBHOOK_SECRETWorker secret + App webhook config HMAC for /webhooks/github(App push deliveries). Rotate by setting on Worker AND on the App’s webhook secret field.NEMAR_WEBHOOK_TOKENWorker secret + nemarDatasetsorg Actions secretBearer token for /webhooks/publish-version-doiand/webhooks/llm-enrich. Rotate by setting on the Worker AND on the org secret.Historical note: before the secret-untangle, both endpoints read
GITHUB_WEBHOOK_SECRET, so the org secret and Worker secret had to match the App webhook secret. Rotating one without the others 401’d every DOI mint until the value was re-synced. The endpoints now preferNEMAR_WEBHOOK_TOKENand fall back toGITHUB_WEBHOOK_SECRETonly if the new var is unset — set the new Worker secret to drop the coupling. -
Refresh existing dataset repos so they pick up the new workflow templates with the App-token step:
Terminal window nemar admin ci add <dataset-id>Or sweep the catalog if many at once (the
/tmp/refresh-archive-workflow.shpattern from the May 2026 sweep works as a reference).
Acceptance
Section titled “Acceptance”- App permissions show Actions: Read & write and the update is accepted on both org installations.
- App permissions show Checks: Read & write (added in Phase 4 of
epic nemarOrg/nemar-cli#601 for the central
run-bids-validationworkflow’s check-run posts). The grant must be re-accepted on thenemarOrgandnemarDatasetsinstallations after the App definition changes. -
NEMAR_APP_ID,NEMAR_APP_PRIVATE_KEY, andNEMAR_WEBHOOK_TOKENexist as org secrets onnemarDatasets. - At least one dataset repo’s most recent
pr-merge,llm-enrichment, orversion-doiworkflow run shows the “Mint App installation token” step succeeding, and subsequent writes are attributed tonemar-publish-bot[bot]in the run log. - App webhook is Active, URL is
https://api.nemar.org/webhooks/github, webhook secret matches WorkerGITHUB_WEBHOOK_SECRET, and Push is checked under Subscribe to events. - After pushing a version tag on any dataset repo, the central
nemarDatasets/.githubshows arun-version-doirun withevent=repository_dispatch(not just manualworkflow_dispatch). -
NEMAR_WEBHOOK_TOKENexists as a Worker secret AND as thenemarDatasetsorg Actions secret with the same value.
Cross-references
Section titled “Cross-references”- Existing PAT troubleshooting: Publishing workflow
- Tracking epic: #432