Skip to content
Merged
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
35 changes: 18 additions & 17 deletions actions/setup/js/check_membership.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,14 @@ async function main() {
// Check if the actor has the required repository permissions
const result = await checkRepositoryPermission(actor, owner, repo, requiredPermissions);

if (result.error) {
core.setOutput("is_team_member", "false");
core.setOutput("result", "api_error");
core.setOutput("error_message", `Repository permission check failed: ${result.error}`);
return;
}

if (result.authorized) {
core.setOutput("is_team_member", "true");
core.setOutput("result", "authorized");
core.setOutput("user_permission", result.permission);
} else {
// User doesn't have required permissions, check if they're an allowed bot
// User doesn't have required permissions (or the permission check failed with an error).
// Always attempt the bot allowlist fallback before giving up, so that GitHub Apps whose
// actor is not a recognized GitHub user (e.g. "Copilot") are not silently denied.
if (allowedBots && allowedBots.length > 0) {
core.info(`Checking if actor '${actor}' is in allowed bots list: ${allowedBots.join(", ")}`);
Comment on lines +60 to 64

Expand All @@ -84,7 +79,7 @@ async function main() {
core.warning(`Bot '${actor}' is in the allowed list but not active/installed on ${owner}/${repo}`);
core.setOutput("is_team_member", "false");
core.setOutput("result", "bot_not_active");
core.setOutput("user_permission", result.permission);
core.setOutput("user_permission", result.permission ?? "bot");
core.setOutput("error_message", `Access denied: Bot '${actor}' is not active/installed on this repository`);
return;
} else {
Expand All @@ -94,14 +89,20 @@ async function main() {
}

// Not authorized by role or bot
core.setOutput("is_team_member", "false");
core.setOutput("result", "insufficient_permissions");
core.setOutput("user_permission", result.permission);
core.setOutput(
"error_message",
`Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}. ` +
`To allow this user to run the workflow, add their role to the frontmatter. Example: roles: [${requiredPermissions.join(", ")}, ${result.permission}]`
);
if (result.error) {
core.setOutput("is_team_member", "false");
core.setOutput("result", "api_error");
core.setOutput("error_message", `Repository permission check failed: ${result.error}`);
} else {
core.setOutput("is_team_member", "false");
core.setOutput("result", "insufficient_permissions");
core.setOutput("user_permission", result.permission);
core.setOutput(
"error_message",
`Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}. ` +
`To allow this user to run the workflow, add their role to the frontmatter. Example: roles: [${requiredPermissions.join(", ")}, ${result.permission}]`
);
}
}
}

Expand Down
45 changes: 45 additions & 0 deletions actions/setup/js/check_membership.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,51 @@ describe("check_membership.cjs", () => {
expect(mockCore.setOutput).toHaveBeenCalledWith("result", "insufficient_permissions");
});

it("should authorize a bot in the allowlist when permission check returns an API error (e.g. GitHub App not a user)", async () => {
process.env.GH_AW_ALLOWED_BOTS = "Copilot";
mockContext.actor = "Copilot";

const notAUserError = new Error("Copilot is not a user");
mockGithub.rest.repos.getCollaboratorPermissionLevel
.mockRejectedValueOnce(notAUserError) // initial permission check → error
.mockResolvedValueOnce({ data: { permission: "none" } }); // bot status check (Copilot[bot] form) → active

await runScript();

expect(mockCore.setOutput).toHaveBeenCalledWith("is_team_member", "true");
expect(mockCore.setOutput).toHaveBeenCalledWith("result", "authorized_bot");
expect(mockCore.setOutput).toHaveBeenCalledWith("user_permission", "bot");
});

it("should return bot_not_active when permission check returns API error and bot is not installed", async () => {
process.env.GH_AW_ALLOWED_BOTS = "Copilot";
mockContext.actor = "Copilot";

const notAUserError = new Error("Copilot is not a user");
const notFoundError = { status: 404, message: "Not Found" };
mockGithub.rest.repos.getCollaboratorPermissionLevel
.mockRejectedValueOnce(notAUserError) // initial permission check → error
.mockRejectedValue(notFoundError); // all bot status checks → 404

await runScript();

expect(mockCore.setOutput).toHaveBeenCalledWith("is_team_member", "false");
expect(mockCore.setOutput).toHaveBeenCalledWith("result", "bot_not_active");
});

it("should return api_error when permission check fails and actor is not in allowed bots list", async () => {
process.env.GH_AW_ALLOWED_BOTS = "some-other-bot";
mockContext.actor = "Copilot";

const notAUserError = new Error("Copilot is not a user");
mockGithub.rest.repos.getCollaboratorPermissionLevel.mockRejectedValue(notAUserError);

await runScript();

expect(mockCore.setOutput).toHaveBeenCalledWith("is_team_member", "false");
expect(mockCore.setOutput).toHaveBeenCalledWith("result", "api_error");
});

it("should authorize a bot with [bot] suffix in the allowlist via slug fallback", async () => {
process.env.GH_AW_ALLOWED_BOTS = "copilot";
mockContext.actor = "copilot[bot]";
Expand Down
Loading