Compare commits

..

1 Commits

Author SHA1 Message Date
Ashwin Bhat
e35c991da9 fix: wrap github MCP config with mcpServers in issue-triage workflow
Update the MCP configuration structure to properly wrap the github
server configuration within an mcpServers object, matching the
expected format for MCP config files.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-03 11:05:51 -07:00
6 changed files with 14 additions and 246 deletions

View File

@@ -160,7 +160,6 @@ runs:
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' }} IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' }}
BASE_BRANCH: ${{ steps.prepare.outputs.BASE_BRANCH }} BASE_BRANCH: ${{ steps.prepare.outputs.BASE_BRANCH }}
CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }} CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }}
CLAUDE_CANCELLED: ${{ cancelled() }}
OUTPUT_FILE: ${{ steps.claude-code.outputs.execution_file || '' }} OUTPUT_FILE: ${{ steps.claude-code.outputs.execution_file || '' }}
TRIGGER_USERNAME: ${{ github.event.comment.user.login || github.event.issue.user.login || github.event.pull_request.user.login || github.event.sender.login || github.triggering_actor || github.actor || '' }} TRIGGER_USERNAME: ${{ github.event.comment.user.login || github.event.issue.user.login || github.event.pull_request.user.login || github.event.sender.login || github.triggering_actor || github.actor || '' }}
PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }} PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }}

View File

