mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-07-28 06:48:30 +08:00
Move the allowed_bots check in checkHumanActor and checkWritePermissions so it only fires after the actor has been resolved as a non-User account (GitHub App / bot, or unresolvable app actor). Actors that resolve to a regular User account go through the standard human/write checks regardless of allowed_bots. The Copilot-style path (GITHUB_ACTOR not ending in [bot] and not resolvable as a user) is unchanged: it still falls through to the existing 404 catch, which already consults allowed_bots once the API has reported the actor is not a user. Update tests to match and add coverage for the User-account path.
219 lines
7.7 KiB
TypeScript
219 lines
7.7 KiB
TypeScript
#!/usr/bin/env bun
|
|
|
|
import { describe, test, expect } from "bun:test";
|
|
import { checkHumanActor } from "../src/github/validation/actor";
|
|
import type { Octokit } from "@octokit/rest";
|
|
import { createMockContext } from "./mockContext";
|
|
|
|
function createMockOctokit(userType: string): Octokit {
|
|
return {
|
|
users: {
|
|
getByUsername: async () => ({
|
|
data: {
|
|
type: userType,
|
|
},
|
|
}),
|
|
},
|
|
} as unknown as Octokit;
|
|
}
|
|
|
|
describe("checkHumanActor", () => {
|
|
test("should pass for human actor", async () => {
|
|
const mockOctokit = createMockOctokit("User");
|
|
const context = createMockContext();
|
|
context.actor = "human-user";
|
|
|
|
await expect(
|
|
checkHumanActor(mockOctokit, context),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
test("should throw error for bot actor when not allowed", async () => {
|
|
const mockOctokit = createMockOctokit("Bot");
|
|
const context = createMockContext();
|
|
context.actor = "test-bot[bot]";
|
|
context.inputs.allowedBots = "";
|
|
|
|
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
|
|
"Workflow initiated by non-human actor: test-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
|
|
);
|
|
});
|
|
|
|
test("should pass for bot actor when all bots allowed", async () => {
|
|
const mockOctokit = createMockOctokit("Bot");
|
|
const context = createMockContext();
|
|
context.actor = "test-bot[bot]";
|
|
context.inputs.allowedBots = "*";
|
|
|
|
await expect(
|
|
checkHumanActor(mockOctokit, context),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
test("should pass for specific bot when in allowed list", async () => {
|
|
const mockOctokit = createMockOctokit("Bot");
|
|
const context = createMockContext();
|
|
context.actor = "dependabot[bot]";
|
|
context.inputs.allowedBots = "dependabot[bot],renovate[bot]";
|
|
|
|
await expect(
|
|
checkHumanActor(mockOctokit, context),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
test("should pass for specific bot when in allowed list (without [bot])", async () => {
|
|
const mockOctokit = createMockOctokit("Bot");
|
|
const context = createMockContext();
|
|
context.actor = "dependabot[bot]";
|
|
context.inputs.allowedBots = "dependabot,renovate";
|
|
|
|
await expect(
|
|
checkHumanActor(mockOctokit, context),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
test("should throw error for bot not in allowed list", async () => {
|
|
const mockOctokit = createMockOctokit("Bot");
|
|
const context = createMockContext();
|
|
context.actor = "other-bot[bot]";
|
|
context.inputs.allowedBots = "dependabot[bot],renovate[bot]";
|
|
|
|
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
|
|
"Workflow initiated by non-human actor: other-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
|
|
);
|
|
});
|
|
|
|
test("should throw error for bot not in allowed list (without [bot])", async () => {
|
|
const mockOctokit = createMockOctokit("Bot");
|
|
const context = createMockContext();
|
|
context.actor = "other-bot[bot]";
|
|
context.inputs.allowedBots = "dependabot,renovate";
|
|
|
|
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
|
|
"Workflow initiated by non-human actor: other-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
|
|
);
|
|
});
|
|
|
|
describe("non-[bot] actors (e.g. GitHub Copilot)", () => {
|
|
// GitHub Copilot SWE Agent sets GITHUB_ACTOR="Copilot" which is not a
|
|
// valid GitHub user and doesn't end with [bot], causing 404 on the
|
|
// Users API. allowed_bots is applied once the API has resolved the
|
|
// actor as not being a regular user account.
|
|
|
|
function createMockOctokitThat404s(): Octokit {
|
|
return {
|
|
users: {
|
|
getByUsername: async () => {
|
|
const err = new Error("Not Found");
|
|
(err as any).status = 404;
|
|
throw err;
|
|
},
|
|
},
|
|
} as unknown as Octokit;
|
|
}
|
|
|
|
test("should pass for non-[bot] actor when in allowed_bots list", async () => {
|
|
const mockOctokit = createMockOctokitThat404s();
|
|
const context = createMockContext();
|
|
context.actor = "Copilot";
|
|
context.inputs.allowedBots = "copilot,cursor";
|
|
|
|
await expect(
|
|
checkHumanActor(mockOctokit, context),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
test("should pass for non-[bot] actor when all bots are allowed", async () => {
|
|
const mockOctokit = createMockOctokitThat404s();
|
|
const context = createMockContext();
|
|
context.actor = "Copilot";
|
|
context.inputs.allowedBots = "*";
|
|
|
|
await expect(
|
|
checkHumanActor(mockOctokit, context),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
test("should throw with clear message for non-[bot] actor that 404s and is not in allowed list", async () => {
|
|
const mockOctokit = createMockOctokitThat404s();
|
|
const context = createMockContext();
|
|
context.actor = "Copilot";
|
|
context.inputs.allowedBots = "cursor";
|
|
|
|
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
|
|
"Workflow initiated by non-human actor: copilot (actor not found on GitHub). Add bot to allowed_bots list or use '*' to allow all bots.",
|
|
);
|
|
});
|
|
|
|
test("should throw with clear message for non-[bot] actor that 404s and allowed_bots is empty", async () => {
|
|
const mockOctokit = createMockOctokitThat404s();
|
|
const context = createMockContext();
|
|
context.actor = "Copilot";
|
|
context.inputs.allowedBots = "";
|
|
|
|
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
|
|
"Workflow initiated by non-human actor: copilot (actor not found on GitHub). Add bot to allowed_bots list or use '*' to allow all bots.",
|
|
);
|
|
});
|
|
|
|
test("should match allowed_bots case-insensitively for non-[bot] actors", async () => {
|
|
const mockOctokit = createMockOctokitThat404s();
|
|
const context = createMockContext();
|
|
context.actor = "Copilot";
|
|
context.inputs.allowedBots = "COPILOT";
|
|
|
|
await expect(
|
|
checkHumanActor(mockOctokit, context),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("account type resolution", () => {
|
|
// The Users API resolves the actor's account type before allowed_bots
|
|
// is consulted. allowed_bots is only relevant for Bot accounts and
|
|
// unresolvable app actors; it does not change behavior for regular
|
|
// User accounts.
|
|
|
|
test("should pass for a User account whose name matches allowed_bots", async () => {
|
|
const mockOctokit = createMockOctokit("User");
|
|
const context = createMockContext();
|
|
context.actor = "renovate";
|
|
context.inputs.allowedBots = "renovate";
|
|
|
|
await expect(
|
|
checkHumanActor(mockOctokit, context),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
test("should pass for a User account when allowed_bots is '*'", async () => {
|
|
const mockOctokit = createMockOctokit("User");
|
|
const context = createMockContext();
|
|
context.actor = "some-user";
|
|
context.inputs.allowedBots = "*";
|
|
|
|
await expect(
|
|
checkHumanActor(mockOctokit, context),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
test("should resolve account type even when actor name appears in allowed_bots", async () => {
|
|
// The Users API call should not be short-circuited by allowed_bots,
|
|
// so an unexpected API error propagates instead of being swallowed.
|
|
const mockOctokit = {
|
|
users: {
|
|
getByUsername: async () => {
|
|
throw new Error("Internal Server Error");
|
|
},
|
|
},
|
|
} as unknown as Octokit;
|
|
const context = createMockContext();
|
|
context.actor = "some-user";
|
|
context.inputs.allowedBots = "some-user";
|
|
|
|
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
|
|
"Internal Server Error",
|
|
);
|
|
});
|
|
});
|
|
});
|