feat: add sticky_comment input to reduce GitHub comment spam (#211)

* feat: no claude spam

* feat: add silent property

* feat: add silent property

* feat: add silent property

* chore: call me a sticky comment

* chore: applying review comments

* chore: apply review comments

* format

* reword

---------

Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
This commit is contained in:
Dmitriy Shekhovtsov
2025-07-01 19:37:39 +02:00
committed by GitHub
parent bcb072b63f
commit 79f2086fce
7 changed files with 50 additions and 2 deletions

View File

@@ -85,6 +85,7 @@ jobs:
| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - |
| `max_turns` | Maximum number of conversation turns Claude can take (limits back-and-forth exchanges) | No | - |
| `timeout_minutes` | Timeout in minutes for execution | No | `30` |
| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` |
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - |
| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - |

View File

@@ -78,6 +78,10 @@ inputs:
description: "Timeout in minutes for execution"
required: false
default: "30"
use_sticky_comment:
description: "Use just one comment to deliver issue/PR comments"
required: false
default: "false"
outputs:
execution_file:
@@ -116,6 +120,7 @@ runs:
MCP_CONFIG: ${{ inputs.mcp_config }}
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
GITHUB_RUN_ID: ${{ github.run_id }}
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
- name: Run Claude Code
id: claude-code
@@ -180,6 +185,7 @@ runs:
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_ERROR: ${{ steps.prepare.outputs.prepare_error || '' }}
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
- name: Display Claude Code Report
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''

View File

@@ -36,6 +36,7 @@ export type ParsedGitHubContext = {
directPrompt: string;
baseBranch?: string;
branchPrefix: string;
useStickyComment: boolean;
};
};
@@ -62,6 +63,7 @@ export function parseGitHubContext(): ParsedGitHubContext {
directPrompt: process.env.DIRECT_PROMPT ?? "",
baseBranch: process.env.BASE_BRANCH,
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
useStickyComment: process.env.STICKY_COMMENT === "true",
},
};

View File

@@ -9,6 +9,7 @@ import { appendFileSync } from "fs";
import { createJobRunLink, createCommentBody } from "./common";
import {
isPullRequestReviewCommentEvent,
isPullRequestEvent,
type ParsedGitHubContext,
} from "../../context";
import type { Octokit } from "@octokit/rest";
@@ -25,8 +26,39 @@ export async function createInitialComment(
try {
let response;
// Only use createReplyForReviewComment if it's a PR review comment AND we have a comment_id
if (isPullRequestReviewCommentEvent(context)) {
if (
context.inputs.useStickyComment &&
context.isPR &&
!isPullRequestEvent(context)
) {
const comments = await octokit.rest.issues.listComments({
owner,
repo,
issue_number: context.entityNumber,
});
const existingComment = comments.data.find(
(comment) =>
comment.user?.login.indexOf("claude[bot]") !== -1 ||
comment.body === initialBody,
);
if (existingComment) {
response = await octokit.rest.issues.updateComment({
owner,
repo,
comment_id: existingComment.id,
body: initialBody,
});
} else {
// Create new comment if no existing one found
response = await octokit.rest.issues.createComment({
owner,
repo,
issue_number: context.entityNumber,
body: initialBody,
});
}
} else if (isPullRequestReviewCommentEvent(context)) {
// Only use createReplyForReviewComment if it's a PR review comment AND we have a comment_id
response = await octokit.rest.pulls.createReplyForReviewComment({
owner,
repo,

View File

@@ -20,6 +20,7 @@ const defaultInputs = {
useVertex: false,
timeoutMinutes: 30,
branchPrefix: "claude/",
useStickyComment: false,
};
const defaultRepository = {

View File

@@ -68,6 +68,7 @@ describe("checkWritePermissions", () => {
customInstructions: "",
directPrompt: "",
branchPrefix: "claude/",
useStickyComment: false,
},
});

View File

@@ -36,6 +36,7 @@ describe("checkContainsTrigger", () => {
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
},
});
expect(checkContainsTrigger(context)).toBe(true);
@@ -64,6 +65,7 @@ describe("checkContainsTrigger", () => {
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
},
});
expect(checkContainsTrigger(context)).toBe(false);
@@ -276,6 +278,7 @@ describe("checkContainsTrigger", () => {
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
},
});
expect(checkContainsTrigger(context)).toBe(true);
@@ -305,6 +308,7 @@ describe("checkContainsTrigger", () => {
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
},
});
expect(checkContainsTrigger(context)).toBe(true);
@@ -334,6 +338,7 @@ describe("checkContainsTrigger", () => {
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
},
});
expect(checkContainsTrigger(context)).toBe(false);