From bada850c0f67be2f5bd95e1827b3b155841ca4f8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 12:20:25 +0000
Subject: [PATCH 01/14] Initial plan
From a27b1225daab230e6dd0aad2afda7b96008a77d9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 12:26:09 +0000
Subject: [PATCH 02/14] Initial commit: Add rate limit support infrastructure
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.github/aw/actions-lock.json | 5 +++++
.github/workflows/release.lock.yml | 4 ++--
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json
index c9ea38685d2..3688f8f2b9e 100644
--- a/.github/aw/actions-lock.json
+++ b/.github/aw/actions-lock.json
@@ -125,6 +125,11 @@
"version": "v2.0.3",
"sha": "e95548e56dfa95d4e1a28d6f422fafe75c4c26fb"
},
+ "docker/build-push-action@v6": {
+ "repo": "docker/build-push-action",
+ "version": "v6",
+ "sha": "ee4ca427a2f43b6a16632044ca514c076267da23"
+ },
"docker/build-push-action@v6.18.0": {
"repo": "docker/build-push-action",
"version": "v6.18.0",
diff --git a/.github/workflows/release.lock.yml b/.github/workflows/release.lock.yml
index 3a4828e86df..626adc4d786 100644
--- a/.github/workflows/release.lock.yml
+++ b/.github/workflows/release.lock.yml
@@ -1196,7 +1196,7 @@ jobs:
- name: Setup Docker Buildx (pre-validation)
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Build Docker image (validation only)
- uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
+ uses: docker/build-push-action@ee4ca427a2f43b6a16632044ca514c076267da23 # v6
with:
build-args: |
BINARY=dist/linux-amd64
@@ -1285,7 +1285,7 @@ jobs:
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image (amd64)
id: build
- uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
+ uses: docker/build-push-action@ee4ca427a2f43b6a16632044ca514c076267da23 # v6
with:
build-args: |
BINARY=dist/linux-amd64
From 1dca0a3631d7a2348fcb044451a54a931fb3671a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 12:30:57 +0000
Subject: [PATCH 03/14] Add rate limiting infrastructure and JavaScript
implementation
- Add rate-limit configuration to FrontmatterConfig and WorkflowData
- Add extractRateLimitConfig to parse rate-limit from frontmatter
- Add generateRateLimitCheck to create rate limit check step
- Implement check_rate_limit.cjs with comprehensive rate limiting logic
- Add 13 test cases covering various scenarios
- Add constants: CheckRateLimitStepID, RateLimitOkOutput
- Add default rate limit constants (5 runs per 60 minutes)
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/check_rate_limit.cjs | 184 ++++++++
actions/setup/js/check_rate_limit.test.cjs | 436 ++++++++++++++++++
pkg/constants/constants.go | 6 +
pkg/workflow/compiler_activation_jobs.go | 15 +
.../compiler_orchestrator_workflow.go | 1 +
pkg/workflow/compiler_types.go | 1 +
pkg/workflow/data/action_pins.json | 5 +
pkg/workflow/frontmatter_types.go | 13 +-
pkg/workflow/role_checks.go | 87 ++++
9 files changed, 746 insertions(+), 2 deletions(-)
create mode 100644 actions/setup/js/check_rate_limit.cjs
create mode 100644 actions/setup/js/check_rate_limit.test.cjs
diff --git a/actions/setup/js/check_rate_limit.cjs b/actions/setup/js/check_rate_limit.cjs
new file mode 100644
index 00000000000..22b5dc5c0bd
--- /dev/null
+++ b/actions/setup/js/check_rate_limit.cjs
@@ -0,0 +1,184 @@
+// @ts-check
+///
+
+/**
+ * Rate limit check for per-user per-workflow triggers
+ * Prevents users from triggering workflows too frequently
+ */
+
+async function main() {
+ const actor = context.actor;
+ const owner = context.repo.owner;
+ const repo = context.repo.repo;
+ const workflowId = context.workflow;
+ const eventName = context.eventName;
+ const runId = context.runId;
+
+ // 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 || "";
+
+ 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}`);
+
+ // Parse events to apply rate limiting to
+ const limitedEvents = eventsList ? eventsList.split(",").map(e => e.trim()) : [];
+
+ // If specific events are configured, check if current event should be limited
+ if (limitedEvents.length > 0) {
+ if (!limitedEvents.includes(eventName)) {
+ core.info(`✅ Event '${eventName}' is not subject to rate limiting`);
+ core.info(` Rate limiting applies only to: ${limitedEvents.join(", ")}`);
+ core.setOutput("rate_limit_ok", "true");
+ return;
+ }
+ core.info(` Event '${eventName}' is subject to rate limiting`);
+ } else {
+ core.info(` Rate limiting applies to all programmatically triggered events`);
+ }
+
+ // Calculate time threshold
+ const windowMs = windowMinutes * 60 * 1000;
+ const thresholdTime = new Date(Date.now() - windowMs);
+ const thresholdISO = thresholdTime.toISOString();
+
+ core.info(` Time window: runs created after ${thresholdISO}`);
+
+ try {
+ // Collect recent workflow runs by event type
+ // This allows us to aggregate counts and short-circuit when max is exceeded
+ let totalRecentRuns = 0;
+ const runsPerEvent = {};
+
+ core.info(`📊 Querying workflow runs for '${workflowId}'...`);
+
+ // Query workflow runs (paginated if needed)
+ let page = 1;
+ let hasMore = true;
+ const perPage = 100;
+
+ while (hasMore && totalRecentRuns < maxRuns) {
+ core.info(` Fetching page ${page} (up to ${perPage} runs per page)...`);
+
+ const response = await github.rest.actions.listWorkflowRuns({
+ owner,
+ repo,
+ workflow_id: workflowId,
+ per_page: perPage,
+ page,
+ });
+
+ const runs = response.data.workflow_runs;
+ core.info(` Retrieved ${runs.length} runs from page ${page}`);
+
+ if (runs.length === 0) {
+ hasMore = false;
+ break;
+ }
+
+ // Filter runs by actor and time window
+ for (const run of runs) {
+ // Stop processing if we've already exceeded the limit
+ if (totalRecentRuns >= maxRuns) {
+ core.info(` Short-circuit: Already found ${totalRecentRuns} runs (>= max ${maxRuns})`);
+ hasMore = false;
+ break;
+ }
+
+ // Skip if run is older than the time window
+ const runCreatedAt = new Date(run.created_at);
+ if (runCreatedAt < thresholdTime) {
+ core.info(` Skipping run ${run.id} - created before threshold (${run.created_at})`);
+ continue;
+ }
+
+ // Check if run is by the same actor
+ if (run.actor?.login !== actor) {
+ continue;
+ }
+
+ // Skip the current run (we're checking if we should allow THIS run)
+ if (run.id === runId) {
+ continue;
+ }
+
+ // If specific events are configured, only count matching events
+ const runEvent = run.event;
+ if (limitedEvents.length > 0 && !limitedEvents.includes(runEvent)) {
+ continue;
+ }
+
+ // Count this run
+ totalRecentRuns++;
+ 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}`);
+ }
+
+ // Check if we should fetch more pages
+ if (runs.length < perPage || totalRecentRuns >= maxRuns) {
+ hasMore = false;
+ } else {
+ page++;
+ }
+ }
+
+ // Log summary by event type
+ core.info(`📈 Rate limit summary for user '${actor}':`);
+ core.info(` Total recent runs in last ${windowMinutes} minutes: ${totalRecentRuns}`);
+ core.info(` Maximum allowed: ${maxRuns}`);
+
+ if (Object.keys(runsPerEvent).length > 0) {
+ core.info(` Breakdown by event type:`);
+ for (const [event, count] of Object.entries(runsPerEvent)) {
+ core.info(` - ${event}: ${count} runs`);
+ }
+ }
+
+ // Check if rate limit is exceeded
+ if (totalRecentRuns >= maxRuns) {
+ core.warning(`⚠️ Rate limit exceeded for user '${actor}' on workflow '${workflowId}'`);
+ core.warning(` User has triggered ${totalRecentRuns} runs in the last ${windowMinutes} minutes (max: ${maxRuns})`);
+ core.warning(` Cancelling current workflow run...`);
+
+ // Cancel the current workflow run
+ try {
+ await github.rest.actions.cancelWorkflowRun({
+ owner,
+ repo,
+ run_id: runId,
+ });
+ core.warning(`✅ Workflow run ${runId} cancelled successfully`);
+ } catch (cancelError) {
+ const errorMsg = cancelError instanceof Error ? cancelError.message : String(cancelError);
+ core.error(`❌ Failed to cancel workflow run: ${errorMsg}`);
+ // Continue anyway - the rate limit output will still be set to false
+ }
+
+ core.setOutput("rate_limit_ok", "false");
+ return;
+ }
+
+ // Rate limit not exceeded
+ core.info(`✅ Rate limit check passed`);
+ core.info(` User '${actor}' has ${totalRecentRuns} runs in the last ${windowMinutes} minutes`);
+ core.info(` Remaining quota: ${maxRuns - totalRecentRuns} runs`);
+ core.setOutput("rate_limit_ok", "true");
+ } catch (error) {
+ const errorMsg = error instanceof Error ? error.message : String(error);
+ const errorStack = error instanceof Error ? error.stack : "";
+ core.error(`❌ Rate limit check failed: ${errorMsg}`);
+ if (errorStack) {
+ core.error(` Stack trace: ${errorStack}`);
+ }
+
+ // On error, allow the workflow to proceed (fail-open)
+ // This prevents rate limiting from blocking workflows due to API issues
+ core.warning(`⚠️ Allowing workflow to proceed due to rate limit check error`);
+ core.setOutput("rate_limit_ok", "true");
+ }
+}
+
+module.exports = { main };
diff --git a/actions/setup/js/check_rate_limit.test.cjs b/actions/setup/js/check_rate_limit.test.cjs
new file mode 100644
index 00000000000..2cd09fb4048
--- /dev/null
+++ b/actions/setup/js/check_rate_limit.test.cjs
@@ -0,0 +1,436 @@
+// @ts-check
+import { describe, it, expect, beforeEach, vi } from "vitest";
+
+describe("check_rate_limit", () => {
+ let mockCore;
+ let mockGithub;
+ let mockContext;
+ let checkRateLimit;
+
+ beforeEach(async () => {
+ // Mock @actions/core
+ mockCore = {
+ info: vi.fn(),
+ warning: vi.fn(),
+ error: vi.fn(),
+ setOutput: vi.fn(),
+ setFailed: vi.fn(),
+ };
+
+ // Mock @actions/github
+ mockGithub = {
+ rest: {
+ actions: {
+ listWorkflowRuns: vi.fn(),
+ cancelWorkflowRun: vi.fn(),
+ },
+ },
+ };
+
+ // Mock context
+ mockContext = {
+ actor: "test-user",
+ repo: {
+ owner: "test-owner",
+ repo: "test-repo",
+ },
+ workflow: "test-workflow",
+ eventName: "workflow_dispatch",
+ runId: 123456,
+ };
+
+ // Setup global mocks
+ global.core = mockCore;
+ global.github = mockGithub;
+ global.context = mockContext;
+
+ // Reset environment variables
+ delete process.env.GH_AW_RATE_LIMIT_MAX;
+ delete process.env.GH_AW_RATE_LIMIT_WINDOW;
+ delete process.env.GH_AW_RATE_LIMIT_EVENTS;
+
+ // Reload the module to get fresh instance
+ vi.resetModules();
+ checkRateLimit = await import("./check_rate_limit.cjs");
+ });
+
+ it("should pass when no recent runs by actor", async () => {
+ mockGithub.rest.actions.listWorkflowRuns.mockResolvedValue({
+ data: {
+ workflow_runs: [],
+ },
+ });
+
+ await checkRateLimit.main();
+
+ expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Rate limit check passed"));
+ });
+
+ it("should pass when recent runs are below limit", async () => {
+ const oneHourAgo = new Date(Date.now() - 30 * 60 * 1000); // 30 minutes ago
+
+ mockGithub.rest.actions.listWorkflowRuns.mockResolvedValue({
+ data: {
+ workflow_runs: [
+ {
+ id: 111111,
+ run_number: 1,
+ created_at: oneHourAgo.toISOString(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ {
+ id: 222222,
+ run_number: 2,
+ created_at: oneHourAgo.toISOString(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ ],
+ },
+ });
+
+ await checkRateLimit.main();
+
+ expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Total recent runs in last 60 minutes: 2"));
+ });
+
+ it("should fail when rate limit is exceeded", async () => {
+ process.env.GH_AW_RATE_LIMIT_MAX = "3";
+ const recentTime = new Date(Date.now() - 10 * 60 * 1000); // 10 minutes ago
+
+ mockGithub.rest.actions.listWorkflowRuns.mockResolvedValue({
+ data: {
+ workflow_runs: [
+ {
+ id: 111111,
+ run_number: 1,
+ created_at: recentTime.toISOString(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ {
+ id: 222222,
+ run_number: 2,
+ created_at: recentTime.toISOString(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ {
+ id: 333333,
+ run_number: 3,
+ created_at: recentTime.toISOString(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ ],
+ },
+ });
+
+ mockGithub.rest.actions.cancelWorkflowRun.mockResolvedValue({});
+
+ await checkRateLimit.main();
+
+ expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "false");
+ expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Rate limit exceeded"));
+ expect(mockGithub.rest.actions.cancelWorkflowRun).toHaveBeenCalledWith({
+ owner: "test-owner",
+ repo: "test-repo",
+ run_id: 123456,
+ });
+ });
+
+ it("should only count runs by the same actor", 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(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ {
+ id: 222222,
+ run_number: 2,
+ created_at: recentTime.toISOString(),
+ actor: { login: "other-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ {
+ id: 333333,
+ run_number: 3,
+ created_at: recentTime.toISOString(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ ],
+ },
+ });
+
+ await checkRateLimit.main();
+
+ expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Total recent runs in last 60 minutes: 2"));
+ });
+
+ it("should exclude runs older than the time window", async () => {
+ const twoHoursAgo = new Date(Date.now() - 120 * 60 * 1000);
+ const recentTime = new Date(Date.now() - 10 * 60 * 1000);
+
+ mockGithub.rest.actions.listWorkflowRuns.mockResolvedValue({
+ data: {
+ workflow_runs: [
+ {
+ id: 111111,
+ run_number: 1,
+ created_at: twoHoursAgo.toISOString(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ {
+ id: 222222,
+ run_number: 2,
+ created_at: recentTime.toISOString(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ ],
+ },
+ });
+
+ 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 exclude the current run from the count", async () => {
+ const recentTime = new Date(Date.now() - 10 * 60 * 1000);
+
+ mockGithub.rest.actions.listWorkflowRuns.mockResolvedValue({
+ data: {
+ workflow_runs: [
+ {
+ id: 123456, // Current run ID
+ run_number: 1,
+ created_at: recentTime.toISOString(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "in_progress",
+ },
+ {
+ id: 222222,
+ run_number: 2,
+ created_at: recentTime.toISOString(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ ],
+ },
+ });
+
+ 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 only count specified event types when events filter is set", async () => {
+ process.env.GH_AW_RATE_LIMIT_EVENTS = "workflow_dispatch,issue_comment";
+ 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(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ {
+ id: 222222,
+ run_number: 2,
+ created_at: recentTime.toISOString(),
+ actor: { login: "test-user" },
+ event: "push",
+ status: "completed",
+ },
+ {
+ id: 333333,
+ run_number: 3,
+ created_at: recentTime.toISOString(),
+ actor: { login: "test-user" },
+ event: "issue_comment",
+ status: "completed",
+ },
+ ],
+ },
+ });
+
+ await checkRateLimit.main();
+
+ expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Total recent runs in last 60 minutes: 2"));
+ });
+
+ it("should skip rate limiting if current event is not in the events filter", async () => {
+ process.env.GH_AW_RATE_LIMIT_EVENTS = "issue_comment,pull_request";
+ mockContext.eventName = "workflow_dispatch";
+
+ await checkRateLimit.main();
+
+ expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Event 'workflow_dispatch' is not subject to rate limiting"));
+ expect(mockGithub.rest.actions.listWorkflowRuns).not.toHaveBeenCalled();
+ });
+
+ it("should use custom max and window values", async () => {
+ process.env.GH_AW_RATE_LIMIT_MAX = "10";
+ process.env.GH_AW_RATE_LIMIT_WINDOW = "30";
+
+ mockGithub.rest.actions.listWorkflowRuns.mockResolvedValue({
+ data: {
+ workflow_runs: [],
+ },
+ });
+
+ await checkRateLimit.main();
+
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("max=10 runs per 30 minutes"));
+ });
+
+ it("should short-circuit when max is exceeded during pagination", async () => {
+ process.env.GH_AW_RATE_LIMIT_MAX = "2";
+ const recentTime = new Date(Date.now() - 10 * 60 * 1000);
+
+ // First page returns 2 runs (exceeds limit)
+ mockGithub.rest.actions.listWorkflowRuns.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ {
+ id: 111111,
+ run_number: 1,
+ created_at: recentTime.toISOString(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ {
+ id: 222222,
+ run_number: 2,
+ created_at: recentTime.toISOString(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ ],
+ },
+ });
+
+ mockGithub.rest.actions.cancelWorkflowRun.mockResolvedValue({});
+
+ await checkRateLimit.main();
+
+ // Should only call once, not fetch second page
+ expect(mockGithub.rest.actions.listWorkflowRuns).toHaveBeenCalledTimes(1);
+ expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "false");
+ });
+
+ it("should fail-open on API errors", async () => {
+ mockGithub.rest.actions.listWorkflowRuns.mockRejectedValue(new Error("API error"));
+
+ await checkRateLimit.main();
+
+ expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Rate limit check failed"));
+ expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Allowing workflow to proceed"));
+ expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
+ });
+
+ it("should continue even if cancellation fails", async () => {
+ process.env.GH_AW_RATE_LIMIT_MAX = "1";
+ 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(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ ],
+ },
+ });
+
+ mockGithub.rest.actions.cancelWorkflowRun.mockRejectedValue(new Error("Cancellation failed"));
+
+ await checkRateLimit.main();
+
+ expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Failed to cancel workflow run"));
+ expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "false");
+ });
+
+ it("should provide breakdown by event type", 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(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ {
+ id: 222222,
+ run_number: 2,
+ created_at: recentTime.toISOString(),
+ actor: { login: "test-user" },
+ event: "issue_comment",
+ status: "completed",
+ },
+ {
+ id: 333333,
+ run_number: 3,
+ created_at: recentTime.toISOString(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ ],
+ },
+ });
+
+ await checkRateLimit.main();
+
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Breakdown by event type:"));
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("workflow_dispatch: 2 runs"));
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("issue_comment: 1 runs"));
+ });
+});
diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go
index f3309c73226..78dbe69ef7d 100644
--- a/pkg/constants/constants.go
+++ b/pkg/constants/constants.go
@@ -607,6 +607,7 @@ const CheckStopTimeStepID StepID = "check_stop_time"
const CheckSkipIfMatchStepID StepID = "check_skip_if_match"
const CheckSkipIfNoMatchStepID StepID = "check_skip_if_no_match"
const CheckCommandPositionStepID StepID = "check_command_position"
+const CheckRateLimitStepID StepID = "check_rate_limit"
// Output names for pre-activation job steps
const IsTeamMemberOutput = "is_team_member"
@@ -615,8 +616,13 @@ const SkipCheckOkOutput = "skip_check_ok"
const SkipNoMatchCheckOkOutput = "skip_no_match_check_ok"
const CommandPositionOkOutput = "command_position_ok"
const MatchedCommandOutput = "matched_command"
+const RateLimitOkOutput = "rate_limit_ok"
const ActivatedOutput = "activated"
+// Rate limit defaults
+const DefaultRateLimitMax = 5 // Default maximum runs per time window
+const DefaultRateLimitWindow = 60 // Default time window in minutes (1 hour)
+
// Agentic engine name constants using EngineName type for type safety
const (
// CopilotEngine is the GitHub Copilot engine identifier
diff --git a/pkg/workflow/compiler_activation_jobs.go b/pkg/workflow/compiler_activation_jobs.go
index fa68aaa9e43..a2fe66abf23 100644
--- a/pkg/workflow/compiler_activation_jobs.go
+++ b/pkg/workflow/compiler_activation_jobs.go
@@ -89,6 +89,11 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec
steps = c.generateMembershipCheck(data, steps)
}
+ // Add rate limit check if configured
+ if data.RateLimit != nil {
+ steps = c.generateRateLimitCheck(data, steps)
+ }
+
// Add stop-time check if configured
if data.StopTime != "" {
// Extract workflow name for the stop-time check
@@ -207,6 +212,16 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec
conditions = append(conditions, skipNoMatchCheckOk)
}
+ if data.RateLimit != nil {
+ // Add rate limit check condition
+ rateLimitCheck := BuildComparison(
+ BuildPropertyAccess(fmt.Sprintf("steps.%s.outputs.%s", constants.CheckRateLimitStepID, constants.RateLimitOkOutput)),
+ "==",
+ BuildStringLiteral("true"),
+ )
+ conditions = append(conditions, rateLimitCheck)
+ }
+
if len(data.Command) > 0 {
// Add command position check condition
commandPositionCheck := BuildComparison(
diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go
index 57d9598ed85..1691b267861 100644
--- a/pkg/workflow/compiler_orchestrator_workflow.go
+++ b/pkg/workflow/compiler_orchestrator_workflow.go
@@ -419,6 +419,7 @@ func (c *Compiler) extractAdditionalConfigurations(
workflowData.Roles = c.extractRoles(frontmatter)
workflowData.Bots = c.extractBots(frontmatter)
+ workflowData.RateLimit = c.extractRateLimitConfig(frontmatter)
// Use the already extracted output configuration
workflowData.SafeOutputs = safeOutputs
diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go
index 3bed652e2eb..2a5d51091f1 100644
--- a/pkg/workflow/compiler_types.go
+++ b/pkg/workflow/compiler_types.go
@@ -431,6 +431,7 @@ type WorkflowData struct {
SafeInputs *SafeInputsConfig // safe-inputs configuration for custom MCP tools
Roles []string // permission levels required to trigger workflow
Bots []string // allow list of bot identifiers that can trigger workflow
+ RateLimit *RateLimitConfig // rate limiting configuration for workflow triggers
CacheMemoryConfig *CacheMemoryConfig // parsed cache-memory configuration
RepoMemoryConfig *RepoMemoryConfig // parsed repo-memory configuration
Runtimes map[string]any // runtime version overrides from frontmatter
diff --git a/pkg/workflow/data/action_pins.json b/pkg/workflow/data/action_pins.json
index c9ea38685d2..3688f8f2b9e 100644
--- a/pkg/workflow/data/action_pins.json
+++ b/pkg/workflow/data/action_pins.json
@@ -125,6 +125,11 @@
"version": "v2.0.3",
"sha": "e95548e56dfa95d4e1a28d6f422fafe75c4c26fb"
},
+ "docker/build-push-action@v6": {
+ "repo": "docker/build-push-action",
+ "version": "v6",
+ "sha": "ee4ca427a2f43b6a16632044ca514c076267da23"
+ },
"docker/build-push-action@v6.18.0": {
"repo": "docker/build-push-action",
"version": "v6.18.0",
diff --git a/pkg/workflow/frontmatter_types.go b/pkg/workflow/frontmatter_types.go
index 0ececb8ee7b..f9bced5aa32 100644
--- a/pkg/workflow/frontmatter_types.go
+++ b/pkg/workflow/frontmatter_types.go
@@ -76,6 +76,14 @@ type PluginsConfig struct {
GitHubToken string `json:"github-token,omitempty"` // Custom GitHub token for plugin installation
}
+// RateLimitConfig represents rate limiting configuration for workflow triggers
+// Limits how many times a user can trigger a workflow within a time window
+type RateLimitConfig struct {
+ Max int `json:"max,omitempty"` // Maximum number of runs allowed per time window (default: 5)
+ Window int `json:"window,omitempty"` // Time window in minutes (default: 60)
+ Events []string `json:"events,omitempty"` // Event types to apply rate limiting to (e.g., ["workflow_dispatch", "issue_comment"])
+}
+
// FrontmatterConfig represents the structured configuration from workflow frontmatter
// This provides compile-time type safety and clearer error messages compared to map[string]any
type FrontmatterConfig struct {
@@ -143,8 +151,9 @@ type FrontmatterConfig struct {
GithubToken string `json:"github-token,omitempty"`
// Command/bot configuration
- Roles []string `json:"roles,omitempty"`
- Bots []string `json:"bots,omitempty"`
+ Roles []string `json:"roles,omitempty"`
+ Bots []string `json:"bots,omitempty"`
+ RateLimit *RateLimitConfig `json:"rate-limit,omitempty"`
}
// unmarshalFromMap converts a value from a map[string]any to a destination variable
diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go
index 9d89a5ef7ae..e8bd4300d2a 100644
--- a/pkg/workflow/role_checks.go
+++ b/pkg/workflow/role_checks.go
@@ -37,6 +37,42 @@ func (c *Compiler) generateMembershipCheck(data *WorkflowData, steps []string) [
return steps
}
+// generateRateLimitCheck generates steps for rate limiting check
+func (c *Compiler) generateRateLimitCheck(data *WorkflowData, steps []string) []string {
+ steps = append(steps, " - name: Check user rate limit\n")
+ steps = append(steps, fmt.Sprintf(" id: %s\n", constants.CheckRateLimitStepID))
+ steps = append(steps, fmt.Sprintf(" uses: %s\n", GetActionPin("actions/github-script")))
+
+ // Add environment variables for rate limit check
+ steps = append(steps, " env:\n")
+
+ // Set max (default: 5)
+ max := constants.DefaultRateLimitMax
+ if data.RateLimit.Max > 0 {
+ max = data.RateLimit.Max
+ }
+ steps = append(steps, fmt.Sprintf(" GH_AW_RATE_LIMIT_MAX: \"%d\"\n", max))
+
+ // Set window (default: 60 minutes)
+ window := constants.DefaultRateLimitWindow
+ if data.RateLimit.Window > 0 {
+ window = data.RateLimit.Window
+ }
+ steps = append(steps, fmt.Sprintf(" GH_AW_RATE_LIMIT_WINDOW: \"%d\"\n", window))
+
+ // Set events to check (if specified)
+ if len(data.RateLimit.Events) > 0 {
+ steps = append(steps, fmt.Sprintf(" GH_AW_RATE_LIMIT_EVENTS: %s\n", strings.Join(data.RateLimit.Events, ",")))
+ }
+
+ steps = append(steps, " with:\n")
+ steps = append(steps, " github-token: ${{ secrets.GITHUB_TOKEN }}\n")
+ steps = append(steps, " script: |\n")
+ steps = append(steps, generateGitHubScriptWithRequire("check_rate_limit.cjs"))
+
+ return steps
+}
+
// extractRoles extracts the 'roles' field from frontmatter to determine permission requirements
func (c *Compiler) extractRoles(frontmatter map[string]any) []string {
if rolesValue, exists := frontmatter["roles"]; exists {
@@ -101,6 +137,57 @@ func (c *Compiler) extractBots(frontmatter map[string]any) []string {
return []string{}
}
+// extractRateLimitConfig extracts the 'rate-limit' field from frontmatter
+func (c *Compiler) extractRateLimitConfig(frontmatter map[string]any) *RateLimitConfig {
+ if rateLimitValue, exists := frontmatter["rate-limit"]; exists && rateLimitValue != nil {
+ switch v := rateLimitValue.(type) {
+ case map[string]any:
+ config := &RateLimitConfig{}
+
+ // Extract max (default: 5)
+ if maxValue, ok := v["max"]; ok {
+ switch max := maxValue.(type) {
+ case int:
+ config.Max = max
+ case float64:
+ config.Max = int(max)
+ }
+ }
+
+ // Extract window (default: 60 minutes)
+ if windowValue, ok := v["window"]; ok {
+ switch window := windowValue.(type) {
+ case int:
+ config.Window = window
+ case float64:
+ config.Window = int(window)
+ }
+ }
+
+ // Extract events
+ if eventsValue, ok := v["events"]; ok {
+ switch events := eventsValue.(type) {
+ case []any:
+ for _, item := range events {
+ if str, ok := item.(string); ok {
+ config.Events = append(config.Events, str)
+ }
+ }
+ case []string:
+ config.Events = events
+ case string:
+ config.Events = []string{events}
+ }
+ }
+
+ roleLog.Printf("Extracted rate-limit config: max=%d, window=%d, events=%v", config.Max, config.Window, config.Events)
+ return config
+ }
+ }
+ roleLog.Print("No rate-limit configuration specified")
+ return nil
+}
+
// needsRoleCheck determines if the workflow needs permission checks with full context
func (c *Compiler) needsRoleCheck(data *WorkflowData, frontmatter map[string]any) bool {
// If user explicitly specified "roles: all", no permission checks needed
From 0cdc6c0febff3273bf194e1cb33e267a178498b4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 12:34:08 +0000
Subject: [PATCH 04/14] Fix rate-limit extraction to handle uint64 and add test
workflow
- Update extractRateLimitConfig to handle uint64, int64, int, and float64
- Add rate-limit schema definition to main_workflow_schema.json
- Create test-rate-limit.md workflow demonstrating rate limiting
- Rebuild binary to embed updated schema
- Successfully compile test workflow with rate limits (max=3, window=30)
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.github/workflows/test-rate-limit.lock.yml | 516 +++++++++++++++++++
.github/workflows/test-rate-limit.md | 16 +
pkg/parser/schemas/main_workflow_schema.json | 41 ++
pkg/workflow/role_checks.go | 8 +
4 files changed, 581 insertions(+)
create mode 100644 .github/workflows/test-rate-limit.lock.yml
create mode 100644 .github/workflows/test-rate-limit.md
diff --git a/.github/workflows/test-rate-limit.lock.yml b/.github/workflows/test-rate-limit.lock.yml
new file mode 100644
index 00000000000..3f3e6a087d5
--- /dev/null
+++ b/.github/workflows/test-rate-limit.lock.yml
@@ -0,0 +1,516 @@
+#
+# ___ _ _
+# / _ \ | | (_)
+# | |_| | __ _ ___ _ __ | |_ _ ___
+# | _ |/ _` |/ _ \ '_ \| __| |/ __|
+# | | | | (_| | __/ | | | |_| | (__
+# \_| |_/\__, |\___|_| |_|\__|_|\___|
+# __/ |
+# _ _ |___/
+# | | | | / _| |
+# | | | | ___ _ __ _ __| |_| | _____ ____
+# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___|
+# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
+# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
+#
+# This file was automatically generated by gh-aw. DO NOT EDIT.
+#
+# To update this file, edit the corresponding .md file and run:
+# gh aw compile
+# For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md
+#
+#
+# frontmatter-hash: 19edebfa0f1d89f75903ffc8672771a2febe1ddd248592762851b1fcedc5f158
+
+name: "Test Rate Limiting"
+"on":
+ issue_comment:
+ types:
+ - created
+ workflow_dispatch:
+
+permissions: {}
+
+concurrency:
+ group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}"
+
+run-name: "Test Rate Limiting"
+
+jobs:
+ activation:
+ needs: pre_activation
+ if: needs.pre_activation.outputs.activated == 'true'
+ runs-on: ubuntu-slim
+ permissions:
+ contents: read
+ outputs:
+ comment_id: ""
+ comment_repo: ""
+ steps:
+ - name: Checkout actions folder
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ sparse-checkout: |
+ actions
+ persist-credentials: false
+ - name: Setup Scripts
+ uses: ./actions/setup
+ with:
+ destination: /opt/gh-aw/actions
+ - name: Check workflow file timestamps
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_WORKFLOW_FILE: "test-rate-limit.lock.yml"
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs');
+ await main();
+
+ agent:
+ needs: activation
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ outputs:
+ checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }}
+ model: ${{ steps.generate_aw_info.outputs.model }}
+ secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }}
+ steps:
+ - name: Checkout actions folder
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ sparse-checkout: |
+ actions
+ persist-credentials: false
+ - name: Setup Scripts
+ uses: ./actions/setup
+ with:
+ destination: /opt/gh-aw/actions
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ - name: Create gh-aw temp directory
+ run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh
+ - name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Checkout PR branch
+ id: checkout-pr
+ if: |
+ github.event.pull_request
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs');
+ await main();
+ - name: Generate agentic run info
+ id: generate_aw_info
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const fs = require('fs');
+
+ const awInfo = {
+ engine_id: "copilot",
+ engine_name: "GitHub Copilot CLI",
+ model: process.env.GH_AW_MODEL_AGENT_COPILOT || "",
+ version: "",
+ agent_version: "0.0.406",
+ workflow_name: "Test Rate Limiting",
+ experimental: false,
+ supports_tools_allowlist: true,
+ supports_http_transport: true,
+ run_id: context.runId,
+ run_number: context.runNumber,
+ run_attempt: process.env.GITHUB_RUN_ATTEMPT,
+ repository: context.repo.owner + '/' + context.repo.repo,
+ ref: context.ref,
+ sha: context.sha,
+ actor: context.actor,
+ event_name: context.eventName,
+ staged: false,
+ allowed_domains: ["defaults"],
+ firewall_enabled: true,
+ awf_version: "v0.14.0",
+ awmg_version: "",
+ steps: {
+ firewall: "squid"
+ },
+ created_at: new Date().toISOString()
+ };
+
+ // Write to /tmp/gh-aw directory to avoid inclusion in PR
+ const tmpPath = '/tmp/gh-aw/aw_info.json';
+ fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2));
+ console.log('Generated aw_info.json at:', tmpPath);
+ console.log(JSON.stringify(awInfo, null, 2));
+
+ // Set model as output for reuse in other steps/jobs
+ core.setOutput('model', awInfo.model);
+ - name: Validate COPILOT_GITHUB_TOKEN secret
+ id: validate-secret
+ run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default
+ env:
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ - name: Install GitHub Copilot CLI
+ run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.406
+ - name: Install awf binary
+ run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.14.0
+ - name: Determine automatic lockdown mode for GitHub MCP server
+ id: determine-automatic-lockdown
+ env:
+ TOKEN_CHECK: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
+ if: env.TOKEN_CHECK != ''
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs');
+ await determineAutomaticLockdown(github, context, core);
+ - name: Download container images
+ run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.14.0 ghcr.io/github/gh-aw-firewall/squid:0.14.0 ghcr.io/github/gh-aw-mcpg:v0.1.0 ghcr.io/github/github-mcp-server:v0.30.3
+ - name: Start MCP gateway
+ id: start-mcp-gateway
+ env:
+ GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }}
+ GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ run: |
+ set -eo pipefail
+ mkdir -p /tmp/gh-aw/mcp-config
+
+ # Export gateway environment variables for MCP config and gateway script
+ export MCP_GATEWAY_PORT="80"
+ export MCP_GATEWAY_DOMAIN="host.docker.internal"
+ MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=')
+ echo "::add-mask::${MCP_GATEWAY_API_KEY}"
+ export MCP_GATEWAY_API_KEY
+ export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads"
+ mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}"
+ export DEBUG="*"
+
+ export GH_AW_ENGINE="copilot"
+ export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.0'
+
+ mkdir -p /home/runner/.copilot
+ cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh
+ {
+ "mcpServers": {
+ "github": {
+ "type": "stdio",
+ "container": "ghcr.io/github/github-mcp-server:v0.30.3",
+ "env": {
+ "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN",
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}",
+ "GITHUB_READ_ONLY": "1",
+ "GITHUB_TOOLSETS": "context,repos,issues,pull_requests"
+ }
+ }
+ },
+ "gateway": {
+ "port": $MCP_GATEWAY_PORT,
+ "domain": "${MCP_GATEWAY_DOMAIN}",
+ "apiKey": "${MCP_GATEWAY_API_KEY}",
+ "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
+ }
+ }
+ MCPCONFIG_EOF
+ - name: Generate workflow overview
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs');
+ await generateWorkflowOverview(core);
+ - name: Create prompt with built-in context
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
+ GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
+ GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
+ GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }}
+ run: |
+ bash /opt/gh-aw/actions/create_prompt_first.sh
+ cat << 'PROMPT_EOF' > "$GH_AW_PROMPT"
+
+ PROMPT_EOF
+ cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT"
+ cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT"
+ cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT"
+
+ The following GitHub context information is available for this workflow:
+ {{#if __GH_AW_GITHUB_ACTOR__ }}
+ - **actor**: __GH_AW_GITHUB_ACTOR__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_REPOSITORY__ }}
+ - **repository**: __GH_AW_GITHUB_REPOSITORY__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_WORKSPACE__ }}
+ - **workspace**: __GH_AW_GITHUB_WORKSPACE__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }}
+ - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }}
+ - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }}
+ - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }}
+ - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_RUN_ID__ }}
+ - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__
+ {{/if}}
+
+
+ PROMPT_EOF
+ if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then
+ cat "/opt/gh-aw/prompts/pr_context_prompt.md" >> "$GH_AW_PROMPT"
+ fi
+ cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT"
+
+ PROMPT_EOF
+ cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT"
+ {{#runtime-import .github/workflows/test-rate-limit.md}}
+ PROMPT_EOF
+ - name: Substitute placeholders
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
+ GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
+ GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
+ GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }}
+ with:
+ script: |
+ const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs');
+
+ // Call the substitution function
+ return await substitutePlaceholders({
+ file: process.env.GH_AW_PROMPT,
+ substitutions: {
+ GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR,
+ GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID,
+ GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER,
+ GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER,
+ GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER,
+ GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY,
+ GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID,
+ GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE,
+ GH_AW_IS_PR_COMMENT: process.env.GH_AW_IS_PR_COMMENT
+ }
+ });
+ - name: Interpolate variables and render templates
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs');
+ await main();
+ - name: Validate prompt placeholders
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh
+ - name: Print prompt
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ run: bash /opt/gh-aw/actions/print_prompt_summary.sh
+ - name: Clean git credentials
+ run: bash /opt/gh-aw/actions/clean_git_credentials.sh
+ - name: Execute GitHub Copilot CLI
+ id: agentic_execution
+ # Copilot CLI tool arguments (sorted):
+ timeout-minutes: 20
+ run: |
+ set -o pipefail
+ sudo -E awf --enable-chroot --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.14.0 --skip-pull \
+ -- '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"}' \
+ 2>&1 | tee /tmp/gh-aw/agent-stdio.log
+ env:
+ COPILOT_AGENT_RUNNER_TYPE: STANDALONE
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json
+ GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }}
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GITHUB_HEAD_REF: ${{ github.head_ref }}
+ GITHUB_REF_NAME: ${{ github.ref_name }}
+ GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }}
+ GITHUB_WORKSPACE: ${{ github.workspace }}
+ XDG_CONFIG_HOME: /home/runner
+ - name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Copy Copilot session state files to logs
+ if: always()
+ continue-on-error: true
+ run: |
+ # Copy Copilot session state files to logs folder for artifact collection
+ # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them
+ SESSION_STATE_DIR="$HOME/.copilot/session-state"
+ LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs"
+
+ if [ -d "$SESSION_STATE_DIR" ]; then
+ echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR"
+ mkdir -p "$LOGS_DIR"
+ cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true
+ echo "Session state files copied successfully"
+ else
+ echo "No session-state directory found at $SESSION_STATE_DIR"
+ fi
+ - name: Stop MCP gateway
+ if: always()
+ continue-on-error: true
+ env:
+ MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }}
+ MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }}
+ GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }}
+ run: |
+ bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID"
+ - name: Redact secrets in logs
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs');
+ await main();
+ env:
+ GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN'
+ SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
+ SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
+ SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Upload engine output files
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ with:
+ name: agent_outputs
+ path: |
+ /tmp/gh-aw/sandbox/agent/logs/
+ /tmp/gh-aw/redacted-urls.log
+ if-no-files-found: ignore
+ - name: Parse agent logs for step summary
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs');
+ await main();
+ - name: Parse MCP gateway logs for step summary
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs');
+ await main();
+ - name: Print firewall logs
+ if: always()
+ continue-on-error: true
+ env:
+ AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs
+ run: |
+ # Fix permissions on firewall logs so they can be uploaded as artifacts
+ # AWF runs with sudo, creating files owned by root
+ sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true
+ awf logs summary | tee -a "$GITHUB_STEP_SUMMARY"
+ - name: Upload agent artifacts
+ if: always()
+ continue-on-error: true
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ with:
+ name: agent-artifacts
+ path: |
+ /tmp/gh-aw/aw-prompts/prompt.txt
+ /tmp/gh-aw/aw_info.json
+ /tmp/gh-aw/mcp-logs/
+ /tmp/gh-aw/sandbox/firewall/logs/
+ /tmp/gh-aw/agent-stdio.log
+ /tmp/gh-aw/agent/
+ if-no-files-found: ignore
+
+ pre_activation:
+ runs-on: ubuntu-slim
+ permissions:
+ contents: read
+ outputs:
+ activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
+ steps:
+ - name: Checkout actions folder
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ sparse-checkout: |
+ actions
+ persist-credentials: false
+ - name: Setup Scripts
+ uses: ./actions/setup
+ with:
+ destination: /opt/gh-aw/actions
+ - name: Check team membership for workflow
+ id: check_membership
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_REQUIRED_ROLES: admin,maintainer,write
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/check_membership.cjs');
+ await main();
+ - name: Check user rate limit
+ id: check_rate_limit
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_RATE_LIMIT_MAX: "3"
+ GH_AW_RATE_LIMIT_WINDOW: "30"
+ GH_AW_RATE_LIMIT_EVENTS: workflow_dispatch,issue_comment
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/check_rate_limit.cjs');
+ await main();
+
diff --git a/.github/workflows/test-rate-limit.md b/.github/workflows/test-rate-limit.md
new file mode 100644
index 00000000000..472e5831a98
--- /dev/null
+++ b/.github/workflows/test-rate-limit.md
@@ -0,0 +1,16 @@
+---
+name: Test Rate Limiting
+engine: copilot
+on:
+ workflow_dispatch:
+ issue_comment:
+ types: [created]
+rate-limit:
+ max: 3
+ window: 30
+ events: [workflow_dispatch, issue_comment]
+---
+
+Test workflow to demonstrate rate limiting functionality.
+
+This workflow limits each user to 3 runs within a 30-minute window for workflow_dispatch and issue_comment events.
diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json
index 77da432af36..4e47e23662a 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -6075,6 +6075,47 @@
"description": "Bot identifier/name (e.g., 'dependabot[bot]', 'renovate[bot]', 'github-actions[bot]')"
}
},
+ "rate-limit": {
+ "type": "object",
+ "description": "Rate limiting configuration to restrict how frequently users can trigger the workflow. Helps prevent abuse and resource exhaustion from programmatically triggered events.",
+ "properties": {
+ "max": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100,
+ "default": 5,
+ "description": "Maximum number of workflow runs allowed per user within the time window. Defaults to 5."
+ },
+ "window": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 1440,
+ "default": 60,
+ "description": "Time window in minutes for rate limiting. Defaults to 60 (1 hour)."
+ },
+ "events": {
+ "type": "array",
+ "description": "Optional list of event types to apply rate limiting to. If not specified, rate limiting applies to all programmatically triggered events (e.g., workflow_dispatch, issue_comment, pull_request_review).",
+ "items": {
+ "type": "string",
+ "enum": ["workflow_dispatch", "issue_comment", "pull_request_review", "pull_request_review_comment", "issues", "pull_request", "discussion_comment", "discussion"]
+ },
+ "minItems": 1
+ }
+ },
+ "additionalProperties": false,
+ "examples": [
+ {
+ "max": 5,
+ "window": 60
+ },
+ {
+ "max": 10,
+ "window": 30,
+ "events": ["workflow_dispatch", "issue_comment"]
+ }
+ ]
+ },
"strict": {
"type": "boolean",
"default": true,
diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go
index e8bd4300d2a..938e3659572 100644
--- a/pkg/workflow/role_checks.go
+++ b/pkg/workflow/role_checks.go
@@ -149,6 +149,10 @@ func (c *Compiler) extractRateLimitConfig(frontmatter map[string]any) *RateLimit
switch max := maxValue.(type) {
case int:
config.Max = max
+ case int64:
+ config.Max = int(max)
+ case uint64:
+ config.Max = int(max)
case float64:
config.Max = int(max)
}
@@ -159,6 +163,10 @@ func (c *Compiler) extractRateLimitConfig(frontmatter map[string]any) *RateLimit
switch window := windowValue.(type) {
case int:
config.Window = window
+ case int64:
+ config.Window = int(window)
+ case uint64:
+ config.Window = int(window)
case float64:
config.Window = int(window)
}
From c09defcd0171ea72b32f203a2c59f832fe97574e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 12:41:04 +0000
Subject: [PATCH 05/14] Fix action pins test to expect 39 pins
Update TestGetActionPinsSorting to expect 39 pins (added docker/build-push-action@v6)
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.../src/content/docs/agent-factory-status.mdx | 1 +
.../docs/reference/frontmatter-full.md | 21 +++++++++++++++++++
pkg/workflow/action_pins_test.go | 6 +++---
3 files changed, 25 insertions(+), 3 deletions(-)
diff --git a/docs/src/content/docs/agent-factory-status.mdx b/docs/src/content/docs/agent-factory-status.mdx
index 1e457203134..f0dffdf6107 100644
--- a/docs/src/content/docs/agent-factory-status.mdx
+++ b/docs/src/content/docs/agent-factory-status.mdx
@@ -142,6 +142,7 @@ These are experimental agentic workflows used by the GitHub Next team to learn,
| [Test Create PR Error Handling](https://github.com/github/gh-aw/blob/main/.github/workflows/test-create-pr-error-handling.md) | claude | [](https://github.com/github/gh-aw/actions/workflows/test-create-pr-error-handling.lock.yml) | - | - |
| [Test Dispatcher Workflow](https://github.com/github/gh-aw/blob/main/.github/workflows/test-dispatcher.md) | copilot | [](https://github.com/github/gh-aw/actions/workflows/test-dispatcher.lock.yml) | - | - |
| [Test Project URL Explicit Requirement](https://github.com/github/gh-aw/blob/main/.github/workflows/test-project-url-default.md) | copilot | [](https://github.com/github/gh-aw/actions/workflows/test-project-url-default.lock.yml) | - | - |
+| [Test Rate Limiting](https://github.com/github/gh-aw/blob/main/.github/workflows/test-rate-limit.md) | copilot | [](https://github.com/github/gh-aw/actions/workflows/test-rate-limit.lock.yml) | - | - |
| [Test Workflow](https://github.com/github/gh-aw/blob/main/.github/workflows/test-workflow.md) | copilot | [](https://github.com/github/gh-aw/actions/workflows/test-workflow.lock.yml) | - | - |
| [The Daily Repository Chronicle](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-repo-chronicle.md) | copilot | [](https://github.com/github/gh-aw/actions/workflows/daily-repo-chronicle.lock.yml) | `0 16 * * 1-5` | - |
| [The Great Escapi](https://github.com/github/gh-aw/blob/main/.github/workflows/firewall-escape.md) | copilot | [](https://github.com/github/gh-aw/actions/workflows/firewall-escape.lock.yml) | - | - |
diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md
index 7a26eb63577..df46b2a4428 100644
--- a/docs/src/content/docs/reference/frontmatter-full.md
+++ b/docs/src/content/docs/reference/frontmatter-full.md
@@ -3498,6 +3498,27 @@ bots: []
# Array of Bot identifier/name (e.g., 'dependabot[bot]', 'renovate[bot]',
# 'github-actions[bot]')
+# Rate limiting configuration to restrict how frequently users can trigger the
+# workflow. Helps prevent abuse and resource exhaustion from programmatically
+# triggered events.
+# (optional)
+rate-limit:
+ # Maximum number of workflow runs allowed per user within the time window.
+ # Defaults to 5.
+ # (optional)
+ max: 1
+
+ # Time window in minutes for rate limiting. Defaults to 60 (1 hour).
+ # (optional)
+ window: 1
+
+ # Optional list of event types to apply rate limiting to. If not specified, rate
+ # limiting applies to all programmatically triggered events (e.g.,
+ # workflow_dispatch, issue_comment, pull_request_review).
+ # (optional)
+ events: []
+ # Array of strings
+
# Enable strict mode validation for enhanced security and compliance. Strict mode
# enforces: (1) Write Permissions - refuses contents:write, issues:write,
# pull-requests:write; requires safe-outputs instead, (2) Network Configuration -
diff --git a/pkg/workflow/action_pins_test.go b/pkg/workflow/action_pins_test.go
index 9cbe8948a94..b7bfe74af25 100644
--- a/pkg/workflow/action_pins_test.go
+++ b/pkg/workflow/action_pins_test.go
@@ -297,9 +297,9 @@ func TestApplyActionPinToStep(t *testing.T) {
func TestGetActionPinsSorting(t *testing.T) {
pins := getActionPins()
- // Verify we got all the pins (38 as of February 2026)
- if len(pins) != 38 {
- t.Errorf("getActionPins() returned %d pins, expected 38", len(pins))
+ // Verify we got all the pins (39 as of February 2026)
+ if len(pins) != 39 {
+ t.Errorf("getActionPins() returned %d pins, expected 39", len(pins))
}
// Verify they are sorted by version (descending) then by repository name (ascending)
From e9171c39711baa727d9f8a8cdde9dffd3a363706 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 12:42:07 +0000
Subject: [PATCH 06/14] Add comprehensive rate limiting documentation
- Document rate-limit configuration options
- Explain how rate limiting works internally
- Provide usage examples for different scenarios
- Include troubleshooting guide
- Add testing instructions
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
docs/RATE_LIMITING.md | 296 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 296 insertions(+)
create mode 100644 docs/RATE_LIMITING.md
diff --git a/docs/RATE_LIMITING.md b/docs/RATE_LIMITING.md
new file mode 100644
index 00000000000..29cc39e6618
--- /dev/null
+++ b/docs/RATE_LIMITING.md
@@ -0,0 +1,296 @@
+# Rate Limiting for Agentic Workflows
+
+## Overview
+
+The rate limiting feature prevents users from triggering workflows too frequently, helping to:
+- Prevent abuse and resource exhaustion
+- Control costs from programmatic workflow triggers
+- Protect against accidental infinite loops
+- Ensure fair resource allocation across users
+
+## Configuration
+
+Rate limiting is configured in the workflow frontmatter using the `rate-limit` field:
+
+```yaml
+---
+name: My Workflow
+engine: copilot
+on:
+ workflow_dispatch:
+ issue_comment:
+ types: [created]
+rate-limit:
+ max: 5 # Maximum runs per time window (default: 5)
+ window: 60 # Time window in minutes (default: 60)
+ events: # Optional: specific events to limit
+ - workflow_dispatch
+ - issue_comment
+---
+```
+
+## Parameters
+
+### `max` (integer, optional)
+- Maximum number of workflow runs allowed per user within the time window
+- Default: 5
+- Range: 1-100
+- Example: `max: 10` allows 10 runs per window
+
+### `window` (integer, optional)
+- Time window in minutes for rate limiting
+- Default: 60 (1 hour)
+- Range: 1-1440 (up to 24 hours)
+- Example: `window: 30` creates a 30-minute window
+
+### `events` (array, optional)
+- Specific event types to apply rate limiting to
+- If not specified, applies to all programmatically triggered events
+- Supported events:
+ - `workflow_dispatch`
+ - `issue_comment`
+ - `pull_request_review`
+ - `pull_request_review_comment`
+ - `issues`
+ - `pull_request`
+ - `discussion_comment`
+ - `discussion`
+
+## How It Works
+
+1. **Pre-Activation Check**: Rate limiting is enforced in the pre-activation job, before the main workflow runs
+2. **Per-User Per-Workflow**: Limits are applied individually for each user and workflow
+3. **Recent Runs Query**: The system queries recent workflow runs from the GitHub API
+4. **Filtering**: Runs are filtered by:
+ - Actor (user who triggered the workflow)
+ - Time window (only runs within the configured window)
+ - Event type (if `events` is configured)
+ - Excludes the current run from the count
+5. **Progressive Aggregation**: Uses pagination with short-circuit logic for efficiency
+6. **Automatic Cancellation**: If the limit is exceeded, the current run is automatically cancelled
+
+## Examples
+
+### Basic Rate Limiting (Default)
+```yaml
+rate-limit:
+ max: 5
+ window: 60
+```
+Allows 5 runs per hour for all programmatic events.
+
+### Strict Rate Limiting
+```yaml
+rate-limit:
+ max: 3
+ window: 30
+ events: [workflow_dispatch, issue_comment]
+```
+Allows only 3 runs per 30 minutes for manual triggers and issue comments.
+
+### Generous Rate Limiting
+```yaml
+rate-limit:
+ max: 20
+ window: 120
+```
+Allows 20 runs per 2 hours for all events.
+
+## Behavior Details
+
+### When Rate Limit is Exceeded
+- The workflow run is automatically cancelled
+- A warning message is logged with details:
+ - Current run count
+ - Maximum allowed
+ - Time window
+- The activation output is set to false, preventing the main job from running
+
+### Logging
+The rate limit check provides extensive logging:
+```
+🔍 Checking rate limit for user 'username' on workflow 'workflow-name'
+ Configuration: max=5 runs per 60 minutes
+ Current event: workflow_dispatch
+ Time window: runs created after 2026-02-11T11:24:33.098Z
+📊 Querying workflow runs for 'workflow-name'...
+ Fetching page 1 (up to 100 runs per page)...
+ Retrieved 10 runs from page 1
+ ✓ Run #5 (123456) by username - event: workflow_dispatch, created: 2026-02-11T11:15:00.000Z, status: completed
+📈 Rate limit summary for user 'username':
+ Total recent runs in last 60 minutes: 3
+ Maximum allowed: 5
+ Breakdown by event type:
+ - workflow_dispatch: 2 runs
+ - issue_comment: 1 runs
+✅ Rate limit check passed
+ User 'username' has 3 runs in the last 60 minutes
+ Remaining quota: 2 runs
+```
+
+### Error Handling
+- **Fail-Open**: If the rate limit check fails due to API errors, the workflow is allowed to proceed
+- This ensures that temporary API issues don't block legitimate workflow runs
+- Errors are logged with details for troubleshooting
+
+### Performance Optimization
+- **Short-Circuit Logic**: Stops querying additional pages once the limit is reached
+- **Progressive Filtering**: Filters by actor and time window progressively
+- **Pagination**: Efficiently handles workflows with many runs
+
+## Integration with Pre-Activation Job
+
+The rate limit check is automatically added to the pre-activation job when configured:
+
+```yaml
+jobs:
+ pre-activation:
+ runs-on: ubuntu-latest
+ outputs:
+ activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
+ steps:
+ - name: Check team membership
+ # ... membership check ...
+
+ - name: Check user rate limit
+ id: check_rate_limit
+ uses: actions/github-script@v8
+ env:
+ GH_AW_RATE_LIMIT_MAX: "5"
+ GH_AW_RATE_LIMIT_WINDOW: "60"
+ GH_AW_RATE_LIMIT_EVENTS: workflow_dispatch,issue_comment
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { main } = require('/opt/gh-aw/actions/check_rate_limit.cjs');
+ await main();
+```
+
+The activation output combines all pre-activation checks using AND logic, so the workflow only proceeds if all checks pass.
+
+## Use Cases
+
+### Preventing Abuse
+```yaml
+rate-limit:
+ max: 3
+ window: 60
+ events: [workflow_dispatch]
+```
+Limits manual workflow triggers to prevent spam or abuse.
+
+### Cost Control
+```yaml
+rate-limit:
+ max: 10
+ window: 120
+```
+Controls costs by limiting how often expensive workflows can be triggered.
+
+### Fair Resource Allocation
+```yaml
+rate-limit:
+ max: 5
+ window: 30
+```
+Ensures fair access to shared resources across multiple users.
+
+### Development vs Production
+Development workflows might have stricter limits:
+```yaml
+# Development
+rate-limit:
+ max: 3
+ window: 30
+
+# Production
+rate-limit:
+ max: 20
+ window: 60
+```
+
+## Testing
+
+A test workflow is provided at `.github/workflows/test-rate-limit.md`:
+
+```yaml
+---
+name: Test Rate Limiting
+engine: copilot
+on:
+ workflow_dispatch:
+ issue_comment:
+ types: [created]
+rate-limit:
+ max: 3
+ window: 30
+ events: [workflow_dispatch, issue_comment]
+---
+
+Test workflow to demonstrate rate limiting functionality.
+This workflow limits each user to 3 runs within a 30-minute window.
+```
+
+To test:
+1. Trigger the workflow manually 4 times in quick succession
+2. The 4th run should be automatically cancelled with a rate limit warning
+3. Wait 30 minutes for the window to reset
+4. Trigger again to confirm the limit resets
+
+## Troubleshooting
+
+### Rate Limit Not Working
+- Check that `rate-limit` is in the workflow frontmatter
+- Verify the schema is valid (run `gh aw compile`)
+- Check pre-activation job logs for rate limit check output
+
+### Unexpected Cancellations
+- Review the rate limit configuration (`max` and `window`)
+- Check if other users are triggering the same workflow
+- Verify event filters are configured correctly
+
+### API Errors
+- Rate limit checks fail-open on API errors
+- Check GitHub API status if issues persist
+- Review workflow run logs for detailed error messages
+
+## Schema Definition
+
+The rate-limit field is validated against this JSON schema:
+
+```json
+{
+ "type": "object",
+ "properties": {
+ "max": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100,
+ "default": 5
+ },
+ "window": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 1440,
+ "default": 60
+ },
+ "events": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "workflow_dispatch",
+ "issue_comment",
+ "pull_request_review",
+ "pull_request_review_comment",
+ "issues",
+ "pull_request",
+ "discussion_comment",
+ "discussion"
+ ]
+ },
+ "minItems": 1
+ }
+ }
+}
+```
From 5b7584f3c0ae4dcdc23f2d473cfc59524f695197 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 12:54:07 +0000
Subject: [PATCH 07/14] Filter out cancelled workflow runs from rate limit
calculation
- Skip runs with status "cancelled" when counting toward rate limit
- Add test case to verify cancelled runs are excluded
- Update documentation to reflect this behavior
- Cancelled runs are now explicitly logged as skipped
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/check_rate_limit.cjs | 6 ++++
actions/setup/js/check_rate_limit.test.cjs | 42 ++++++++++++++++++++++
docs/RATE_LIMITING.md | 2 ++
3 files changed, 50 insertions(+)
diff --git a/actions/setup/js/check_rate_limit.cjs b/actions/setup/js/check_rate_limit.cjs
index 22b5dc5c0bd..c1e66fb98ce 100644
--- a/actions/setup/js/check_rate_limit.cjs
+++ b/actions/setup/js/check_rate_limit.cjs
@@ -104,6 +104,12 @@ async function main() {
continue;
}
+ // Skip cancelled workflow runs (they don't count toward the rate limit)
+ if (run.status === "cancelled") {
+ core.info(` Skipping run ${run.id} - cancelled (status: ${run.status})`);
+ continue;
+ }
+
// If specific events are configured, only count matching events
const runEvent = run.event;
if (limitedEvents.length > 0 && !limitedEvents.includes(runEvent)) {
diff --git a/actions/setup/js/check_rate_limit.test.cjs b/actions/setup/js/check_rate_limit.test.cjs
index 2cd09fb4048..8556f58df17 100644
--- a/actions/setup/js/check_rate_limit.test.cjs
+++ b/actions/setup/js/check_rate_limit.test.cjs
@@ -252,6 +252,48 @@ describe("check_rate_limit", () => {
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Total recent runs in last 60 minutes: 1"));
});
+ it("should exclude cancelled runs from the count", 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(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "cancelled",
+ },
+ {
+ id: 222222,
+ run_number: 2,
+ created_at: recentTime.toISOString(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ {
+ id: 333333,
+ run_number: 3,
+ created_at: recentTime.toISOString(),
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "cancelled",
+ },
+ ],
+ },
+ });
+
+ 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"));
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Skipping run 111111 - cancelled"));
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Skipping run 333333 - cancelled"));
+ });
+
it("should only count specified event types when events filter is set", async () => {
process.env.GH_AW_RATE_LIMIT_EVENTS = "workflow_dispatch,issue_comment";
const recentTime = new Date(Date.now() - 10 * 60 * 1000);
diff --git a/docs/RATE_LIMITING.md b/docs/RATE_LIMITING.md
index 29cc39e6618..42e837e3bcb 100644
--- a/docs/RATE_LIMITING.md
+++ b/docs/RATE_LIMITING.md
@@ -66,6 +66,7 @@ rate-limit:
- Time window (only runs within the configured window)
- Event type (if `events` is configured)
- Excludes the current run from the count
+ - Excludes cancelled runs (cancelled runs don't count toward the limit)
5. **Progressive Aggregation**: Uses pagination with short-circuit logic for efficiency
6. **Automatic Cancellation**: If the limit is exceeded, the current run is automatically cancelled
@@ -116,6 +117,7 @@ The rate limit check provides extensive logging:
📊 Querying workflow runs for 'workflow-name'...
Fetching page 1 (up to 100 runs per page)...
Retrieved 10 runs from page 1
+ Skipping run 123457 - cancelled (status: cancelled)
✓ Run #5 (123456) by username - event: workflow_dispatch, created: 2026-02-11T11:15:00.000Z, status: completed
📈 Rate limit summary for user 'username':
Total recent runs in last 60 minutes: 3
From 1b35433c58a12af50e9504d43a9eb6e1c5cd7ec2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 13:17:59 +0000
Subject: [PATCH 08/14] Reduce maximum rate limit window to 3 hours (180
minutes)
- Update JSON schema: maximum window changed from 1440 to 180 minutes
- Update documentation to reflect 3-hour maximum (was 24 hours)
- Schema validation now rejects window values > 180 minutes
- Test workflow with window: 30 still compiles successfully
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
docs/RATE_LIMITING.md | 4 ++--
pkg/parser/schemas/main_workflow_schema.json | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/RATE_LIMITING.md b/docs/RATE_LIMITING.md
index 42e837e3bcb..4e449ee4f9c 100644
--- a/docs/RATE_LIMITING.md
+++ b/docs/RATE_LIMITING.md
@@ -40,7 +40,7 @@ rate-limit:
### `window` (integer, optional)
- Time window in minutes for rate limiting
- Default: 60 (1 hour)
-- Range: 1-1440 (up to 24 hours)
+- Range: 1-180 (up to 3 hours)
- Example: `window: 30` creates a 30-minute window
### `events` (array, optional)
@@ -273,7 +273,7 @@ The rate-limit field is validated against this JSON schema:
"window": {
"type": "integer",
"minimum": 1,
- "maximum": 1440,
+ "maximum": 180,
"default": 60
},
"events": {
diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json
index 4e47e23662a..119ceba7d4e 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -6089,9 +6089,9 @@
"window": {
"type": "integer",
"minimum": 1,
- "maximum": 1440,
+ "maximum": 180,
"default": 60,
- "description": "Time window in minutes for rate limiting. Defaults to 60 (1 hour)."
+ "description": "Time window in minutes for rate limiting. Defaults to 60 (1 hour). Maximum: 180 (3 hours)."
},
"events": {
"type": "array",
From 9c810029c099b3e737784b465b777656e37a9e67 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 13:35:16 +0000
Subject: [PATCH 09/14] Changes before error encountered
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/check_rate_limit.cjs | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/actions/setup/js/check_rate_limit.cjs b/actions/setup/js/check_rate_limit.cjs
index c1e66fb98ce..afd37e252a9 100644
--- a/actions/setup/js/check_rate_limit.cjs
+++ b/actions/setup/js/check_rate_limit.cjs
@@ -110,6 +110,18 @@ async function main() {
continue;
}
+ // Skip runs that completed in less than 15 seconds (treat as cancelled/failed fast)
+ if (run.created_at && run.updated_at) {
+ const runStart = new Date(run.created_at);
+ const runEnd = new Date(run.updated_at);
+ const durationSeconds = (runEnd - runStart) / 1000;
+
+ if (durationSeconds < 15) {
+ core.info(` Skipping run ${run.id} - ran for less than 15s (${durationSeconds.toFixed(1)}s)`);
+ continue;
+ }
+ }
+
// If specific events are configured, only count matching events
const runEvent = run.event;
if (limitedEvents.length > 0 && !limitedEvents.includes(runEvent)) {
From f64a1eec0a488524ccc870abcd25b7331cb6b16d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 17:33:54 +0000
Subject: [PATCH 10/14] Make rate-limit.max required (range 1-10) and add 15s
duration filter
Schema changes:
- Make 'max' field required (no longer optional)
- Change max range from 1-100 to 1-10
- Remove default value from max (now required)
- Keep window default at 60 minutes
JavaScript implementation:
- Skip runs that completed in less than 15 seconds
- Calculate duration using created_at and updated_at timestamps
- Log skipped short-duration runs
- Add test case for 15-second threshold
Documentation updates:
- Mark max as required in all docs
- Update range from 1-100 to 1-10
- Document 15-second duration filter
- Update schema definition section
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.github/workflows/test-rate-limit.lock.yml | 4 +--
actions/setup/js/check_rate_limit.cjs | 4 +--
actions/setup/js/check_rate_limit.test.cjs | 37 ++++++++++++++++++++
docs/RATE_LIMITING.md | 12 +++----
pkg/parser/schemas/main_workflow_schema.json | 6 ++--
5 files changed, 50 insertions(+), 13 deletions(-)
diff --git a/.github/workflows/test-rate-limit.lock.yml b/.github/workflows/test-rate-limit.lock.yml
index 3f3e6a087d5..1ef57cbe77b 100644
--- a/.github/workflows/test-rate-limit.lock.yml
+++ b/.github/workflows/test-rate-limit.lock.yml
@@ -207,7 +207,7 @@ jobs:
export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.0'
mkdir -p /home/runner/.copilot
- cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh
+ cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh
{
"mcpServers": {
"github": {
@@ -228,7 +228,7 @@ jobs:
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
}
}
- MCPCONFIG_EOF
+ GH_AW_MCP_CONFIG_EOF
- name: Generate workflow overview
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
diff --git a/actions/setup/js/check_rate_limit.cjs b/actions/setup/js/check_rate_limit.cjs
index afd37e252a9..bc9645b1142 100644
--- a/actions/setup/js/check_rate_limit.cjs
+++ b/actions/setup/js/check_rate_limit.cjs
@@ -114,8 +114,8 @@ async function main() {
if (run.created_at && run.updated_at) {
const runStart = new Date(run.created_at);
const runEnd = new Date(run.updated_at);
- const durationSeconds = (runEnd - runStart) / 1000;
-
+ const durationSeconds = (runEnd.getTime() - runStart.getTime()) / 1000;
+
if (durationSeconds < 15) {
core.info(` Skipping run ${run.id} - ran for less than 15s (${durationSeconds.toFixed(1)}s)`);
continue;
diff --git a/actions/setup/js/check_rate_limit.test.cjs b/actions/setup/js/check_rate_limit.test.cjs
index 8556f58df17..3ad52187941 100644
--- a/actions/setup/js/check_rate_limit.test.cjs
+++ b/actions/setup/js/check_rate_limit.test.cjs
@@ -294,6 +294,43 @@ describe("check_rate_limit", () => {
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Skipping run 333333 - cancelled"));
});
+ it("should exclude runs that lasted less than 15 seconds", async () => {
+ const recentTime = new Date(Date.now() - 10 * 60 * 1000);
+ const tenSecondsLater = new Date(recentTime.getTime() + 10 * 1000);
+ const twentySecondsLater = new Date(recentTime.getTime() + 20 * 1000);
+
+ mockGithub.rest.actions.listWorkflowRuns.mockResolvedValue({
+ data: {
+ workflow_runs: [
+ {
+ id: 111111,
+ run_number: 1,
+ created_at: recentTime.toISOString(),
+ updated_at: tenSecondsLater.toISOString(), // 10 seconds duration
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ {
+ id: 222222,
+ run_number: 2,
+ created_at: recentTime.toISOString(),
+ updated_at: twentySecondsLater.toISOString(), // 20 seconds duration
+ actor: { login: "test-user" },
+ event: "workflow_dispatch",
+ status: "completed",
+ },
+ ],
+ },
+ });
+
+ 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"));
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Skipping run 111111 - ran for less than 15s"));
+ });
+
it("should only count specified event types when events filter is set", async () => {
process.env.GH_AW_RATE_LIMIT_EVENTS = "workflow_dispatch,issue_comment";
const recentTime = new Date(Date.now() - 10 * 60 * 1000);
diff --git a/docs/RATE_LIMITING.md b/docs/RATE_LIMITING.md
index 4e449ee4f9c..de018728af1 100644
--- a/docs/RATE_LIMITING.md
+++ b/docs/RATE_LIMITING.md
@@ -31,11 +31,10 @@ rate-limit:
## Parameters
-### `max` (integer, optional)
+### `max` (integer, **required**)
- Maximum number of workflow runs allowed per user within the time window
-- Default: 5
-- Range: 1-100
-- Example: `max: 10` allows 10 runs per window
+- Range: 1-10
+- Example: `max: 5` allows 5 runs per window
### `window` (integer, optional)
- Time window in minutes for rate limiting
@@ -67,6 +66,7 @@ rate-limit:
- Event type (if `events` is configured)
- Excludes the current run from the count
- Excludes cancelled runs (cancelled runs don't count toward the limit)
+ - Excludes runs that completed in less than 15 seconds (treated as failed fast/cancelled)
5. **Progressive Aggregation**: Uses pagination with short-circuit logic for efficiency
6. **Automatic Cancellation**: If the limit is exceeded, the current run is automatically cancelled
@@ -263,12 +263,12 @@ The rate-limit field is validated against this JSON schema:
```json
{
"type": "object",
+ "required": ["max"],
"properties": {
"max": {
"type": "integer",
"minimum": 1,
- "maximum": 100,
- "default": 5
+ "maximum": 10
},
"window": {
"type": "integer",
diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json
index 279a7935ed0..26fafcdde08 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -6090,13 +6090,13 @@
"rate-limit": {
"type": "object",
"description": "Rate limiting configuration to restrict how frequently users can trigger the workflow. Helps prevent abuse and resource exhaustion from programmatically triggered events.",
+ "required": ["max"],
"properties": {
"max": {
"type": "integer",
"minimum": 1,
- "maximum": 100,
- "default": 5,
- "description": "Maximum number of workflow runs allowed per user within the time window. Defaults to 5."
+ "maximum": 10,
+ "description": "Maximum number of workflow runs allowed per user within the time window. Required field."
},
"window": {
"type": "integer",
From 396a26ccaafe0705160068596d79adf07e2607f6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 18:16:38 +0000
Subject: [PATCH 11/14] Fix critical issues: workflow ID, conclusion field, and
permissions
Critical fixes based on code review:
1. Use workflow file from GITHUB_WORKFLOW_REF instead of workflow name
- Parse workflow file from GITHUB_WORKFLOW_REF environment variable
- Fall back to workflow name if parsing fails
- Fixes API calls to listWorkflowRuns
2. Check run.conclusion instead of run.status for cancelled runs
- GitHub uses conclusion: 'cancelled' with status: 'completed'
- Update check from run.status === 'cancelled' to run.conclusion === 'cancelled'
- Update test fixtures to use correct fields
3. Add actions: read permission to pre-activation job
- Required for querying workflow runs via GitHub API
- Added when rate-limit is configured
4. Add programmatic events filter when no events specified
- Only apply rate limiting to known programmatic triggers
- Skip for non-programmatic events (push, schedule, etc.)
- Prevents unintended rate limiting
5. Quote GH_AW_RATE_LIMIT_EVENTS environment variable
- Improves YAML robustness and consistency
Tests:
- Added 3 new test cases (18 total, all passing)
- Test programmatic events filtering
- Test workflow ID resolution from GITHUB_WORKFLOW_REF
- Test fallback behavior
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.github/workflows/test-rate-limit.lock.yml | 3 +-
actions/setup/js/check_rate_limit.cjs | 37 ++++++++++++++--
actions/setup/js/check_rate_limit.test.cjs | 50 +++++++++++++++++++++-
pkg/workflow/compiler_activation_jobs.go | 8 ++++
pkg/workflow/role_checks.go | 2 +-
5 files changed, 92 insertions(+), 8 deletions(-)
diff --git a/.github/workflows/test-rate-limit.lock.yml b/.github/workflows/test-rate-limit.lock.yml
index 1ef57cbe77b..dce14f954b4 100644
--- a/.github/workflows/test-rate-limit.lock.yml
+++ b/.github/workflows/test-rate-limit.lock.yml
@@ -473,6 +473,7 @@ jobs:
pre_activation:
runs-on: ubuntu-slim
permissions:
+ actions: read
contents: read
outputs:
activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
@@ -505,7 +506,7 @@ jobs:
env:
GH_AW_RATE_LIMIT_MAX: "3"
GH_AW_RATE_LIMIT_WINDOW: "30"
- GH_AW_RATE_LIMIT_EVENTS: workflow_dispatch,issue_comment
+ GH_AW_RATE_LIMIT_EVENTS: "workflow_dispatch,issue_comment"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
diff --git a/actions/setup/js/check_rate_limit.cjs b/actions/setup/js/check_rate_limit.cjs
index bc9645b1142..67773b91860 100644
--- a/actions/setup/js/check_rate_limit.cjs
+++ b/actions/setup/js/check_rate_limit.cjs
@@ -10,10 +10,27 @@ async function main() {
const actor = context.actor;
const owner = context.repo.owner;
const repo = context.repo.repo;
- const workflowId = context.workflow;
const eventName = context.eventName;
const runId = context.runId;
+ // 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
+
+ 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];
+ core.info(` Using workflow file: ${workflowId} (from GITHUB_WORKFLOW_REF)`);
+ } else {
+ core.info(` Using workflow name: ${workflowId} (fallback - could not parse GITHUB_WORKFLOW_REF)`);
+ }
+ } else {
+ core.info(` Using workflow name: ${workflowId} (GITHUB_WORKFLOW_REF not available)`);
+ }
+
// 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);
@@ -36,7 +53,18 @@ async function main() {
}
core.info(` Event '${eventName}' is subject to rate limiting`);
} else {
- core.info(` Rate limiting applies to all programmatically triggered events`);
+ // 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)) {
+ core.info(`✅ Event '${eventName}' is not a programmatic trigger; skipping rate limiting`);
+ core.info(` Rate limiting applies to: ${programmaticEvents.join(", ")}`);
+ core.setOutput("rate_limit_ok", "true");
+ return;
+ }
+
+ core.info(` Rate limiting applies to programmatic events: ${programmaticEvents.join(", ")}`);
}
// Calculate time threshold
@@ -105,8 +133,9 @@ async function main() {
}
// Skip cancelled workflow runs (they don't count toward the rate limit)
- if (run.status === "cancelled") {
- core.info(` Skipping run ${run.id} - cancelled (status: ${run.status})`);
+ // GitHub uses conclusion: 'cancelled' with status: 'completed' for cancelled runs
+ if (run.conclusion === "cancelled") {
+ core.info(` Skipping run ${run.id} - cancelled (conclusion: ${run.conclusion})`);
continue;
}
diff --git a/actions/setup/js/check_rate_limit.test.cjs b/actions/setup/js/check_rate_limit.test.cjs
index 3ad52187941..765b763ba76 100644
--- a/actions/setup/js/check_rate_limit.test.cjs
+++ b/actions/setup/js/check_rate_limit.test.cjs
@@ -264,7 +264,8 @@ describe("check_rate_limit", () => {
created_at: recentTime.toISOString(),
actor: { login: "test-user" },
event: "workflow_dispatch",
- status: "cancelled",
+ status: "completed",
+ conclusion: "cancelled",
},
{
id: 222222,
@@ -273,6 +274,7 @@ describe("check_rate_limit", () => {
actor: { login: "test-user" },
event: "workflow_dispatch",
status: "completed",
+ conclusion: "success",
},
{
id: 333333,
@@ -280,7 +282,8 @@ describe("check_rate_limit", () => {
created_at: recentTime.toISOString(),
actor: { login: "test-user" },
event: "workflow_dispatch",
- status: "cancelled",
+ status: "completed",
+ conclusion: "cancelled",
},
],
},
@@ -512,4 +515,47 @@ describe("check_rate_limit", () => {
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("workflow_dispatch: 2 runs"));
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("issue_comment: 1 runs"));
});
+
+ it("should skip rate limiting for non-programmatic events when no events filter is set", async () => {
+ mockContext.eventName = "push";
+
+ await checkRateLimit.main();
+
+ expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Event 'push' is not a programmatic trigger"));
+ expect(mockGithub.rest.actions.listWorkflowRuns).not.toHaveBeenCalled();
+ });
+
+ it("should use workflow file from GITHUB_WORKFLOW_REF when available", async () => {
+ process.env.GITHUB_WORKFLOW_REF = "owner/repo/.github/workflows/test.lock.yml@refs/heads/main";
+
+ mockGithub.rest.actions.listWorkflowRuns.mockResolvedValue({
+ data: {
+ workflow_runs: [],
+ },
+ });
+
+ await checkRateLimit.main();
+
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using workflow file: test.lock.yml"));
+ expect(mockGithub.rest.actions.listWorkflowRuns).toHaveBeenCalledWith(
+ expect.objectContaining({
+ workflow_id: "test.lock.yml",
+ })
+ );
+ });
+
+ it("should fall back to workflow name when GITHUB_WORKFLOW_REF is not parseable", async () => {
+ process.env.GITHUB_WORKFLOW_REF = "invalid-format";
+
+ mockGithub.rest.actions.listWorkflowRuns.mockResolvedValue({
+ data: {
+ workflow_runs: [],
+ },
+ });
+
+ await checkRateLimit.main();
+
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using workflow name: test-workflow (fallback"));
+ });
});
diff --git a/pkg/workflow/compiler_activation_jobs.go b/pkg/workflow/compiler_activation_jobs.go
index a2fe66abf23..813d45238c1 100644
--- a/pkg/workflow/compiler_activation_jobs.go
+++ b/pkg/workflow/compiler_activation_jobs.go
@@ -56,6 +56,14 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec
perms.Set(PermissionDiscussions, PermissionWrite)
}
+ // Add actions: read permission if rate limiting is configured (needed to query workflow runs)
+ if data.RateLimit != nil {
+ if perms == nil {
+ perms = NewPermissions()
+ }
+ perms.Set(PermissionActions, PermissionRead)
+ }
+
// Set permissions if any were configured
if perms != nil {
permissions = perms.RenderToYAML()
diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go
index 938e3659572..a1328200115 100644
--- a/pkg/workflow/role_checks.go
+++ b/pkg/workflow/role_checks.go
@@ -62,7 +62,7 @@ func (c *Compiler) generateRateLimitCheck(data *WorkflowData, steps []string) []
// Set events to check (if specified)
if len(data.RateLimit.Events) > 0 {
- steps = append(steps, fmt.Sprintf(" GH_AW_RATE_LIMIT_EVENTS: %s\n", strings.Join(data.RateLimit.Events, ",")))
+ steps = append(steps, fmt.Sprintf(" GH_AW_RATE_LIMIT_EVENTS: %q\n", strings.Join(data.RateLimit.Events, ",")))
}
steps = append(steps, " with:\n")
From dbaa63add1e0c57312408f26dea779d11e6e6983 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 18:35:00 +0000
Subject: [PATCH 12/14] Apply rate limiting to agentic workflows with issue
triggers
- Add rate-limit (max: 5, window: 60) to ai-moderator.md (issues, issue_comment)
- Add rate-limit (max: 5, window: 60) to auto-triage-issues.md (issues)
- Add rate-limit (max: 5, window: 60) to example-custom-error-patterns.md (issues)
- Add rate-limit (max: 5, window: 60) to workflow-generator.md (issues)
- Remove test-rate-limit.md and its lock file
- Recompile all 148 workflows successfully
- actions: read permission automatically added to pre-activation jobs
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.github/workflows/ai-moderator.lock.yml | 19 +-
.github/workflows/ai-moderator.md | 4 +
.github/workflows/auto-triage-issues.lock.yml | 19 +-
.github/workflows/auto-triage-issues.md | 4 +
.../example-custom-error-patterns.lock.yml | 19 +-
.../example-custom-error-patterns.md | 5 +-
.github/workflows/test-rate-limit.lock.yml | 517 ------------------
.github/workflows/test-rate-limit.md | 16 -
.github/workflows/workflow-generator.lock.yml | 19 +-
.github/workflows/workflow-generator.md | 4 +
10 files changed, 84 insertions(+), 542 deletions(-)
delete mode 100644 .github/workflows/test-rate-limit.lock.yml
delete mode 100644 .github/workflows/test-rate-limit.md
diff --git a/.github/workflows/ai-moderator.lock.yml b/.github/workflows/ai-moderator.lock.yml
index 852d7cfa4d8..d07f42bc482 100644
--- a/.github/workflows/ai-moderator.lock.yml
+++ b/.github/workflows/ai-moderator.lock.yml
@@ -24,7 +24,7 @@
# Imports:
# - shared/mood.md
#
-# frontmatter-hash: 8100c57c738a6edcbae1a4445a8370c200ee4dabe1da348a6ab75c64c7e57e47
+# frontmatter-hash: 2c0c2d09eb172ff1b3867c0298c4c50def936a66aac1b14996a196ea48662c2f
name: "AI Moderator"
"on":
@@ -991,9 +991,10 @@ jobs:
pre_activation:
runs-on: ubuntu-slim
permissions:
+ actions: read
contents: read
outputs:
- activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }}
+ activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
steps:
- name: Checkout actions folder
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -1018,6 +1019,20 @@ jobs:
setupGlobals(core, github, context, exec, io);
const { main } = require('/opt/gh-aw/actions/check_membership.cjs');
await main();
+ - name: Check user rate limit
+ id: check_rate_limit
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_RATE_LIMIT_MAX: "5"
+ GH_AW_RATE_LIMIT_WINDOW: "60"
+ GH_AW_RATE_LIMIT_EVENTS: "issues,issue_comment"
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/check_rate_limit.cjs');
+ await main();
safe_outputs:
needs:
diff --git a/.github/workflows/ai-moderator.md b/.github/workflows/ai-moderator.md
index 944104a304e..71833c241b7 100644
--- a/.github/workflows/ai-moderator.md
+++ b/.github/workflows/ai-moderator.md
@@ -14,6 +14,10 @@ on:
description: 'Issue URL to moderate (e.g., https://github.com/owner/repo/issues/123)'
required: true
type: string
+rate-limit:
+ max: 5
+ window: 60
+ events: [issues, issue_comment]
engine:
id: copilot
model: gpt-5.1-codex-mini
diff --git a/.github/workflows/auto-triage-issues.lock.yml b/.github/workflows/auto-triage-issues.lock.yml
index 9c7b0764365..68502badb4a 100644
--- a/.github/workflows/auto-triage-issues.lock.yml
+++ b/.github/workflows/auto-triage-issues.lock.yml
@@ -26,7 +26,7 @@
# - shared/mood.md
# - shared/reporting.md
#
-# frontmatter-hash: e45aaf413ad8d0b82e2190a8aab5711d2c9842f2a68dae9334a386ceae611752
+# frontmatter-hash: 948ba3839d6083bd933117ad41c2410fd690889345d83f761c8c387389deeee8
name: "Auto-Triage Issues"
"on":
@@ -1049,9 +1049,10 @@ jobs:
pre_activation:
runs-on: ubuntu-slim
permissions:
+ actions: read
contents: read
outputs:
- activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }}
+ activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
steps:
- name: Checkout actions folder
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -1075,6 +1076,20 @@ jobs:
setupGlobals(core, github, context, exec, io);
const { main } = require('/opt/gh-aw/actions/check_membership.cjs');
await main();
+ - name: Check user rate limit
+ id: check_rate_limit
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_RATE_LIMIT_MAX: "5"
+ GH_AW_RATE_LIMIT_WINDOW: "60"
+ GH_AW_RATE_LIMIT_EVENTS: "issues"
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/check_rate_limit.cjs');
+ await main();
safe_outputs:
needs:
diff --git a/.github/workflows/auto-triage-issues.md b/.github/workflows/auto-triage-issues.md
index b4a63cff50b..f5e58a6cedf 100644
--- a/.github/workflows/auto-triage-issues.md
+++ b/.github/workflows/auto-triage-issues.md
@@ -5,6 +5,10 @@ on:
issues:
types: [opened, edited]
schedule: every 6h
+rate-limit:
+ max: 5
+ window: 60
+ events: [issues]
permissions:
contents: read
issues: read
diff --git a/.github/workflows/example-custom-error-patterns.lock.yml b/.github/workflows/example-custom-error-patterns.lock.yml
index 11f4a54309d..c2870e54c40 100644
--- a/.github/workflows/example-custom-error-patterns.lock.yml
+++ b/.github/workflows/example-custom-error-patterns.lock.yml
@@ -24,7 +24,7 @@
# Imports:
# - shared/mood.md
#
-# frontmatter-hash: 8777360f3ce21656e3f8bce4d00fdd94f20a9295969d3172977e453a086a06fc
+# frontmatter-hash: 01403571c2bd8d2864e2d8ad6a80c019b48dfaf1ac2b61ee2f35c9a19145fd5d
name: "Example: Custom Error Patterns"
"on":
@@ -475,9 +475,10 @@ jobs:
pre_activation:
runs-on: ubuntu-slim
permissions:
+ actions: read
contents: read
outputs:
- activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }}
+ activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
steps:
- name: Checkout actions folder
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -501,4 +502,18 @@ jobs:
setupGlobals(core, github, context, exec, io);
const { main } = require('/opt/gh-aw/actions/check_membership.cjs');
await main();
+ - name: Check user rate limit
+ id: check_rate_limit
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_RATE_LIMIT_MAX: "5"
+ GH_AW_RATE_LIMIT_WINDOW: "60"
+ GH_AW_RATE_LIMIT_EVENTS: "issues"
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/check_rate_limit.cjs');
+ await main();
diff --git a/.github/workflows/example-custom-error-patterns.md b/.github/workflows/example-custom-error-patterns.md
index e170273716c..546b02fb068 100644
--- a/.github/workflows/example-custom-error-patterns.md
+++ b/.github/workflows/example-custom-error-patterns.md
@@ -2,7 +2,10 @@
on:
issues:
types: [opened]
-
+rate-limit:
+ max: 5
+ window: 60
+ events: [issues]
permissions:
contents: read
issues: read
diff --git a/.github/workflows/test-rate-limit.lock.yml b/.github/workflows/test-rate-limit.lock.yml
deleted file mode 100644
index dce14f954b4..00000000000
--- a/.github/workflows/test-rate-limit.lock.yml
+++ /dev/null
@@ -1,517 +0,0 @@
-#
-# ___ _ _
-# / _ \ | | (_)
-# | |_| | __ _ ___ _ __ | |_ _ ___
-# | _ |/ _` |/ _ \ '_ \| __| |/ __|
-# | | | | (_| | __/ | | | |_| | (__
-# \_| |_/\__, |\___|_| |_|\__|_|\___|
-# __/ |
-# _ _ |___/
-# | | | | / _| |
-# | | | | ___ _ __ _ __| |_| | _____ ____
-# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___|
-# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
-# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
-#
-# This file was automatically generated by gh-aw. DO NOT EDIT.
-#
-# To update this file, edit the corresponding .md file and run:
-# gh aw compile
-# For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md
-#
-#
-# frontmatter-hash: 19edebfa0f1d89f75903ffc8672771a2febe1ddd248592762851b1fcedc5f158
-
-name: "Test Rate Limiting"
-"on":
- issue_comment:
- types:
- - created
- workflow_dispatch:
-
-permissions: {}
-
-concurrency:
- group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}"
-
-run-name: "Test Rate Limiting"
-
-jobs:
- activation:
- needs: pre_activation
- if: needs.pre_activation.outputs.activated == 'true'
- runs-on: ubuntu-slim
- permissions:
- contents: read
- outputs:
- comment_id: ""
- comment_repo: ""
- steps:
- - name: Checkout actions folder
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- sparse-checkout: |
- actions
- persist-credentials: false
- - name: Setup Scripts
- uses: ./actions/setup
- with:
- destination: /opt/gh-aw/actions
- - name: Check workflow file timestamps
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_WORKFLOW_FILE: "test-rate-limit.lock.yml"
- with:
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs');
- await main();
-
- agent:
- needs: activation
- runs-on: ubuntu-latest
- permissions:
- contents: read
- outputs:
- checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }}
- model: ${{ steps.generate_aw_info.outputs.model }}
- secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }}
- steps:
- - name: Checkout actions folder
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- sparse-checkout: |
- actions
- persist-credentials: false
- - name: Setup Scripts
- uses: ./actions/setup
- with:
- destination: /opt/gh-aw/actions
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- persist-credentials: false
- - name: Create gh-aw temp directory
- run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh
- - name: Configure Git credentials
- env:
- REPO_NAME: ${{ github.repository }}
- SERVER_URL: ${{ github.server_url }}
- run: |
- git config --global user.email "github-actions[bot]@users.noreply.github.com"
- git config --global user.name "github-actions[bot]"
- # Re-authenticate git with GitHub token
- SERVER_URL_STRIPPED="${SERVER_URL#https://}"
- git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
- echo "Git configured with standard GitHub Actions identity"
- - name: Checkout PR branch
- id: checkout-pr
- if: |
- github.event.pull_request
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
- with:
- github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs');
- await main();
- - name: Generate agentic run info
- id: generate_aw_info
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const fs = require('fs');
-
- const awInfo = {
- engine_id: "copilot",
- engine_name: "GitHub Copilot CLI",
- model: process.env.GH_AW_MODEL_AGENT_COPILOT || "",
- version: "",
- agent_version: "0.0.406",
- workflow_name: "Test Rate Limiting",
- experimental: false,
- supports_tools_allowlist: true,
- supports_http_transport: true,
- run_id: context.runId,
- run_number: context.runNumber,
- run_attempt: process.env.GITHUB_RUN_ATTEMPT,
- repository: context.repo.owner + '/' + context.repo.repo,
- ref: context.ref,
- sha: context.sha,
- actor: context.actor,
- event_name: context.eventName,
- staged: false,
- allowed_domains: ["defaults"],
- firewall_enabled: true,
- awf_version: "v0.14.0",
- awmg_version: "",
- steps: {
- firewall: "squid"
- },
- created_at: new Date().toISOString()
- };
-
- // Write to /tmp/gh-aw directory to avoid inclusion in PR
- const tmpPath = '/tmp/gh-aw/aw_info.json';
- fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2));
- console.log('Generated aw_info.json at:', tmpPath);
- console.log(JSON.stringify(awInfo, null, 2));
-
- // Set model as output for reuse in other steps/jobs
- core.setOutput('model', awInfo.model);
- - name: Validate COPILOT_GITHUB_TOKEN secret
- id: validate-secret
- run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default
- env:
- COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- - name: Install GitHub Copilot CLI
- run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.406
- - name: Install awf binary
- run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.14.0
- - name: Determine automatic lockdown mode for GitHub MCP server
- id: determine-automatic-lockdown
- env:
- TOKEN_CHECK: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
- if: env.TOKEN_CHECK != ''
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs');
- await determineAutomaticLockdown(github, context, core);
- - name: Download container images
- run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.14.0 ghcr.io/github/gh-aw-firewall/squid:0.14.0 ghcr.io/github/gh-aw-mcpg:v0.1.0 ghcr.io/github/github-mcp-server:v0.30.3
- - name: Start MCP gateway
- id: start-mcp-gateway
- env:
- GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }}
- GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
- run: |
- set -eo pipefail
- mkdir -p /tmp/gh-aw/mcp-config
-
- # Export gateway environment variables for MCP config and gateway script
- export MCP_GATEWAY_PORT="80"
- export MCP_GATEWAY_DOMAIN="host.docker.internal"
- MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=')
- echo "::add-mask::${MCP_GATEWAY_API_KEY}"
- export MCP_GATEWAY_API_KEY
- export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads"
- mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}"
- export DEBUG="*"
-
- export GH_AW_ENGINE="copilot"
- export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.0'
-
- mkdir -p /home/runner/.copilot
- cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh
- {
- "mcpServers": {
- "github": {
- "type": "stdio",
- "container": "ghcr.io/github/github-mcp-server:v0.30.3",
- "env": {
- "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN",
- "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}",
- "GITHUB_READ_ONLY": "1",
- "GITHUB_TOOLSETS": "context,repos,issues,pull_requests"
- }
- }
- },
- "gateway": {
- "port": $MCP_GATEWAY_PORT,
- "domain": "${MCP_GATEWAY_DOMAIN}",
- "apiKey": "${MCP_GATEWAY_API_KEY}",
- "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
- }
- }
- GH_AW_MCP_CONFIG_EOF
- - name: Generate workflow overview
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs');
- await generateWorkflowOverview(core);
- - name: Create prompt with built-in context
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- GH_AW_GITHUB_ACTOR: ${{ github.actor }}
- GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
- GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
- GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
- GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
- GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
- GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
- GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
- GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }}
- run: |
- bash /opt/gh-aw/actions/create_prompt_first.sh
- cat << 'PROMPT_EOF' > "$GH_AW_PROMPT"
-
- PROMPT_EOF
- cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT"
- cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT"
- cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT"
-
- The following GitHub context information is available for this workflow:
- {{#if __GH_AW_GITHUB_ACTOR__ }}
- - **actor**: __GH_AW_GITHUB_ACTOR__
- {{/if}}
- {{#if __GH_AW_GITHUB_REPOSITORY__ }}
- - **repository**: __GH_AW_GITHUB_REPOSITORY__
- {{/if}}
- {{#if __GH_AW_GITHUB_WORKSPACE__ }}
- - **workspace**: __GH_AW_GITHUB_WORKSPACE__
- {{/if}}
- {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }}
- - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__
- {{/if}}
- {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }}
- - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__
- {{/if}}
- {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }}
- - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__
- {{/if}}
- {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }}
- - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__
- {{/if}}
- {{#if __GH_AW_GITHUB_RUN_ID__ }}
- - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__
- {{/if}}
-
-
- PROMPT_EOF
- if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then
- cat "/opt/gh-aw/prompts/pr_context_prompt.md" >> "$GH_AW_PROMPT"
- fi
- cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT"
-
- PROMPT_EOF
- cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT"
- {{#runtime-import .github/workflows/test-rate-limit.md}}
- PROMPT_EOF
- - name: Substitute placeholders
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- GH_AW_GITHUB_ACTOR: ${{ github.actor }}
- GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
- GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
- GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
- GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
- GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
- GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
- GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
- GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }}
- with:
- script: |
- const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs');
-
- // Call the substitution function
- return await substitutePlaceholders({
- file: process.env.GH_AW_PROMPT,
- substitutions: {
- GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR,
- GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID,
- GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER,
- GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER,
- GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER,
- GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY,
- GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID,
- GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE,
- GH_AW_IS_PR_COMMENT: process.env.GH_AW_IS_PR_COMMENT
- }
- });
- - name: Interpolate variables and render templates
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- with:
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs');
- await main();
- - name: Validate prompt placeholders
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh
- - name: Print prompt
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- run: bash /opt/gh-aw/actions/print_prompt_summary.sh
- - name: Clean git credentials
- run: bash /opt/gh-aw/actions/clean_git_credentials.sh
- - name: Execute GitHub Copilot CLI
- id: agentic_execution
- # Copilot CLI tool arguments (sorted):
- timeout-minutes: 20
- run: |
- set -o pipefail
- sudo -E awf --enable-chroot --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.14.0 --skip-pull \
- -- '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"}' \
- 2>&1 | tee /tmp/gh-aw/agent-stdio.log
- env:
- COPILOT_AGENT_RUNNER_TYPE: STANDALONE
- COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json
- GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }}
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- GITHUB_HEAD_REF: ${{ github.head_ref }}
- GITHUB_REF_NAME: ${{ github.ref_name }}
- GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }}
- GITHUB_WORKSPACE: ${{ github.workspace }}
- XDG_CONFIG_HOME: /home/runner
- - name: Configure Git credentials
- env:
- REPO_NAME: ${{ github.repository }}
- SERVER_URL: ${{ github.server_url }}
- run: |
- git config --global user.email "github-actions[bot]@users.noreply.github.com"
- git config --global user.name "github-actions[bot]"
- # Re-authenticate git with GitHub token
- SERVER_URL_STRIPPED="${SERVER_URL#https://}"
- git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
- echo "Git configured with standard GitHub Actions identity"
- - name: Copy Copilot session state files to logs
- if: always()
- continue-on-error: true
- run: |
- # Copy Copilot session state files to logs folder for artifact collection
- # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them
- SESSION_STATE_DIR="$HOME/.copilot/session-state"
- LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs"
-
- if [ -d "$SESSION_STATE_DIR" ]; then
- echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR"
- mkdir -p "$LOGS_DIR"
- cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true
- echo "Session state files copied successfully"
- else
- echo "No session-state directory found at $SESSION_STATE_DIR"
- fi
- - name: Stop MCP gateway
- if: always()
- continue-on-error: true
- env:
- MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }}
- MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }}
- GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }}
- run: |
- bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID"
- - name: Redact secrets in logs
- if: always()
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs');
- await main();
- env:
- GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN'
- SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
- SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
- SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Upload engine output files
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
- with:
- name: agent_outputs
- path: |
- /tmp/gh-aw/sandbox/agent/logs/
- /tmp/gh-aw/redacted-urls.log
- if-no-files-found: ignore
- - name: Parse agent logs for step summary
- if: always()
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/
- with:
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs');
- await main();
- - name: Parse MCP gateway logs for step summary
- if: always()
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs');
- await main();
- - name: Print firewall logs
- if: always()
- continue-on-error: true
- env:
- AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs
- run: |
- # Fix permissions on firewall logs so they can be uploaded as artifacts
- # AWF runs with sudo, creating files owned by root
- sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true
- awf logs summary | tee -a "$GITHUB_STEP_SUMMARY"
- - name: Upload agent artifacts
- if: always()
- continue-on-error: true
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
- with:
- name: agent-artifacts
- path: |
- /tmp/gh-aw/aw-prompts/prompt.txt
- /tmp/gh-aw/aw_info.json
- /tmp/gh-aw/mcp-logs/
- /tmp/gh-aw/sandbox/firewall/logs/
- /tmp/gh-aw/agent-stdio.log
- /tmp/gh-aw/agent/
- if-no-files-found: ignore
-
- pre_activation:
- runs-on: ubuntu-slim
- permissions:
- actions: read
- contents: read
- outputs:
- activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
- steps:
- - name: Checkout actions folder
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- sparse-checkout: |
- actions
- persist-credentials: false
- - name: Setup Scripts
- uses: ./actions/setup
- with:
- destination: /opt/gh-aw/actions
- - name: Check team membership for workflow
- id: check_membership
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_REQUIRED_ROLES: admin,maintainer,write
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/check_membership.cjs');
- await main();
- - name: Check user rate limit
- id: check_rate_limit
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_RATE_LIMIT_MAX: "3"
- GH_AW_RATE_LIMIT_WINDOW: "30"
- GH_AW_RATE_LIMIT_EVENTS: "workflow_dispatch,issue_comment"
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/check_rate_limit.cjs');
- await main();
-
diff --git a/.github/workflows/test-rate-limit.md b/.github/workflows/test-rate-limit.md
deleted file mode 100644
index 472e5831a98..00000000000
--- a/.github/workflows/test-rate-limit.md
+++ /dev/null
@@ -1,16 +0,0 @@
----
-name: Test Rate Limiting
-engine: copilot
-on:
- workflow_dispatch:
- issue_comment:
- types: [created]
-rate-limit:
- max: 3
- window: 30
- events: [workflow_dispatch, issue_comment]
----
-
-Test workflow to demonstrate rate limiting functionality.
-
-This workflow limits each user to 3 runs within a 30-minute window for workflow_dispatch and issue_comment events.
diff --git a/.github/workflows/workflow-generator.lock.yml b/.github/workflows/workflow-generator.lock.yml
index 62efef60fb2..43182e0ca83 100644
--- a/.github/workflows/workflow-generator.lock.yml
+++ b/.github/workflows/workflow-generator.lock.yml
@@ -25,7 +25,7 @@
# Imports:
# - shared/mood.md
#
-# frontmatter-hash: 2c2bf31df99a83c301214912d0cbe9e07f12cd4a8f188b2852bc94dc403d40d2
+# frontmatter-hash: 6715100ea6afc52b4d7cf99e50cca3e199f957dd0125e9f61572902be1b0a14d
name: "Workflow Generator"
"on":
@@ -1101,12 +1101,13 @@ jobs:
if: startsWith(github.event.issue.title, '[Workflow]')
runs-on: ubuntu-slim
permissions:
+ actions: read
contents: read
discussions: write
issues: write
pull-requests: write
outputs:
- activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }}
+ activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
steps:
- name: Checkout actions folder
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -1143,6 +1144,20 @@ jobs:
setupGlobals(core, github, context, exec, io);
const { main } = require('/opt/gh-aw/actions/check_membership.cjs');
await main();
+ - name: Check user rate limit
+ id: check_rate_limit
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_RATE_LIMIT_MAX: "5"
+ GH_AW_RATE_LIMIT_WINDOW: "60"
+ GH_AW_RATE_LIMIT_EVENTS: "issues"
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/check_rate_limit.cjs');
+ await main();
safe_outputs:
needs:
diff --git a/.github/workflows/workflow-generator.md b/.github/workflows/workflow-generator.md
index 9d48a0a7837..93fa607acbc 100644
--- a/.github/workflows/workflow-generator.md
+++ b/.github/workflows/workflow-generator.md
@@ -5,6 +5,10 @@ on:
types: [opened]
lock-for-agent: true
reaction: "eyes"
+rate-limit:
+ max: 5
+ window: 60
+ events: [issues]
permissions:
contents: read
issues: read
From 9c9fc7de22751710ad5e8b4bede81c4ed7e88d12 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 19:01:56 +0000
Subject: [PATCH 13/14] Make rate-limit.events optional with automatic
inference
- Add inferEventsFromTriggers() to automatically detect programmatic triggers
- Infer events from workflow's 'on:' section when not explicitly specified
- Only programmatic triggers are inferred (issues, issue_comment, workflow_dispatch, etc.)
- Non-programmatic triggers (push, schedule, etc.) are filtered out
- Remove explicit events from production workflows (now inferred automatically)
- Update documentation to explain automatic inference
- Add comprehensive tests for event inference (5 test cases, all passing)
Production workflows updated:
- ai-moderator: infers [issues, issue_comment, workflow_dispatch]
- auto-triage-issues: infers [issues, workflow_dispatch]
- example-custom-error-patterns: infers [issues]
- workflow-generator: infers [issues]
All 148 workflows recompiled successfully with automatic event inference.
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.github/workflows/ai-moderator.lock.yml | 4 +-
.github/workflows/ai-moderator.md | 1 -
.github/workflows/auto-triage-issues.lock.yml | 4 +-
.github/workflows/auto-triage-issues.md | 1 -
.../example-custom-error-patterns.lock.yml | 2 +-
.../example-custom-error-patterns.md | 1 -
.github/workflows/workflow-generator.lock.yml | 2 +-
.github/workflows/workflow-generator.md | 1 -
docs/RATE_LIMITING.md | 38 ++++++---
pkg/workflow/role_checks.go | 50 +++++++++++
pkg/workflow/role_checks_test.go | 85 +++++++++++++++++++
11 files changed, 167 insertions(+), 22 deletions(-)
diff --git a/.github/workflows/ai-moderator.lock.yml b/.github/workflows/ai-moderator.lock.yml
index d07f42bc482..a2074a41e02 100644
--- a/.github/workflows/ai-moderator.lock.yml
+++ b/.github/workflows/ai-moderator.lock.yml
@@ -24,7 +24,7 @@
# Imports:
# - shared/mood.md
#
-# frontmatter-hash: 2c0c2d09eb172ff1b3867c0298c4c50def936a66aac1b14996a196ea48662c2f
+# frontmatter-hash: ae515f3b3a547d9b86e5347378b5d34ce798ff0eb460bd6c912305e358b305c1
name: "AI Moderator"
"on":
@@ -1025,7 +1025,7 @@ jobs:
env:
GH_AW_RATE_LIMIT_MAX: "5"
GH_AW_RATE_LIMIT_WINDOW: "60"
- GH_AW_RATE_LIMIT_EVENTS: "issues,issue_comment"
+ GH_AW_RATE_LIMIT_EVENTS: "workflow_dispatch,issues,issue_comment"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
diff --git a/.github/workflows/ai-moderator.md b/.github/workflows/ai-moderator.md
index 71833c241b7..46850ac9e0a 100644
--- a/.github/workflows/ai-moderator.md
+++ b/.github/workflows/ai-moderator.md
@@ -17,7 +17,6 @@ on:
rate-limit:
max: 5
window: 60
- events: [issues, issue_comment]
engine:
id: copilot
model: gpt-5.1-codex-mini
diff --git a/.github/workflows/auto-triage-issues.lock.yml b/.github/workflows/auto-triage-issues.lock.yml
index 68502badb4a..08f3259fbb3 100644
--- a/.github/workflows/auto-triage-issues.lock.yml
+++ b/.github/workflows/auto-triage-issues.lock.yml
@@ -26,7 +26,7 @@
# - shared/mood.md
# - shared/reporting.md
#
-# frontmatter-hash: 948ba3839d6083bd933117ad41c2410fd690889345d83f761c8c387389deeee8
+# frontmatter-hash: 9c6fbb9920281fff3373c0db01b6a5e5fe93b637d2902977763c0634733725be
name: "Auto-Triage Issues"
"on":
@@ -1082,7 +1082,7 @@ jobs:
env:
GH_AW_RATE_LIMIT_MAX: "5"
GH_AW_RATE_LIMIT_WINDOW: "60"
- GH_AW_RATE_LIMIT_EVENTS: "issues"
+ GH_AW_RATE_LIMIT_EVENTS: "issues,workflow_dispatch"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
diff --git a/.github/workflows/auto-triage-issues.md b/.github/workflows/auto-triage-issues.md
index f5e58a6cedf..89bbdaed523 100644
--- a/.github/workflows/auto-triage-issues.md
+++ b/.github/workflows/auto-triage-issues.md
@@ -8,7 +8,6 @@ on:
rate-limit:
max: 5
window: 60
- events: [issues]
permissions:
contents: read
issues: read
diff --git a/.github/workflows/example-custom-error-patterns.lock.yml b/.github/workflows/example-custom-error-patterns.lock.yml
index c2870e54c40..57ae4344403 100644
--- a/.github/workflows/example-custom-error-patterns.lock.yml
+++ b/.github/workflows/example-custom-error-patterns.lock.yml
@@ -24,7 +24,7 @@
# Imports:
# - shared/mood.md
#
-# frontmatter-hash: 01403571c2bd8d2864e2d8ad6a80c019b48dfaf1ac2b61ee2f35c9a19145fd5d
+# frontmatter-hash: 5fb42e71fdea5fafa3ea342aac01e6373a703bf85c191fe80ab16d873ee25131
name: "Example: Custom Error Patterns"
"on":
diff --git a/.github/workflows/example-custom-error-patterns.md b/.github/workflows/example-custom-error-patterns.md
index 546b02fb068..4d45390e492 100644
--- a/.github/workflows/example-custom-error-patterns.md
+++ b/.github/workflows/example-custom-error-patterns.md
@@ -5,7 +5,6 @@ on:
rate-limit:
max: 5
window: 60
- events: [issues]
permissions:
contents: read
issues: read
diff --git a/.github/workflows/workflow-generator.lock.yml b/.github/workflows/workflow-generator.lock.yml
index 43182e0ca83..372090bb92b 100644
--- a/.github/workflows/workflow-generator.lock.yml
+++ b/.github/workflows/workflow-generator.lock.yml
@@ -25,7 +25,7 @@
# Imports:
# - shared/mood.md
#
-# frontmatter-hash: 6715100ea6afc52b4d7cf99e50cca3e199f957dd0125e9f61572902be1b0a14d
+# frontmatter-hash: 1bf8a53ac78955ad6bc31224322cd512e946710820a359cc08613e147865408c
name: "Workflow Generator"
"on":
diff --git a/.github/workflows/workflow-generator.md b/.github/workflows/workflow-generator.md
index 93fa607acbc..c6ca7df0cfb 100644
--- a/.github/workflows/workflow-generator.md
+++ b/.github/workflows/workflow-generator.md
@@ -8,7 +8,6 @@ on:
rate-limit:
max: 5
window: 60
- events: [issues]
permissions:
contents: read
issues: read
diff --git a/docs/RATE_LIMITING.md b/docs/RATE_LIMITING.md
index de018728af1..f3863120847 100644
--- a/docs/RATE_LIMITING.md
+++ b/docs/RATE_LIMITING.md
@@ -21,11 +21,9 @@ on:
issue_comment:
types: [created]
rate-limit:
- max: 5 # Maximum runs per time window (default: 5)
- window: 60 # Time window in minutes (default: 60)
- events: # Optional: specific events to limit
- - workflow_dispatch
- - issue_comment
+ max: 5 # Required: 1-10 runs
+ window: 60 # Optional: minutes (default 60, max 180)
+ # events field is optional - automatically inferred from 'on:' triggers
---
```
@@ -44,7 +42,9 @@ rate-limit:
### `events` (array, optional)
- Specific event types to apply rate limiting to
-- If not specified, applies to all programmatically triggered events
+- **If not specified, automatically inferred from the workflow's `on:` triggers**
+- Only programmatic trigger types are included in the inference
+- Can be explicitly set to override the inference
- Supported events:
- `workflow_dispatch`
- `issue_comment`
@@ -72,30 +72,44 @@ rate-limit:
## Examples
-### Basic Rate Limiting (Default)
+### Automatic Event Inference (Recommended)
```yaml
+on:
+ issues:
+ types: [opened]
+ issue_comment:
+ types: [created]
rate-limit:
max: 5
window: 60
+ # Events automatically inferred: [issues, issue_comment]
```
-Allows 5 runs per hour for all programmatic events.
+Events are automatically inferred from the workflow's triggers. Simplest configuration.
-### Strict Rate Limiting
+### Basic Rate Limiting (Default Window)
+```yaml
+rate-limit:
+ max: 5
+ window: 60
+```
+Allows 5 runs per hour. Events inferred from `on:` section.
+
+### Explicit Event Filtering
```yaml
rate-limit:
max: 3
window: 30
events: [workflow_dispatch, issue_comment]
```
-Allows only 3 runs per 30 minutes for manual triggers and issue comments.
+Explicitly specify events to override inference. Allows only 3 runs per 30 minutes for the specified events.
### Generous Rate Limiting
```yaml
rate-limit:
- max: 20
+ max: 10
window: 120
```
-Allows 20 runs per 2 hours for all events.
+Allows 10 runs per 2 hours. Events inferred from triggers.
## Behavior Details
diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go
index a1328200115..2446f1e9b27 100644
--- a/pkg/workflow/role_checks.go
+++ b/pkg/workflow/role_checks.go
@@ -186,6 +186,12 @@ func (c *Compiler) extractRateLimitConfig(frontmatter map[string]any) *RateLimit
case string:
config.Events = []string{events}
}
+ } else {
+ // If events not specified, infer from the 'on:' section of frontmatter
+ config.Events = c.inferEventsFromTriggers(frontmatter)
+ if len(config.Events) > 0 {
+ roleLog.Printf("Inferred events from workflow triggers: %v", config.Events)
+ }
}
roleLog.Printf("Extracted rate-limit config: max=%d, window=%d, events=%v", config.Max, config.Window, config.Events)
@@ -196,6 +202,50 @@ func (c *Compiler) extractRateLimitConfig(frontmatter map[string]any) *RateLimit
return nil
}
+// inferEventsFromTriggers infers rate-limit events from the workflow's 'on:' triggers
+func (c *Compiler) inferEventsFromTriggers(frontmatter map[string]any) []string {
+ onValue, exists := frontmatter["on"]
+ if !exists || onValue == nil {
+ return nil
+ }
+
+ var events []string
+ programmaticTriggers := map[string]string{
+ "workflow_dispatch": "workflow_dispatch",
+ "repository_dispatch": "repository_dispatch",
+ "issues": "issues",
+ "issue_comment": "issue_comment",
+ "pull_request": "pull_request",
+ "pull_request_review": "pull_request_review",
+ "pull_request_review_comment": "pull_request_review_comment",
+ "discussion": "discussion",
+ "discussion_comment": "discussion_comment",
+ }
+
+ switch on := onValue.(type) {
+ case map[string]any:
+ for trigger := range on {
+ if eventName, ok := programmaticTriggers[trigger]; ok {
+ events = append(events, eventName)
+ }
+ }
+ case []any:
+ for _, item := range on {
+ if triggerStr, ok := item.(string); ok {
+ if eventName, ok := programmaticTriggers[triggerStr]; ok {
+ events = append(events, eventName)
+ }
+ }
+ }
+ case string:
+ if eventName, ok := programmaticTriggers[on]; ok {
+ events = []string{eventName}
+ }
+ }
+
+ return events
+}
+
// needsRoleCheck determines if the workflow needs permission checks with full context
func (c *Compiler) needsRoleCheck(data *WorkflowData, frontmatter map[string]any) bool {
// If user explicitly specified "roles: all", no permission checks needed
diff --git a/pkg/workflow/role_checks_test.go b/pkg/workflow/role_checks_test.go
index 3bae6859e1e..2065f13dd68 100644
--- a/pkg/workflow/role_checks_test.go
+++ b/pkg/workflow/role_checks_test.go
@@ -9,6 +9,7 @@ import (
"testing"
"github.com/github/gh-aw/pkg/testutil"
+ "github.com/stretchr/testify/assert"
)
// TestRoleMembershipUsesGitHubToken tests that the role membership check
@@ -146,3 +147,87 @@ Test that role membership check uses GITHUB_TOKEN with bots.`
t.Errorf("Expected check_membership step to explicitly use 'github-token: ${{ secrets.GITHUB_TOKEN }}'")
}
}
+
+func TestInferEventsFromTriggers(t *testing.T) {
+ c := &Compiler{}
+
+ tests := []struct {
+ name string
+ frontmatter map[string]any
+ expected []string
+ }{
+ {
+ name: "infer from map with multiple triggers",
+ frontmatter: map[string]any{
+ "on": map[string]any{
+ "issues": map[string]any{"types": []any{"opened"}},
+ "issue_comment": map[string]any{"types": []any{"created"}},
+ "workflow_dispatch": nil,
+ },
+ },
+ expected: []string{"issues", "issue_comment", "workflow_dispatch"},
+ },
+ {
+ name: "infer only programmatic triggers",
+ frontmatter: map[string]any{
+ "on": map[string]any{
+ "push": map[string]any{},
+ "issues": map[string]any{},
+ "schedule": "daily",
+ },
+ },
+ expected: []string{"issues"},
+ },
+ {
+ name: "no triggers",
+ frontmatter: map[string]any{
+ "on": map[string]any{},
+ },
+ expected: nil,
+ },
+ {
+ name: "missing on section",
+ frontmatter: map[string]any{},
+ expected: nil,
+ },
+ {
+ name: "all programmatic triggers",
+ frontmatter: map[string]any{
+ "on": map[string]any{
+ "workflow_dispatch": nil,
+ "repository_dispatch": nil,
+ "issues": map[string]any{},
+ "issue_comment": map[string]any{},
+ "pull_request": map[string]any{},
+ "pull_request_review": map[string]any{},
+ "pull_request_review_comment": map[string]any{},
+ "discussion": map[string]any{},
+ "discussion_comment": map[string]any{},
+ },
+ },
+ expected: []string{
+ "workflow_dispatch",
+ "repository_dispatch",
+ "issues",
+ "issue_comment",
+ "pull_request",
+ "pull_request_review",
+ "pull_request_review_comment",
+ "discussion",
+ "discussion_comment",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := c.inferEventsFromTriggers(tt.frontmatter)
+ // Use ElementsMatch since map iteration order is non-deterministic
+ if len(tt.expected) > 0 && len(result) > 0 {
+ assert.ElementsMatch(t, tt.expected, result, "Inferred events should match expected")
+ } else {
+ assert.Equal(t, tt.expected, result, "Inferred events should match expected")
+ }
+ })
+ }
+}
From 907aab7f7d34160bc7a779b0e99560b2ee8de226 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Wed, 11 Feb 2026 21:49:17 +0000
Subject: [PATCH 14/14] Add changeset [skip-ci]
---
.changeset/patch-rate-limit-programmatic-events.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/patch-rate-limit-programmatic-events.md
diff --git a/.changeset/patch-rate-limit-programmatic-events.md b/.changeset/patch-rate-limit-programmatic-events.md
new file mode 100644
index 00000000000..cd2d5ac6418
--- /dev/null
+++ b/.changeset/patch-rate-limit-programmatic-events.md
@@ -0,0 +1,5 @@
+---
+"gh-aw": patch
+---
+
+Protect workflows from abusive programmatic triggers by adding configurable per-user, per-workflow rate limiting with automatic event inference, filtered cancellations, and window controls.