Skip to content

Commit bdeed64

Browse files
ralyodioclaude
andauthored
feat(bounties): support GITHUB_TOKEN auth for issue comments (#489)
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 <noreply@anthropic.com>
1 parent 0c782ec commit bdeed64

2 files changed

Lines changed: 43 additions & 21 deletions

File tree

.env.example

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,15 @@ LNBITS_WALLET_ID=
4242
LNBITS_ADMIN_KEY=
4343
LNBITS_INVOICE_KEY=
4444

45-
# GitHub App (optional) — lets bounties post a status comment on the GitHub
46-
# issue they fund and mark it paid on payout. Register a GitHub App, install it
47-
# on the target repos, and supply the App ID + private key (PEM). Newlines in
48-
# the key may be escaped as \n on a single line. If unset, the comment feature
49-
# is silently skipped.
45+
# GitHub (optional) — lets bounties post a status comment on the GitHub issue
46+
# they fund and mark it paid on payout. Two auth modes, token takes precedence:
47+
#
48+
# 1. GITHUB_TOKEN — a PAT or `gh auth token`. Simplest: it already covers every
49+
# repo you can write to (no per-repo install). Comments post as that user.
50+
# 2. GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY — a GitHub App installed on the
51+
# target repos. PEM newlines may be escaped as \n on a single line.
52+
#
53+
# If neither is set, the comment feature is silently skipped.
54+
GITHUB_TOKEN=
5055
GITHUB_APP_ID=
5156
GITHUB_APP_PRIVATE_KEY=

src/lib/github-app.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
// Minimal GitHub App client for posting/editing a single status comment on the
2-
// GitHub issue a bounty is funding. Zero external dependencies — the App JWT is
3-
// signed with node:crypto (RS256), and all calls go through fetch against the
4-
// REST API.
1+
// Minimal GitHub client for posting/editing a single status comment on the
2+
// GitHub issue a bounty is funding. Zero external dependencies — all calls go
3+
// through fetch against the REST API.
54
//
6-
// Configure via env:
7-
// GITHUB_APP_ID — the numeric App ID
8-
// GITHUB_APP_PRIVATE_KEY — the App private key (PEM). Literal "\n" sequences
9-
// are normalized so the key can live on one env line.
5+
// Two auth modes, checked in this order:
106
//
11-
// The App must be installed on the target repo. We resolve the installation on
12-
// demand (GET /repos/{owner}/{repo}/installation) so no installation_id needs to
13-
// be stored — if the App isn't installed, the call returns null and callers skip
14-
// the comment gracefully.
7+
// 1. GITHUB_TOKEN — a personal access token or `gh auth token` value. Simplest
8+
// setup: the token already covers every repo the user can write to, so
9+
// there is no per-repo install. Comments post as that user.
10+
//
11+
// 2. GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY — a GitHub App. The App JWT is
12+
// signed with node:crypto (RS256); the installation is resolved on demand
13+
// (GET /repos/{owner}/{repo}/installation) so no installation_id is stored.
14+
// The App must be installed on the target repo, else the call is skipped.
15+
//
16+
// If neither is configured (or the call fails / the App isn't installed), the
17+
// comment is silently skipped and callers carry on.
1518
import crypto from "crypto";
1619

1720
const API = "https://api.github.com";
@@ -25,6 +28,20 @@ export function isGitHubAppConfigured(): boolean {
2528
return Boolean(process.env.GITHUB_APP_ID && process.env.GITHUB_APP_PRIVATE_KEY);
2629
}
2730

31+
/** True if any GitHub auth mode (token or App) is configured. */
32+
export function isGitHubConfigured(): boolean {
33+
return Boolean(process.env.GITHUB_TOKEN) || isGitHubAppConfigured();
34+
}
35+
36+
// Resolve a bearer token for REST calls on {owner}/{repo}. Prefers a static
37+
// GITHUB_TOKEN; otherwise mints a short-lived App installation token. Returns
38+
// null if no mode is configured or the App isn't installed on the repo.
39+
async function resolveToken(owner: string, repo: string): Promise<string | null> {
40+
if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
41+
if (!isGitHubAppConfigured()) return null;
42+
return installationToken(owner, repo);
43+
}
44+
2845
function base64url(input: Buffer | string): string {
2946
return Buffer.from(input)
3047
.toString("base64")
@@ -84,9 +101,9 @@ export async function postIssueComment(
84101
issueNumber: number,
85102
body: string
86103
): Promise<number | null> {
87-
if (!isGitHubAppConfigured()) return null;
104+
if (!isGitHubConfigured()) return null;
88105
try {
89-
const token = await installationToken(owner, repo);
106+
const token = await resolveToken(owner, repo);
90107
if (!token) return null;
91108
const res = await fetch(`${API}/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
92109
method: "POST",
@@ -115,9 +132,9 @@ export async function updateIssueComment(
115132
commentId: number,
116133
body: string
117134
): Promise<boolean> {
118-
if (!isGitHubAppConfigured()) return false;
135+
if (!isGitHubConfigured()) return false;
119136
try {
120-
const token = await installationToken(owner, repo);
137+
const token = await resolveToken(owner, repo);
121138
if (!token) return false;
122139
const res = await fetch(`${API}/repos/${owner}/${repo}/issues/comments/${commentId}`, {
123140
method: "PATCH",

0 commit comments

Comments
 (0)