Problem
The processor service currently authenticates to GitHub using a personal access token (GITHUB_TOKEN). This has several drawbacks for a self-hosted product:
- PATs are tied to individual user accounts, not the organization.
- They require manual creation and rotation.
- Operators must navigate GitHub's token UI and copy-paste credentials into their deployment.
Proposal
Implement a one-time setup wizard on the processor service that uses GitHub's App Manifest Flow to automate GitHub App creation and credential storage. After running the wizard once, the service authenticates as a GitHub App installation — no manual credential handling required.
Setup flow (operator perspective)
- Deploy with
THREADOPS_SETUP_ENABLED=true and a random THREADOPS_SETUP_TOKEN.
- Visit
/setup?org=<github-org>&token=<setup-token> in a browser.
- Click "Create GitHub App" — redirected to GitHub to confirm.
- Redirected back to the service, which exchanges the temporary code for App credentials and stores them in Secret Manager.
- Redirected to GitHub to install the App on chosen repositories.
- Redirected back to a "Setup complete" page.
- Redeploy with
THREADOPS_SETUP_ENABLED=false. Done.
Architecture
Browser → GET /setup → serves HTML form pointing at GitHub
→ GitHub manifest creation → operator names app, clicks create
→ GET /github/callback?code= → exchanges code for credentials,
writes to Secret Manager,
redirects to install page
→ GitHub install page → operator selects repos
→ GET /github/installed?installation_id= → writes installation ID,
renders success page
What to build
1. Terraform: Secret Manager secrets
Create terraform/secrets.tf. Provision three secrets (no initial version — the setup handler writes the first version at runtime):
threadops-github-app-id
threadops-github-private-key
threadops-github-installation-id
Grant the processor service account secretAccessor (runtime reads) and secretVersionAdder (setup writes) on all three.
2. Terraform: Cloud Run changes
Modify the processor Cloud Run config to:
- Mount
threadops-github-private-key as a volume at /secrets/github/private-key.pem.
- Expose
GITHUB_APP_ID and GITHUB_INSTALLATION_ID as env vars resolved from Secret Manager.
- Add env vars:
THREADOPS_BASE_URL, THREADOPS_SETUP_ENABLED (default false), THREADOPS_SETUP_TOKEN.
3. Secret Manager client wrapper
Create internal/secretsmanager/client.go — a thin wrapper exposing only the operations the setup handler needs:
SetAppID(ctx, int64), SetPrivateKey(ctx, []byte), SetInstallationID(ctx, int64) — used during setup.
AppID(ctx), PrivateKey(ctx), InstallationID(ctx) — used at startup (fallback if volume mounts / env vars aren't available yet).
4. Setup HTTP handlers
Create internal/setup/handler.go with three handlers:
GET /setup — accepts ?org= and ?token=. Validates the setup token. Renders an HTML form that POSTs a manifest to GitHub's app-creation endpoint. The manifest requests issues: write permission, disables webhooks, and sets setup_url to /github/installed.
GET /github/callback — extracts code param, calls POST api.github.com/app-manifests/<code>/conversions, stores the returned id and pem in Secret Manager, redirects to the App's install page.
GET /github/installed — extracts installation_id param, stores it in Secret Manager, renders a success page.
5. Route registration in processor
Modify services/processor/main.go:
- When
THREADOPS_SETUP_ENABLED=true, create the setup handler and register the three routes.
- The setup handler needs a
secretsmanager.Client instance, the base URL, and the setup token from env vars.
6. GitHub App client initialization
Create internal/github/client.go. At startup, read credentials from the mounted secret / env vars and construct an authenticated ghinstallation-based transport. Return a *github.Client (from google/go-github).
If credentials aren't available yet (setup hasn't run), the service should start anyway but return 503 on issue-filing requests with a message pointing to /setup.
7. Replace PAT-based GitHub client in processor
Update services/processor/main.go and services/processor/github.go:
- Replace the current
githubHTTPClient (which uses a plain GITHUB_TOKEN) with the new App-based client from internal/github/.
- The
GitHubClient interface in handler.go stays the same — only the concrete implementation changes.
- Remove
GITHUB_TOKEN from required env vars.
8. New Go dependencies
github.com/bradleyfalzon/ghinstallation/v2
github.com/google/go-github/v72
cloud.google.com/go/secretmanager
Security
- Setup routes are gated behind
THREADOPS_SETUP_ENABLED and a THREADOPS_SETUP_TOKEN query parameter.
- The callback handler doesn't need token validation — GitHub's temporary code is single-use and only useful server-side.
- After setup, operators disable the routes by unsetting the env var.
Out of scope
- Webhook receiving from GitHub (ThreadOps only pushes to GitHub).
- Multiple GitHub App installations / multi-tenancy.
- Automatic Cloud Run revision restart after setup (documented as a manual step in the operator runbook).
Problem
The processor service currently authenticates to GitHub using a personal access token (
GITHUB_TOKEN). This has several drawbacks for a self-hosted product:Proposal
Implement a one-time setup wizard on the processor service that uses GitHub's App Manifest Flow to automate GitHub App creation and credential storage. After running the wizard once, the service authenticates as a GitHub App installation — no manual credential handling required.
Setup flow (operator perspective)
THREADOPS_SETUP_ENABLED=trueand a randomTHREADOPS_SETUP_TOKEN./setup?org=<github-org>&token=<setup-token>in a browser.THREADOPS_SETUP_ENABLED=false. Done.Architecture
What to build
1. Terraform: Secret Manager secrets
Create
terraform/secrets.tf. Provision three secrets (no initial version — the setup handler writes the first version at runtime):threadops-github-app-idthreadops-github-private-keythreadops-github-installation-idGrant the processor service account
secretAccessor(runtime reads) andsecretVersionAdder(setup writes) on all three.2. Terraform: Cloud Run changes
Modify the processor Cloud Run config to:
threadops-github-private-keyas a volume at/secrets/github/private-key.pem.GITHUB_APP_IDandGITHUB_INSTALLATION_IDas env vars resolved from Secret Manager.THREADOPS_BASE_URL,THREADOPS_SETUP_ENABLED(defaultfalse),THREADOPS_SETUP_TOKEN.3. Secret Manager client wrapper
Create
internal/secretsmanager/client.go— a thin wrapper exposing only the operations the setup handler needs:SetAppID(ctx, int64),SetPrivateKey(ctx, []byte),SetInstallationID(ctx, int64)— used during setup.AppID(ctx),PrivateKey(ctx),InstallationID(ctx)— used at startup (fallback if volume mounts / env vars aren't available yet).4. Setup HTTP handlers
Create
internal/setup/handler.gowith three handlers:GET /setup— accepts?org=and?token=. Validates the setup token. Renders an HTML form that POSTs a manifest to GitHub's app-creation endpoint. The manifest requestsissues: writepermission, disables webhooks, and setssetup_urlto/github/installed.GET /github/callback— extractscodeparam, callsPOST api.github.com/app-manifests/<code>/conversions, stores the returnedidandpemin Secret Manager, redirects to the App's install page.GET /github/installed— extractsinstallation_idparam, stores it in Secret Manager, renders a success page.5. Route registration in processor
Modify
services/processor/main.go:THREADOPS_SETUP_ENABLED=true, create the setup handler and register the three routes.secretsmanager.Clientinstance, the base URL, and the setup token from env vars.6. GitHub App client initialization
Create
internal/github/client.go. At startup, read credentials from the mounted secret / env vars and construct an authenticatedghinstallation-based transport. Return a*github.Client(fromgoogle/go-github).If credentials aren't available yet (setup hasn't run), the service should start anyway but return
503on issue-filing requests with a message pointing to/setup.7. Replace PAT-based GitHub client in processor
Update
services/processor/main.goandservices/processor/github.go:githubHTTPClient(which uses a plainGITHUB_TOKEN) with the new App-based client frominternal/github/.GitHubClientinterface inhandler.gostays the same — only the concrete implementation changes.GITHUB_TOKENfrom required env vars.8. New Go dependencies
Security
THREADOPS_SETUP_ENABLEDand aTHREADOPS_SETUP_TOKENquery parameter.Out of scope