Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1922352d86 | |||
| c0f31415a3 | |||
| 706ce04895 | |||
| 09be8e3068 | |||
| c6c1210fa0 | |||
| 0368512b9e | |||
| 9fb6135fd2 | |||
| bb78e5f94b | |||
| c668578c6f | |||
| 7f1566d9c2 | |||
| dd482566c2 | |||
| 57029c32a3 | |||
| 757d336475 | |||
| d03debab4b | |||
| a05829f781 | |||
| c8ba7940e3 | |||
| 710fdd0fa4 | |||
| 4f5ee28b8a | |||
| 806458b95a | |||
| 2c856e3337 | |||
| a93c34e61b | |||
| cd20491d22 | |||
| 1a6ce6728c | |||
| 3b39f2c8d8 | |||
| ec0eeb1d18 | |||
| 8ef805b9fc | |||
| 6e93fd9a72 | |||
| 9567d84442 | |||
| d79564db5e | |||
| a7a0e87fd8 | |||
| 7050b8de75 | |||
| 2fc3ddee16 |
@@ -29,7 +29,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "24"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
@@ -60,21 +60,6 @@ jobs:
|
|||||||
echo "✅ Tag ${{ steps.version.outputs.tag }} does not exist - will create release"
|
echo "✅ Tag ${{ steps.version.outputs.tag }} does not exist - will create release"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Verify built files are up to date
|
|
||||||
if: steps.check_tag.outputs.exists == 'false'
|
|
||||||
run: |
|
|
||||||
# Check if there are any uncommitted changes
|
|
||||||
if [[ -n $(git status --porcelain) ]]; then
|
|
||||||
echo "❌ Error: There are uncommitted changes. Built files should be committed via pre-commit hook."
|
|
||||||
git status
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "✅ All built files are up to date"
|
|
||||||
|
|
||||||
- name: Build for npm with zshy
|
|
||||||
if: steps.check_tag.outputs.exists == 'false'
|
|
||||||
run: pnpm build:npm
|
|
||||||
|
|
||||||
- name: Create and push tags
|
- name: Create and push tags
|
||||||
if: steps.check_tag.outputs.exists == 'false'
|
if: steps.check_tag.outputs.exists == 'false'
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
# Ensure lockfile is up to date
|
|
||||||
echo "🔒 Updating lockfile..."
|
|
||||||
pnpm install --lockfile-only
|
|
||||||
|
|
||||||
# Build the action before committing
|
|
||||||
echo "🔨 Building action..."
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Add the built files and lockfile to the commit
|
|
||||||
git add entry.cjs pnpm-lock.yaml
|
|
||||||
+26
-9
@@ -10,17 +10,34 @@ inputs:
|
|||||||
anthropic_api_key:
|
anthropic_api_key:
|
||||||
description: "Anthropic API key for Claude Code authentication"
|
description: "Anthropic API key for Claude Code authentication"
|
||||||
required: false
|
required: false
|
||||||
github_token:
|
|
||||||
description: "GitHub token for repository access"
|
|
||||||
required: false
|
|
||||||
github_installation_token:
|
|
||||||
description: "GitHub App installation token"
|
|
||||||
required: false
|
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "node20"
|
using: "composite"
|
||||||
main: "entry.cjs"
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
- name: Setup Node.js 24
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
cache: "pnpm"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
shell: bash
|
||||||
|
working-directory: ${{ github.action_path }}
|
||||||
|
- name: Run agent
|
||||||
|
run: node entry.ts
|
||||||
|
shell: bash
|
||||||
|
working-directory: ${{ github.action_path }}
|
||||||
|
env:
|
||||||
|
INPUTS_JSON: ${{ toJSON(inputs) }}
|
||||||
|
|
||||||
branding:
|
branding:
|
||||||
icon: "code"
|
icon: "code"
|
||||||
color: "orange"
|
color: "green"
|
||||||
|
|||||||
+29
-41
@@ -3,6 +3,7 @@ import * as core from "@actions/core";
|
|||||||
import { createMcpConfig } from "../mcp/config.ts";
|
import { createMcpConfig } from "../mcp/config.ts";
|
||||||
import { spawn } from "../utils/subprocess.ts";
|
import { spawn } from "../utils/subprocess.ts";
|
||||||
import { boxString, tableString } from "../utils/table.ts";
|
import { boxString, tableString } from "../utils/table.ts";
|
||||||
|
import { instructions } from "./shared.ts";
|
||||||
import type { Agent, AgentConfig, AgentResult } from "./types.ts";
|
import type { Agent, AgentConfig, AgentResult } from "./types.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,6 +11,7 @@ import type { Agent, AgentConfig, AgentResult } from "./types.ts";
|
|||||||
*/
|
*/
|
||||||
export class ClaudeAgent implements Agent {
|
export class ClaudeAgent implements Agent {
|
||||||
private apiKey: string;
|
private apiKey: string;
|
||||||
|
private githubInstallationToken: string;
|
||||||
public runStats = {
|
public runStats = {
|
||||||
toolsUsed: 0,
|
toolsUsed: 0,
|
||||||
turns: 0,
|
turns: 0,
|
||||||
@@ -17,10 +19,8 @@ export class ClaudeAgent implements Agent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constructor(config: AgentConfig) {
|
constructor(config: AgentConfig) {
|
||||||
if (!config.apiKey) {
|
|
||||||
throw new Error("Claude agent requires an API key");
|
|
||||||
}
|
|
||||||
this.apiKey = config.apiKey;
|
this.apiKey = config.apiKey;
|
||||||
|
this.githubInstallationToken = config.githubInstallationToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,10 +49,7 @@ export class ClaudeAgent implements Agent {
|
|||||||
try {
|
try {
|
||||||
const result = await spawn({
|
const result = await spawn({
|
||||||
cmd: "bash",
|
cmd: "bash",
|
||||||
args: [
|
args: ["-c", "curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93"],
|
||||||
"-c",
|
|
||||||
"curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93",
|
|
||||||
],
|
|
||||||
env: { ANTHROPIC_API_KEY: this.apiKey },
|
env: { ANTHROPIC_API_KEY: this.apiKey },
|
||||||
timeout: 120000, // 2 minute timeout
|
timeout: 120000, // 2 minute timeout
|
||||||
onStdout: () => {},
|
onStdout: () => {},
|
||||||
@@ -60,9 +57,7 @@ export class ClaudeAgent implements Agent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Error(
|
throw new Error(`Installation failed with exit code ${result.exitCode}: ${result.stderr}`);
|
||||||
`Installation failed with exit code ${result.exitCode}: ${result.stderr}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
core.info("Claude Code installed successfully");
|
core.info("Claude Code installed successfully");
|
||||||
@@ -79,7 +74,16 @@ export class ClaudeAgent implements Agent {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const claudePath = `${process.env.HOME}/.local/bin/claude`;
|
const claudePath = `${process.env.HOME}/.local/bin/claude`;
|
||||||
|
|
||||||
|
const env = {
|
||||||
|
ANTHROPIC_API_KEY: this.apiKey,
|
||||||
|
};
|
||||||
|
|
||||||
console.log(boxString(prompt, { title: "Prompt" }));
|
console.log(boxString(prompt, { title: "Prompt" }));
|
||||||
|
|
||||||
|
const mcpConfig = createMcpConfig(this.githubInstallationToken);
|
||||||
|
console.log("📋 MCP Config:", mcpConfig);
|
||||||
|
|
||||||
const args = [
|
const args = [
|
||||||
"--print",
|
"--print",
|
||||||
"--output-format",
|
"--output-format",
|
||||||
@@ -88,22 +92,10 @@ export class ClaudeAgent implements Agent {
|
|||||||
"--debug",
|
"--debug",
|
||||||
"--permission-mode",
|
"--permission-mode",
|
||||||
"bypassPermissions",
|
"bypassPermissions",
|
||||||
|
"--mcp-config",
|
||||||
|
mcpConfig,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!process.env.GITHUB_INSTALLATION_TOKEN) {
|
|
||||||
throw new Error(
|
|
||||||
"GITHUB_INSTALLATION_TOKEN is required for GitHub integration"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mcpConfig = createMcpConfig(process.env.GITHUB_INSTALLATION_TOKEN);
|
|
||||||
console.log("📋 MCP Config:", mcpConfig);
|
|
||||||
args.push("--mcp-config", mcpConfig);
|
|
||||||
|
|
||||||
const env = {
|
|
||||||
ANTHROPIC_API_KEY: this.apiKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
core.startGroup("🔄 Run details");
|
core.startGroup("🔄 Run details");
|
||||||
|
|
||||||
this.runStats = {
|
this.runStats = {
|
||||||
@@ -119,7 +111,7 @@ export class ClaudeAgent implements Agent {
|
|||||||
cmd: claudePath,
|
cmd: claudePath,
|
||||||
args,
|
args,
|
||||||
env,
|
env,
|
||||||
input: prompt,
|
input: `${instructions} ${prompt}`,
|
||||||
timeout: 10 * 60 * 1000, // 10 minutes
|
timeout: 10 * 60 * 1000, // 10 minutes
|
||||||
onStdout: (_chunk) => {
|
onStdout: (_chunk) => {
|
||||||
processJSONChunk(_chunk, this);
|
processJSONChunk(_chunk, this);
|
||||||
@@ -159,8 +151,7 @@ export class ClaudeAgent implements Agent {
|
|||||||
try {
|
try {
|
||||||
core.endGroup();
|
core.endGroup();
|
||||||
} catch {}
|
} catch {}
|
||||||
const errorMessage =
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||||
error instanceof Error ? error.message : "Unknown error";
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `Failed to execute Claude Code: ${errorMessage}`,
|
error: `Failed to execute Claude Code: ${errorMessage}`,
|
||||||
@@ -174,8 +165,15 @@ export class ClaudeAgent implements Agent {
|
|||||||
*/
|
*/
|
||||||
function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
||||||
try {
|
try {
|
||||||
|
// Skip debug lines that start with [DEBUG] or [debug]
|
||||||
|
const trimmedChunk = chunk.trim();
|
||||||
|
if (trimmedChunk.startsWith("[DEBUG]") || trimmedChunk.startsWith("[debug]")) {
|
||||||
|
console.log(chunk);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(chunk);
|
console.log(chunk);
|
||||||
const parsedChunk = JSON.parse(chunk.trim());
|
const parsedChunk = JSON.parse(trimmedChunk);
|
||||||
|
|
||||||
switch (parsedChunk.type) {
|
switch (parsedChunk.type) {
|
||||||
case "system":
|
case "system":
|
||||||
@@ -186,12 +184,7 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
["model", parsedChunk.model],
|
["model", parsedChunk.model],
|
||||||
["cwd", parsedChunk.cwd],
|
["cwd", parsedChunk.cwd],
|
||||||
["permission_mode", parsedChunk.permissionMode],
|
["permission_mode", parsedChunk.permissionMode],
|
||||||
[
|
["tools", parsedChunk.tools?.length ? `${parsedChunk.tools.length} tools` : "none"],
|
||||||
"tools",
|
|
||||||
parsedChunk.tools?.length
|
|
||||||
? `${parsedChunk.tools.length} tools`
|
|
||||||
: "none",
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
"mcp_servers",
|
"mcp_servers",
|
||||||
parsedChunk.mcp_servers?.length
|
parsedChunk.mcp_servers?.length
|
||||||
@@ -218,9 +211,7 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
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()) {
|
||||||
core.info(
|
core.info(boxString(content.text.trim(), { title: "Claude Code" }));
|
||||||
boxString(content.text.trim(), { title: "Claude Code" })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if (content.type === "tool_use") {
|
} else if (content.type === "tool_use") {
|
||||||
if (agent) {
|
if (agent) {
|
||||||
@@ -305,10 +296,7 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
if (parsedChunk.subtype === "success") {
|
if (parsedChunk.subtype === "success") {
|
||||||
core.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],
|
||||||
["Output Tokens", parsedChunk.usage?.output_tokens || 0],
|
["Output Tokens", parsedChunk.usage?.output_tokens || 0],
|
||||||
["Duration", `${parsedChunk.duration_ms}ms`],
|
["Duration", `${parsedChunk.duration_ms}ms`],
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { mcpServerName } from "../mcp/config.ts";
|
||||||
|
|
||||||
|
export const instructions = `- use the ${mcpServerName} MCP server to interact with github
|
||||||
|
- if ${mcpServerName} is not available or doesn't include the functionality you need, describe why and bail
|
||||||
|
- do not under any circumstances use the gh cli
|
||||||
|
- if prompted by a comment to respond to create a new issue, pr or anything else, after succeeding,
|
||||||
|
also respond to the original comment with a very brief message containing a link to it
|
||||||
|
`;
|
||||||
+2
-2
@@ -29,6 +29,6 @@ export interface AgentResult {
|
|||||||
* Configuration for agent creation
|
* Configuration for agent creation
|
||||||
*/
|
*/
|
||||||
export interface AgentConfig {
|
export interface AgentConfig {
|
||||||
apiKey?: string;
|
apiKey: string;
|
||||||
[key: string]: any;
|
githubInstallationToken: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,49 +2,26 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Entry point for GitHub Action
|
* Entry point for GitHub Action
|
||||||
* This file is bundled to entry.cjs and called directly by GitHub Actions
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { type ExecutionInputs, type MainParams, main } from "./main.ts";
|
import { type } from "arktype";
|
||||||
|
import { Inputs, main } from "./main.ts";
|
||||||
import packageJson from "./package.json" with { type: "json" };
|
import packageJson from "./package.json" with { type: "json" };
|
||||||
import { setupGitHubInstallationToken } from "./utils/github.ts";
|
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
console.log(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||||
const prompt = core.getInput("prompt", { required: true });
|
|
||||||
const anthropic_api_key = core.getInput("anthropic_api_key");
|
|
||||||
|
|
||||||
if (!prompt) {
|
const inputsJson = process.env.INPUTS_JSON;
|
||||||
throw new Error("prompt is required");
|
if (!inputsJson) {
|
||||||
|
throw new Error("INPUTS_JSON environment variable not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputs: ExecutionInputs = {
|
const parsed = type("string.json.parse").assert(inputsJson);
|
||||||
prompt,
|
const inputs = Inputs.assert(parsed);
|
||||||
anthropic_api_key,
|
|
||||||
};
|
|
||||||
|
|
||||||
const githubToken = core.getInput("github_token") || process.env.GITHUB_TOKEN;
|
const result = await main(inputs);
|
||||||
if (githubToken) {
|
|
||||||
inputs.github_token = githubToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
const githubInstallationToken =
|
|
||||||
core.getInput("github_installation_token") || process.env.GITHUB_INSTALLATION_TOKEN;
|
|
||||||
if (githubInstallationToken) {
|
|
||||||
inputs.github_installation_token = githubInstallationToken;
|
|
||||||
} else {
|
|
||||||
await setupGitHubInstallationToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
const params: MainParams = {
|
|
||||||
inputs,
|
|
||||||
env: {},
|
|
||||||
cwd: process.cwd(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await main(params);
|
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || "Agent execution failed");
|
throw new Error(result.error || "Agent execution failed");
|
||||||
@@ -55,7 +32,4 @@ async function run(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
run().catch((error) => {
|
await run();
|
||||||
console.error("Action failed:", error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { build } from "esbuild";
|
|
||||||
|
|
||||||
// Build the GitHub Action bundle only
|
|
||||||
// For npm package builds, use zshy (pnpm build:npm)
|
|
||||||
await build({
|
|
||||||
entryPoints: ["./entry.ts"],
|
|
||||||
bundle: true,
|
|
||||||
outfile: "./entry.cjs",
|
|
||||||
format: "cjs",
|
|
||||||
platform: "node",
|
|
||||||
target: "node20",
|
|
||||||
minify: false,
|
|
||||||
sourcemap: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("✅ Build completed successfully!");
|
|
||||||
+5
-9
@@ -1,13 +1,9 @@
|
|||||||
import type { MainParams } from "../main.ts";
|
import type { Inputs } from "../main.ts";
|
||||||
|
|
||||||
const testParams = {
|
const testParams = {
|
||||||
inputs: {
|
prompt:
|
||||||
prompt:
|
"List all files in the current directory, then create a file called dynamic-test.txt with the content 'This was loaded from a TypeScript file!', then delete it.",
|
||||||
"List all files in the current directory, then create a file called dynamic-test.txt with the content 'This was loaded from a TypeScript file!', then delete it.",
|
anthropic_api_key: "sk-test-key",
|
||||||
anthropic_api_key: "sk-test-key",
|
} satisfies Inputs;
|
||||||
},
|
|
||||||
env: {},
|
|
||||||
cwd: process.cwd(),
|
|
||||||
} satisfies MainParams;
|
|
||||||
|
|
||||||
export default testParams;
|
export default testParams;
|
||||||
|
|||||||
+1
-3
@@ -1,3 +1 @@
|
|||||||
Use the MCP GitHub comment tool to add a comment containing your best frog joke to GitHub issue https://github.com/pullfrogai/scratch/issues/2.
|
create a PR implementing bogosort to https://github.com/pullfrogai/scratch/
|
||||||
|
|
||||||
Do not use the gh cli. If the mcp tool does not work, bail.
|
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
export { ClaudeAgent } from "./agents/claude.ts";
|
export { ClaudeAgent } from "./agents/claude.ts";
|
||||||
export type { Agent, AgentConfig, AgentResult } from "./agents/types.ts";
|
export type { Agent, AgentConfig, AgentResult } from "./agents/types.ts";
|
||||||
export {
|
export {
|
||||||
type ExecutionInputs,
|
type Inputs as ExecutionInputs,
|
||||||
type MainParams,
|
|
||||||
type MainResult,
|
type MainResult,
|
||||||
main,
|
main,
|
||||||
} from "./main.ts";
|
} from "./main.ts";
|
||||||
|
|||||||
@@ -1,24 +1,15 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
import { type } from "arktype";
|
||||||
import { ClaudeAgent } from "./agents/claude.ts";
|
import { ClaudeAgent } from "./agents/claude.ts";
|
||||||
|
import { setupGitHubInstallationToken, parseRepoContext } from "./utils/github.ts";
|
||||||
|
import { setupGitConfig, setupGitAuth } from "./utils/setup.ts";
|
||||||
|
|
||||||
export const EXPECTED_INPUTS: string[] = [
|
export const Inputs = type({
|
||||||
"ANTHROPIC_API_KEY",
|
prompt: "string",
|
||||||
"GITHUB_TOKEN",
|
"anthropic_api_key?": "string | undefined",
|
||||||
"GITHUB_INSTALLATION_TOKEN",
|
});
|
||||||
];
|
|
||||||
|
|
||||||
export interface ExecutionInputs {
|
export type Inputs = typeof Inputs.infer;
|
||||||
prompt: string;
|
|
||||||
anthropic_api_key: string;
|
|
||||||
github_token?: string;
|
|
||||||
github_installation_token?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MainParams {
|
|
||||||
inputs: ExecutionInputs;
|
|
||||||
env: Record<string, string>;
|
|
||||||
cwd: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MainResult {
|
export interface MainResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -26,19 +17,22 @@ export interface MainResult {
|
|||||||
error?: string | undefined;
|
error?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function main(params: MainParams): Promise<MainResult> {
|
export async function main(inputs: Inputs): Promise<MainResult> {
|
||||||
try {
|
try {
|
||||||
const { inputs, env, cwd } = params;
|
|
||||||
|
|
||||||
if (cwd !== process.cwd()) {
|
|
||||||
process.chdir(cwd);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(process.env, env);
|
|
||||||
|
|
||||||
core.info(`→ Starting agent run with Claude Code`);
|
core.info(`→ Starting agent run with Claude Code`);
|
||||||
|
|
||||||
const agent = new ClaudeAgent({ apiKey: inputs.anthropic_api_key });
|
setupGitConfig();
|
||||||
|
|
||||||
|
const githubInstallationToken = await setupGitHubInstallationToken();
|
||||||
|
const repoContext = parseRepoContext();
|
||||||
|
|
||||||
|
setupGitAuth(githubInstallationToken, repoContext);
|
||||||
|
|
||||||
|
const agent = new ClaudeAgent({
|
||||||
|
apiKey: inputs.anthropic_api_key!,
|
||||||
|
githubInstallationToken,
|
||||||
|
});
|
||||||
|
|
||||||
await agent.install();
|
await agent.install();
|
||||||
|
|
||||||
const result = await agent.execute(inputs.prompt);
|
const result = await agent.execute(inputs.prompt);
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { type } from "arktype";
|
||||||
|
import { contextualize, tool } from "./shared.ts";
|
||||||
|
|
||||||
|
export const Comment = type({
|
||||||
|
issueNumber: type.number.describe("the issue number to comment on"),
|
||||||
|
body: type.string.describe("the comment body content"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CommentTool = tool({
|
||||||
|
name: "create_issue_comment",
|
||||||
|
description: "Create a comment on a GitHub issue",
|
||||||
|
parameters: Comment,
|
||||||
|
execute: contextualize(async ({ issueNumber, body }, ctx) => {
|
||||||
|
const result = await ctx.octokit.rest.issues.createComment({
|
||||||
|
owner: ctx.owner,
|
||||||
|
repo: ctx.name,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
body: body,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
commentId: result.data.id,
|
||||||
|
url: result.data.html_url,
|
||||||
|
body: result.data.body,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
+6
-9
@@ -1,24 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* Simple MCP configuration helper for adding our minimal GitHub comment server
|
* Simple MCP configuration helper for adding our minimal GitHub comment server
|
||||||
*/
|
*/
|
||||||
// const actionPath = process.env.GITHUB_ACTION_PATH || process.cwd();
|
|
||||||
|
|
||||||
import { fromHere } from "@ark/fs";
|
import { fromHere } from "@ark/fs";
|
||||||
|
import { parseRepoContext } from "../utils/github.ts";
|
||||||
|
|
||||||
const actionPath = fromHere("..");
|
const actionPath = fromHere("..");
|
||||||
|
|
||||||
|
export const mcpServerName = "gh-pullfrog";
|
||||||
|
|
||||||
export function createMcpConfig(githubInstallationToken: string) {
|
export function createMcpConfig(githubInstallationToken: string) {
|
||||||
const githubRepository = process.env.GITHUB_REPOSITORY;
|
const repoContext = parseRepoContext();
|
||||||
if (!githubRepository) {
|
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
|
||||||
throw new Error(
|
|
||||||
"GITHUB_REPOSITORY environment variable is required for MCP GitHub integration"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify(
|
return JSON.stringify(
|
||||||
{
|
{
|
||||||
mcpServers: {
|
mcpServers: {
|
||||||
minimal_github_comment: {
|
[mcpServerName]: {
|
||||||
command: "node",
|
command: "node",
|
||||||
args: [`${actionPath}/mcp/server.ts`],
|
args: [`${actionPath}/mcp/server.ts`],
|
||||||
env: {
|
env: {
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { type } from "arktype";
|
||||||
|
import { contextualize, tool } from "./shared.ts";
|
||||||
|
|
||||||
|
export const Issue = type({
|
||||||
|
title: type.string.describe("the title of the issue"),
|
||||||
|
body: type.string.describe("the body content of the issue"),
|
||||||
|
labels: type.string
|
||||||
|
.array()
|
||||||
|
.describe("optional array of label names to apply to the issue")
|
||||||
|
.optional(),
|
||||||
|
assignees: type.string
|
||||||
|
.array()
|
||||||
|
.describe("optional array of usernames to assign to the issue")
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const IssueTool = tool({
|
||||||
|
name: "create_issue",
|
||||||
|
description: "Create a new GitHub issue",
|
||||||
|
parameters: Issue,
|
||||||
|
execute: contextualize(async ({ title, body, labels, assignees }, ctx) => {
|
||||||
|
const result = await ctx.octokit.rest.issues.create({
|
||||||
|
owner: ctx.owner,
|
||||||
|
repo: ctx.name,
|
||||||
|
title: title,
|
||||||
|
body: body,
|
||||||
|
labels: labels ?? [],
|
||||||
|
assignees: assignees ?? [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
issueId: result.data.id,
|
||||||
|
number: result.data.number,
|
||||||
|
url: result.data.html_url,
|
||||||
|
title: result.data.title,
|
||||||
|
state: result.data.state,
|
||||||
|
labels: result.data.labels?.map((label) => (typeof label === "string" ? label : label.name)),
|
||||||
|
assignees: result.data.assignees?.map((assignee) => assignee.login),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import { type } from "arktype";
|
||||||
|
import { contextualize, tool } from "./shared.ts";
|
||||||
|
|
||||||
|
export const PullRequest = type({
|
||||||
|
title: type.string.describe("the title of the pull request"),
|
||||||
|
body: type.string.describe("the body content of the pull request"),
|
||||||
|
base: type.string.describe("the base branch to merge into (e.g., 'main')"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PullRequestTool = tool({
|
||||||
|
name: "create_pull_request",
|
||||||
|
description: "Create a pull request from the current branch",
|
||||||
|
parameters: PullRequest,
|
||||||
|
execute: contextualize(async ({ title, body, base }, ctx) => {
|
||||||
|
// Get the current branch name
|
||||||
|
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
||||||
|
encoding: "utf8",
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
console.log(`Current branch: ${currentBranch}`);
|
||||||
|
|
||||||
|
const result = await ctx.octokit.rest.pulls.create({
|
||||||
|
owner: ctx.owner,
|
||||||
|
repo: ctx.name,
|
||||||
|
title: title,
|
||||||
|
body: body,
|
||||||
|
head: currentBranch,
|
||||||
|
base: base,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
pullRequestId: result.data.id,
|
||||||
|
number: result.data.number,
|
||||||
|
url: result.data.html_url,
|
||||||
|
title: result.data.title,
|
||||||
|
head: result.data.head.ref,
|
||||||
|
base: result.data.base.ref,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
+10
-150
@@ -1,156 +1,16 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { writeFileSync } from "node:fs";
|
|
||||||
// Minimal GitHub Issue Comment MCP Server
|
// Minimal GitHub Issue Comment MCP Server
|
||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import { FastMCP } from "fastmcp";
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
import { CommentTool } from "./comment.ts";
|
||||||
import { Octokit } from "@octokit/rest";
|
import { IssueTool } from "./issue.ts";
|
||||||
import { type } from "arktype";
|
import { PullRequestTool } from "./pr.ts";
|
||||||
import { z } from "zod";
|
import { addTools } from "./shared.ts";
|
||||||
import { resolveRepoContext } from "../utils/repo-context.ts";
|
|
||||||
|
|
||||||
// Simple error logging to file
|
const server = new FastMCP({
|
||||||
function logError(message: string, error?: any) {
|
name: "gh-pullfrog",
|
||||||
const timestamp = new Date().toISOString();
|
version: "0.0.1",
|
||||||
const errorText = error ? `\nError: ${error.message}\nStack: ${error.stack}` : "";
|
|
||||||
const logEntry = `[${timestamp}] ${message}${errorText}\n`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
writeFileSync("/tmp/mcp-error.log", logEntry, { flag: "a" });
|
|
||||||
console.error(logEntry);
|
|
||||||
} catch (writeError) {
|
|
||||||
console.error(`Failed to write error log: ${writeError}`);
|
|
||||||
console.error(logEntry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let server: McpServer;
|
|
||||||
|
|
||||||
try {
|
|
||||||
logError("Creating MCP server...");
|
|
||||||
server = new McpServer({
|
|
||||||
name: "Minimal GitHub Issue Comment Server",
|
|
||||||
version: "0.0.1",
|
|
||||||
});
|
|
||||||
logError("MCP server created successfully");
|
|
||||||
} catch (error) {
|
|
||||||
logError("Failed to create MCP server", error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define the schema for creating issue comments
|
|
||||||
const Comment = type({
|
|
||||||
issueNumber: type.number.describe("the issue number to comment on"),
|
|
||||||
body: type.string.describe("the comment body content"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
addTools(server, [CommentTool, IssueTool, PullRequestTool]);
|
||||||
logError("Registering create_issue_comment tool...");
|
|
||||||
|
|
||||||
server.tool(
|
server.start();
|
||||||
"create_issue_comment",
|
|
||||||
"Create a comment on a GitHub issue",
|
|
||||||
{
|
|
||||||
issueNumber: z.number().describe("the issue number to comment on"),
|
|
||||||
body: z.string().describe("the comment body content"),
|
|
||||||
},
|
|
||||||
async ({ issueNumber, body }) => {
|
|
||||||
try {
|
|
||||||
Comment.assert({ issueNumber, body });
|
|
||||||
|
|
||||||
const githubInstallationToken = process.env.GITHUB_INSTALLATION_TOKEN;
|
|
||||||
if (!githubInstallationToken) {
|
|
||||||
throw new Error("GITHUB_INSTALLATION_TOKEN environment variable is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve repository context from environment
|
|
||||||
const repoContext = resolveRepoContext();
|
|
||||||
|
|
||||||
const octokit = new Octokit({
|
|
||||||
auth: githubInstallationToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await octokit.rest.issues.createComment({
|
|
||||||
owner: repoContext.owner,
|
|
||||||
repo: repoContext.name,
|
|
||||||
issue_number: issueNumber,
|
|
||||||
body: body,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: JSON.stringify(
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
commentId: result.data.id,
|
|
||||||
url: result.data.html_url,
|
|
||||||
body: result.data.body,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
logError("Tool execution failed", error);
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: `Error creating comment: ${errorMessage}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
error: errorMessage,
|
|
||||||
isError: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
logError("Tool registered successfully");
|
|
||||||
} catch (error) {
|
|
||||||
logError("Failed to register tool", error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runServer() {
|
|
||||||
try {
|
|
||||||
logError("Starting MCP server...");
|
|
||||||
|
|
||||||
const transport = new StdioServerTransport();
|
|
||||||
logError("Transport created, attempting connection...");
|
|
||||||
|
|
||||||
await server.connect(transport);
|
|
||||||
logError("MCP server connected successfully");
|
|
||||||
|
|
||||||
process.on("exit", () => {
|
|
||||||
logError("Process exiting, closing server...");
|
|
||||||
server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
logError("SIGTERM received, closing server...");
|
|
||||||
server.close();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
logError("SIGINT received, closing server...");
|
|
||||||
server.close();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logError("Server startup failed", error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logError("Initializing MCP server process...");
|
|
||||||
|
|
||||||
runServer().catch((error) => {
|
|
||||||
logError("Unhandled server error", error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { cached } from "@ark/util";
|
||||||
|
import { Octokit } from "@octokit/rest";
|
||||||
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
||||||
|
import type { FastMCP, Tool } from "fastmcp";
|
||||||
|
import { parseRepoContext, type RepoContext } from "../utils/github.ts";
|
||||||
|
|
||||||
|
export interface ToolResult {
|
||||||
|
content: {
|
||||||
|
type: "text";
|
||||||
|
text: string;
|
||||||
|
}[];
|
||||||
|
error?: string;
|
||||||
|
isError?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMcpContext = cached((): McpContext => {
|
||||||
|
const githubInstallationToken = process.env.GITHUB_INSTALLATION_TOKEN;
|
||||||
|
if (!githubInstallationToken) {
|
||||||
|
throw new Error("GITHUB_INSTALLATION_TOKEN environment variable is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...parseRepoContext(),
|
||||||
|
octokit: new Octokit({
|
||||||
|
auth: githubInstallationToken,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface McpContext extends RepoContext {
|
||||||
|
octokit: Octokit;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tool = <const params>(tool: Tool<any, StandardSchemaV1<params>>) => tool;
|
||||||
|
|
||||||
|
export const addTools = (server: FastMCP, tools: Tool<any, any>[]) => {
|
||||||
|
for (const tool of tools) {
|
||||||
|
server.addTool(tool);
|
||||||
|
}
|
||||||
|
return server;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const contextualize =
|
||||||
|
<T>(executor: (params: T, ctx: McpContext) => Promise<Record<string, any>>) =>
|
||||||
|
async (params: T): Promise<ToolResult> => {
|
||||||
|
try {
|
||||||
|
const ctx = getMcpContext();
|
||||||
|
const result = await executor(params, ctx);
|
||||||
|
return handleToolSuccess(result);
|
||||||
|
} catch (error) {
|
||||||
|
return handleToolError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToolSuccess = (data: Record<string, any>): ToolResult => {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(data, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToolError = (error: unknown): ToolResult => {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Error: ${errorMessage}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
error: errorMessage,
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
+12
-21
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.30",
|
"version": "0.0.57",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
@@ -12,39 +12,30 @@
|
|||||||
"main.js",
|
"main.js",
|
||||||
"main.d.ts"
|
"main.d.ts"
|
||||||
],
|
],
|
||||||
"directories": {
|
|
||||||
"example": "examples"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"build": "node esbuild.config.js",
|
|
||||||
"build:npm": "zshy",
|
|
||||||
"build:dev": "node esbuild.config.js",
|
|
||||||
"prepare": "husky",
|
|
||||||
"play": "node play.ts",
|
"play": "node play.ts",
|
||||||
"upDeps": "pnpm up --latest",
|
"upDeps": "pnpm up --latest",
|
||||||
"createLockfile": "pnpm --ignore-workspace install"
|
"lock": "pnpm --ignore-workspace install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ark/fs": "0.49.0",
|
|
||||||
"@actions/core": "^1.11.1",
|
"@actions/core": "^1.11.1",
|
||||||
"@modelcontextprotocol/sdk": "^1.17.5",
|
"@ark/fs": "0.50.0",
|
||||||
|
"@ark/util": "0.50.0",
|
||||||
"@octokit/rest": "^22.0.0",
|
"@octokit/rest": "^22.0.0",
|
||||||
"@octokit/webhooks-types": "^7.6.1",
|
"@octokit/webhooks-types": "^7.6.1",
|
||||||
"arktype": "^2.1.22",
|
"@standard-schema/spec": "1.0.0",
|
||||||
"dotenv": "^17.2.2",
|
"arktype": "^2.1.23",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
"execa": "^9.6.0",
|
"execa": "^9.6.0",
|
||||||
"table": "^6.9.0",
|
"fastmcp": "^3.20.0",
|
||||||
"zod": "^3.24.4"
|
"table": "^6.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^24.7.2",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
"esbuild": "^0.25.9",
|
"typescript": "^5.9.3"
|
||||||
"husky": "^9.0.0",
|
|
||||||
"typescript": "^5.3.0",
|
|
||||||
"zshy": "^0.4.1"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -52,7 +43,7 @@
|
|||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/pullfrog/action/issues"
|
"url": "https://github.com/pullfrog/action/issues"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ import { pathToFileURL } from "node:url";
|
|||||||
import { fromHere } from "@ark/fs";
|
import { fromHere } from "@ark/fs";
|
||||||
import arg from "arg";
|
import arg from "arg";
|
||||||
import { config } from "dotenv";
|
import { config } from "dotenv";
|
||||||
import { main } from "./main.ts";
|
import { type Inputs, main } from "./main.ts";
|
||||||
import packageJson from "./package.json" with { type: "json" };
|
import packageJson from "./package.json" with { type: "json" };
|
||||||
import { runAct } from "./utils/act.ts";
|
import { runAct } from "./utils/act.ts";
|
||||||
import { setupGitHubInstallationToken } from "./utils/github.ts";
|
|
||||||
import { setupTestRepo } from "./utils/setup.ts";
|
import { setupTestRepo } from "./utils/setup.ts";
|
||||||
|
|
||||||
config();
|
config();
|
||||||
@@ -36,38 +35,12 @@ export async function run(
|
|||||||
console.log(prompt);
|
console.log(prompt);
|
||||||
console.log("─".repeat(50));
|
console.log("─".repeat(50));
|
||||||
|
|
||||||
const { EXPECTED_INPUTS } = await import("./main.ts");
|
const inputs: Inputs = {
|
||||||
EXPECTED_INPUTS.forEach((inputName) => {
|
|
||||||
const value = process.env[inputName];
|
|
||||||
if (value) {
|
|
||||||
process.env[`INPUT_${inputName.toLowerCase()}`] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const inputs: any = {
|
|
||||||
prompt,
|
prompt,
|
||||||
anthropic_api_key: process.env.ANTHROPIC_API_KEY || "",
|
anthropic_api_key: process.env.ANTHROPIC_API_KEY,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (process.env.GITHUB_TOKEN) {
|
const result = await main(inputs);
|
||||||
inputs.github_token = process.env.GITHUB_TOKEN;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🔑 Setting up GitHub installation token...");
|
|
||||||
const installationToken = await setupGitHubInstallationToken();
|
|
||||||
inputs.github_installation_token = installationToken;
|
|
||||||
console.log("✅ GitHub installation token setup successfully");
|
|
||||||
|
|
||||||
const envWithToken = {
|
|
||||||
...process.env,
|
|
||||||
GITHUB_INSTALLATION_TOKEN: installationToken,
|
|
||||||
} as Record<string, string>;
|
|
||||||
|
|
||||||
const result = await main({
|
|
||||||
inputs,
|
|
||||||
env: envWithToken,
|
|
||||||
cwd: process.cwd(),
|
|
||||||
});
|
|
||||||
|
|
||||||
process.chdir(originalCwd);
|
process.chdir(originalCwd);
|
||||||
|
|
||||||
|
|||||||
Generated
+293
-458
File diff suppressed because it is too large
Load Diff
+16
-15
@@ -2,21 +2,22 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"rewriteRelativeImportExtensions": true,
|
"rewriteRelativeImportExtensions": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUncheckedSideEffectImports": true,
|
"noUncheckedSideEffectImports": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"exactOptionalPropertyTypes": true,
|
"exactOptionalPropertyTypes": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"stripInternal": true,
|
"stripInternal": true,
|
||||||
"moduleDetection": "force"
|
"moduleDetection": "force",
|
||||||
|
"useUnknownInCatchVariables": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-4
@@ -3,7 +3,7 @@ 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.ts";
|
import { setupTestRepo } from "./setup.ts";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@@ -19,8 +19,6 @@ export function runAct(prompt: string): void {
|
|||||||
|
|
||||||
config({ path: envPath });
|
config({ path: envPath });
|
||||||
|
|
||||||
buildAction(actionPath);
|
|
||||||
|
|
||||||
const workflowPath = join(tempDir, ".github", "workflows", "pullfrog.yml");
|
const workflowPath = join(tempDir, ".github", "workflows", "pullfrog.yml");
|
||||||
|
|
||||||
const distPath = join(actionPath, ".act-dist");
|
const distPath = join(actionPath, ".act-dist");
|
||||||
@@ -54,7 +52,6 @@ export function runAct(prompt: string): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const actCommand = actCommandParts.join(" ");
|
const actCommand = actCommandParts.join(" ");
|
||||||
|
|
||||||
console.log("🚀 Running act with prompt:");
|
console.log("🚀 Running act with prompt:");
|
||||||
|
|||||||
+26
-5
@@ -1,6 +1,5 @@
|
|||||||
import { createSign } from "node:crypto";
|
import { createSign } from "node:crypto";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { resolveRepoContext } from "./repo-context.ts";
|
|
||||||
|
|
||||||
export interface InstallationToken {
|
export interface InstallationToken {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -55,7 +54,7 @@ function isGitHubActionsEnvironment(): boolean {
|
|||||||
|
|
||||||
async function acquireTokenViaOIDC(): Promise<string> {
|
async function acquireTokenViaOIDC(): Promise<string> {
|
||||||
core.info("Generating OIDC token...");
|
core.info("Generating OIDC token...");
|
||||||
|
|
||||||
const oidcToken = await core.getIDToken("pullfrog-api");
|
const oidcToken = await core.getIDToken("pullfrog-api");
|
||||||
core.info("OIDC token generated successfully");
|
core.info("OIDC token generated successfully");
|
||||||
|
|
||||||
@@ -208,7 +207,7 @@ const findInstallationId = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function acquireTokenViaGitHubApp(): Promise<string> {
|
async function acquireTokenViaGitHubApp(): Promise<string> {
|
||||||
const repoContext = resolveRepoContext();
|
const repoContext = parseRepoContext();
|
||||||
|
|
||||||
const config: GitHubAppConfig = {
|
const config: GitHubAppConfig = {
|
||||||
appId: process.env.GITHUB_APP_ID!,
|
appId: process.env.GITHUB_APP_ID!,
|
||||||
@@ -244,9 +243,31 @@ export async function setupGitHubInstallationToken(): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const token = await acquireNewToken();
|
const token = await acquireNewToken();
|
||||||
|
|
||||||
core.setSecret(token);
|
core.setSecret(token);
|
||||||
process.env.GITHUB_INSTALLATION_TOKEN = token;
|
process.env.GITHUB_INSTALLATION_TOKEN = token;
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RepoContext {
|
||||||
|
owner: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse repository context from GITHUB_REPOSITORY environment variable.
|
||||||
|
*/
|
||||||
|
export function parseRepoContext(): RepoContext {
|
||||||
|
const githubRepo = process.env.GITHUB_REPOSITORY;
|
||||||
|
if (!githubRepo) {
|
||||||
|
throw new Error("GITHUB_REPOSITORY environment variable is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [owner, name] = githubRepo.split("/");
|
||||||
|
if (!owner || !name) {
|
||||||
|
throw new Error(`Invalid GITHUB_REPOSITORY format: ${githubRepo}. Expected 'owner/repo'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { owner, name };
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
export interface RepoContext {
|
|
||||||
owner: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve repository context from GITHUB_REPOSITORY environment variable.
|
|
||||||
* Throws if not available.
|
|
||||||
*/
|
|
||||||
export function resolveRepoContext(): RepoContext {
|
|
||||||
const githubRepo = process.env.GITHUB_REPOSITORY;
|
|
||||||
if (!githubRepo) {
|
|
||||||
throw new Error('GITHUB_REPOSITORY environment variable is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
const [owner, name] = githubRepo.split('/');
|
|
||||||
if (!owner || !name) {
|
|
||||||
throw new Error(`Invalid GITHUB_REPOSITORY format: ${githubRepo}. Expected 'owner/repo'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { owner, name };
|
|
||||||
}
|
|
||||||
+26
-7
@@ -1,5 +1,6 @@
|
|||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
import { existsSync, rmSync } from "node:fs";
|
import { existsSync, rmSync } from "node:fs";
|
||||||
|
import type { RepoContext } from "./github.ts";
|
||||||
|
|
||||||
export interface SetupOptions {
|
export interface SetupOptions {
|
||||||
tempDir: string;
|
tempDir: string;
|
||||||
@@ -38,12 +39,30 @@ export function setupTestRepo(options: SetupOptions): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the action bundles
|
* Setup git configuration to avoid identity errors
|
||||||
*/
|
*/
|
||||||
export function buildAction(actionPath: string): void {
|
export function setupGitConfig(): void {
|
||||||
console.log("🔨 Building fresh bundles with esbuild...");
|
console.log("🔧 Setting up git configuration...");
|
||||||
execSync("node esbuild.config.js", {
|
execSync('git config --global user.email "action@pullfrog.ai"', { stdio: "inherit" });
|
||||||
cwd: actionPath,
|
execSync('git config --global user.name "Pullfrog Action"', { stdio: "inherit" });
|
||||||
stdio: "inherit",
|
}
|
||||||
});
|
|
||||||
|
/**
|
||||||
|
* Setup git authentication using GitHub token
|
||||||
|
*/
|
||||||
|
export function setupGitAuth(githubToken: string, repoContext: RepoContext): void {
|
||||||
|
console.log("🔐 Setting up git authentication...");
|
||||||
|
|
||||||
|
// Remove existing git auth headers that actions/checkout might have set
|
||||||
|
try {
|
||||||
|
execSync("git config --unset-all http.https://github.com/.extraheader", { stdio: "inherit" });
|
||||||
|
console.log("✓ Removed existing authentication headers");
|
||||||
|
} catch {
|
||||||
|
console.log("No existing authentication headers to remove");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update remote URL to embed the token
|
||||||
|
const remoteUrl = `https://x-access-token:${githubToken}@github.com/${repoContext.owner}/${repoContext.name}.git`;
|
||||||
|
execSync(`git remote set-url origin "${remoteUrl}"`, { stdio: "inherit" });
|
||||||
|
console.log("✓ Updated remote URL with authentication token");
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -6,6 +6,7 @@ export interface SpawnOptions {
|
|||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
input?: string;
|
input?: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
cwd?: string;
|
||||||
onStdout?: (chunk: string) => void;
|
onStdout?: (chunk: string) => void;
|
||||||
onStderr?: (chunk: string) => void;
|
onStderr?: (chunk: string) => void;
|
||||||
}
|
}
|
||||||
@@ -21,7 +22,7 @@ export interface SpawnResult {
|
|||||||
* Spawn a subprocess with streaming callbacks and buffered results
|
* Spawn a subprocess with streaming callbacks and buffered results
|
||||||
*/
|
*/
|
||||||
export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
||||||
const { cmd, args, env, input, timeout, onStdout, onStderr } = options;
|
const { cmd, args, env, input, timeout, cwd, onStdout, onStderr } = options;
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
let stdoutBuffer = "";
|
let stdoutBuffer = "";
|
||||||
@@ -31,6 +32,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
const child = nodeSpawn(cmd, args, {
|
const child = nodeSpawn(cmd, args, {
|
||||||
env: env ? { ...process.env, ...env } : process.env,
|
env: env ? { ...process.env, ...env } : process.env,
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
cwd: cwd || process.cwd(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let timeoutId: NodeJS.Timeout | undefined;
|
let timeoutId: NodeJS.Timeout | undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user