Skip to content

GitHub App setup via manifest flow #10

@jonaskay

Description

@jonaskay

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)

  1. Deploy with THREADOPS_SETUP_ENABLED=true and a random THREADOPS_SETUP_TOKEN.
  2. Visit /setup?org=<github-org>&token=<setup-token> in a browser.
  3. Click "Create GitHub App" — redirected to GitHub to confirm.
  4. Redirected back to the service, which exchanges the temporary code for App credentials and stores them in Secret Manager.
  5. Redirected to GitHub to install the App on chosen repositories.
  6. Redirected back to a "Setup complete" page.
  7. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions