Compare commits

...

7 Commits

Author SHA1 Message Date
David Blass cacf9674c4 remove pnpm latest 2025-11-05 13:53:55 -05:00
David Blass f73260e3e6 remove pnpm cache 2025-11-05 13:50:47 -05:00
David Blass 3ddd6db7ca add mode, comment edit prompting 2025-11-05 11:08:44 -05:00
David Blass 68499340e4 add todo 2025-11-02 14:30:42 -05:00
David Blass acb06634be rely primarily on inline pr feedback 2025-10-31 04:04:06 -04:00
David Blass 681e08557c improve agent api 2025-10-31 03:15:51 -04:00
David Blass 15a7154aea improve logging 2025-10-31 01:58:43 -04:00
21 changed files with 307 additions and 225 deletions
-3
View File
@@ -20,13 +20,10 @@ runs:
fetch-depth: 1 fetch-depth: 1
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with:
version: latest
- name: Setup Node.js 24 - name: Setup Node.js 24
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "24" node-version: "24"
cache: "pnpm"
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
shell: bash shell: bash
+7 -49
View File
@@ -1,56 +1,16 @@
import * as core from "@actions/core";
import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk"; import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
import { createMcpConfig } from "../mcp/config.ts";
import { log } from "../utils/cli.ts"; import { log } from "../utils/cli.ts";
import { debugLog, isDebug } from "../utils/logging.ts"; import { type Agent, instructions } from "./shared.ts";
import { instructions } from "./shared.ts";
import type { Agent, AgentConfig, AgentResult } from "./types.ts";
/** export const claude: Agent = {
* Claude Code agent implementation run: async ({ prompt, mcpServers, apiKey }) => {
*/ process.env.ANTHROPIC_API_KEY = apiKey;
export class ClaudeAgent implements Agent {
private apiKey: string;
private githubInstallationToken: string;
constructor(config: AgentConfig) {
this.apiKey = config.apiKey;
this.githubInstallationToken = config.githubInstallationToken;
}
/**
* Install is a no-op since Claude CLI is bundled with the SDK
*/
async install(): Promise<void> {
// No installation needed - CLI is bundled with @anthropic-ai/claude-agent-sdk
}
/**
* Execute Claude Code with the given prompt using the SDK
*/
async execute(prompt: string): Promise<AgentResult> {
log.info("Running Claude Agent SDK...");
log.box(prompt, { title: "Prompt" });
const mcpConfig = JSON.parse(createMcpConfig(this.githubInstallationToken));
if (isDebug()) {
debugLog(`📋 MCP Config: ${JSON.stringify(mcpConfig, null, 2)}`);
}
// Initialize session
core.info(`🚀 Starting Claude Agent SDK session...`);
// Set API key environment variable for SDK
process.env.ANTHROPIC_API_KEY = this.apiKey;
// Create the query with SDK options
const queryInstance = query({ const queryInstance = query({
prompt: `${instructions}\n\n${prompt}`, prompt: `${instructions}\n\n${prompt}`,
options: { options: {
permissionMode: "bypassPermissions", permissionMode: "bypassPermissions",
mcpServers: mcpConfig.mcpServers, mcpServers,
}, },
}); });
@@ -60,14 +20,12 @@ export class ClaudeAgent implements Agent {
await handler(message as never); await handler(message as never);
} }
log.success("Task complete.");
return { return {
success: true, success: true,
output: "", output: "",
}; };
} },
} };
type SDKMessageType = SDKMessage["type"]; type SDKMessageType = SDKMessage["type"];
+56 -8
View File
@@ -1,15 +1,63 @@
import { mcpServerName } from "../mcp/config.ts"; import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { ghPullfrogMcpName } 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 * Result returned by agent execution
*/
export interface AgentResult {
success: boolean;
output?: string;
error?: string;
metadata?: Record<string, unknown>;
}
/**
* Configuration for agent creation
*/
export interface AgentConfig {
apiKey: string;
githubInstallationToken: string;
prompt: string;
mcpServers: Record<string, McpServerConfig>;
}
export type Agent = {
run: (config: AgentConfig) => Promise<AgentResult>;
};
export const instructions = `- use the ${ghPullfrogMcpName} MCP server to interact with github
- if ${ghPullfrogMcpName} is not available or doesn't include the functionality you need, describe why and bail
- do not under any circumstances use the gh cli - 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, - 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 also respond to the original comment with a very brief message containing a link to it
- mode selection: choose the appropriate mode based on the prompt payload:
- choose "plan mode" if the prompt asks to:
- create a plan, break down tasks, outline steps, or analyze requirements
- understand the scope of work before implementation
- provide a todo list or task breakdown
- choose "implement" if the prompt asks to:
- implement, build, create, or develop code changes
- make specific changes to files or features
- execute a plan that was previously created
- the prompt includes specific implementation details or requirements
- choose "review" if the prompt asks to:
- review code, PR, or implementation
- provide feedback, suggestions, or identify issues
- check code quality, style, or correctness
- once you've chosen a mode, follow its associated prompts carefully
- when prompted directly (e.g., via issue comment or PR comment):
(1) start by creating a single response comment using mcp__${ghPullfrogMcpName}__create_issue_comment
- the initial comment should say something like "I'll do {summary of request}" where you summarize what was requested
- save the commentId returned from this initial comment creation
(2) use mcp__${ghPullfrogMcpName}__edit_issue_comment to progressively update that same comment as you make progress
- update the comment with current status, completed tasks, and any relevant information
- continue updating the same comment throughout the planning/implementation process
(3) create_issue_comment should only be used once initially - all subsequent updates must use edit_issue_comment with the saved commentId
- if prompted to review a PR: - if prompted to review a PR:
(1) get PR info with mcp__${mcpServerName}__get_pull_request (1) get PR info with mcp__${ghPullfrogMcpName}__get_pull_request (this automatically prepares the repository by fetching and checking out the PR branch)
(2) fetch both branches: git fetch origin <base> --depth=20 && git fetch origin <head> (2) view diff: git diff origin/<base>...origin/<head> (use line numbers from this for inline comments)
(3) checkout the PR branch: git checkout origin/<head> (you MUST do this before reading any files) (3) read files from the checked-out PR branch to understand the implementation
(4) view diff: git diff origin/<base>...origin/<head> (this shows what changed) (4) when submitting review: use the 'comments' array for ALL specific code issues - include the file path and line position from the diff
(5) read files from the checked-out PR branch to understand the implementation (5) only use the 'body' field for a brief summary (1-2 sentences) or for feedback that doesn't apply to a specific code location
replace <base> and <head> with 'base' and 'head' from the PR info replace <base> and <head> with 'base' and 'head' from the PR info
`; `;
-34
View File
@@ -1,34 +0,0 @@
/**
* Standard interface for all Pullfrog agents
*/
export interface Agent {
/**
* Install the agent and any required dependencies
*/
install(): Promise<void>;
/**
* Execute the agent with the given prompt
* @param prompt The prompt to send to the agent
* @param options Additional options specific to the agent
*/
execute(prompt: string, options?: Record<string, any>): Promise<AgentResult>;
}
/**
* Result returned by agent execution
*/
export interface AgentResult {
success: boolean;
output?: string;
error?: string;
metadata?: Record<string, any>;
}
/**
* Configuration for agent creation
*/
export interface AgentConfig {
apiKey: string;
githubInstallationToken: string;
}
+1 -2
View File
@@ -3,8 +3,7 @@
* This exports the main function for programmatic usage * This exports the main function for programmatic usage
*/ */
export { ClaudeAgent } from "./agents/claude.ts"; export type { Agent, AgentConfig, AgentResult } from "./agents/shared.ts";
export type { Agent, AgentConfig, AgentResult } from "./agents/types.ts";
export { export {
type Inputs as ExecutionInputs, type Inputs as ExecutionInputs,
type MainResult, type MainResult,
+20 -9
View File
@@ -1,6 +1,7 @@
import * as core from "@actions/core";
import { type } from "arktype"; import { type } from "arktype";
import { ClaudeAgent } from "./agents/claude.ts"; import { claude } from "./agents/claude.ts";
import { createMcpConfigs } from "./mcp/config.ts";
import { log } from "./utils/cli.ts";
import { parseRepoContext, setupGitHubInstallationToken } from "./utils/github.ts"; import { parseRepoContext, setupGitHubInstallationToken } from "./utils/github.ts";
import { setupGitAuth, setupGitConfig } from "./utils/setup.ts"; import { setupGitAuth, setupGitConfig } from "./utils/setup.ts";
@@ -19,7 +20,7 @@ export interface MainResult {
export async function main(inputs: Inputs): Promise<MainResult> { export async function main(inputs: Inputs): Promise<MainResult> {
try { try {
core.info(`Starting agent run with Claude Code`); log.info("Starting agent run...");
setupGitConfig(); setupGitConfig();
@@ -28,15 +29,20 @@ export async function main(inputs: Inputs): Promise<MainResult> {
setupGitAuth(githubInstallationToken, repoContext); setupGitAuth(githubInstallationToken, repoContext);
const agent = new ClaudeAgent({ const mcpServers = createMcpConfigs(githubInstallationToken);
apiKey: inputs.anthropic_api_key!,
log.debug(`📋 MCP Config: ${JSON.stringify(mcpServers, null, 2)}`);
log.info("Running Claude Agent SDK...");
log.box(inputs.prompt, { title: "Prompt" });
const result = await claude.run({
prompt: inputs.prompt,
mcpServers,
githubInstallationToken, githubInstallationToken,
apiKey: inputs.anthropic_api_key!,
}); });
await agent.install();
const result = await agent.execute(inputs.prompt);
if (!result.success) { if (!result.success) {
return { return {
success: false, success: false,
@@ -45,12 +51,17 @@ export async function main(inputs: Inputs): Promise<MainResult> {
}; };
} }
log.success("Task complete.");
await log.writeSummary();
return { return {
success: true, success: true,
output: result.output || "", output: result.output || "",
}; };
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
log.error(errorMessage);
await log.writeSummary();
return { return {
success: false, success: false,
error: errorMessage, error: errorMessage,
+28 -1
View File
@@ -6,7 +6,7 @@ export const Comment = type({
body: type.string.describe("the comment body content"), body: type.string.describe("the comment body content"),
}); });
export const CommentTool = tool({ export const CreateCommentTool = tool({
name: "create_issue_comment", name: "create_issue_comment",
description: "Create a comment on a GitHub issue", description: "Create a comment on a GitHub issue",
parameters: Comment, parameters: Comment,
@@ -26,3 +26,30 @@ export const CommentTool = tool({
}; };
}), }),
}); });
export const EditComment = type({
commentId: type.number.describe("the ID of the comment to edit"),
body: type.string.describe("the new comment body content"),
});
export const EditCommentTool = tool({
name: "edit_issue_comment",
description: "Edit a GitHub issue comment by its ID",
parameters: EditComment,
execute: contextualize(async ({ commentId, body }, ctx) => {
const result = await ctx.octokit.rest.issues.updateComment({
owner: ctx.owner,
repo: ctx.name,
comment_id: commentId,
body: body,
});
return {
success: true,
commentId: result.data.id,
url: result.data.html_url,
body: result.data.body,
updatedAt: result.data.updated_at,
};
}),
});
+16 -17
View File
@@ -1,32 +1,31 @@
/** /**
* Simple MCP configuration helper for adding our minimal GitHub comment server * Simple MCP configuration helper for adding our minimal GitHub comment server
*/ */
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { fromHere } from "@ark/fs"; import { fromHere } from "@ark/fs";
import { parseRepoContext } from "../utils/github.ts"; import { parseRepoContext } from "../utils/github.ts";
const actionPath = fromHere(".."); const actionPath = fromHere("..");
export const mcpServerName = "gh-pullfrog"; export const ghPullfrogMcpName = "gh-pullfrog";
export function createMcpConfig(githubInstallationToken: string) { export type McpName = typeof ghPullfrogMcpName;
export type McpConfigs = Record<McpName, McpServerConfig>;
export function createMcpConfigs(githubInstallationToken: string): McpConfigs {
const repoContext = parseRepoContext(); const repoContext = parseRepoContext();
const githubRepository = `${repoContext.owner}/${repoContext.name}`; const githubRepository = `${repoContext.owner}/${repoContext.name}`;
return JSON.stringify( return {
{ [ghPullfrogMcpName]: {
mcpServers: { command: "node",
[mcpServerName]: { args: [`${actionPath}/mcp/server.ts`],
command: "node", env: {
args: [`${actionPath}/mcp/server.ts`], GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
env: { GITHUB_REPOSITORY: githubRepository,
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
GITHUB_REPOSITORY: githubRepository,
LOG_LEVEL: process.env.LOG_LEVEL,
},
},
}, },
}, },
null, };
2
);
} }
+2 -1
View File
@@ -1,5 +1,6 @@
import { execSync } from "node:child_process"; import { execSync } from "node:child_process";
import { type } from "arktype"; import { type } from "arktype";
import { log } from "../utils/cli.ts";
import { contextualize, tool } from "./shared.ts"; import { contextualize, tool } from "./shared.ts";
export const PullRequest = type({ export const PullRequest = type({
@@ -18,7 +19,7 @@ export const PullRequestTool = tool({
encoding: "utf8", encoding: "utf8",
}).trim(); }).trim();
console.log(`Current branch: ${currentBranch}`); log.info(`Current branch: ${currentBranch}`);
const result = await ctx.octokit.rest.pulls.create({ const result = await ctx.octokit.rest.pulls.create({
owner: ctx.owner, owner: ctx.owner,
+19 -2
View File
@@ -1,4 +1,6 @@
import { execSync } from "node:child_process";
import { type } from "arktype"; import { type } from "arktype";
import { log } from "../utils/cli.ts";
import { contextualize, tool } from "./shared.ts"; import { contextualize, tool } from "./shared.ts";
export const PullRequestInfo = type({ export const PullRequestInfo = type({
@@ -7,7 +9,8 @@ export const PullRequestInfo = type({
export const PullRequestInfoTool = tool({ export const PullRequestInfoTool = tool({
name: "get_pull_request", name: "get_pull_request",
description: "Retrieve minimal information for a specific pull request by number.", description:
"Retrieve PR information and automatically prepare the repository for review by fetching and checking out the PR branch.",
parameters: PullRequestInfo, parameters: PullRequestInfo,
execute: contextualize(async ({ pull_number }, ctx) => { execute: contextualize(async ({ pull_number }, ctx) => {
const pr = await ctx.octokit.rest.pulls.get({ const pr = await ctx.octokit.rest.pulls.get({
@@ -17,10 +20,24 @@ export const PullRequestInfoTool = tool({
}); });
const data = pr.data; const data = pr.data;
const baseBranch = data.base.ref; const baseBranch = data.base.ref;
const headBranch = data.head.ref; const headBranch = data.head.ref;
if (!baseBranch) {
throw new Error(`Base branch not found for PR #${pull_number}`);
}
// Automatically fetch and checkout branches for review
log.info(`Fetching base branch: origin/${baseBranch}`);
execSync(`git fetch origin ${baseBranch} --depth=20`, { stdio: "inherit" });
log.info(`Fetching PR branch: origin/${headBranch}`);
execSync(`git fetch origin ${headBranch}`, { stdio: "inherit" });
log.info(`Checking out PR branch: origin/${headBranch}`);
execSync(`git checkout origin/${headBranch}`, { stdio: "inherit" });
return { return {
number: data.number, number: data.number,
url: data.html_url, url: data.html_url,
+64 -16
View File
@@ -1,28 +1,58 @@
import type { RestEndpointMethodTypes } from "@octokit/rest";
import { type } from "arktype"; import { type } from "arktype";
import { contextualize, tool } from "./shared.ts"; import { contextualize, tool } from "./shared.ts";
import type { RestEndpointMethodTypes } from "@octokit/rest";
export const Review = type({ export const Review = type({
pull_number: type.number.describe("The pull request number to review"), pull_number: type.number.describe("The pull request number to review"),
event: type.enumerated("APPROVE", "REQUEST_CHANGES", "COMMENT").describe("'APPROVE', 'REQUEST_CHANGES', or 'COMMENT' (the review action)"), event: type
body: type.string.describe("The body content for the review (required for REQUEST_CHANGES or COMMENT)").optional(), .enumerated("APPROVE", "REQUEST_CHANGES", "COMMENT")
commit_id: type.string.describe("Optional SHA of the commit being reviewed. Defaults to latest.").optional(), .describe("'APPROVE', 'REQUEST_CHANGES', or 'COMMENT' (the review action)"),
comments: type body: type.string
({ .describe(
path: type.string.describe("The file path to comment on"), "Brief summary or general feedback that doesn't apply to specific code locations. Keep it concise - most feedback should be in the 'comments' array."
position: type.number.describe("The diff position in the file"), )
body: type.string.describe("The comment text"), .optional(),
}) commit_id: type.string
.array() .describe("Optional SHA of the commit being reviewed. Defaults to latest.")
.describe("Array of draft review comments for specific lines, optional.") .optional(),
.optional() comments: type({
path: type.string.describe("The file path to comment on (relative to repo root)"),
line: type.number.describe(
"The line number in the file (use line numbers from the diff - usually the RIGHT side/new code)"
),
side: type
.enumerated("LEFT", "RIGHT")
.describe(
"Side of the diff: LEFT (old code) or RIGHT (new code). Defaults to RIGHT if not provided."
)
.optional(),
body: type.string.describe("The comment text for this specific line"),
start_line: type.number
.describe("Start line for multi-line comments (optional, for commenting on ranges)")
.optional(),
})
.array()
.describe(
"REQUIRED: Array of inline comments for specific code issues. Use this for all location-specific feedback. Use 'git diff origin/<base>...origin/<head>' to find the correct line numbers (typically use the line numbers shown on the RIGHT side for new code, LEFT side for old code)."
)
.optional(),
}); });
export const ReviewTool = tool({ export const ReviewTool = tool({
name: "submit_pull_request_review", name: "submit_pull_request_review",
description: "Submit a review (approve, request changes, or comment) for an existing pull request.", description:
"Submit a review (approve, request changes, or comment) for an existing pull request. " +
"IMPORTANT: Use 'comments' array for ALL specific code issues at the line-level. " +
"Only use 'body' for a brief summary or feedback that doesn't apply to a specific location.",
parameters: Review, parameters: Review,
execute: contextualize(async ({ pull_number, event, body, commit_id, comments = [] }, ctx) => { execute: contextualize(async ({ pull_number, event, body, commit_id, comments = [] }, ctx) => {
// Get the PR to determine the head commit if commit_id not provided
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
pull_number,
});
// Compose the request // Compose the request
const params: RestEndpointMethodTypes["pulls"]["createReview"]["parameters"] = { const params: RestEndpointMethodTypes["pulls"]["createReview"]["parameters"] = {
owner: ctx.owner, owner: ctx.owner,
@@ -31,8 +61,26 @@ export const ReviewTool = tool({
event, event,
}; };
if (body) params.body = body; if (body) params.body = body;
if (commit_id) params.commit_id = commit_id; if (commit_id) {
if (comments.length > 0) params.comments = comments; params.commit_id = commit_id;
} else {
params.commit_id = pr.data.head.sha;
}
if (comments.length > 0) {
type ReviewComment = (typeof params.comments & {})[number];
// Convert comments to the format expected by GitHub API
params.comments = comments.map((comment) => {
const reviewComment: ReviewComment = {
...comment,
};
reviewComment.side = comment.side || "RIGHT";
if (comment.start_line) {
reviewComment.start_line = comment.start_line;
reviewComment.start_side = comment.side || "RIGHT";
}
return reviewComment;
});
}
const result = await ctx.octokit.rest.pulls.createReview(params); const result = await ctx.octokit.rest.pulls.createReview(params);
return { return {
success: true, success: true,
+10 -3
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env node #!/usr/bin/env node
// Minimal GitHub Issue Comment MCP Server // Minimal GitHub Issue Comment MCP Server
import { FastMCP } from "fastmcp"; import { FastMCP } from "fastmcp";
import { CommentTool } from "./comment.ts"; import { CreateCommentTool, EditCommentTool } from "./comment.ts";
import { IssueTool } from "./issue.ts"; import { IssueTool } from "./issue.ts";
import { PullRequestTool } from "./pr.ts"; import { PullRequestTool } from "./pr.ts";
import { ReviewTool } from "./review.ts";
import { PullRequestInfoTool } from "./prInfo.ts"; import { PullRequestInfoTool } from "./prInfo.ts";
import { ReviewTool } from "./review.ts";
import { addTools } from "./shared.ts"; import { addTools } from "./shared.ts";
const server = new FastMCP({ const server = new FastMCP({
@@ -13,6 +13,13 @@ const server = new FastMCP({
version: "0.0.1", version: "0.0.1",
}); });
addTools(server, [CommentTool, IssueTool, PullRequestTool, ReviewTool, PullRequestInfoTool]); addTools(server, [
CreateCommentTool,
EditCommentTool,
IssueTool,
PullRequestTool,
ReviewTool,
PullRequestInfoTool,
]);
server.start(); server.start();
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@pullfrog/action", "name": "@pullfrog/action",
"version": "0.0.65", "version": "0.0.70",
"type": "module", "type": "module",
"files": [ "files": [
"index.js", "index.js",
@@ -22,12 +22,12 @@
"dependencies": { "dependencies": {
"@actions/core": "^1.11.1", "@actions/core": "^1.11.1",
"@anthropic-ai/claude-agent-sdk": "^0.1.26", "@anthropic-ai/claude-agent-sdk": "^0.1.26",
"@ark/fs": "0.50.0", "@ark/fs": "0.53.0",
"@ark/util": "0.50.0", "@ark/util": "0.53.0",
"@octokit/rest": "^22.0.0", "@octokit/rest": "^22.0.0",
"@octokit/webhooks-types": "^7.6.1", "@octokit/webhooks-types": "^7.6.1",
"@standard-schema/spec": "1.0.0", "@standard-schema/spec": "1.0.0",
"arktype": "^2.1.25", "arktype": "2.1.25",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"execa": "^9.6.0", "execa": "^9.6.0",
"fastmcp": "^3.20.0", "fastmcp": "^3.20.0",
+1 -1
View File
@@ -59,7 +59,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
}); });
if (args["--help"]) { if (args["--help"]) {
console.log(` log.info(`
Usage: tsx play.ts [file] [options] Usage: tsx play.ts [file] [options]
Test the Pullfrog action with various prompts. Test the Pullfrog action with various prompts.
+8 -13
View File
@@ -15,11 +15,11 @@ importers:
specifier: ^0.1.26 specifier: ^0.1.26
version: 0.1.30(zod@3.25.76) version: 0.1.30(zod@3.25.76)
'@ark/fs': '@ark/fs':
specifier: 0.50.0 specifier: 0.53.0
version: 0.50.0 version: 0.53.0
'@ark/util': '@ark/util':
specifier: 0.50.0 specifier: 0.53.0
version: 0.50.0 version: 0.53.0
'@octokit/rest': '@octokit/rest':
specifier: ^22.0.0 specifier: ^22.0.0
version: 22.0.0 version: 22.0.0
@@ -30,7 +30,7 @@ importers:
specifier: 1.0.0 specifier: 1.0.0
version: 1.0.0 version: 1.0.0
arktype: arktype:
specifier: ^2.1.25 specifier: 2.1.25
version: 2.1.25 version: 2.1.25
dotenv: dotenv:
specifier: ^17.2.3 specifier: ^17.2.3
@@ -75,15 +75,12 @@ packages:
peerDependencies: peerDependencies:
zod: ^3.24.1 zod: ^3.24.1
'@ark/fs@0.50.0': '@ark/fs@0.53.0':
resolution: {integrity: sha512-6OrxNt2T+/pL4RUMZK/aiVRLIS3acNs5uSpHDyZAP6+OXwXchFSQ1lJTH+uuGBlDWeQxPD7VmymYXLF5s1Eyhw==} resolution: {integrity: sha512-XL0EbBAZgyy+j9aPhftYaBsbKAW5PTNSKCN6oLRRdrHuHPSAZgR6765/z0YZGhPxHEUNmq0vBoSk8yOLk91dNQ==}
'@ark/schema@0.53.0': '@ark/schema@0.53.0':
resolution: {integrity: sha512-1PB7RThUiTlmIu8jbSurPrhHpVixPd4C+xNBUF/HrjIENCeDcAMg36n5mpMzED7OQGDVIzpfXXiMnaTiutjHJw==} resolution: {integrity: sha512-1PB7RThUiTlmIu8jbSurPrhHpVixPd4C+xNBUF/HrjIENCeDcAMg36n5mpMzED7OQGDVIzpfXXiMnaTiutjHJw==}
'@ark/util@0.50.0':
resolution: {integrity: sha512-tIkgIMVRpkfXRQIEf0G2CJryZVtHVrqcWHMDa5QKo0OEEBu0tHkRSIMm4Ln8cd8Bn9TPZtvc/kE2Gma8RESPSg==}
'@ark/util@0.53.0': '@ark/util@0.53.0':
resolution: {integrity: sha512-TGn4gLlA6dJcQiqrtCtd88JhGb2XBHo6qIejsDre+nxpGuUVW4G3YZGVrwjNBTO0EyR+ykzIo4joHJzOj+/cpA==} resolution: {integrity: sha512-TGn4gLlA6dJcQiqrtCtd88JhGb2XBHo6qIejsDre+nxpGuUVW4G3YZGVrwjNBTO0EyR+ykzIo4joHJzOj+/cpA==}
@@ -879,14 +876,12 @@ snapshots:
'@img/sharp-linux-x64': 0.33.5 '@img/sharp-linux-x64': 0.33.5
'@img/sharp-win32-x64': 0.33.5 '@img/sharp-win32-x64': 0.33.5
'@ark/fs@0.50.0': {} '@ark/fs@0.53.0': {}
'@ark/schema@0.53.0': '@ark/schema@0.53.0':
dependencies: dependencies:
'@ark/util': 0.53.0 '@ark/util': 0.53.0
'@ark/util@0.50.0': {}
'@ark/util@0.53.0': {} '@ark/util@0.53.0': {}
'@borewit/text-codec@0.1.1': {} '@borewit/text-codec@0.1.1': {}
+4
View File
@@ -0,0 +1,4 @@
[] add modes to prompt
[] progressively update comment
[] don't allow rejecting prs
[] fix pnpm caching
+51 -5
View File
@@ -5,6 +5,7 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
const isGitHubActions = !!process.env.GITHUB_ACTIONS; const isGitHubActions = !!process.env.GITHUB_ACTIONS;
const isDebugEnabled = process.env.LOG_LEVEL === "debug";
/** /**
* Print a formatted box with text (for console output) * Print a formatted box with text (for console output)
@@ -87,7 +88,12 @@ function box(
maxWidth?: number; maxWidth?: number;
} }
): void { ): void {
core.info(boxString(text, options)); const boxContent = boxString(text, options);
core.info(boxContent);
if (isGitHubActions) {
// Add as markdown code block for summary (no headers)
core.summary.addRaw(`\`\`\`\n${text}\n\`\`\`\n`);
}
} }
/** /**
@@ -115,10 +121,10 @@ async function summaryTable(
if (isGitHubActions) { if (isGitHubActions) {
const summary = core.summary; const summary = core.summary;
if (title) { if (title) {
summary.addHeading(title); summary.addRaw(`**${title}**\n\n`);
} }
summary.addTable(formattedRows); summary.addTable(formattedRows);
await summary.write(); // Note: Don't write immediately, let it accumulate with other summary content
} }
// Also log to console for visibility // Also log to console for visibility
@@ -133,7 +139,11 @@ async function summaryTable(
* Print a separator line * Print a separator line
*/ */
function separator(length: number = 50): void { function separator(length: number = 50): void {
core.info("─".repeat(length)); const separatorText = "─".repeat(length);
core.info(separatorText);
if (isGitHubActions) {
core.summary.addRaw(`---\n`);
}
} }
/** /**
@@ -145,6 +155,9 @@ export const log = {
*/ */
info: (message: string): void => { info: (message: string): void => {
core.info(message); core.info(message);
if (isGitHubActions) {
core.summary.addRaw(`${message}\n`);
}
}, },
/** /**
@@ -152,6 +165,9 @@ export const log = {
*/ */
warning: (message: string): void => { warning: (message: string): void => {
core.warning(message); core.warning(message);
if (isGitHubActions) {
core.summary.addRaw(`⚠️ ${message}\n`);
}
}, },
/** /**
@@ -159,13 +175,33 @@ export const log = {
*/ */
error: (message: string): void => { error: (message: string): void => {
core.error(message); core.error(message);
if (isGitHubActions) {
core.summary.addRaw(`${message}\n`);
}
}, },
/** /**
* Print success message * Print success message
*/ */
success: (message: string): void => { success: (message: string): void => {
core.info(`${message}`); const successMessage = `${message}`;
core.info(successMessage);
if (isGitHubActions) {
core.summary.addRaw(`${successMessage}\n`);
}
},
/**
* Print debug message (only if LOG_LEVEL=debug)
*/
debug: (message: string): void => {
if (isDebugEnabled) {
if (isGitHubActions) {
core.debug(message);
} else {
core.info(`[DEBUG] ${message}`);
}
}
}, },
/** /**
@@ -183,4 +219,14 @@ export const log = {
* Print a separator line * Print a separator line
*/ */
separator, separator,
/**
* Write all accumulated summary content to the job summary
* Call this at the end of execution to finalize the summary
*/
writeSummary: async (): Promise<void> => {
if (isGitHubActions) {
await core.summary.write();
}
},
}; };
-13
View File
@@ -1,13 +0,0 @@
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
/**
* Create a temporary file with the given content
*/
export function createTempFile(content: string, filename = "temp.txt"): string {
const tempDir = mkdtempSync(join(tmpdir(), "pullfrog-"));
const filePath = join(tempDir, filename);
writeFileSync(filePath, content);
return filePath;
}
+6 -5
View File
@@ -1,5 +1,6 @@
import { createSign } from "node:crypto"; import { createSign } from "node:crypto";
import * as core from "@actions/core"; import * as core from "@actions/core";
import { log } from "./cli.ts";
export interface InstallationToken { export interface InstallationToken {
token: string; token: string;
@@ -53,14 +54,14 @@ function isGitHubActionsEnvironment(): boolean {
} }
async function acquireTokenViaOIDC(): Promise<string> { async function acquireTokenViaOIDC(): Promise<string> {
core.info("Generating OIDC token..."); log.info("Generating OIDC token...");
const oidcToken = await core.getIDToken("pullfrog-api"); const oidcToken = await core.getIDToken("pullfrog-api");
core.info("OIDC token generated successfully"); log.info("OIDC token generated successfully");
const apiUrl = process.env.API_URL || "https://pullfrog.ai"; const apiUrl = process.env.API_URL || "https://pullfrog.ai";
core.info("Exchanging OIDC token for installation token..."); log.info("Exchanging OIDC token for installation token...");
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, { const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
method: "POST", method: "POST",
headers: { headers: {
@@ -77,7 +78,7 @@ async function acquireTokenViaOIDC(): Promise<string> {
} }
const tokenData = (await tokenResponse.json()) as InstallationToken; const tokenData = (await tokenResponse.json()) as InstallationToken;
core.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`); log.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
return tokenData.token; return tokenData.token;
} }
@@ -238,7 +239,7 @@ export async function setupGitHubInstallationToken(): Promise<string> {
const existingToken = checkExistingToken(); const existingToken = checkExistingToken();
if (existingToken) { if (existingToken) {
core.setSecret(existingToken); core.setSecret(existingToken);
core.info("Using provided GitHub installation token"); log.info("Using provided GitHub installation token");
return existingToken; return existingToken;
} }
-30
View File
@@ -1,30 +0,0 @@
/**
* Centralized debug logging utility
* Controls debug behavior based on LOG_LEVEL environment variable
*/
import * as core from "@actions/core";
const isDebugEnabled = process.env.LOG_LEVEL === "debug";
const isGitHubActions = !!process.env.GITHUB_ACTIONS;
/**
* Check if debug logging is enabled
*/
export function isDebug(): boolean {
return isDebugEnabled;
}
/**
* Log debug message if debug is enabled
* Uses core.debug() in GitHub Actions, console.log() locally
*/
export function debugLog(message: string): void {
if (isDebugEnabled) {
if (isGitHubActions) {
core.debug(message);
} else {
console.log(`[DEBUG] ${message}`);
}
}
}
+10 -9
View File
@@ -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 { log } from "./cli.ts";
import type { RepoContext } from "./github.ts"; import type { RepoContext } from "./github.ts";
export interface SetupOptions { export interface SetupOptions {
@@ -20,20 +21,20 @@ export function setupTestRepo(options: SetupOptions): void {
if (existsSync(tempDir)) { if (existsSync(tempDir)) {
if (forceClean) { if (forceClean) {
console.log("🗑️ Removing existing .temp directory..."); log.info("🗑️ Removing existing .temp directory...");
rmSync(tempDir, { recursive: true, force: true }); rmSync(tempDir, { recursive: true, force: true });
console.log("📦 Cloning pullfrogai/scratch into .temp..."); log.info("📦 Cloning pullfrogai/scratch into .temp...");
execSync(`git clone ${repoUrl} ${tempDir}`, { stdio: "inherit" }); execSync(`git clone ${repoUrl} ${tempDir}`, { stdio: "inherit" });
} else { } else {
console.log("📦 Resetting existing .temp repository..."); log.info("📦 Resetting existing .temp repository...");
execSync("git reset --hard HEAD && git clean -fd", { execSync("git reset --hard HEAD && git clean -fd", {
cwd: tempDir, cwd: tempDir,
stdio: "inherit", stdio: "inherit",
}); });
} }
} else { } else {
console.log("📦 Cloning pullfrogai/scratch into .temp..."); log.info("📦 Cloning pullfrogai/scratch into .temp...");
execSync(`git clone ${repoUrl} ${tempDir}`, { stdio: "inherit" }); execSync(`git clone ${repoUrl} ${tempDir}`, { stdio: "inherit" });
} }
} }
@@ -42,7 +43,7 @@ export function setupTestRepo(options: SetupOptions): void {
* Setup git configuration to avoid identity errors * Setup git configuration to avoid identity errors
*/ */
export function setupGitConfig(): void { export function setupGitConfig(): void {
console.log("🔧 Setting up git configuration..."); log.info("🔧 Setting up git configuration...");
execSync('git config user.email "action@pullfrog.ai"', { stdio: "inherit" }); execSync('git config user.email "action@pullfrog.ai"', { stdio: "inherit" });
execSync('git config user.name "Pullfrog Action"', { stdio: "inherit" }); execSync('git config user.name "Pullfrog Action"', { stdio: "inherit" });
} }
@@ -51,18 +52,18 @@ export function setupGitConfig(): void {
* Setup git authentication using GitHub token * Setup git authentication using GitHub token
*/ */
export function setupGitAuth(githubToken: string, repoContext: RepoContext): void { export function setupGitAuth(githubToken: string, repoContext: RepoContext): void {
console.log("🔐 Setting up git authentication..."); log.info("🔐 Setting up git authentication...");
// Remove existing git auth headers that actions/checkout might have set // Remove existing git auth headers that actions/checkout might have set
try { try {
execSync("git config --unset-all http.https://github.com/.extraheader", { stdio: "inherit" }); execSync("git config --unset-all http.https://github.com/.extraheader", { stdio: "inherit" });
console.log("✓ Removed existing authentication headers"); log.info("✓ Removed existing authentication headers");
} catch { } catch {
console.log("No existing authentication headers to remove"); log.info("No existing authentication headers to remove");
} }
// Update remote URL to embed the token // Update remote URL to embed the token
const remoteUrl = `https://x-access-token:${githubToken}@github.com/${repoContext.owner}/${repoContext.name}.git`; const remoteUrl = `https://x-access-token:${githubToken}@github.com/${repoContext.owner}/${repoContext.name}.git`;
execSync(`git remote set-url origin "${remoteUrl}"`, { stdio: "inherit" }); execSync(`git remote set-url origin "${remoteUrl}"`, { stdio: "inherit" });
console.log("✓ Updated remote URL with authentication token"); log.info("✓ Updated remote URL with authentication token");
} }