@@ -146,12 +146,8 @@ async function run() {
duration_api_ms?: number; duration_api_ms?: number;
} | null = null; } | null = null;
let actionFailed = false; let actionFailed = false;
let actionCancelled = false;
let errorDetails: string | undefined; let errorDetails: string | undefined;
// Check if the workflow was cancelled
const isCancelled = process.env.CLAUDE_CANCELLED === "true";
// First check if prepare step failed // First check if prepare step failed
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false"; const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
const prepareError = process.env.PREPARE_ERROR; const prepareError = process.env.PREPARE_ERROR;
@@ -159,18 +155,8 @@ async function run() {
if (!prepareSuccess && prepareError) { if (!prepareSuccess && prepareError) {
actionFailed = true; actionFailed = true;
errorDetails = prepareError; errorDetails = prepareError;
} else if (isCancelled) {
// If the workflow was cancelled, set the cancelled flag
actionCancelled = true;
} else { } else {
// Check if the Claude action failed // Check for existence of output file and parse it if available
// CLAUDE_SUCCESS is set to the result of: steps.claude-code.outputs.conclusion == 'success'
// If the step didn't run or didn't set outputs.conclusion, CLAUDE_SUCCESS will be "false"
// If the step succeeded, CLAUDE_SUCCESS will be "true"
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
actionFailed = !claudeSuccess;
// Try to read execution details from output file
try { try {
const outputFile = process.env.OUTPUT_FILE; const outputFile = process.env.OUTPUT_FILE;
if (outputFile) { if (outputFile) {
@@ -193,10 +179,14 @@ async function run() {
} }
} }
} }
// Check if the Claude action failed
const claudeSuccess = process.env.CLAUDE_SUCCESS !== "false";
actionFailed = !claudeSuccess;
} catch (error) { } catch (error) {
console.error("Error reading output file:", error); console.error("Error reading output file:", error);
// Error reading output file doesn't change the action status // If we can't read the file, check for any failure markers
// We already determined actionFailed based on CLAUDE_SUCCESS actionFailed = process.env.CLAUDE_SUCCESS === "false";
} }
} }
@@ -204,7 +194,6 @@ async function run() {
const commentInput: CommentUpdateInput = { const commentInput: CommentUpdateInput = {
currentBody, currentBody,
actionFailed, actionFailed,
actionCancelled,
executionDetails, executionDetails,
jobUrl, jobUrl,
branchLink, branchLink,

View File

@@ -1,17 +1,17 @@
import { execSync } from "child_process"; import { execSync } from "child_process";
import type { Octokits } from "../api/client";
import { ISSUE_QUERY, PR_QUERY } from "../api/queries/github";
import type { import type {
GitHubPullRequest,
GitHubIssue,
GitHubComment, GitHubComment,
GitHubFile, GitHubFile,
GitHubIssue,
GitHubPullRequest,
GitHubReview, GitHubReview,
IssueQueryResponse,
PullRequestQueryResponse, PullRequestQueryResponse,
IssueQueryResponse,
} from "../types"; } from "../types";
import type { CommentWithImages } from "../utils/image-downloader"; import { PR_QUERY, ISSUE_QUERY } from "../api/queries/github";
import type { Octokits } from "../api/client";
import { downloadCommentImages } from "../utils/image-downloader"; import { downloadCommentImages } from "../utils/image-downloader";
import type { CommentWithImages } from "../utils/image-downloader";
type FetchDataParams = { type FetchDataParams = {
octokits: Octokits; octokits: Octokits;
@@ -101,14 +101,6 @@ export async function fetchGitHubData({
let changedFilesWithSHA: GitHubFileWithSHA[] = []; let changedFilesWithSHA: GitHubFileWithSHA[] = [];
if (isPR && changedFiles.length > 0) { if (isPR && changedFiles.length > 0) {
changedFilesWithSHA = changedFiles.map((file) => { changedFilesWithSHA = changedFiles.map((file) => {
// Don't compute SHA for deleted files
if (file.changeType === "DELETED") {
return {
...file,
sha: "deleted",
};
}
try { try {
// Use git hash-object to compute the SHA for the current file content // Use git hash-object to compute the SHA for the current file content
const sha = execSync(`git hash-object "${file.path}"`, { const sha = execSync(`git hash-object "${file.path}"`, {

View File

@@ -9,7 +9,6 @@ export type ExecutionDetails = {
export type CommentUpdateInput = { export type CommentUpdateInput = {
currentBody: string; currentBody: string;
actionFailed: boolean; actionFailed: boolean;
actionCancelled?: boolean;
executionDetails: ExecutionDetails | null; executionDetails: ExecutionDetails | null;
jobUrl: string; jobUrl: string;
branchLink?: string; branchLink?: string;
@@ -75,7 +74,6 @@ export function updateCommentBody(input: CommentUpdateInput): string {
branchLink, branchLink,
prLink, prLink,
actionFailed, actionFailed,
actionCancelled,
branchName, branchName,
triggerUsername, triggerUsername,
errorDetails, errorDetails,
@@ -114,13 +112,7 @@ export function updateCommentBody(input: CommentUpdateInput): string {
// Build the header // Build the header
let header = ""; let header = "";
if (actionCancelled) { if (actionFailed) {
header = "**Claude's task was cancelled";
if (durationStr) {
header += ` after ${durationStr}`;
}
header += "**";
} else if (actionFailed) {
header = "**Claude encountered an error"; header = "**Claude encountered an error";
if (durationStr) { if (durationStr) {
header += ` after ${durationStr}`; header += ` after ${durationStr}`;

View File

@@ -418,48 +418,4 @@ describe("updateCommentBody", () => {
); );
}); });
}); });
describe("cancellation handling", () => {
it("shows cancellation message when actionCancelled is true", () => {
const input = {
...baseInput,
currentBody: "Claude Code is working…",
actionCancelled: true,
executionDetails: { duration_ms: 30000 }, // 30s
triggerUsername: "test-user",
};
const result = updateCommentBody(input);
expect(result).toContain("**Claude's task was cancelled after 30s**");
expect(result).not.toContain("Claude encountered an error");
expect(result).not.toContain("Claude finished");
});
it("shows cancellation message without duration when duration is missing", () => {
const input = {
...baseInput,
currentBody: "Claude Code is working…",
actionCancelled: true,
triggerUsername: "test-user",
};
const result = updateCommentBody(input);
expect(result).toContain("**Claude's task was cancelled**");
expect(result).not.toContain("after");
});
it("prioritizes cancellation over failure", () => {
const input = {
...baseInput,
currentBody: "Claude Code is working…",
actionCancelled: true,
actionFailed: true, // Both are true, cancellation should take precedence
executionDetails: { duration_ms: 45000 },
};
const result = updateCommentBody(input);
expect(result).toContain("**Claude's task was cancelled after 45s**");
expect(result).not.toContain("Claude encountered an error");
});
});
}); });

View File

@@ -1,160 +0,0 @@
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
describe("update-comment-link workflow status detection", () => {
let originalEnv: NodeJS.ProcessEnv;
beforeEach(() => {
originalEnv = { ...process.env };
});
afterEach(() => {
process.env = originalEnv;
});
test("should detect prepare step failure", () => {
process.env.PREPARE_SUCCESS = "false";
process.env.PREPARE_ERROR = "Failed to fetch issue data";
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
const prepareError = process.env.PREPARE_ERROR;
let actionFailed = false;
let errorDetails: string | undefined;
if (!prepareSuccess && prepareError) {
actionFailed = true;
errorDetails = prepareError;
}
expect(actionFailed).toBe(true);
expect(errorDetails).toBe("Failed to fetch issue data");
});
test("should detect claude-code step failure when prepare succeeds", () => {
process.env.PREPARE_SUCCESS = "true";
process.env.CLAUDE_SUCCESS = "false";
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
const prepareError = process.env.PREPARE_ERROR;
let actionFailed = false;
if (!prepareSuccess && prepareError) {
actionFailed = true;
} else {
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
actionFailed = !claudeSuccess;
}
expect(actionFailed).toBe(true);
});
test("should detect success when both steps succeed", () => {
process.env.PREPARE_SUCCESS = "true";
process.env.CLAUDE_SUCCESS = "true";
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
const prepareError = process.env.PREPARE_ERROR;
let actionFailed = false;
if (!prepareSuccess && prepareError) {
actionFailed = true;
} else {
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
actionFailed = !claudeSuccess;
}
expect(actionFailed).toBe(false);
});
test("should treat missing CLAUDE_SUCCESS env var as failure", () => {
process.env.PREPARE_SUCCESS = "true";
delete process.env.CLAUDE_SUCCESS;
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
const prepareError = process.env.PREPARE_ERROR;
let actionFailed = false;
if (!prepareSuccess && prepareError) {
actionFailed = true;
} else {
// When CLAUDE_SUCCESS is undefined, it's not === "true", so claudeSuccess = false
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
actionFailed = !claudeSuccess;
}
expect(actionFailed).toBe(true);
});
test("should handle undefined PREPARE_SUCCESS as success", () => {
delete process.env.PREPARE_SUCCESS;
delete process.env.PREPARE_ERROR;
process.env.CLAUDE_SUCCESS = "true";
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
const prepareError = process.env.PREPARE_ERROR;
let actionFailed = false;
if (!prepareSuccess && prepareError) {
actionFailed = true;
} else {
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
actionFailed = !claudeSuccess;
}
expect(actionFailed).toBe(false);
});
test("should detect cancellation when CLAUDE_CANCELLED is true", () => {
process.env.PREPARE_SUCCESS = "true";
process.env.CLAUDE_SUCCESS = "false";
process.env.CLAUDE_CANCELLED = "true";
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
const prepareError = process.env.PREPARE_ERROR;
const isCancelled = process.env.CLAUDE_CANCELLED === "true";
let actionFailed = false;
let actionCancelled = false;
if (!prepareSuccess && prepareError) {
actionFailed = true;
} else if (isCancelled) {
actionCancelled = true;
} else {
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
actionFailed = !claudeSuccess;
}
expect(actionFailed).toBe(false);
expect(actionCancelled).toBe(true);
});
test("should not detect cancellation when CLAUDE_CANCELLED is false", () => {
process.env.PREPARE_SUCCESS = "true";
process.env.CLAUDE_SUCCESS = "false";
process.env.CLAUDE_CANCELLED = "false";
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
const prepareError = process.env.PREPARE_ERROR;
const isCancelled = process.env.CLAUDE_CANCELLED === "true";
let actionFailed = false;
let actionCancelled = false;
if (!prepareSuccess && prepareError) {
actionFailed = true;
} else if (isCancelled) {
actionCancelled = true;
} else {
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
actionFailed = !claudeSuccess;
}
expect(actionFailed).toBe(true);
expect(actionCancelled).toBe(false);
});
});