Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/pr-review-filtered-authors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
pull-requests: write
id-token: write
steps:
- name: Checkout repository
Expand Down
2 changes: 1 addition & 1 deletion examples/pr-review-filtered-paths.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
pull-requests: write
id-token: write
steps:
- name: Checkout repository
Expand Down
63 changes: 63 additions & 0 deletions src/entrypoints/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,56 @@ async function writeStepSummary(executionFile: string): Promise<void> {
}
}

/**
* Scan the Claude execution file for inline comment tool calls that failed
* with a GitHub permission error. Returns an actionable failure message when
* found, or null when the file is absent or contains no such errors.
*
* "Resource not accessible by integration" is GitHub's canonical response
* when a GitHub App token lacks the required permission scope — in this case
* `pull-requests: write`.
*/
function detectCommentPermissionFailure(executionFile: string): string | null {
const PERMISSION_ERROR = "Resource not accessible by integration";
try {
const messages: unknown[] = JSON.parse(readFileSync(executionFile, "utf-8"));
let failedCount = 0;
for (const msg of messages) {
if (
typeof msg !== "object" ||
msg === null ||
(msg as { type?: string }).type !== "user"
)
continue;
const content = (msg as { message?: { content?: unknown[] } }).message
?.content;
if (!Array.isArray(content)) continue;
for (const item of content) {
if (
typeof item === "object" &&
item !== null &&
(item as { type?: string }).type === "tool_result" &&
(item as { is_error?: boolean }).is_error === true &&
typeof (item as { content?: string }).content === "string" &&
(item as { content: string }).content.includes(PERMISSION_ERROR)
) {
failedCount++;
}
}
}
if (failedCount > 0) {
return (
`Claude attempted to post ${failedCount} review comment(s) but received a GitHub permission error. ` +
`Add \`pull-requests: write\` to your workflow's permissions block:\n\n` +
` permissions:\n pull-requests: write`
);
}
} catch {
// Parsing failure is non-fatal — don't mask the original conclusion
}
return null;
}

async function run() {
let githubToken: string | undefined;
let commentId: number | undefined;
Expand Down Expand Up @@ -277,6 +327,19 @@ async function run() {
claudeSuccess = claudeResult.conclusion === "success";
executionFile = claudeResult.executionFile;

// If Claude exited "success" but failed to post review comments due to
// insufficient permissions, surface that as an action failure. The MCP server
// returns isError:true on 403s, so the evidence is in the execution file.
// "Resource not accessible by integration" is GitHub's canonical error for
// this permission class — safe to match without ambiguity.
if (claudeSuccess && executionFile && existsSync(executionFile)) {
const permissionFailure = detectCommentPermissionFailure(executionFile);
if (permissionFailure) {
claudeSuccess = false;
core.setFailed(permissionFailure);
}
}

// Set action-level outputs
if (claudeResult.executionFile) {
core.setOutput("execution_file", claudeResult.executionFile);
Expand Down
5 changes: 4 additions & 1 deletion src/mcp/github-inline-comment-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,10 @@ server.tool(

// Provide more helpful error messages for common issues
let helpMessage = "";
if (errorMessage.includes("Validation Failed")) {
if (errorMessage.includes("Resource not accessible by integration")) {
helpMessage =
"\n\nThis error means the workflow token lacks permission to post review comments. Add `pull-requests: write` to your workflow's permissions block:\n\n```yaml\npermissions:\n pull-requests: write\n```";
} else if (errorMessage.includes("Validation Failed")) {
helpMessage =
"\n\nThis usually means the line number doesn't exist in the diff or the file path is incorrect. Make sure you're commenting on lines that are part of the PR's changes.";
} else if (errorMessage.includes("Not Found")) {
Expand Down