Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f73260e3e6 | |||
| 3ddd6db7ca | |||
| 68499340e4 | |||
| acb06634be | |||
| 681e08557c | |||
| 15a7154aea |
@@ -26,7 +26,6 @@ runs:
|
|||||||
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
@@ -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
@@ -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
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.65",
|
"version": "0.0.69",
|
||||||
"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",
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Generated
+8
-13
@@ -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': {}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[] add modes to prompt
|
||||||
|
[] progressively update comment
|
||||||
|
[] don't allow rejecting prs
|
||||||
|
[] fix pnpm caching
|
||||||
+51
-5
@@ -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();
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user