From 35dcabf342996012843bd3e6c6dd98422fffc3e5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 04:53:37 +0000 Subject: [PATCH 1/3] jsweep: clean check_rate_limit.cjs - Extract PROGRAMMATIC_EVENTS as a module-level constant - Destructure context variables at top of main() - Simplify workflowId extraction using workflowRefMatch?.[1] - Use ?? instead of || for env var defaults - Simplify permission data access with nested destructuring - Use ?? for runsPerEvent accumulation - Add 2 new tests: runs without updated_at and multi-page pagination Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- actions/setup/js/check_rate_limit.cjs | 50 +++++++++-------- actions/setup/js/check_rate_limit.test.cjs | 64 ++++++++++++++++++++++ 2 files changed, 90 insertions(+), 24 deletions(-) diff --git a/actions/setup/js/check_rate_limit.cjs b/actions/setup/js/check_rate_limit.cjs index 1ed2bea9cf1..aa773c4a28d 100644 --- a/actions/setup/js/check_rate_limit.cjs +++ b/actions/setup/js/check_rate_limit.cjs @@ -9,26 +9,30 @@ const { fetchAndLogRateLimit } = require("./github_rate_limit_logger.cjs"); * Prevents users from triggering workflows too frequently */ +const PROGRAMMATIC_EVENTS = ["workflow_dispatch", "repository_dispatch", "issue_comment", "pull_request_review", "pull_request_review_comment", "discussion_comment"]; + async function main() { - const actor = context.actor; - const owner = context.repo.owner; - const repo = context.repo.repo; - const eventName = context.eventName; - const runId = context.runId; + const { + actor, + repo: { owner, repo }, + workflow, + eventName, + runId, + } = context; // Capture a rate-limit snapshot at the start of the check for observability. await fetchAndLogRateLimit(github, "check_rate_limit_start"); // Get workflow file name from GITHUB_WORKFLOW_REF (format: "owner/repo/.github/workflows/file.yml@ref") // or fall back to GITHUB_WORKFLOW (workflow name) - const workflowRef = process.env.GITHUB_WORKFLOW_REF || ""; - let workflowId = context.workflow; // Default to workflow name + const workflowRef = process.env.GITHUB_WORKFLOW_REF ?? ""; + // Extract workflow file from the ref (e.g., ".github/workflows/test.lock.yml@refs/heads/main") + const workflowRefMatch = workflowRef.match(/\.github\/workflows\/([^@]+)/); + let workflowId = workflow; // Default to workflow name if (workflowRef) { - // Extract workflow file from the ref (e.g., ".github/workflows/test.lock.yml@refs/heads/main") - const match = workflowRef.match(/\.github\/workflows\/([^@]+)/); - if (match && match[1]) { - workflowId = match[1]; + if (workflowRefMatch?.[1]) { + workflowId = workflowRefMatch[1]; core.info(` Using workflow file: ${workflowId} (from GITHUB_WORKFLOW_REF)`); } else { core.info(` Using workflow name: ${workflowId} (fallback - could not parse GITHUB_WORKFLOW_REF)`); @@ -38,11 +42,11 @@ async function main() { } // Get configuration from environment variables - 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 || ""; + 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"; + const ignoredRolesList = process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES ?? "admin,maintain,write"; core.info(`🔍 Checking rate limit for user '${actor}' on workflow '${workflowId}'`); core.info(` Configuration: max=${maxRuns} runs per ${windowMinutes} minutes`); @@ -54,14 +58,14 @@ async function main() { try { // Check user's permission level in the repository - const permResponse = await github.rest.repos.getCollaboratorPermissionLevel({ + const { + data: { permission: userPermission }, + } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username: actor, }); - const { data: permissionData } = permResponse; - const userPermission = permissionData.permission; core.info(` User '${actor}' has permission level: ${userPermission}`); // Map GitHub permission levels to role names @@ -92,16 +96,14 @@ async function main() { } else { // When no specific events are configured, apply rate limiting only to // known programmatic triggers. Allow all other events. - const programmaticEvents = ["workflow_dispatch", "repository_dispatch", "issue_comment", "pull_request_review", "pull_request_review_comment", "discussion_comment"]; - - if (!programmaticEvents.includes(eventName)) { + if (!PROGRAMMATIC_EVENTS.includes(eventName)) { core.info(`✅ Event '${eventName}' is not a programmatic trigger; skipping rate limiting`); - core.info(` Rate limiting applies to: ${programmaticEvents.join(", ")}`); + core.info(` Rate limiting applies to: ${PROGRAMMATIC_EVENTS.join(", ")}`); core.setOutput("rate_limit_ok", "true"); return; } - core.info(` Rate limiting applies to programmatic events: ${programmaticEvents.join(", ")}`); + core.info(` Rate limiting applies to programmatic events: ${PROGRAMMATIC_EVENTS.join(", ")}`); } // Calculate time threshold @@ -197,7 +199,7 @@ async function main() { // Count this run totalRecentRuns++; - runsPerEvent[runEvent] = (runsPerEvent[runEvent] || 0) + 1; + runsPerEvent[runEvent] = (runsPerEvent[runEvent] ?? 0) + 1; core.info(` ✓ Run #${run.run_number} (${run.id}) by ${run.actor?.login} - ` + `event: ${runEvent}, created: ${run.created_at}, status: ${run.status}`); } diff --git a/actions/setup/js/check_rate_limit.test.cjs b/actions/setup/js/check_rate_limit.test.cjs index f82dfaed667..3dfb359c7b4 100644 --- a/actions/setup/js/check_rate_limit.test.cjs +++ b/actions/setup/js/check_rate_limit.test.cjs @@ -768,4 +768,68 @@ describe("check_rate_limit", () => { expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Stack trace:")); expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true"); }); + + it("should count runs without updated_at (no duration check applied)", async () => { + const recentTime = new Date(Date.now() - 10 * 60 * 1000); + + mockGithub.rest.actions.listWorkflowRuns.mockResolvedValue({ + data: { + workflow_runs: [ + { + id: 111111, + run_number: 1, + created_at: recentTime.toISOString(), + // no updated_at — duration check skipped, run should be counted + actor: { login: "test-user" }, + event: "workflow_dispatch", + status: "in_progress", + }, + ], + }, + }); + + await checkRateLimit.main(); + + expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Total recent runs in last 60 minutes: 1")); + }); + + it("should fetch additional pages when first page is full", async () => { + process.env.GH_AW_RATE_LIMIT_MAX = "10"; + const recentTime = new Date(Date.now() - 10 * 60 * 1000); + + const makeRunOtherUser = id => ({ + id, + run_number: id, + created_at: recentTime.toISOString(), + actor: { login: "other-user" }, // not counted for test-user + event: "workflow_dispatch", + status: "completed", + }); + + const makeRunTestUser = id => ({ + id, + run_number: id, + created_at: recentTime.toISOString(), + actor: { login: "test-user" }, + event: "workflow_dispatch", + status: "completed", + }); + + // First page is full (100 runs) but all by a different user → no match, fetches page 2 + mockGithub.rest.actions.listWorkflowRuns + .mockResolvedValueOnce({ + data: { workflow_runs: Array.from({ length: 100 }, (_, i) => makeRunOtherUser(i + 1)) }, + }) + .mockResolvedValueOnce({ + data: { workflow_runs: [makeRunTestUser(101), makeRunTestUser(102)] }, + }); + + await checkRateLimit.main(); + + expect(mockGithub.rest.actions.listWorkflowRuns).toHaveBeenCalledTimes(2); + expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Fetching page 2")); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Total recent runs in last 60 minutes: 2")); + }); }); From 16255105a0de281f4862708b820428a37a9b7a75 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 05:30:19 +0000 Subject: [PATCH 2/3] fix: normalize env vars with .trim() to handle empty/whitespace values in check_rate_limit.cjs Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8c57323f-841b-4fd1-8dde-bc3da1f2ca4c Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/check_rate_limit.cjs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/check_rate_limit.cjs b/actions/setup/js/check_rate_limit.cjs index aa773c4a28d..c6a8d59f52f 100644 --- a/actions/setup/js/check_rate_limit.cjs +++ b/actions/setup/js/check_rate_limit.cjs @@ -42,11 +42,12 @@ async function main() { } // Get configuration from environment variables - 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 ?? ""; + // Use .trim() + || so that empty/whitespace-only values also fall back to defaults + const maxRuns = parseInt(process.env.GH_AW_RATE_LIMIT_MAX?.trim() || "5", 10); + const windowMinutes = parseInt(process.env.GH_AW_RATE_LIMIT_WINDOW?.trim() || "60", 10); + const eventsList = process.env.GH_AW_RATE_LIMIT_EVENTS?.trim() ?? ""; // Default: admin, maintain, and write roles are exempt from rate limiting - const ignoredRolesList = process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES ?? "admin,maintain,write"; + const ignoredRolesList = process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES?.trim() || "admin,maintain,write"; core.info(`🔍 Checking rate limit for user '${actor}' on workflow '${workflowId}'`); core.info(` Configuration: max=${maxRuns} runs per ${windowMinutes} minutes`); From a26f47f1ce087d19440522f998221339482943de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 05:31:15 +0000 Subject: [PATCH 3/3] fix: use consistent .trim() || pattern for all env vars in check_rate_limit.cjs Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8c57323f-841b-4fd1-8dde-bc3da1f2ca4c Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/check_rate_limit.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/setup/js/check_rate_limit.cjs b/actions/setup/js/check_rate_limit.cjs index c6a8d59f52f..59b9441f11a 100644 --- a/actions/setup/js/check_rate_limit.cjs +++ b/actions/setup/js/check_rate_limit.cjs @@ -45,7 +45,7 @@ async function main() { // Use .trim() + || so that empty/whitespace-only values also fall back to defaults const maxRuns = parseInt(process.env.GH_AW_RATE_LIMIT_MAX?.trim() || "5", 10); const windowMinutes = parseInt(process.env.GH_AW_RATE_LIMIT_WINDOW?.trim() || "60", 10); - const eventsList = process.env.GH_AW_RATE_LIMIT_EVENTS?.trim() ?? ""; + const eventsList = process.env.GH_AW_RATE_LIMIT_EVENTS?.trim() || ""; // Default: admin, maintain, and write roles are exempt from rate limiting const ignoredRolesList = process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES?.trim() || "admin,maintain,write";