This commit is contained in:
Colin McDonnell
2026-01-13 06:02:29 +00:00
parent 9903072286
commit a57866a8cd
11 changed files with 27384 additions and 1262 deletions
-7
View File
@@ -4,10 +4,3 @@ if git diff --cached --name-only | grep -q "^package.json$"; then
pnpm lock
git add pnpm-lock.yaml
fi
# Check if entry needs rebuilding (entry.ts, esbuild.config.js, or any .ts files)
if git diff --cached --name-only | grep -qE "^(entry\.ts|esbuild\.config\.js|.*\.ts)$"; then
echo "🔨 Building action..."
pnpm build
git add entry
fi
+1 -2
View File
@@ -1,5 +1,4 @@
<p align="center">
<p align="center">
<h1 align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://pullfrog.com/frog-white-200px.png">
+3
View File
@@ -10,6 +10,9 @@ inputs:
description: "Effort level: nothink (fast), think (default), max (most capable)"
required: false
default: "think"
cwd:
description: "Working directory for the agent (defaults to GITHUB_WORKSPACE)"
required: false
runs:
using: "node24"
+1221 -1242
View File
File diff suppressed because one or more lines are too long
+5 -5
View File
@@ -9,12 +9,12 @@ import { Inputs, main } from "./main.ts";
import { log } from "./utils/cli.ts";
async function run(): Promise<void> {
// Change to GITHUB_WORKSPACE if set (this is where actions/checkout puts the repo)
// Change to cwd input or GITHUB_WORKSPACE (where actions/checkout puts the repo)
// JavaScript actions run from the action's directory, not the checked out repo
if (process.env.GITHUB_WORKSPACE && process.cwd() !== process.env.GITHUB_WORKSPACE) {
log.debug(`Changing to GITHUB_WORKSPACE: ${process.env.GITHUB_WORKSPACE}`);
process.chdir(process.env.GITHUB_WORKSPACE);
log.debug(`New working directory: ${process.cwd()}`);
const cwd = core.getInput("cwd") || process.env.GITHUB_WORKSPACE;
if (cwd && process.cwd() !== cwd) {
log.debug(`changing to working directory: ${cwd}`);
process.chdir(cwd);
}
try {
+8
View File
@@ -67,4 +67,12 @@ await build({
plugins: [stripShebangPlugin],
});
// Build the get-installation-token action
await build({
...sharedConfig,
entryPoints: ["./get-installation-token/entry.ts"],
outfile: "./get-installation-token/entry",
plugins: [stripShebangPlugin],
});
console.log("✅ Build completed successfully!");
+21
View File
@@ -0,0 +1,21 @@
name: "Get Installation Token"
description: "Get a GitHub App installation token for the current repository"
author: "Pullfrog"
inputs:
repos:
description: "Comma-separated list of additional repo names to grant access to (e.g., 'repo1,repo2'). Current repo is always included."
required: false
outputs:
token:
description: "GitHub App installation token"
runs:
using: "node24"
main: "entry"
post: "entry"
branding:
icon: "key"
color: "green"
+26030
View File
File diff suppressed because one or more lines are too long
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env node
/**
* entry point for get-installation-token action.
* handles both main and post execution using the isPost state pattern.
*/
import * as core from "@actions/core";
import { acquireInstallationToken, revokeInstallationToken } from "./token.ts";
const STATE_TOKEN = "token";
const STATE_IS_POST = "isPost";
async function main(): Promise<void> {
core.saveState(STATE_IS_POST, "true");
const reposInput = core.getInput("repos");
const repos = reposInput
? reposInput.split(",").map((r) => r.trim()).filter(Boolean)
: undefined;
const token = await acquireInstallationToken({ repos });
// mask the token in logs
core.setSecret(token);
// save token to state for post cleanup
core.saveState(STATE_TOKEN, token);
// set as output
core.setOutput("token", token);
const scope = repos?.length
? `current repo + ${repos.join(", ")}`
: "current repo only";
core.info(`installation token acquired successfully (${scope})`);
}
async function post(): Promise<void> {
const token = core.getState(STATE_TOKEN);
if (!token) {
core.debug("no token found in state, skipping revocation");
return;
}
await revokeInstallationToken(token);
core.info("installation token revoked successfully");
}
async function run(): Promise<void> {
try {
const isPost = core.getState(STATE_IS_POST) === "true";
if (isPost) {
await post();
} else {
await main();
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
core.setFailed(message);
}
}
await run();
+14
View File
@@ -0,0 +1,14 @@
/**
* token acquisition and revocation for get-installation-token action.
* reuses the existing github.ts utilities.
*/
import { acquireNewToken, revokeGitHubInstallationToken } from "../utils/github.ts";
export async function acquireInstallationToken(opts?: { repos?: string[] }): Promise<string> {
return acquireNewToken(opts);
}
export async function revokeInstallationToken(token: string): Promise<void> {
return revokeGitHubInstallationToken(token);
}
+15 -6
View File
@@ -54,12 +54,17 @@ function isOIDCAvailable(): boolean {
);
}
async function acquireTokenViaOIDC(): Promise<string> {
async function acquireTokenViaOIDC(opts?: { repos?: string[] }): Promise<string> {
log.info("» generating OIDC token...");
const oidcToken = await core.getIDToken("pullfrog-api");
const apiUrl = process.env.API_URL || "https://pullfrog.com";
const params = new URLSearchParams();
if (opts?.repos?.length) {
params.set("repos", opts.repos.join(","));
}
const queryString = params.toString() ? `?${params.toString()}` : "";
log.info("» exchanging OIDC token for installation token...");
@@ -68,7 +73,7 @@ async function acquireTokenViaOIDC(): Promise<string> {
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token${queryString}`, {
method: "POST",
headers: {
Authorization: `Bearer ${oidcToken}`,
@@ -84,7 +89,11 @@ async function acquireTokenViaOIDC(): Promise<string> {
}
const tokenData = (await tokenResponse.json()) as InstallationToken;
log.info(`» installation token obtained for ${tokenData.repository || "all repositories"}`);
const owner = tokenData.repository?.split("/")[0];
const repoList = opts?.repos?.length
? [tokenData.repository, ...opts.repos.map((r) => `${owner}/${r}`)].join(", ")
: tokenData.repository;
log.info(`» installation token obtained for ${repoList}`);
return tokenData.token;
} catch (error) {
@@ -238,9 +247,9 @@ async function acquireTokenViaGitHubApp(): Promise<string> {
return token;
}
async function acquireNewToken(): Promise<string> {
export async function acquireNewToken(opts?: { repos?: string[] }): Promise<string> {
if (isOIDCAvailable()) {
return await retry(() => acquireTokenViaOIDC(), { label: "token exchange" });
return await retry(() => acquireTokenViaOIDC(opts), { label: "token exchange" });
} else {
return await acquireTokenViaGitHubApp();
}
@@ -277,7 +286,7 @@ export function getGitHubInstallationToken(): string {
return githubInstallationToken;
}
async function revokeGitHubInstallationToken(token: string): Promise<void> {
export async function revokeGitHubInstallationToken(token: string): Promise<void> {
const apiUrl = process.env.GITHUB_API_URL || "https://api.github.com";
try {