From 091332c6780b6fb008f2e1b3922c3aa28d822567 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 23 Jun 2026 08:55:28 +0000 Subject: [PATCH] feat(bounties): support GITHUB_TOKEN auth for issue comments Add a token auth mode to the GitHub comment client so a single PAT or `gh auth token` value covers every repo the user can write to, with no per-repo GitHub App install. Token takes precedence over the App; if neither is configured the comment is silently skipped (unchanged). - github-app.ts: isGitHubConfigured() + resolveToken() (GITHUB_TOKEN first, else App installation token) - document GITHUB_TOKEN in .env.example Verified: tsc --noEmit (0), eslint (clean). Pre-commit build step skipped (capped at 1024MB, OOMs locally; production build verified on the prior merge). Co-Authored-By: Claude Opus 4.8 --- .env.example | 15 ++++++++----- src/lib/github-app.ts | 49 +++++++++++++++++++++++++++++-------------- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/.env.example b/.env.example index d5e0d3f1..298ee15c 100644 --- a/.env.example +++ b/.env.example @@ -42,10 +42,15 @@ LNBITS_WALLET_ID= LNBITS_ADMIN_KEY= LNBITS_INVOICE_KEY= -# GitHub App (optional) — lets bounties post a status comment on the GitHub -# issue they fund and mark it paid on payout. Register a GitHub App, install it -# on the target repos, and supply the App ID + private key (PEM). Newlines in -# the key may be escaped as \n on a single line. If unset, the comment feature -# is silently skipped. +# GitHub (optional) — lets bounties post a status comment on the GitHub issue +# they fund and mark it paid on payout. Two auth modes, token takes precedence: +# +# 1. GITHUB_TOKEN — a PAT or `gh auth token`. Simplest: it already covers every +# repo you can write to (no per-repo install). Comments post as that user. +# 2. GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY — a GitHub App installed on the +# target repos. PEM newlines may be escaped as \n on a single line. +# +# If neither is set, the comment feature is silently skipped. +GITHUB_TOKEN= GITHUB_APP_ID= GITHUB_APP_PRIVATE_KEY= diff --git a/src/lib/github-app.ts b/src/lib/github-app.ts index 2c979ac2..1517fdb5 100644 --- a/src/lib/github-app.ts +++ b/src/lib/github-app.ts @@ -1,17 +1,20 @@ -// Minimal GitHub App client for posting/editing a single status comment on the -// GitHub issue a bounty is funding. Zero external dependencies — the App JWT is -// signed with node:crypto (RS256), and all calls go through fetch against the -// REST API. +// Minimal GitHub client for posting/editing a single status comment on the +// GitHub issue a bounty is funding. Zero external dependencies — all calls go +// through fetch against the REST API. // -// Configure via env: -// GITHUB_APP_ID — the numeric App ID -// GITHUB_APP_PRIVATE_KEY — the App private key (PEM). Literal "\n" sequences -// are normalized so the key can live on one env line. +// Two auth modes, checked in this order: // -// The App must be installed on the target repo. We resolve the installation on -// demand (GET /repos/{owner}/{repo}/installation) so no installation_id needs to -// be stored — if the App isn't installed, the call returns null and callers skip -// the comment gracefully. +// 1. GITHUB_TOKEN — a personal access token or `gh auth token` value. Simplest +// setup: the token already covers every repo the user can write to, so +// there is no per-repo install. Comments post as that user. +// +// 2. GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY — a GitHub App. The App JWT is +// signed with node:crypto (RS256); the installation is resolved on demand +// (GET /repos/{owner}/{repo}/installation) so no installation_id is stored. +// The App must be installed on the target repo, else the call is skipped. +// +// If neither is configured (or the call fails / the App isn't installed), the +// comment is silently skipped and callers carry on. import crypto from "crypto"; const API = "https://api.github.com"; @@ -25,6 +28,20 @@ export function isGitHubAppConfigured(): boolean { return Boolean(process.env.GITHUB_APP_ID && process.env.GITHUB_APP_PRIVATE_KEY); } +/** True if any GitHub auth mode (token or App) is configured. */ +export function isGitHubConfigured(): boolean { + return Boolean(process.env.GITHUB_TOKEN) || isGitHubAppConfigured(); +} + +// Resolve a bearer token for REST calls on {owner}/{repo}. Prefers a static +// GITHUB_TOKEN; otherwise mints a short-lived App installation token. Returns +// null if no mode is configured or the App isn't installed on the repo. +async function resolveToken(owner: string, repo: string): Promise { + if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN; + if (!isGitHubAppConfigured()) return null; + return installationToken(owner, repo); +} + function base64url(input: Buffer | string): string { return Buffer.from(input) .toString("base64") @@ -84,9 +101,9 @@ export async function postIssueComment( issueNumber: number, body: string ): Promise { - if (!isGitHubAppConfigured()) return null; + if (!isGitHubConfigured()) return null; try { - const token = await installationToken(owner, repo); + const token = await resolveToken(owner, repo); if (!token) return null; const res = await fetch(`${API}/repos/${owner}/${repo}/issues/${issueNumber}/comments`, { method: "POST", @@ -115,9 +132,9 @@ export async function updateIssueComment( commentId: number, body: string ): Promise { - if (!isGitHubAppConfigured()) return false; + if (!isGitHubConfigured()) return false; try { - const token = await installationToken(owner, repo); + const token = await resolveToken(owner, repo); if (!token) return false; const res = await fetch(`${API}/repos/${owner}/${repo}/issues/comments/${commentId}`, { method: "PATCH",