diff --git a/src/github/validation/permissions.ts b/src/github/validation/permissions.ts index 731fcd41c..6a71bfc7b 100644 --- a/src/github/validation/permissions.ts +++ b/src/github/validation/permissions.ts @@ -66,7 +66,18 @@ export async function checkWritePermissions( core.warning(`Actor has insufficient permissions: ${permissionLevel}`); return false; } - } catch (error) { + } catch (error: any) { + // Handle 404 errors for non-user actors (e.g., "Copilot") + // The collaborator permission API returns 404 when the actor is not a + // regular GitHub user. These are GitHub system actors that inherently + // operate with the permissions granted by the workflow configuration. + if (error?.status === 404) { + core.info( + `Actor "${actor}" is not a GitHub user (received 404). Treating as a non-user actor and allowing.`, + ); + return true; + } + core.error(`Failed to check permissions: ${error}`); throw new Error(`Failed to check permissions for ${actor}: ${error}`); } diff --git a/test/permissions.test.ts b/test/permissions.test.ts index cf2efdb0e..0050a1bce 100644 --- a/test/permissions.test.ts +++ b/test/permissions.test.ts @@ -140,7 +140,53 @@ describe("checkWritePermissions", () => { expect(result).toBe(true); }); - test("should throw error when permission check fails", async () => { + test("should return true when API returns 404 for non-user actor", async () => { + const httpError = Object.assign( + new Error("Copilot is not a user"), + { status: 404 }, + ); + const mockOctokit = { + repos: { + getCollaboratorPermissionLevel: async () => { + throw httpError; + }, + }, + } as any; + const context = createContext(); + context.actor = "Copilot"; + + const result = await checkWritePermissions(mockOctokit, context); + + expect(result).toBe(true); + expect(coreInfoSpy).toHaveBeenCalledWith( + 'Actor "Copilot" is not a GitHub user (received 404). Treating as a non-user actor and allowing.', + ); + }); + + test("should return true when API returns 404 for any non-user actor name", async () => { + const httpError = Object.assign( + new Error("some-service is not a user"), + { status: 404 }, + ); + const mockOctokit = { + repos: { + getCollaboratorPermissionLevel: async () => { + throw httpError; + }, + }, + } as any; + const context = createContext(); + context.actor = "some-service"; + + const result = await checkWritePermissions(mockOctokit, context); + + expect(result).toBe(true); + expect(coreInfoSpy).toHaveBeenCalledWith( + 'Actor "some-service" is not a GitHub user (received 404). Treating as a non-user actor and allowing.', + ); + }); + + test("should throw error when permission check fails with non-404 error", async () => { const error = new Error("API error"); const mockOctokit = { repos: { @@ -160,6 +206,27 @@ describe("checkWritePermissions", () => { ); }); + test("should throw error for 500 server errors", async () => { + const httpError = Object.assign( + new Error("Internal Server Error"), + { status: 500 }, + ); + const mockOctokit = { + repos: { + getCollaboratorPermissionLevel: async () => { + throw httpError; + }, + }, + } as any; + const context = createContext(); + + await expect(checkWritePermissions(mockOctokit, context)).rejects.toThrow( + "Failed to check permissions for test-user", + ); + + expect(coreErrorSpy).toHaveBeenCalled(); + }); + test("should call API with correct parameters", async () => { let capturedParams: any; const mockOctokit = {