fix test token scoping: override GITHUB_TOKEN via OIDC in ensureGitHubToken

the runner's GITHUB_TOKEN (scoped to pullfrog/app) was leaking into
test subprocesses targeting pullfrog/test-repo, causing 400s from the
Pullfrog API on run-context fetches.

instead of deleting GITHUB_TOKEN from the subprocess env,
ensureGitHubToken now always mints a fresh OIDC token scoped to
GITHUB_REPOSITORY when OIDC is available — replacing any inherited
token with a correctly-scoped one.

also adds an informative throw in acquireTokenViaGitHubApp when
GITHUB_APP_ID/GITHUB_PRIVATE_KEY are missing.

Made-with: Cursor
This commit is contained in:
Colin McDonnell
2026-03-12 06:15:01 +00:00
committed by pullfrog[bot]
parent 4a8c432a48
commit 250fe7eaa1
6 changed files with 43 additions and 20 deletions
+7 -2
View File
@@ -143506,10 +143506,15 @@ var findInstallationId = async (jwt2, repoOwner, repoName) => {
); );
}; };
async function acquireTokenViaGitHubApp(opts) { async function acquireTokenViaGitHubApp(opts) {
if (!process.env.GITHUB_APP_ID || !process.env.GITHUB_PRIVATE_KEY) {
throw new Error(
"cannot acquire token via GitHub App: GITHUB_APP_ID and GITHUB_PRIVATE_KEY must be set"
);
}
const repoContext = parseRepoContext(); const repoContext = parseRepoContext();
const config3 = { const config3 = {
appId: process.env.GITHUB_APP_ID, appId: process.env.GITHUB_APP_ID,
privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n"), privateKey: process.env.GITHUB_PRIVATE_KEY.replace(/\\n/g, "\n"),
repoOwner: repoContext.owner, repoOwner: repoContext.owner,
repoName: repoContext.name repoName: repoContext.name
}; };
@@ -149732,7 +149737,7 @@ import { isAbsolute, resolve } from "node:path";
// package.json // package.json
var package_default = { var package_default = {
name: "@pullfrog/pullfrog", name: "@pullfrog/pullfrog",
version: "0.0.179", version: "0.0.180",
type: "module", type: "module",
files: [ files: [
"index.js", "index.js",
+6 -1
View File
@@ -25898,10 +25898,15 @@ var findInstallationId = async (jwt, repoOwner, repoName) => {
); );
}; };
async function acquireTokenViaGitHubApp(opts) { async function acquireTokenViaGitHubApp(opts) {
if (!process.env.GITHUB_APP_ID || !process.env.GITHUB_PRIVATE_KEY) {
throw new Error(
"cannot acquire token via GitHub App: GITHUB_APP_ID and GITHUB_PRIVATE_KEY must be set"
);
}
const repoContext = parseRepoContext(); const repoContext = parseRepoContext();
const config = { const config = {
appId: process.env.GITHUB_APP_ID, appId: process.env.GITHUB_APP_ID,
privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n"), privateKey: process.env.GITHUB_PRIVATE_KEY.replace(/\\n/g, "\n"),
repoOwner: repoContext.owner, repoOwner: repoContext.owner,
repoName: repoContext.name repoName: repoContext.name
}; };
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@pullfrog/pullfrog", "name": "@pullfrog/pullfrog",
"version": "0.0.179", "version": "0.0.180",
"type": "module", "type": "module",
"files": [ "files": [
"index.js", "index.js",
+1 -1
View File
@@ -41284,7 +41284,7 @@ var core3 = __toESM(require_core(), 1);
// package.json // package.json
var package_default = { var package_default = {
name: "@pullfrog/pullfrog", name: "@pullfrog/pullfrog",
version: "0.0.179", version: "0.0.180",
type: "module", type: "module",
files: [ files: [
"index.js", "index.js",
-4
View File
@@ -216,10 +216,6 @@ export async function runAgentStreaming(options: RunStreamingOptions): Promise<A
GITHUB_OUTPUT: githubOutputFile, GITHUB_OUTPUT: githubOutputFile,
}; };
// clear CI runner's GITHUB_TOKEN so ensureGitHubToken() mints a
// properly scoped token for the target GITHUB_REPOSITORY via OIDC
delete subEnv.GITHUB_TOKEN;
const child = spawn("node", ["play.ts", "--raw", JSON.stringify(fixture)], { const child = spawn("node", ["play.ts", "--raw", JSON.stringify(fixture)], {
cwd: actionDir, cwd: actionDir,
env: subEnv as Record<string, string>, env: subEnv as Record<string, string>,
+28 -11
View File
@@ -277,11 +277,17 @@ const findInstallationId = async (
// for local development only // for local development only
async function acquireTokenViaGitHubApp(opts?: AcquireTokenOptions): Promise<string> { async function acquireTokenViaGitHubApp(opts?: AcquireTokenOptions): Promise<string> {
if (!process.env.GITHUB_APP_ID || !process.env.GITHUB_PRIVATE_KEY) {
throw new Error(
"cannot acquire token via GitHub App: GITHUB_APP_ID and GITHUB_PRIVATE_KEY must be set"
);
}
const repoContext = parseRepoContext(); const repoContext = parseRepoContext();
const config: GitHubAppConfig = { const config: GitHubAppConfig = {
appId: process.env.GITHUB_APP_ID!, appId: process.env.GITHUB_APP_ID,
privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n")!, privateKey: process.env.GITHUB_PRIVATE_KEY.replace(/\\n/g, "\n"),
repoOwner: repoContext.owner, repoOwner: repoContext.owner,
repoName: repoContext.name, repoName: repoContext.name,
}; };
@@ -292,20 +298,31 @@ async function acquireTokenViaGitHubApp(opts?: AcquireTokenOptions): Promise<str
} }
/** /**
* Ensure a GitHub token is available in the environment. * ensure a GitHub token is available in the environment.
* *
* If neither `GITHUB_TOKEN` nor `GH_TOKEN` is set, attempts to acquire an * when OIDC is available (CI), always mints a fresh token scoped to
* installation token using `GITHUB_APP_ID` / `GITHUB_PRIVATE_KEY`. * GITHUB_REPOSITORY — overriding any inherited GITHUB_TOKEN that may
* be scoped to the wrong repo.
* *
* **Not intended for production use** — this is a convenience for local * otherwise falls back to GitHub App credentials for local development.
* development and test harnesses where tokens aren't pre-provisioned. *
* only called from play.ts (test/dev path) — the live action calls
* main() directly and never calls this.
*/ */
export async function ensureGitHubToken(): Promise<void> { export async function ensureGitHubToken(): Promise<void> {
// when OIDC is available, always mint a fresh token scoped to
// GITHUB_REPOSITORY. the inherited GITHUB_TOKEN may be scoped to a
// different repo (e.g., runner token for pullfrog/app when tests
// target pullfrog/test-repo).
if (isOIDCAvailable()) {
const token = await acquireNewToken();
process.env.GITHUB_TOKEN = token;
return;
}
if (!process.env.GITHUB_TOKEN && !process.env.GH_TOKEN) { if (!process.env.GITHUB_TOKEN && !process.env.GH_TOKEN) {
if (isOIDCAvailable() || (process.env.GITHUB_APP_ID && process.env.GITHUB_PRIVATE_KEY)) { const token = await acquireNewToken();
const token = await acquireNewToken(); process.env.GITHUB_TOKEN = token;
process.env.GITHUB_TOKEN = token;
}
} }
} }