Skip to content

GitHub App integration #10

@rocketstack-matt

Description

@rocketstack-matt

Implementation Plan

Context

Roadmapper currently uses a single shared GITHUB_TOKEN (PAT) for all GitHub API calls — fetching issues, verifying .roadmapper files, and checking repo existence during registration. This shares a single 5,000 req/hr rate limit pool across all users. As adoption grows, this becomes a bottleneck.

GitHub App integration gives each installation its own 5,000 req/hr rate limit pool, scaling linearly with users. It also provides a more professional onboarding experience — users install the app on GitHub rather than manually creating and committing .roadmapper files.

Goal: Add GitHub App as an alternative auth/verification path alongside the existing .roadmapper key approach. Both paths coexist cleanly, and the old approach can be removed later without touching the GitHub App code.

Constraints:

  • Public repos only for now (permissions: issues:read, metadata:read)
  • Auth + rate limits only — no webhook-driven cache invalidation
  • Graceful fallback: if GitHub App env vars aren't set, everything works exactly as today
  • Clean separation: GitHub App logic in its own modules

Environment Variables

Variable Description
GITHUB_APP_ID App ID from GitHub App settings
GITHUB_APP_PRIVATE_KEY PEM private key (base64-encoded for Vercel)
GITHUB_APP_WEBHOOK_SECRET Webhook secret for signature verification

New Dependencies

  • jsonwebtoken — sign JWTs for GitHub App authentication

New Redis Keys

gh-app:installation:{owner}/{repo}  → String (installation ID)
gh-app:token:{installationId}       → String (installation access token, ~55min TTL)

New Files

1. lib/github-app.js — GitHub App authentication

Core GitHub App logic, isolated from the rest of the system:

  • isGitHubAppConfigured() — returns true if GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY are set
  • generateAppJwt() — signs a JWT with the private key (10-minute expiry per GitHub spec)
  • getInstallationToken(installationId) — checks Redis for cached token, if missing/expired exchanges JWT for installation access token via POST /app/installations/{id}/access_tokens, caches with ~55min TTL
  • getInstallationId(owner, repo) — checks Redis for cached installation ID, if missing fetches via GET /repos/{owner}/{repo}/installation, caches result
  • getTokenForRepo(owner, repo) — combines the above: get installation ID → get installation token. Returns null if app not installed on this repo

All functions return null on failure (not installed, not configured, etc.) — callers fall through to the next auth method.

2. lib/github-token.js — Token resolution abstraction

Single function that replaces the 4 scattered GITHUB_TOKEN checks:

const resolveGitHubToken = async (owner, repo) => {
  // 1. Try GitHub App installation token (per-repo, own rate limit pool)
  if (isGitHubAppConfigured()) {
    const token = await getTokenForRepo(owner, repo);
    if (token) return { token, source: 'app' };
  }
  // 2. Fall back to shared PAT
  if (process.env.GITHUB_TOKEN) {
    return { token: process.env.GITHUB_TOKEN, source: 'pat' };
  }
  // 3. Unauthenticated (60 req/hr)
  return { token: null, source: 'none' };
};

3. api/github/webhook.js — Installation lifecycle webhook

Minimal webhook endpoint for installation and installation_repositories events:

  • Verifies webhook signature using GITHUB_APP_WEBHOOK_SECRET
  • On installation.created / installation_repositories.added: stores gh-app:installation:{owner}/{repo} for each granted repo
  • On installation.deleted / installation_repositories.removed: deletes the Redis keys
  • Returns 200 OK for all other events

4. api/github/setup.js — Post-installation redirect

After a user installs the app on GitHub, GitHub redirects to this URL:

  • Receives installation_id and setup_action query params from GitHub
  • Redirects the user back to the Roadmapper landing page with a success message

Modified Files

1. roadmap.js — Use token resolution in fetchIssues

Add optional githubToken parameter to fetchIssues(owner, repo, cacheTtlSeconds, githubToken). Replace the 2 inline GITHUB_TOKEN checks with if (githubToken) headers['Authorization'] = .... The caller (middleware) resolves the token and passes it in.

2. lib/verify.js — Dual-path verification

Add verifyRepoViaApp(owner, repo) that checks for a GitHub App installation. Modify verifyRepo() to try GitHub App first, then fall back to .roadmapper. Also update fetchRoadmapperFile to use resolveGitHubToken instead of inline GITHUB_TOKEN check.

3. lib/middleware.js — Token resolution in middleware

After verification succeeds, resolve the GitHub token via resolveGitHubToken(owner, repo) and attach to req.githubToken.

4. api/roadmap.js — Pass resolved token to fetchIssues

Change fetchIssues(owner, repo, req.cacheTtl) to fetchIssues(owner, repo, req.cacheTtl, req.githubToken).

5. api/register.js — Use token resolution for repo existence check

Replace inline GITHUB_TOKEN check with resolveGitHubToken.

6. api/index.js — Landing page updates

Add an "Install GitHub App" button alongside the existing registration form, giving users two onboarding paths.

7. server.js + vercel.json — Add webhook and setup routes

Add POST /api/github/webhook and GET /api/github/setup routes in both files.


Test Strategy

New test files:

  • tests/lib/github-app.test.js — JWT generation, installation token caching, token refresh, graceful failures
  • tests/lib/github-token.test.js — Token resolution priority: app token > PAT > null
  • tests/api-github-webhook.test.js — Webhook signature verification, installation events, Redis keys
  • tests/api-github-setup.test.js — Redirect behavior

Updated test files:

  • tests/lib/verify.test.js — Dual-path verification
  • tests/lib/middleware.test.jsreq.githubToken attachment
  • tests/roadmap.test.jsfetchIssues with githubToken parameter

GitHub App Registration Checklist (manual, one-time)

  • Create GitHub App at github.com/settings/apps/new
  • Permissions: Issues: Read, Metadata: Read
  • Subscribe to events: Installation, Installation repositories
  • Webhook URL: https://roadmapper.rocketstack.co/api/github/webhook
  • Setup URL: https://roadmapper.rocketstack.co/api/github/setup
  • Generate and download private key
  • Add env vars to Vercel: GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY (base64), GITHUB_APP_WEBHOOK_SECRET

Metadata

Metadata

Assignees

No one assigned

    Labels

    Roadmap: NowTop priority - currently working on this

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions