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
31 changes: 31 additions & 0 deletions actions/setup/js/check_rate_limit.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,42 @@ async function main() {
const maxRuns = parseInt(process.env.GH_AW_RATE_LIMIT_MAX || "5", 10);
const windowMinutes = parseInt(process.env.GH_AW_RATE_LIMIT_WINDOW || "60", 10);
const eventsList = process.env.GH_AW_RATE_LIMIT_EVENTS || "";
// Default: admin, maintain, and write roles are exempt from rate limiting
const ignoredRolesList = process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES || "admin,maintain,write";
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES || "admin,maintain,write" makes an explicitly empty env var (used to represent ignored-roles: []) fall back to the defaults, preventing users from overriding to “no ignored roles”. Use a nullish check (??) instead so "" is preserved.

This issue also appears on line 45 of the same file.

Suggested change
const ignoredRolesList = process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES || "admin,maintain,write";
const ignoredRolesList = process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES ?? "admin,maintain,write";

Copilot uses AI. Check for mistakes.

core.info(`🔍 Checking rate limit for user '${actor}' on workflow '${workflowId}'`);
core.info(` Configuration: max=${maxRuns} runs per ${windowMinutes} minutes`);
core.info(` Current event: ${eventName}`);

// Check if user has an ignored role (exempt from rate limiting)
const ignoredRoles = ignoredRolesList.split(",").map(r => r.trim());
core.info(` Ignored roles: ${ignoredRoles.join(", ")}`);

try {
// Check user's permission level in the repository
const { data: permissionData } = await github.rest.repos.getCollaboratorPermissionLevel({
owner,
repo,
username: actor,
});

const userPermission = permissionData.permission;
core.info(` User '${actor}' has permission level: ${userPermission}`);

// Map GitHub permission levels to role names
// GitHub uses: admin, maintain, write, triage, read
if (ignoredRoles.includes(userPermission)) {
core.info(`✅ User '${actor}' has ignored role '${userPermission}'; skipping rate limit check`);
core.setOutput("rate_limit_ok", "true");
return;
}
} catch (error) {
// If we can't check permissions, continue with rate limiting (fail-secure)
const errorMsg = error instanceof Error ? error.message : String(error);
core.warning(`⚠️ Could not check user permissions: ${errorMsg}`);
core.warning(` Continuing with rate limit check for user '${actor}'`);
}

// Parse events to apply rate limiting to
const limitedEvents = eventsList ? eventsList.split(",").map(e => e.trim()) : [];

Expand Down
161 changes: 161 additions & 0 deletions actions/setup/js/check_rate_limit.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ describe("check_rate_limit", () => {
delete process.env.GH_AW_RATE_LIMIT_MAX;
delete process.env.GH_AW_RATE_LIMIT_WINDOW;
delete process.env.GH_AW_RATE_LIMIT_EVENTS;
delete process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES;

// Reset repos mock
mockGithub.rest.repos = undefined;

// Reload the module to get fresh instance
vi.resetModules();
Expand Down Expand Up @@ -558,4 +562,161 @@ describe("check_rate_limit", () => {

expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using workflow name: test-workflow (fallback"));
});

it("should use default ignored roles (admin, maintain, write) when not specified", async () => {
// Don't set GH_AW_RATE_LIMIT_IGNORED_ROLES, so it uses default

Comment on lines +566 to +568
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s coverage for default ignored roles and custom lists, but no test for the explicit empty override (e.g. GH_AW_RATE_LIMIT_IGNORED_ROLES=""), which should result in no exemptions (admin should go through the rate-limit path). Add a test to lock in the intended ignored-roles: [] semantics.

Copilot uses AI. Check for mistakes.
// Mock the permission check to return write
mockGithub.rest.repos = {
getCollaboratorPermissionLevel: vi.fn().mockResolvedValue({
data: {
permission: "write",
},
}),
};

await checkRateLimit.main();

expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Ignored roles: admin, maintain, write"));
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("User 'test-user' has permission level: write"));
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("User 'test-user' has ignored role 'write'; skipping rate limit check"));
expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
expect(mockGithub.rest.actions.listWorkflowRuns).not.toHaveBeenCalled();
});

it("should apply rate limiting to triage users by default", async () => {
// Don't set GH_AW_RATE_LIMIT_IGNORED_ROLES, so it uses default (admin, maintain, write)

mockGithub.rest.repos = {
getCollaboratorPermissionLevel: vi.fn().mockResolvedValue({
data: {
permission: "triage",
},
}),
};

mockGithub.rest.actions = {
listWorkflowRuns: vi.fn().mockResolvedValue({
data: {
workflow_runs: [],
},
}),
cancelWorkflowRun: vi.fn(),
};

await checkRateLimit.main();

expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Ignored roles: admin, maintain, write"));
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("User 'test-user' has permission level: triage"));
expect(mockGithub.rest.actions.listWorkflowRuns).toHaveBeenCalled();
expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
});

it("should skip rate limiting for users with ignored roles", async () => {
process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES = "admin,maintain";

// Mock the permission check to return admin
mockGithub.rest.repos = {
getCollaboratorPermissionLevel: vi.fn().mockResolvedValue({
data: {
permission: "admin",
},
}),
};

await checkRateLimit.main();

expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Ignored roles: admin, maintain"));
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("User 'test-user' has permission level: admin"));
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("User 'test-user' has ignored role 'admin'; skipping rate limit check"));
expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
expect(mockGithub.rest.actions.listWorkflowRuns).not.toHaveBeenCalled();
});

it("should skip rate limiting for users with maintain permission when in ignored roles", async () => {
process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES = "admin,maintain";

mockGithub.rest.repos = {
getCollaboratorPermissionLevel: vi.fn().mockResolvedValue({
data: {
permission: "maintain",
},
}),
};

await checkRateLimit.main();

expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("User 'test-user' has ignored role 'maintain'; skipping rate limit check"));
expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
expect(mockGithub.rest.actions.listWorkflowRuns).not.toHaveBeenCalled();
});

it("should apply rate limiting for users without ignored roles", async () => {
process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES = "admin,maintain";

mockGithub.rest.repos = {
getCollaboratorPermissionLevel: vi.fn().mockResolvedValue({
data: {
permission: "write",
},
}),
};

mockGithub.rest.actions = {
listWorkflowRuns: vi.fn().mockResolvedValue({
data: {
workflow_runs: [],
},
}),
cancelWorkflowRun: vi.fn(),
};

await checkRateLimit.main();

expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("User 'test-user' has permission level: write"));
expect(mockGithub.rest.actions.listWorkflowRuns).toHaveBeenCalled();
expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
});

it("should continue with rate limiting if permission check fails", async () => {
process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES = "admin";

mockGithub.rest.repos = {
getCollaboratorPermissionLevel: vi.fn().mockRejectedValue(new Error("API error")),
};

mockGithub.rest.actions = {
listWorkflowRuns: vi.fn().mockResolvedValue({
data: {
workflow_runs: [],
},
}),
cancelWorkflowRun: vi.fn(),
};

await checkRateLimit.main();

expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Could not check user permissions"));
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Continuing with rate limit check"));
expect(mockGithub.rest.actions.listWorkflowRuns).toHaveBeenCalled();
expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
});

it("should handle single ignored role as string", async () => {
process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES = "admin";

mockGithub.rest.repos = {
getCollaboratorPermissionLevel: vi.fn().mockResolvedValue({
data: {
permission: "admin",
},
}),
};

await checkRateLimit.main();

expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Ignored roles: admin"));
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("User 'test-user' has ignored role 'admin'; skipping rate limit check"));
expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
});
});
Loading
Loading