Skip to content

Commit 7bd3e01

Browse files
[jsweep] Clean check_permissions_utils.cjs (#7985)
1 parent f696f2a commit 7bd3e01

4 files changed

Lines changed: 184 additions & 24 deletions

File tree

.changeset/patch-modernize-check-permissions-utils.md

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

actions/setup/js/check_permissions_utils.cjs

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,15 @@
1111
* @returns {string[]} Array of required permission levels
1212
*/
1313
function parseRequiredPermissions() {
14-
const requiredPermissionsEnv = process.env.GH_AW_REQUIRED_ROLES;
15-
return requiredPermissionsEnv ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") : [];
14+
return process.env.GH_AW_REQUIRED_ROLES?.split(",").filter(p => p.trim()) ?? [];
1615
}
1716

1817
/**
1918
* Parse allowed bot identifiers from environment variable
2019
* @returns {string[]} Array of allowed bot identifiers
2120
*/
2221
function parseAllowedBots() {
23-
const allowedBotsEnv = process.env.GH_AW_ALLOWED_BOTS;
24-
return allowedBotsEnv ? allowedBotsEnv.split(",").filter(b => b.trim() !== "") : [];
22+
return process.env.GH_AW_ALLOWED_BOTS?.split(",").filter(b => b.trim()) ?? [];
2523
}
2624

2725
/**
@@ -55,17 +53,17 @@ async function checkBotStatus(actor, owner, repo) {
5553
return { isBot: true, isActive: true };
5654
} catch (botError) {
5755
// If we get a 404, the bot is not installed/active on this repository
58-
if (typeof botError === "object" && botError !== null && "status" in botError && botError.status === 404) {
56+
if (botError?.status === 404) {
5957
core.warning(`Bot '${actor}' is not active/installed on ${owner}/${repo}`);
6058
return { isBot: true, isActive: false };
6159
}
6260
// For other errors, we'll treat as inactive to be safe
63-
const errorMessage = botError instanceof Error ? botError.message : String(botError);
61+
const errorMessage = botError?.message ?? String(botError);
6462
core.warning(`Failed to check bot status: ${errorMessage}`);
6563
return { isBot: true, isActive: false, error: errorMessage };
6664
}
6765
} catch (error) {
68-
const errorMessage = error instanceof Error ? error.message : String(error);
66+
const errorMessage = error?.message ?? String(error);
6967
core.warning(`Error checking bot status: ${errorMessage}`);
7068
return { isBot: false, isActive: false, error: errorMessage };
7169
}
@@ -94,17 +92,17 @@ async function checkRepositoryPermission(actor, owner, repo, requiredPermissions
9492
core.info(`Repository permission level: ${permission}`);
9593

9694
// Check if user has one of the required permission levels
97-
for (const requiredPerm of requiredPermissions) {
98-
if (permission === requiredPerm || (requiredPerm === "maintainer" && permission === "maintain")) {
99-
core.info(`✅ User has ${permission} access to repository`);
100-
return { authorized: true, permission: permission };
101-
}
95+
const hasPermission = requiredPermissions.some(requiredPerm => permission === requiredPerm || (requiredPerm === "maintainer" && permission === "maintain"));
96+
97+
if (hasPermission) {
98+
core.info(`✅ User has ${permission} access to repository`);
99+
return { authorized: true, permission };
102100
}
103101

104102
core.warning(`User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}`);
105-
return { authorized: false, permission: permission };
103+
return { authorized: false, permission };
106104
} catch (repoError) {
107-
const errorMessage = repoError instanceof Error ? repoError.message : String(repoError);
105+
const errorMessage = repoError?.message ?? String(repoError);
108106
core.warning(`Repository permission check failed: ${errorMessage}`);
109107
return { authorized: false, error: errorMessage };
110108
}

actions/setup/js/check_permissions_utils.test.cjs

Lines changed: 158 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,29 +28,76 @@ global.github = mockGithub;
2828

2929
describe("check_permissions_utils", () => {
3030
let parseRequiredPermissions;
31+
let parseAllowedBots;
3132
let checkRepositoryPermission;
33+
let checkBotStatus;
3234
let originalEnv;
3335

3436
beforeEach(async () => {
3537
// Reset all mocks
3638
vi.clearAllMocks();
3739

3840
// Store original environment
39-
originalEnv = process.env.GH_AW_REQUIRED_ROLES;
41+
originalEnv = {
42+
GH_AW_REQUIRED_ROLES: process.env.GH_AW_REQUIRED_ROLES,
43+
GH_AW_ALLOWED_BOTS: process.env.GH_AW_ALLOWED_BOTS,
44+
};
4045

4146
// Import the module functions
4247
const module = await import("./check_permissions_utils.cjs");
4348
parseRequiredPermissions = module.parseRequiredPermissions;
49+
parseAllowedBots = module.parseAllowedBots;
4450
checkRepositoryPermission = module.checkRepositoryPermission;
51+
checkBotStatus = module.checkBotStatus;
4552
});
4653

4754
afterEach(() => {
4855
// Restore original environment
49-
if (originalEnv !== undefined) {
50-
process.env.GH_AW_REQUIRED_ROLES = originalEnv;
51-
} else {
52-
delete process.env.GH_AW_REQUIRED_ROLES;
53-
}
56+
Object.keys(originalEnv).forEach(key => {
57+
if (originalEnv[key] !== undefined) {
58+
process.env[key] = originalEnv[key];
59+
} else {
60+
delete process.env[key];
61+
}
62+
});
63+
});
64+
65+
describe("parseAllowedBots", () => {
66+
it("should parse comma-separated bot identifiers", () => {
67+
process.env.GH_AW_ALLOWED_BOTS = "dependabot[bot],renovate[bot],github-actions[bot]";
68+
const result = parseAllowedBots();
69+
expect(result).toEqual(["dependabot[bot]", "renovate[bot]", "github-actions[bot]"]);
70+
});
71+
72+
it("should filter out empty strings", () => {
73+
process.env.GH_AW_ALLOWED_BOTS = "dependabot[bot],,renovate[bot],";
74+
const result = parseAllowedBots();
75+
expect(result).toEqual(["dependabot[bot]", "renovate[bot]"]);
76+
});
77+
78+
it("should filter out whitespace-only entries", () => {
79+
process.env.GH_AW_ALLOWED_BOTS = "dependabot[bot], ,renovate[bot]";
80+
const result = parseAllowedBots();
81+
expect(result).toEqual(["dependabot[bot]", "renovate[bot]"]);
82+
});
83+
84+
it("should return empty array when env var is not set", () => {
85+
delete process.env.GH_AW_ALLOWED_BOTS;
86+
const result = parseAllowedBots();
87+
expect(result).toEqual([]);
88+
});
89+
90+
it("should return empty array when env var is empty string", () => {
91+
process.env.GH_AW_ALLOWED_BOTS = "";
92+
const result = parseAllowedBots();
93+
expect(result).toEqual([]);
94+
});
95+
96+
it("should handle single bot identifier", () => {
97+
process.env.GH_AW_ALLOWED_BOTS = "dependabot[bot]";
98+
const result = parseAllowedBots();
99+
expect(result).toEqual(["dependabot[bot]"]);
100+
});
54101
});
55102

56103
describe("parseRequiredPermissions", () => {
@@ -222,4 +269,109 @@ describe("check_permissions_utils", () => {
222269
expect(successLog[0]).toContain("write");
223270
});
224271
});
272+
273+
describe("checkBotStatus", () => {
274+
it("should identify bot by [bot] suffix", async () => {
275+
mockGithub.rest.repos.getCollaboratorPermissionLevel.mockResolvedValue({
276+
data: { permission: "write" },
277+
});
278+
279+
const result = await checkBotStatus("dependabot[bot]", "testowner", "testrepo");
280+
281+
expect(result).toEqual({
282+
isBot: true,
283+
isActive: true,
284+
});
285+
286+
expect(mockCore.info).toHaveBeenCalledWith("Checking if bot 'dependabot[bot]' is active on testowner/testrepo");
287+
expect(mockCore.info).toHaveBeenCalledWith("Bot 'dependabot[bot]' is active with permission level: write");
288+
});
289+
290+
it("should return false for non-bot users", async () => {
291+
const result = await checkBotStatus("regularuser", "testowner", "testrepo");
292+
293+
expect(result).toEqual({
294+
isBot: false,
295+
isActive: false,
296+
});
297+
298+
expect(mockGithub.rest.repos.getCollaboratorPermissionLevel).not.toHaveBeenCalled();
299+
});
300+
301+
it("should handle 404 error for inactive bot", async () => {
302+
const apiError = { status: 404, message: "Not Found" };
303+
mockGithub.rest.repos.getCollaboratorPermissionLevel.mockRejectedValue(apiError);
304+
305+
const result = await checkBotStatus("renovate[bot]", "testowner", "testrepo");
306+
307+
expect(result).toEqual({
308+
isBot: true,
309+
isActive: false,
310+
});
311+
312+
expect(mockCore.warning).toHaveBeenCalledWith("Bot 'renovate[bot]' is not active/installed on testowner/testrepo");
313+
});
314+
315+
it("should handle other API errors", async () => {
316+
const apiError = new Error("API rate limit exceeded");
317+
mockGithub.rest.repos.getCollaboratorPermissionLevel.mockRejectedValue(apiError);
318+
319+
const result = await checkBotStatus("github-actions[bot]", "testowner", "testrepo");
320+
321+
expect(result).toEqual({
322+
isBot: true,
323+
isActive: false,
324+
error: "API rate limit exceeded",
325+
});
326+
327+
expect(mockCore.warning).toHaveBeenCalledWith("Failed to check bot status: API rate limit exceeded");
328+
});
329+
330+
it("should handle non-Error API failures", async () => {
331+
mockGithub.rest.repos.getCollaboratorPermissionLevel.mockRejectedValue("String error");
332+
333+
const result = await checkBotStatus("bot[bot]", "testowner", "testrepo");
334+
335+
expect(result).toEqual({
336+
isBot: true,
337+
isActive: false,
338+
error: "String error",
339+
});
340+
341+
expect(mockCore.warning).toHaveBeenCalledWith("Failed to check bot status: String error");
342+
});
343+
344+
it("should handle unexpected errors gracefully", async () => {
345+
// Simulate an error during bot detection
346+
const unexpectedError = new Error("Unexpected error");
347+
mockGithub.rest.repos.getCollaboratorPermissionLevel.mockImplementation(() => {
348+
throw unexpectedError;
349+
});
350+
351+
const result = await checkBotStatus("test[bot]", "testowner", "testrepo");
352+
353+
expect(result).toEqual({
354+
isBot: true,
355+
isActive: false,
356+
error: "Unexpected error",
357+
});
358+
});
359+
360+
it("should verify bot is installed on repository", async () => {
361+
mockGithub.rest.repos.getCollaboratorPermissionLevel.mockResolvedValue({
362+
data: { permission: "admin" },
363+
});
364+
365+
const result = await checkBotStatus("dependabot[bot]", "testowner", "testrepo");
366+
367+
expect(mockGithub.rest.repos.getCollaboratorPermissionLevel).toHaveBeenCalledWith({
368+
owner: "testowner",
369+
repo: "testrepo",
370+
username: "dependabot[bot]",
371+
});
372+
373+
expect(result.isBot).toBe(true);
374+
expect(result.isActive).toBe(true);
375+
});
376+
});
225377
});

actions/setup/js/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@
2222
"test:js-watch": "vitest",
2323
"test:js-coverage": "vitest run --coverage",
2424
"format:terser": "find . -name '*.cjs' -type f -not -path './node_modules/*' | while read file; do if ! grep -qE '^(await |/// <reference|// @ts-check)|await import' \"$file\"; then npx terser \"$file\" -b -c dead_code=true,unused=true,collapse_vars=true --ecma 2022 -o \"$file.tmp\" && mv \"$file.tmp\" \"$file\"; fi; done",
25-
"format:cjs": "npx prettier --write '**/*.cjs' '**/*.ts' '**/*.json'",
26-
"lint:cjs": "npx prettier --check '**/*.cjs' '**/*.ts' '**/*.json'",
25+
"format:cjs": "npx prettier --write '**/*.cjs' '**/*.ts' '**/*.json' --ignore-path ../../../.prettierignore",
26+
"lint:cjs": "npx prettier --check '**/*.cjs' '**/*.ts' '**/*.json' --ignore-path ../../../.prettierignore",
2727
"format:schema": "npx prettier --write ../../../pkg/workflow/schemas/github-workflow.json --ignore-path /dev/null",
28-
"format:pkg-json": "npx prettier --write '../../../**/*.json' '!../../../pkg/workflow/js/**/*.json' '!../../../**/package.json' '!../../../**/package-lock.json'",
29-
"check:pkg-json": "npx prettier --check '../../../**/*.json' '!../../../pkg/workflow/js/**/*.json' '!../../../**/package.json' '!../../../**/package-lock.json'"
28+
"format:pkg-json": "npx prettier --write '../../../**/*.json' '!../../../pkg/workflow/js/**/*.json' --ignore-path ../../../.prettierignore",
29+
"check:pkg-json": "npx prettier --check '../../../**/*.json' '!../../../pkg/workflow/js/**/*.json' --ignore-path ../../../.prettierignore"
3030
}
3131
}

0 commit comments

Comments
 (0)