cleanup, add InstallationToken type

This commit is contained in:
ssalbdivad
2025-09-23 12:48:35 -04:00
parent f2a1c3c1bb
commit e13c5eed00
13 changed files with 127 additions and 144 deletions
+3 -3
View File
@@ -1,8 +1,8 @@
import { access, constants } from "node:fs/promises"; import { access, constants } from "node:fs/promises";
import * as core from "@actions/core"; import * as core from "@actions/core";
import { boxString, tableString } from "../utils"; import { spawn } from "../utils/subprocess.ts";
import { spawn } from "../utils/subprocess"; import { boxString, tableString } from "../utils/table.ts";
import type { Agent, AgentConfig, AgentResult } from "./types"; import type { Agent, AgentConfig, AgentResult } from "./types.ts";
/** /**
* Claude Code agent implementation * Claude Code agent implementation
-2
View File
@@ -1,2 +0,0 @@
export * from "./claude";
export * from "./types";
+74 -74
View File
@@ -25497,53 +25497,11 @@ var require_src = __commonJS({
var core4 = __toESM(require_core(), 1); var core4 = __toESM(require_core(), 1);
// main.ts // main.ts
var core3 = __toESM(require_core(), 1); var core2 = __toESM(require_core(), 1);
// agents/claude.ts // agents/claude.ts
var import_promises = require("node:fs/promises"); var import_promises = require("node:fs/promises");
var core2 = __toESM(require_core(), 1);
// utils/github.ts
var core = __toESM(require_core(), 1); var core = __toESM(require_core(), 1);
async function setupGitHubInstallationToken() {
const inputToken = core.getInput("github_installation_token");
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
const existingToken = inputToken || envToken;
if (existingToken) {
core.setSecret(existingToken);
core.info("Using provided GitHub installation token");
return existingToken;
}
core.info("Generating OIDC token...");
try {
const oidcToken = await core.getIDToken("pullfrog-api");
core.info("OIDC token generated successfully");
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
core.info("Exchanging OIDC token for installation token...");
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
method: "POST",
headers: {
Authorization: `Bearer ${oidcToken}`,
"Content-Type": "application/json"
}
});
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
throw new Error(
`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText} - ${errorText}`
);
}
const tokenData = await tokenResponse.json();
core.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
core.setSecret(tokenData.token);
process.env.GITHUB_INSTALLATION_TOKEN = tokenData.token;
return tokenData.token;
} catch (error2) {
throw new Error(
`Failed to setup GitHub installation token: ${error2 instanceof Error ? error2.message : "Unknown error"}`
);
}
}
// utils/subprocess.ts // utils/subprocess.ts
var import_node_child_process = require("node:child_process"); var import_node_child_process = require("node:child_process");
@@ -25742,10 +25700,10 @@ var ClaudeAgent = class {
*/ */
async install() { async install() {
if (await this.isClaudeInstalled()) { if (await this.isClaudeInstalled()) {
core2.info("Claude Code is already installed, skipping installation"); core.info("Claude Code is already installed, skipping installation");
return; return;
} }
core2.info("Installing Claude Code..."); core.info("Installing Claude Code...");
try { try {
const result = await spawn({ const result = await spawn({
cmd: "bash", cmd: "bash",
@@ -25760,7 +25718,7 @@ var ClaudeAgent = class {
if (result.exitCode !== 0) { if (result.exitCode !== 0) {
throw new Error(`Installation failed with exit code ${result.exitCode}: ${result.stderr}`); throw new Error(`Installation failed with exit code ${result.exitCode}: ${result.stderr}`);
} }
core2.info("Claude Code installed successfully"); core.info("Claude Code installed successfully");
} catch (error2) { } catch (error2) {
throw new Error(`Failed to install Claude Code: ${error2}`); throw new Error(`Failed to install Claude Code: ${error2}`);
} }
@@ -25769,7 +25727,7 @@ var ClaudeAgent = class {
* Execute Claude Code with the given prompt * Execute Claude Code with the given prompt
*/ */
async execute(prompt) { async execute(prompt) {
core2.info("Running Claude Code..."); core.info("Running Claude Code...");
try { try {
const claudePath = `${process.env.HOME}/.local/bin/claude`; const claudePath = `${process.env.HOME}/.local/bin/claude`;
console.log(boxString(prompt, { title: "Prompt" })); console.log(boxString(prompt, { title: "Prompt" }));
@@ -25784,7 +25742,7 @@ var ClaudeAgent = class {
const env = { const env = {
ANTHROPIC_API_KEY: this.apiKey ANTHROPIC_API_KEY: this.apiKey
}; };
core2.startGroup("\u{1F504} Run details"); core.startGroup("\u{1F504} Run details");
this.runStats = { this.runStats = {
toolsUsed: 0, toolsUsed: 0,
turns: 0, turns: 0,
@@ -25818,11 +25776,11 @@ Stderr: ${result.stderr}`
); );
} }
const duration = Date.now() - this.runStats.startTime; const duration = Date.now() - this.runStats.startTime;
core2.info( core.info(
`\u{1F4CA} Run Summary: ${this.runStats.toolsUsed} tools used, ${this.runStats.turns} turns, ${duration}ms duration` `\u{1F4CA} Run Summary: ${this.runStats.toolsUsed} tools used, ${this.runStats.turns} turns, ${duration}ms duration`
); );
core2.info("\u2705 Task complete."); core.info("\u2705 Task complete.");
core2.endGroup(); core.endGroup();
return { return {
success: true, success: true,
output: finalResult, output: finalResult,
@@ -25835,7 +25793,7 @@ Stderr: ${result.stderr}`
}; };
} catch (error2) { } catch (error2) {
try { try {
core2.endGroup(); core.endGroup();
} catch { } catch {
} }
const errorMessage = error2 instanceof Error ? error2.message : "Unknown error"; const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
@@ -25853,8 +25811,8 @@ function processJSONChunk(chunk, agent) {
switch (parsedChunk.type) { switch (parsedChunk.type) {
case "system": case "system":
if (parsedChunk.subtype === "init") { if (parsedChunk.subtype === "init") {
core2.info(`\u{1F680} Starting Claude Code session...`); core.info(`\u{1F680} Starting Claude Code session...`);
core2.info( core.info(
tableString([ tableString([
["model", parsedChunk.model], ["model", parsedChunk.model],
["cwd", parsedChunk.cwd], ["cwd", parsedChunk.cwd],
@@ -25880,51 +25838,51 @@ function processJSONChunk(chunk, agent) {
for (const content of parsedChunk.message.content) { for (const content of parsedChunk.message.content) {
if (content.type === "text") { if (content.type === "text") {
if (content.text.trim()) { if (content.text.trim()) {
core2.info(boxString(content.text.trim(), { title: "Claude Code" })); core.info(boxString(content.text.trim(), { title: "Claude Code" }));
} }
} else if (content.type === "tool_use") { } else if (content.type === "tool_use") {
if (agent) { if (agent) {
agent.runStats.toolsUsed++; agent.runStats.toolsUsed++;
} }
const toolName = content.name; const toolName = content.name;
core2.info(`\u2192 ${toolName}`); core.info(`\u2192 ${toolName}`);
if (content.input) { if (content.input) {
const input = content.input; const input = content.input;
if (input.description) { if (input.description) {
core2.info(` \u2514\u2500 ${input.description}`); core.info(` \u2514\u2500 ${input.description}`);
} }
if (input.command) { if (input.command) {
core2.info(` \u2514\u2500 command: ${input.command}`); core.info(` \u2514\u2500 command: ${input.command}`);
} }
if (input.file_path) { if (input.file_path) {
core2.info(` \u2514\u2500 file: ${input.file_path}`); core.info(` \u2514\u2500 file: ${input.file_path}`);
} }
if (input.content) { if (input.content) {
const contentPreview = input.content.length > 100 ? `${input.content.substring(0, 100)}...` : input.content; const contentPreview = input.content.length > 100 ? `${input.content.substring(0, 100)}...` : input.content;
core2.info(` \u2514\u2500 content: ${contentPreview}`); core.info(` \u2514\u2500 content: ${contentPreview}`);
} }
if (input.query) { if (input.query) {
core2.info(` \u2514\u2500 query: ${input.query}`); core.info(` \u2514\u2500 query: ${input.query}`);
} }
if (input.pattern) { if (input.pattern) {
core2.info(` \u2514\u2500 pattern: ${input.pattern}`); core.info(` \u2514\u2500 pattern: ${input.pattern}`);
} }
if (input.url) { if (input.url) {
core2.info(` \u2514\u2500 url: ${input.url}`); core.info(` \u2514\u2500 url: ${input.url}`);
} }
if (input.edits && Array.isArray(input.edits)) { if (input.edits && Array.isArray(input.edits)) {
core2.info(` \u2514\u2500 edits: ${input.edits.length} changes`); core.info(` \u2514\u2500 edits: ${input.edits.length} changes`);
input.edits.forEach((edit, index) => { input.edits.forEach((edit, index) => {
if (edit.file_path) { if (edit.file_path) {
core2.info(` ${index + 1}. ${edit.file_path}`); core.info(` ${index + 1}. ${edit.file_path}`);
} }
}); });
} }
if (input.task) { if (input.task) {
core2.info(` \u2514\u2500 task: ${input.task}`); core.info(` \u2514\u2500 task: ${input.task}`);
} }
if (input.bash_command) { if (input.bash_command) {
core2.info(` \u2514\u2500 bash_command: ${input.bash_command}`); core.info(` \u2514\u2500 bash_command: ${input.bash_command}`);
} }
} }
} }
@@ -25936,7 +25894,7 @@ function processJSONChunk(chunk, agent) {
for (const content of parsedChunk.message.content) { for (const content of parsedChunk.message.content) {
if (content.type === "tool_result") { if (content.type === "tool_result") {
if (content.is_error) { if (content.is_error) {
core2.warning(`\u274C Tool error: ${content.content}`); core.warning(`\u274C Tool error: ${content.content}`);
} else { } else {
const _resultContent = content.content.trim(); const _resultContent = content.content.trim();
} }
@@ -25946,7 +25904,7 @@ function processJSONChunk(chunk, agent) {
break; break;
case "result": case "result":
if (parsedChunk.subtype === "success") { if (parsedChunk.subtype === "success") {
core2.info( core.info(
tableString([ tableString([
["Cost", `$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`], ["Cost", `$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`],
["Input Tokens", parsedChunk.usage?.input_tokens || 0], ["Input Tokens", parsedChunk.usage?.input_tokens || 0],
@@ -25956,16 +25914,16 @@ function processJSONChunk(chunk, agent) {
]) ])
); );
} else { } else {
core2.error(`\u274C Failed: ${parsedChunk.error || "Unknown error"}`); core.error(`\u274C Failed: ${parsedChunk.error || "Unknown error"}`);
} }
break; break;
default: default:
core2.debug(`\u{1F4E6} Unknown chunk type: ${parsedChunk.type}`); core.debug(`\u{1F4E6} Unknown chunk type: ${parsedChunk.type}`);
break; break;
} }
} catch (error2) { } catch (error2) {
core2.debug(`Failed to parse chunk: ${error2}`); core.debug(`Failed to parse chunk: ${error2}`);
core2.debug(`Raw chunk: ${chunk.substring(0, 200)}...`); core.debug(`Raw chunk: ${chunk.substring(0, 200)}...`);
} }
} }
@@ -25977,7 +25935,7 @@ async function main(params) {
process.chdir(cwd); process.chdir(cwd);
} }
Object.assign(process.env, env); Object.assign(process.env, env);
core3.info(`\u2192 Starting agent run with Claude Code`); core2.info(`\u2192 Starting agent run with Claude Code`);
const agent = new ClaudeAgent({ apiKey: inputs.anthropic_api_key }); const agent = new ClaudeAgent({ apiKey: inputs.anthropic_api_key });
await agent.install(); await agent.install();
const result = await agent.execute(inputs.prompt); const result = await agent.execute(inputs.prompt);
@@ -26001,6 +25959,48 @@ async function main(params) {
} }
} }
// utils/github.ts
var core3 = __toESM(require_core(), 1);
async function setupGitHubInstallationToken() {
const inputToken = core3.getInput("github_installation_token");
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
const existingToken = inputToken || envToken;
if (existingToken) {
core3.setSecret(existingToken);
core3.info("Using provided GitHub installation token");
return existingToken;
}
core3.info("Generating OIDC token...");
try {
const oidcToken = await core3.getIDToken("pullfrog-api");
core3.info("OIDC token generated successfully");
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
core3.info("Exchanging OIDC token for installation token...");
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
method: "POST",
headers: {
Authorization: `Bearer ${oidcToken}`,
"Content-Type": "application/json"
}
});
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
throw new Error(
`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText} - ${errorText}`
);
}
const tokenData = await tokenResponse.json();
core3.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
core3.setSecret(tokenData.token);
process.env.GITHUB_INSTALLATION_TOKEN = tokenData.token;
return tokenData.token;
} catch (error2) {
throw new Error(
`Failed to setup GitHub installation token: ${error2 instanceof Error ? error2.message : "Unknown error"}`
);
}
}
// entry.ts // entry.ts
async function run() { async function run() {
try { try {
+4 -4
View File
@@ -6,8 +6,8 @@
*/ */
import * as core from "@actions/core"; import * as core from "@actions/core";
import { main } from "./main"; import { type MainParams, main } from "./main.ts";
import { setupGitHubInstallationToken } from "./utils"; import { setupGitHubInstallationToken } from "./utils/github.ts";
async function run(): Promise<void> { async function run(): Promise<void> {
try { try {
@@ -40,9 +40,9 @@ async function run(): Promise<void> {
await setupGitHubInstallationToken(); await setupGitHubInstallationToken();
} }
const params = { const params: MainParams = {
inputs, inputs,
env: {} as Record<string, string>, env: {},
cwd: process.cwd(), cwd: process.cwd(),
}; };
+1 -1
View File
@@ -1,4 +1,4 @@
import type { MainParams } from "../main"; import type { MainParams } from "../main.ts";
const testParams = { const testParams = {
inputs: { inputs: {
+3 -3
View File
@@ -3,11 +3,11 @@
* This exports the main function for programmatic usage * This exports the main function for programmatic usage
*/ */
export { ClaudeAgent } from "./agents"; export { ClaudeAgent } from "./agents/claude.ts";
export type { Agent, AgentConfig, AgentResult } from "./agents/types"; export type { Agent, AgentConfig, AgentResult } from "./agents/types.ts";
export { export {
type ExecutionInputs, type ExecutionInputs,
type MainParams, type MainParams,
type MainResult, type MainResult,
main, main,
} from "./main"; } from "./main.ts";
+1 -1
View File
@@ -1,5 +1,5 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
import { ClaudeAgent } from "./agents"; import { ClaudeAgent } from "./agents/claude.ts";
// Expected environment variables that should be passed as inputs // Expected environment variables that should be passed as inputs
export const EXPECTED_INPUTS: string[] = [ export const EXPECTED_INPUTS: string[] = [
+3 -1
View File
@@ -22,10 +22,12 @@
"build:npm": "zshy", "build:npm": "zshy",
"build:dev": "node esbuild.config.js", "build:dev": "node esbuild.config.js",
"prepare": "husky", "prepare": "husky",
"play": "tsx play.ts" "play": "node play.ts",
"upDeps": "pnpm up --latest"
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.11.1", "@actions/core": "^1.11.1",
"@octokit/webhooks-types": "^7.6.1",
"dotenv": "^17.2.2", "dotenv": "^17.2.2",
"execa": "^9.6.0", "execa": "^9.6.0",
"table": "^6.9.0" "table": "^6.9.0"
+4 -4
View File
@@ -3,9 +3,9 @@ import { dirname, extname, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url"; import { fileURLToPath, pathToFileURL } from "node:url";
import arg from "arg"; import arg from "arg";
import { config } from "dotenv"; import { config } from "dotenv";
import { main } from "./main"; import { main } from "./main.ts";
import { runAct } from "./utils/act"; import { runAct } from "./utils/act.ts";
import { setupTestRepo } from "./utils/setup"; import { setupTestRepo } from "./utils/setup.ts";
// Load environment variables from .env file // Load environment variables from .env file
config(); config();
@@ -40,7 +40,7 @@ export async function run(
console.log("─".repeat(50)); console.log("─".repeat(50));
// Set environment variables from our .env for the action to use // Set environment variables from our .env for the action to use
const { EXPECTED_INPUTS } = await import("./main"); const { EXPECTED_INPUTS } = await import("./main.ts");
EXPECTED_INPUTS.forEach((inputName) => { EXPECTED_INPUTS.forEach((inputName) => {
const value = process.env[inputName]; const value = process.env[inputName];
if (value) { if (value) {
+17 -40
View File
@@ -1,45 +1,22 @@
{ {
// Visit https://aka.ms/tsconfig to read more about this file
"compilerOptions": { "compilerOptions": {
// File Layout
// "rootDir": "./src",
"outDir": "./dist", "outDir": "./dist",
"module": "NodeNext",
// Environment Settings "target": "ESNext",
// See also https://aka.ms/tsconfig/module "moduleResolution": "NodeNext",
"module": "esnext", "lib": ["ESNext"],
"moduleResolution": "bundler", "allowImportingTsExtensions": true,
"target": "esnext", "rewriteRelativeImportExtensions": true,
"types": [], "skipLibCheck": true,
// For nodejs: "strict": true,
// "lib": ["esnext"], "noUncheckedSideEffectImports": true,
// "types": ["node"], "declaration": true,
// and npm install -D @types/node "verbatimModuleSyntax": true,
"esModuleInterop": true,
// Other Outputs "resolveJsonModule": true,
"sourceMap": true, "exactOptionalPropertyTypes": true,
"declaration": true, "forceConsistentCasingInFileNames": true,
"declarationMap": true, "stripInternal": true,
"moduleDetection": "force"
// Stricter Typechecking Options
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Style Options
// "noImplicitReturns": true,
// "noImplicitOverride": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "noFallthroughCasesInSwitch": true,
// "noPropertyAccessFromIndexSignature": true,
// Recommended Options
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true
} }
} }
+5 -5
View File
@@ -3,19 +3,19 @@ import { existsSync } from "node:fs";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { config } from "dotenv"; import { config } from "dotenv";
import { buildAction, setupTestRepo } from "./setup"; import { buildAction, setupTestRepo } from "./setup.ts";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const tempDir = join(__dirname, "..", ".temp");
const actionPath = join(__dirname, "..");
const envPath = join(__dirname, "..", "..", ".env");
// Environment variables that should be passed as secrets to the workflow // Environment variables that should be passed as secrets to the workflow
const ENV_VARS = ["ANTHROPIC_API_KEY", "GITHUB_INSTALLATION_TOKEN"]; const ENV_VARS = ["ANTHROPIC_API_KEY", "GITHUB_INSTALLATION_TOKEN"];
export function runAct(prompt: string): void { export function runAct(prompt: string): void {
const tempDir = join(__dirname, "..", ".temp");
const actionPath = join(__dirname, "..");
const envPath = join(__dirname, "..", "..", ".env");
// Setup test repository // Setup test repository
setupTestRepo({ tempDir }); setupTestRepo({ tempDir });
+12 -1
View File
@@ -1,5 +1,15 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
export interface InstallationToken {
token: string;
expires_at: string;
installation_id: number;
repository: string;
ref: string;
runner_environment: string;
owner?: string;
}
/** /**
* Setup GitHub installation token for the action * Setup GitHub installation token for the action
*/ */
@@ -42,7 +52,8 @@ export async function setupGitHubInstallationToken(): Promise<string> {
); );
} }
const tokenData = await tokenResponse.json(); // This type is enforced by us when the response is created
const tokenData = (await tokenResponse.json()) as InstallationToken;
core.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`); core.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
// Mask the token in logs for security // Mask the token in logs for security
-5
View File
@@ -1,5 +0,0 @@
export * from "./files";
export * from "./github";
export * from "./setup";
export * from "./subprocess";
export * from "./table";