diff --git a/actions/setup/js/check_membership.cjs b/actions/setup/js/check_membership.cjs index 699d38bb419..49103a93e6b 100644 --- a/actions/setup/js/check_membership.cjs +++ b/actions/setup/js/check_membership.cjs @@ -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(", ")}`); @@ -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 { @@ -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}]` + ); + } } } diff --git a/actions/setup/js/check_membership.test.cjs b/actions/setup/js/check_membership.test.cjs index 81d61457d3b..9cd490489f5 100644 --- a/actions/setup/js/check_membership.test.cjs +++ b/actions/setup/js/check_membership.test.cjs @@ -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]";