fix: add checkHumanActor to agent mode and concurrency to workflow examples
Fixes issue #641 where users were getting banned due to rapid successive Claude runs triggered by the synchronize event. Changes: - Add checkHumanActor call to agent mode's prepare() method to reject bot-triggered workflows unless explicitly allowed via allowed_bots - Update checkHumanActor to accept GitHubContext (union type) instead of just ParsedGitHubContext - Add concurrency protection to all PR review workflow examples to prevent multiple reviews running simultaneously on the same PR - Add tests for bot rejection/allowance in agent mode 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9acae263e7
commit
df10c4692a
@ -54,6 +54,11 @@ on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
# Prevent multiple reviews from running simultaneously on the same PR
|
||||
concurrency:
|
||||
group: claude-review-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
review-by-author:
|
||||
if: |
|
||||
|
||||
@ -27,6 +27,11 @@ on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
# Prevent multiple reviews from running simultaneously on the same PR
|
||||
concurrency:
|
||||
group: claude-review-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
@ -81,6 +86,11 @@ on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, ready_for_review, reopened]
|
||||
|
||||
# Prevent multiple reviews from running simultaneously on the same PR
|
||||
concurrency:
|
||||
group: claude-review-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
@ -145,6 +155,11 @@ on:
|
||||
- "src/api/**"
|
||||
- "config/security.yml"
|
||||
|
||||
# Prevent multiple reviews from running simultaneously on the same PR
|
||||
concurrency:
|
||||
group: claude-review-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
security-review:
|
||||
runs-on: ubuntu-latest
|
||||
@ -202,6 +217,11 @@ on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
# Prevent multiple reviews from running simultaneously on the same PR
|
||||
concurrency:
|
||||
group: claude-review-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
external-review:
|
||||
if: github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
@ -260,6 +280,11 @@ on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
# Prevent multiple reviews from running simultaneously on the same PR
|
||||
concurrency:
|
||||
group: claude-review-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
checklist-review:
|
||||
runs-on: ubuntu-latest
|
||||
@ -448,6 +473,11 @@ on:
|
||||
- "src/api/**/*.ts"
|
||||
- "src/routes/**/*.ts"
|
||||
|
||||
# Prevent multiple reviews from running simultaneously on the same PR
|
||||
concurrency:
|
||||
group: claude-review-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
doc-sync:
|
||||
runs-on: ubuntu-latest
|
||||
@ -504,6 +534,11 @@ on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
# Prevent multiple reviews from running simultaneously on the same PR
|
||||
concurrency:
|
||||
group: claude-review-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@ -7,6 +7,11 @@ on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, ready_for_review, reopened]
|
||||
|
||||
# Prevent multiple reviews from running simultaneously on the same PR
|
||||
concurrency:
|
||||
group: claude-review-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
review-with-tracking:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@ -4,6 +4,11 @@ on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
# Prevent multiple reviews from running simultaneously on the same PR
|
||||
concurrency:
|
||||
group: claude-review-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
review-by-author:
|
||||
# Only run for PRs from specific authors
|
||||
|
||||
@ -10,6 +10,11 @@ on:
|
||||
- "api/**/*.py"
|
||||
# You can add more specific patterns as needed
|
||||
|
||||
# Prevent multiple reviews from running simultaneously on the same PR
|
||||
concurrency:
|
||||
group: claude-review-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
claude-review-paths:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@ -6,11 +6,11 @@
|
||||
*/
|
||||
|
||||
import type { Octokit } from "@octokit/rest";
|
||||
import type { ParsedGitHubContext } from "../context";
|
||||
import type { GitHubContext } from "../context";
|
||||
|
||||
export async function checkHumanActor(
|
||||
octokit: Octokit,
|
||||
githubContext: ParsedGitHubContext,
|
||||
githubContext: GitHubContext,
|
||||
) {
|
||||
// Fetch user information from GitHub API
|
||||
const { data: userData } = await octokit.users.getByUsername({
|
||||
|
||||
@ -5,6 +5,7 @@ import type { PreparedContext } from "../../create-prompt/types";
|
||||
import { prepareMcpConfig } from "../../mcp/install-mcp-server";
|
||||
import { parseAllowedTools } from "./parse-tools";
|
||||
import { configureGitAuth } from "../../github/operations/git-config";
|
||||
import { checkHumanActor } from "../../github/validation/actor";
|
||||
import type { GitHubContext } from "../../github/context";
|
||||
import { isEntityContext } from "../../github/context";
|
||||
|
||||
@ -77,7 +78,14 @@ export const agentMode: Mode = {
|
||||
return false;
|
||||
},
|
||||
|
||||
async prepare({ context, githubToken }: ModeOptions): Promise<ModeResult> {
|
||||
async prepare({
|
||||
context,
|
||||
octokit,
|
||||
githubToken,
|
||||
}: ModeOptions): Promise<ModeResult> {
|
||||
// Check if actor is human (prevents bot-triggered loops)
|
||||
await checkHumanActor(octokit.rest, context);
|
||||
|
||||
// Configure git authentication for agent mode (same as tag mode)
|
||||
if (!context.inputs.useCommitSigning) {
|
||||
// Use bot_id and bot_name from inputs directly
|
||||
|
||||
@ -145,12 +145,12 @@ describe("Agent Mode", () => {
|
||||
users: {
|
||||
getAuthenticated: mock(() =>
|
||||
Promise.resolve({
|
||||
data: { login: "test-user", id: 12345 },
|
||||
data: { login: "test-user", id: 12345, type: "User" },
|
||||
}),
|
||||
),
|
||||
getByUsername: mock(() =>
|
||||
Promise.resolve({
|
||||
data: { login: "test-user", id: 12345 },
|
||||
data: { login: "test-user", id: 12345, type: "User" },
|
||||
}),
|
||||
),
|
||||
},
|
||||
@ -187,6 +187,65 @@ describe("Agent Mode", () => {
|
||||
process.env.GITHUB_REF_NAME = originalRefName;
|
||||
});
|
||||
|
||||
test("prepare method rejects bot actors without allowed_bots", async () => {
|
||||
const contextWithPrompts = createMockAutomationContext({
|
||||
eventName: "workflow_dispatch",
|
||||
});
|
||||
contextWithPrompts.actor = "claude[bot]";
|
||||
contextWithPrompts.inputs.allowedBots = "";
|
||||
|
||||
const mockOctokit = {
|
||||
rest: {
|
||||
users: {
|
||||
getByUsername: mock(() =>
|
||||
Promise.resolve({
|
||||
data: { login: "claude[bot]", id: 12345, type: "Bot" },
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
await expect(
|
||||
agentMode.prepare({
|
||||
context: contextWithPrompts,
|
||||
octokit: mockOctokit,
|
||||
githubToken: "test-token",
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"Workflow initiated by non-human actor: claude (type: Bot)",
|
||||
);
|
||||
});
|
||||
|
||||
test("prepare method allows bot actors when in allowed_bots list", async () => {
|
||||
const contextWithPrompts = createMockAutomationContext({
|
||||
eventName: "workflow_dispatch",
|
||||
});
|
||||
contextWithPrompts.actor = "dependabot[bot]";
|
||||
contextWithPrompts.inputs.allowedBots = "dependabot";
|
||||
|
||||
const mockOctokit = {
|
||||
rest: {
|
||||
users: {
|
||||
getByUsername: mock(() =>
|
||||
Promise.resolve({
|
||||
data: { login: "dependabot[bot]", id: 12345, type: "Bot" },
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
// Should not throw - bot is in allowed list
|
||||
await expect(
|
||||
agentMode.prepare({
|
||||
context: contextWithPrompts,
|
||||
octokit: mockOctokit,
|
||||
githubToken: "test-token",
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
test("prepare method creates prompt file with correct content", async () => {
|
||||
const contextWithPrompts = createMockAutomationContext({
|
||||
eventName: "workflow_dispatch",
|
||||
@ -199,12 +258,12 @@ describe("Agent Mode", () => {
|
||||
users: {
|
||||
getAuthenticated: mock(() =>
|
||||
Promise.resolve({
|
||||
data: { login: "test-user", id: 12345 },
|
||||
data: { login: "test-user", id: 12345, type: "User" },
|
||||
}),
|
||||
),
|
||||
getByUsername: mock(() =>
|
||||
Promise.resolve({
|
||||
data: { login: "test-user", id: 12345 },
|
||||
data: { login: "test-user", id: 12345, type: "User" },
|
||||
}),
|
||||
),
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user