diff --git a/src/github/validation/actor.ts b/src/github/validation/actor.ts index 763d704..395d083 100644 --- a/src/github/validation/actor.ts +++ b/src/github/validation/actor.ts @@ -18,14 +18,31 @@ export async function checkHumanActor( // check but still guard against self-triggering (infinite loop prevention). if (detectPlatform() === "forgejo") { const botName = githubContext.inputs.botName.replace(/\[bot\]$/, ""); - if ( + const isBotActor = githubContext.actor === botName || - githubContext.actor === githubContext.inputs.botName - ) { - throw new Error( - `Blocking self-trigger on Forgejo: actor '${githubContext.actor}' matches bot_name '${githubContext.inputs.botName}'. This prevents infinite loops.`, + githubContext.actor === githubContext.inputs.botName; + + if (isBotActor) { + // Self-trigger loop risk only exists for comment/issue events (tag mode). + // PR and other events (agent mode, workflow_dispatch) are safe — the bot + // pushing a branch or being triggered by schedule can't cause loops. + const loopRiskEvents = [ + "issue_comment", + "issues", + "pull_request_review_comment", + "pull_request_review", + ]; + if (loopRiskEvents.includes(githubContext.eventName)) { + throw new Error( + `Blocking self-trigger on Forgejo: actor '${githubContext.actor}' matches bot_name '${githubContext.inputs.botName}'. This prevents infinite loops.`, + ); + } + console.log( + `Forgejo bot actor '${githubContext.actor}' allowed for event '${githubContext.eventName}' (no loop risk)`, ); + return; } + console.log( `Forgejo actor check passed for: ${githubContext.actor} (bot_name: ${githubContext.inputs.botName})`, ); diff --git a/test/actor.test.ts b/test/actor.test.ts index 79e75d6..0c3f486 100644 --- a/test/actor.test.ts +++ b/test/actor.test.ts @@ -205,5 +205,41 @@ describe("checkHumanActor", () => { "Blocking self-trigger on Forgejo", ); }); + + test("should allow bot actor on pull_request events (no loop risk)", async () => { + const mockOctokit = createMockOctokit("User"); + const context = createMockContext(); + context.actor = "claude"; + context.inputs.botName = "claude[bot]"; + (context as { eventName: string }).eventName = "pull_request"; + + await expect( + checkHumanActor(mockOctokit, context), + ).resolves.toBeUndefined(); + }); + + test("should allow bot actor on workflow_dispatch events (no loop risk)", async () => { + const mockOctokit = createMockOctokit("User"); + const context = createMockContext(); + context.actor = "claude"; + context.inputs.botName = "claude[bot]"; + (context as { eventName: string }).eventName = "workflow_dispatch"; + + await expect( + checkHumanActor(mockOctokit, context), + ).resolves.toBeUndefined(); + }); + + test("should block bot actor on issue_comment events (loop risk)", async () => { + const mockOctokit = createMockOctokit("User"); + const context = createMockContext(); + context.actor = "claude"; + context.inputs.botName = "claude[bot]"; + // eventName defaults to "issue_comment" from createMockContext + + await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow( + "Blocking self-trigger on Forgejo", + ); + }); }); });