From ed9c1ffc9a0def98e2e6c2330bd6a4db8a3d214a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 14 Apr 2026 04:05:06 +0000
Subject: [PATCH 1/3] feat: clarify rate-limit as per-user and add
max-in-progress field for agent tasks
- Update rate-limit schema description to clearly say 'per-user workflow rate limit'
- Update max/window/events/ignored-roles field descriptions to reference 'per-user'
- Add max-in-progress field to limit concurrent in-progress GitHub agent tasks
- Add MaxInProgress field to RateLimitConfig Go struct
- Extract max-in-progress in extractRateLimitConfig
- Pass GH_AW_RATE_LIMIT_MAX_IN_PROGRESS env var in generateRateLimitCheck
- Add issues: read permission when max-in-progress is set
- Add NewPermissionsContentsReadIssuesRead factory function
- Implement max-in-progress check in check_rate_limit.cjs using agent tasks API
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/36ab6d0c-9ada-4759-84ed-248230cc118e
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/check_rate_limit.cjs | 58 +++++++++++++++++++-
pkg/parser/schemas/main_workflow_schema.json | 20 +++++--
pkg/workflow/compiler_pre_activation_job.go | 4 ++
pkg/workflow/frontmatter_types.go | 14 +++--
pkg/workflow/permissions_factory.go | 8 +++
pkg/workflow/role_checks.go | 21 ++++++-
6 files changed, 112 insertions(+), 13 deletions(-)
diff --git a/actions/setup/js/check_rate_limit.cjs b/actions/setup/js/check_rate_limit.cjs
index 59b9441f11a..2dd0640b268 100644
--- a/actions/setup/js/check_rate_limit.cjs
+++ b/actions/setup/js/check_rate_limit.cjs
@@ -48,6 +48,8 @@ async function main() {
const eventsList = process.env.GH_AW_RATE_LIMIT_EVENTS?.trim() || "";
// Default: admin, maintain, and write roles are exempt from rate limiting
const ignoredRolesList = process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES?.trim() || "admin,maintain,write";
+ const maxInProgressStr = process.env.GH_AW_RATE_LIMIT_MAX_IN_PROGRESS?.trim() || "";
+ const maxInProgress = maxInProgressStr ? parseInt(maxInProgressStr, 10) : 0;
core.info(`🔍 Checking rate limit for user '${actor}' on workflow '${workflowId}'`);
core.info(` Configuration: max=${maxRuns} runs per ${windowMinutes} minutes`);
@@ -252,7 +254,6 @@ async function main() {
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) {
core.error(`❌ Rate limit check failed: ${getErrorMessage(error)}`);
if (error instanceof Error && error.stack) {
@@ -263,7 +264,62 @@ async function main() {
// 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");
+ return;
}
+
+ // Check max-in-progress GitHub agent tasks if configured
+ if (maxInProgress > 0) {
+ core.info(`🔍 Checking max-in-progress agent tasks (max: ${maxInProgress})`);
+ try {
+ // Query in-progress agent tasks using the GitHub agent tasks API
+ // Requires issues: read permission
+ const response = await github.request("GET /repos/{owner}/{repo}/agent/tasks", {
+ owner,
+ repo,
+ status: "in_progress",
+ headers: {
+ "X-GitHub-Api-Version": "2026-03-10",
+ },
+ });
+
+ const inProgressTasks = response.data.tasks ?? response.data ?? [];
+ const inProgressCount = Array.isArray(inProgressTasks) ? inProgressTasks.length : 0;
+
+ core.info(`📊 In-progress agent tasks for '${owner}/${repo}': ${inProgressCount} (max: ${maxInProgress})`);
+
+ if (inProgressCount >= maxInProgress) {
+ core.warning(`⚠️ Max in-progress agent tasks reached for '${owner}/${repo}'`);
+ core.warning(` There are ${inProgressCount} in-progress tasks (max: ${maxInProgress})`);
+ core.warning(` Cancelling current workflow run...`);
+
+ try {
+ await github.rest.actions.cancelWorkflowRun({
+ owner,
+ repo,
+ run_id: runId,
+ });
+ core.warning(`✅ Workflow run ${runId} cancelled successfully`);
+ } catch (cancelError) {
+ core.error(`❌ Failed to cancel workflow run: ${getErrorMessage(cancelError)}`);
+ }
+
+ core.setOutput("rate_limit_ok", "false");
+ return;
+ }
+
+ core.info(`✅ Max-in-progress check passed (${inProgressCount}/${maxInProgress} tasks in progress)`);
+ } catch (error) {
+ core.error(`❌ Max-in-progress check failed: ${getErrorMessage(error)}`);
+ if (error instanceof Error && error.stack) {
+ core.error(` Stack trace: ${error.stack}`);
+ }
+
+ // On error, allow the workflow to proceed (fail-open)
+ core.warning(`⚠️ Allowing workflow to proceed due to max-in-progress check error`);
+ }
+ }
+
+ core.setOutput("rate_limit_ok", "true");
}
module.exports = { main };
diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json
index ec59a0ce135..d76b79ffd5e 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -8427,11 +8427,11 @@
},
"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.",
+ "description": "Per-user workflow rate limit configuration to restrict how frequently individual users can trigger this workflow. Helps prevent abuse and resource exhaustion from programmatically triggered events. Optionally also limits the total number of in-progress GitHub agent tasks for the repository.",
"required": ["max"],
"properties": {
"max": {
- "description": "Maximum number of workflow runs allowed per user within the time window. Required field. Supports integer or GitHub Actions expression (e.g. '${{ inputs.max }}').",
+ "description": "Maximum number of workflow runs allowed per user within the time window. This is a per-user limit: each user is tracked independently. Required field. Supports integer or GitHub Actions expression (e.g. '${{ inputs.max }}').",
"oneOf": [
{
"type": "integer",
@@ -8450,11 +8450,11 @@
"minimum": 1,
"maximum": 180,
"default": 60,
- "description": "Time window in minutes for rate limiting. Defaults to 60 (1 hour). Maximum: 180 (3 hours)."
+ "description": "Time window in minutes for the per-user rate limit. Defaults to 60 (1 hour). Maximum: 180 (3 hours)."
},
"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).",
+ "description": "Optional list of event types to apply the per-user rate limit 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"]
@@ -8463,12 +8463,17 @@
},
"ignored-roles": {
"type": "array",
- "description": "Optional list of roles that are exempt from rate limiting. Defaults to ['admin', 'maintain', 'write'] if not specified. Users with any of these roles will not be subject to rate limiting checks. To apply rate limiting to all users, set to an empty array: []",
+ "description": "Optional list of roles that are exempt from the per-user rate limit. Defaults to ['admin', 'maintain', 'write'] if not specified. Users with any of these roles will not be subject to rate limiting checks. To apply rate limiting to all users, set to an empty array: []",
"items": {
"type": "string",
"enum": ["admin", "maintain", "write", "triage", "read"]
},
"minItems": 0
+ },
+ "max-in-progress": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Maximum number of in-progress GitHub agent tasks allowed for this repository before blocking new workflow runs. If the number of currently in-progress agent tasks meets or exceeds this value, the current run is cancelled. Queries the GitHub agent tasks API (GET /repos/{owner}/{repo}/agent/tasks). Requires issues: read permission. See https://docs.github.com/en/rest/agent-tasks/agent-tasks"
}
},
"additionalProperties": false,
@@ -8486,6 +8491,11 @@
"max": 5,
"window": 60,
"ignored-roles": ["admin", "maintain"]
+ },
+ {
+ "max": 5,
+ "window": 60,
+ "max-in-progress": 3
}
]
},
diff --git a/pkg/workflow/compiler_pre_activation_job.go b/pkg/workflow/compiler_pre_activation_job.go
index 51069ef63bb..3854ace97e4 100644
--- a/pkg/workflow/compiler_pre_activation_job.go
+++ b/pkg/workflow/compiler_pre_activation_job.go
@@ -54,6 +54,10 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec
perms = NewPermissions()
}
perms.Set(PermissionActions, PermissionRead)
+ // Add issues: read permission if max-in-progress is configured (needed to query agent tasks)
+ if data.RateLimit.MaxInProgress > 0 {
+ perms.Set(PermissionIssues, PermissionRead)
+ }
}
// Merge on.permissions into the pre-activation job permissions.
diff --git a/pkg/workflow/frontmatter_types.go b/pkg/workflow/frontmatter_types.go
index 0bd65c9f2ab..ec1887f715d 100644
--- a/pkg/workflow/frontmatter_types.go
+++ b/pkg/workflow/frontmatter_types.go
@@ -109,13 +109,15 @@ type PermissionsConfig struct {
GitHubAppPermissionsConfig
}
-// RateLimitConfig represents rate limiting configuration for workflow triggers
-// Limits how many times a user can trigger a workflow within a time window
+// RateLimitConfig represents per-user rate limiting configuration for workflow triggers.
+// Controls how many times a user can trigger this workflow within a time window,
+// and optionally limits the maximum number of in-progress GitHub agent tasks for the repo.
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"])
- IgnoredRoles []string `json:"ignored-roles,omitempty"` // Roles that are exempt from rate limiting (e.g., ["admin", "maintainer"])
+ Max int `json:"max,omitempty"` // Maximum number of workflow runs allowed per user within the 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"])
+ IgnoredRoles []string `json:"ignored-roles,omitempty"` // Roles that are exempt from rate limiting (e.g., ["admin", "maintainer"])
+ MaxInProgress int `json:"max-in-progress,omitempty"` // Maximum number of in-progress GitHub agent tasks allowed for the repo before blocking new runs
}
// OTLPConfig holds configuration for OTLP (OpenTelemetry Protocol) trace export.
diff --git a/pkg/workflow/permissions_factory.go b/pkg/workflow/permissions_factory.go
index 0a7ef8fe1e0..3f7c943ef4d 100644
--- a/pkg/workflow/permissions_factory.go
+++ b/pkg/workflow/permissions_factory.go
@@ -73,6 +73,14 @@ func NewPermissionsContentsRead() *Permissions {
})
}
+// NewPermissionsContentsReadIssuesRead creates permissions with contents: read and issues: read
+func NewPermissionsContentsReadIssuesRead() *Permissions {
+ return NewPermissionsFromMap(map[PermissionScope]PermissionLevel{
+ PermissionContents: PermissionRead,
+ PermissionIssues: PermissionRead,
+ })
+}
+
// NewPermissionsContentsReadIssuesWrite creates permissions with contents: read and issues: write
func NewPermissionsContentsReadIssuesWrite() *Permissions {
return NewPermissionsFromMap(map[PermissionScope]PermissionLevel{
diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go
index e3354a51cf6..52f87c2abea 100644
--- a/pkg/workflow/role_checks.go
+++ b/pkg/workflow/role_checks.go
@@ -83,6 +83,11 @@ func (c *Compiler) generateRateLimitCheck(data *WorkflowData, steps []string) []
steps = append(steps, fmt.Sprintf(" GH_AW_RATE_LIMIT_IGNORED_ROLES: %q\n", strings.Join(ignoredRoles, ",")))
}
+ // Set max-in-progress (if specified)
+ if data.RateLimit.MaxInProgress > 0 {
+ steps = append(steps, fmt.Sprintf(" GH_AW_RATE_LIMIT_MAX_IN_PROGRESS: \"%d\"\n", data.RateLimit.MaxInProgress))
+ }
+
steps = append(steps, " with:\n")
steps = append(steps, " github-token: ${{ secrets.GITHUB_TOKEN }}\n")
steps = append(steps, " script: |\n")
@@ -242,7 +247,21 @@ func (c *Compiler) extractRateLimitConfig(frontmatter map[string]any) *RateLimit
roleLog.Print("No ignored-roles specified, using defaults: admin, maintain, write")
}
- roleLog.Printf("Extracted rate-limit config: max=%d, window=%d, events=%v, ignored-roles=%v", config.Max, config.Window, config.Events, config.IgnoredRoles)
+ // Extract max-in-progress
+ if maxInProgressValue, ok := v["max-in-progress"]; ok {
+ switch maxInProgress := maxInProgressValue.(type) {
+ case int:
+ config.MaxInProgress = maxInProgress
+ case int64:
+ config.MaxInProgress = int(maxInProgress)
+ case uint64:
+ config.MaxInProgress = int(maxInProgress)
+ case float64:
+ config.MaxInProgress = int(maxInProgress)
+ }
+ }
+
+ roleLog.Printf("Extracted rate-limit config: max=%d, window=%d, events=%v, ignored-roles=%v, max-in-progress=%d", config.Max, config.Window, config.Events, config.IgnoredRoles, config.MaxInProgress)
return config
}
}
From 4fa2ac3a096c42e9d767d1591ee6e6f0672d6148 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 14 Apr 2026 04:07:10 +0000
Subject: [PATCH 2/3] fix: per-user rate limit error now continues to
max-in-progress check instead of returning early
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/36ab6d0c-9ada-4759-84ed-248230cc118e
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/check_rate_limit.cjs | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/actions/setup/js/check_rate_limit.cjs b/actions/setup/js/check_rate_limit.cjs
index 2dd0640b268..48bb8036b46 100644
--- a/actions/setup/js/check_rate_limit.cjs
+++ b/actions/setup/js/check_rate_limit.cjs
@@ -261,10 +261,9 @@ async function main() {
}
// On error, allow the workflow to proceed (fail-open)
- // This prevents rate limiting from blocking workflows due to API issues
+ // This prevents rate limiting from blocking workflows due to API issues.
+ // Continue to the max-in-progress check rather than returning early.
core.warning(`⚠️ Allowing workflow to proceed due to rate limit check error`);
- core.setOutput("rate_limit_ok", "true");
- return;
}
// Check max-in-progress GitHub agent tasks if configured
From 406da21fd6d20f064ca1b1d476c42a033a7d8503 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 14 Apr 2026 04:44:03 +0000
Subject: [PATCH 3/3] feat: rename rate-limit max/window fields and add
migration codemod
- Rename rate-limit.max -> rate-limit.max-runs-per-user in schema, Go struct, and extraction logic
- Rename rate-limit.window -> rate-limit.max-runs-per-user-window
- Keep backward-compat fallback in extractRateLimitConfig for both old field names
- Add codemod_rate_limit_fields.go with getRateLimitFieldsCodemod() to auto-rename fields
- Register new codemod in GetAllCodemods()
- Update fix_codemods_test.go count and expected order
- Update all workflow .md files using rate-limit to new field names
- Update test content in rate_limit_experimental_warning_test.go
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/ef79c11f-42ae-4ead-a817-c789b247832b
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.github/workflows/ai-moderator.lock.yml | 26 +-
.github/workflows/ai-moderator.md | 4 +-
.github/workflows/auto-triage-issues.lock.yml | 22 +-
.github/workflows/auto-triage-issues.md | 4 +-
.github/workflows/workflow-generator.lock.yml | 22 +-
.github/workflows/workflow-generator.md | 4 +-
pkg/cli/codemod_rate_limit_fields.go | 92 +++++++
pkg/cli/codemod_rate_limit_fields_test.go | 240 ++++++++++++++++++
pkg/cli/fix_codemods.go | 1 +
pkg/cli/fix_codemods_test.go | 3 +-
pkg/parser/schemas/main_workflow_schema.json | 22 +-
pkg/workflow/frontmatter_types.go | 10 +-
.../rate_limit_experimental_warning_test.go | 12 +-
pkg/workflow/role_checks.go | 32 ++-
14 files changed, 426 insertions(+), 68 deletions(-)
create mode 100644 pkg/cli/codemod_rate_limit_fields.go
create mode 100644 pkg/cli/codemod_rate_limit_fields_test.go
diff --git a/.github/workflows/ai-moderator.lock.yml b/.github/workflows/ai-moderator.lock.yml
index 857c5b2ccec..66c89e4a003 100644
--- a/.github/workflows/ai-moderator.lock.yml
+++ b/.github/workflows/ai-moderator.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"15950b65d2a702132a14bc0478af6fc4877e0c5f90d1eba28f6bc53fbb698094","strict":true,"agent_id":"codex"}
+# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"af1e2953bedbc8b7a1c59116a75b87e072afbd917fefc234b0901d7608354da8","strict":true,"agent_id":"codex"}
# gh-aw-manifest: {"version":1,"secrets":["CODEX_API_KEY","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN","OPENAI_API_KEY"],"actions":[{"repo":"actions/cache","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/setup-node","sha":"53b83947a5a98c8d113130e565377fae1a50d02f","version":"v6.3.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20","digest":"sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20@sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20","digest":"sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20@sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20","digest":"sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20@sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19","digest":"sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.2.19@sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0","digest":"sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28","pinned_image":"ghcr.io/github/github-mcp-server:v0.32.0@sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28"},{"image":"node:lts-alpine","digest":"sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b","pinned_image":"node:lts-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b"}]}
# ___ _ _
# / _ \ | | (_)
@@ -207,15 +207,15 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_1d161161775b0eab_EOF'
+ cat << 'GH_AW_PROMPT_27f6c021899a60c4_EOF'
- GH_AW_PROMPT_1d161161775b0eab_EOF
+ GH_AW_PROMPT_27f6c021899a60c4_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
- cat << 'GH_AW_PROMPT_1d161161775b0eab_EOF'
+ cat << 'GH_AW_PROMPT_27f6c021899a60c4_EOF'
Tools: add_labels, hide_comment(max:5), missing_tool, missing_data, noop
@@ -247,12 +247,12 @@ jobs:
{{/if}}
- GH_AW_PROMPT_1d161161775b0eab_EOF
+ GH_AW_PROMPT_27f6c021899a60c4_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
- cat << 'GH_AW_PROMPT_1d161161775b0eab_EOF'
+ cat << 'GH_AW_PROMPT_27f6c021899a60c4_EOF'
{{#runtime-import .github/workflows/ai-moderator.md}}
- GH_AW_PROMPT_1d161161775b0eab_EOF
+ GH_AW_PROMPT_27f6c021899a60c4_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
@@ -434,9 +434,9 @@ jobs:
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
- cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_ba605b016681ab87_EOF'
+ cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_6b19074e066842da_EOF'
{"add_labels":{"allowed":["spam","ai-generated","link-spam","ai-inspected"],"target":"*"},"create_report_incomplete_issue":{},"hide_comment":{"allowed_reasons":["spam"],"max":5},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
- GH_AW_SAFE_OUTPUTS_CONFIG_ba605b016681ab87_EOF
+ GH_AW_SAFE_OUTPUTS_CONFIG_6b19074e066842da_EOF
- name: Write Safe Outputs Tools
env:
GH_AW_TOOLS_META_JSON: |
@@ -638,7 +638,7 @@ jobs:
export GH_AW_ENGINE="codex"
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 MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -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_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -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 -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -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.2.19'
- cat > /tmp/gh-aw/mcp-config/config.toml << GH_AW_MCP_CONFIG_e7783151e5530535_EOF
+ cat > /tmp/gh-aw/mcp-config/config.toml << GH_AW_MCP_CONFIG_c82d86036dd35b82_EOF
[history]
persistence = "none"
@@ -665,10 +665,10 @@ jobs:
[mcp_servers.safeoutputs."guard-policies".write-sink]
accept = ["*"]
- GH_AW_MCP_CONFIG_e7783151e5530535_EOF
+ GH_AW_MCP_CONFIG_c82d86036dd35b82_EOF
# Generate JSON config for MCP gateway
- cat << GH_AW_MCP_CONFIG_e7783151e5530535_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh"
+ cat << GH_AW_MCP_CONFIG_c82d86036dd35b82_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh"
{
"mcpServers": {
"github": {
@@ -711,7 +711,7 @@ jobs:
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
}
}
- GH_AW_MCP_CONFIG_e7783151e5530535_EOF
+ GH_AW_MCP_CONFIG_c82d86036dd35b82_EOF
- name: Download activation artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
diff --git a/.github/workflows/ai-moderator.md b/.github/workflows/ai-moderator.md
index 09486a9c4b3..701802157cc 100644
--- a/.github/workflows/ai-moderator.md
+++ b/.github/workflows/ai-moderator.md
@@ -14,8 +14,8 @@ on:
skip-roles: [admin, maintainer, write, triage]
skip-bots: [github-actions, copilot, dependabot, renovate, github-copilot-enterprise, copilot-swe-agent]
rate-limit:
- max: 5
- window: 60
+ max-runs-per-user: 5
+ max-runs-per-user-window: 60
concurrency:
group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}"
cancel-in-progress: false
diff --git a/.github/workflows/auto-triage-issues.lock.yml b/.github/workflows/auto-triage-issues.lock.yml
index 6e87d609cec..47612f65964 100644
--- a/.github/workflows/auto-triage-issues.lock.yml
+++ b/.github/workflows/auto-triage-issues.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"395091b3248ae0bfdd9169fe4e375b45de1a524b5a1c7ac4189864a829171a9e","strict":true,"agent_id":"copilot"}
+# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"5862a4bd2aba141ad15e903d7a4a67f1ccded243a92e6572ed65132a859895f5","strict":true,"agent_id":"copilot"}
# gh-aw-manifest: {"version":1,"secrets":["GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20","digest":"sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20@sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20","digest":"sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20@sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20","digest":"sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20@sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19","digest":"sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.2.19@sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0","digest":"sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28","pinned_image":"ghcr.io/github/github-mcp-server:v0.32.0@sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28"},{"image":"node:lts-alpine","digest":"sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b","pinned_image":"node:lts-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b"}]}
# ___ _ _
# / _ \ | | (_)
@@ -177,14 +177,14 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_348e9f22fc6fd7c7_EOF'
+ cat << 'GH_AW_PROMPT_cd6140c31ec529aa_EOF'
- GH_AW_PROMPT_348e9f22fc6fd7c7_EOF
+ GH_AW_PROMPT_cd6140c31ec529aa_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
- cat << 'GH_AW_PROMPT_348e9f22fc6fd7c7_EOF'
+ cat << 'GH_AW_PROMPT_cd6140c31ec529aa_EOF'
Tools: create_discussion, add_labels(max:10), missing_tool, missing_data, noop
@@ -216,14 +216,14 @@ jobs:
{{/if}}
- GH_AW_PROMPT_348e9f22fc6fd7c7_EOF
+ GH_AW_PROMPT_cd6140c31ec529aa_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
- cat << 'GH_AW_PROMPT_348e9f22fc6fd7c7_EOF'
+ cat << 'GH_AW_PROMPT_cd6140c31ec529aa_EOF'
{{#runtime-import .github/workflows/shared/github-guard-policy.md}}
{{#runtime-import .github/workflows/shared/reporting.md}}
{{#runtime-import .github/workflows/auto-triage-issues.md}}
- GH_AW_PROMPT_348e9f22fc6fd7c7_EOF
+ GH_AW_PROMPT_cd6140c31ec529aa_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
@@ -398,9 +398,9 @@ jobs:
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
- cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_d225981bc842f158_EOF'
+ cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_1af373ead35accd7_EOF'
{"add_labels":{"max":10},"create_discussion":{"category":"audits","close_older_discussions":true,"expires":24,"fallback_to_issue":true,"max":1,"title_prefix":"[Auto-Triage] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
- GH_AW_SAFE_OUTPUTS_CONFIG_d225981bc842f158_EOF
+ GH_AW_SAFE_OUTPUTS_CONFIG_1af373ead35accd7_EOF
- name: Write Safe Outputs Tools
env:
GH_AW_TOOLS_META_JSON: |
@@ -606,7 +606,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 MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -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_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -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 -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -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.2.19'
mkdir -p /home/runner/.copilot
- cat << GH_AW_MCP_CONFIG_17800b88a17b863b_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh"
+ cat << GH_AW_MCP_CONFIG_aee730b3c097c24f_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh"
{
"mcpServers": {
"github": {
@@ -650,7 +650,7 @@ jobs:
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
}
}
- GH_AW_MCP_CONFIG_17800b88a17b863b_EOF
+ GH_AW_MCP_CONFIG_aee730b3c097c24f_EOF
- name: Download activation artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
diff --git a/.github/workflows/auto-triage-issues.md b/.github/workflows/auto-triage-issues.md
index abaabb7bb0c..a5a985286ef 100644
--- a/.github/workflows/auto-triage-issues.md
+++ b/.github/workflows/auto-triage-issues.md
@@ -7,8 +7,8 @@ on:
schedule: every 6h
workflow_dispatch:
rate-limit:
- max: 5
- window: 60
+ max-runs-per-user: 5
+ max-runs-per-user-window: 60
permissions:
contents: read
issues: read
diff --git a/.github/workflows/workflow-generator.lock.yml b/.github/workflows/workflow-generator.lock.yml
index 1863d034612..caa79ee1cbf 100644
--- a/.github/workflows/workflow-generator.lock.yml
+++ b/.github/workflows/workflow-generator.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"cfd7f6135eab81d11cbd703b3436241bc379da2ede370ecb3285f2186bde6d06","strict":true,"agent_id":"copilot"}
+# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"7b6b93ab303bfe6cae087f99a41e89b31276e2d728cdf007409fc09d4007b74e","strict":true,"agent_id":"copilot"}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_AGENT_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20","digest":"sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20@sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20","digest":"sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20@sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20","digest":"sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20@sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19","digest":"sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.2.19@sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0","digest":"sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28","pinned_image":"ghcr.io/github/github-mcp-server:v0.32.0@sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28"},{"image":"node:lts-alpine","digest":"sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b","pinned_image":"node:lts-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b"}]}
# ___ _ _
# / _ \ | | (_)
@@ -201,14 +201,14 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_81789fea3498ccb0_EOF'
+ cat << 'GH_AW_PROMPT_b025bb242ab3eed8_EOF'
- GH_AW_PROMPT_81789fea3498ccb0_EOF
+ GH_AW_PROMPT_b025bb242ab3eed8_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
- cat << 'GH_AW_PROMPT_81789fea3498ccb0_EOF'
+ cat << 'GH_AW_PROMPT_b025bb242ab3eed8_EOF'
Tools: update_issue, assign_to_agent, missing_tool, missing_data, noop
@@ -240,13 +240,13 @@ jobs:
{{/if}}
- GH_AW_PROMPT_81789fea3498ccb0_EOF
+ GH_AW_PROMPT_b025bb242ab3eed8_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
- cat << 'GH_AW_PROMPT_81789fea3498ccb0_EOF'
+ cat << 'GH_AW_PROMPT_b025bb242ab3eed8_EOF'
{{#runtime-import .github/workflows/shared/github-guard-policy.md}}
{{#runtime-import .github/workflows/workflow-generator.md}}
- GH_AW_PROMPT_81789fea3498ccb0_EOF
+ GH_AW_PROMPT_b025bb242ab3eed8_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
@@ -421,9 +421,9 @@ jobs:
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
- cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_3f58b4200a5b8f57_EOF'
+ cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_c58abdf8d5b2660d_EOF'
{"assign_to_agent":{"allowed":["copilot"],"max":1,"target":"triggering"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"update_issue":{"allow_body":true,"allow_status":true,"max":1}}
- GH_AW_SAFE_OUTPUTS_CONFIG_3f58b4200a5b8f57_EOF
+ GH_AW_SAFE_OUTPUTS_CONFIG_c58abdf8d5b2660d_EOF
- name: Write Safe Outputs Tools
env:
GH_AW_TOOLS_META_JSON: |
@@ -663,7 +663,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 MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -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_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -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 -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -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.2.19'
mkdir -p /home/runner/.copilot
- cat << GH_AW_MCP_CONFIG_a89a20c6f3ef975e_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh"
+ cat << GH_AW_MCP_CONFIG_cbd70b665437b402_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh"
{
"mcpServers": {
"github": {
@@ -707,7 +707,7 @@ jobs:
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
}
}
- GH_AW_MCP_CONFIG_a89a20c6f3ef975e_EOF
+ GH_AW_MCP_CONFIG_cbd70b665437b402_EOF
- name: Download activation artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
diff --git a/.github/workflows/workflow-generator.md b/.github/workflows/workflow-generator.md
index 71011e0d3c9..955bb8e71de 100644
--- a/.github/workflows/workflow-generator.md
+++ b/.github/workflows/workflow-generator.md
@@ -6,8 +6,8 @@ on:
lock-for-agent: true
reaction: "eyes"
rate-limit:
- max: 5
- window: 60
+ max-runs-per-user: 5
+ max-runs-per-user-window: 60
permissions:
contents: read
issues: read
diff --git a/pkg/cli/codemod_rate_limit_fields.go b/pkg/cli/codemod_rate_limit_fields.go
new file mode 100644
index 00000000000..4a73a8ce73e
--- /dev/null
+++ b/pkg/cli/codemod_rate_limit_fields.go
@@ -0,0 +1,92 @@
+package cli
+
+import (
+ "strings"
+
+ "github.com/github/gh-aw/pkg/logger"
+)
+
+var rateLimitFieldsCodemodLog = logger.New("cli:codemod_rate_limit_fields")
+
+// getRateLimitFieldsCodemod creates a codemod for migrating rate-limit field names:
+// - max -> max-runs-per-user
+// - window -> max-runs-per-user-window
+func getRateLimitFieldsCodemod() Codemod {
+ return Codemod{
+ ID: "rate-limit-fields-migration",
+ Name: "Rename rate-limit fields for clarity",
+ Description: "Renames 'rate-limit.max' to 'rate-limit.max-runs-per-user' and 'rate-limit.window' to 'rate-limit.max-runs-per-user-window'",
+ IntroducedIn: "0.20.0",
+ Apply: func(content string, frontmatter map[string]any) (string, bool, error) {
+ // Check if rate-limit block exists with old field names
+ rateLimitValue, hasRateLimit := frontmatter["rate-limit"]
+ if !hasRateLimit {
+ return content, false, nil
+ }
+ rateLimitMap, ok := rateLimitValue.(map[string]any)
+ if !ok {
+ return content, false, nil
+ }
+
+ _, hasMax := rateLimitMap["max"]
+ _, hasWindow := rateLimitMap["window"]
+ if !hasMax && !hasWindow {
+ return content, false, nil
+ }
+
+ newContent, applied, err := applyFrontmatterLineTransform(content, func(lines []string) ([]string, bool) {
+ var modified bool
+ var inRateLimitBlock bool
+ var rateLimitIndent string
+
+ result := make([]string, len(lines))
+ for i, line := range lines {
+ trimmedLine := strings.TrimSpace(line)
+
+ // Detect entering the rate-limit block
+ if strings.HasPrefix(trimmedLine, "rate-limit:") {
+ inRateLimitBlock = true
+ rateLimitIndent = getIndentation(line)
+ result[i] = line
+ continue
+ }
+
+ // Detect leaving the rate-limit block
+ if inRateLimitBlock && len(trimmedLine) > 0 && !strings.HasPrefix(trimmedLine, "#") {
+ if hasExitedBlock(line, rateLimitIndent) {
+ inRateLimitBlock = false
+ }
+ }
+
+ // Apply renames inside the rate-limit block
+ if inRateLimitBlock {
+ if strings.HasPrefix(trimmedLine, "max:") {
+ replacedLine, didReplace := findAndReplaceInLine(line, "max", "max-runs-per-user")
+ if didReplace {
+ result[i] = replacedLine
+ modified = true
+ rateLimitFieldsCodemodLog.Printf("Renamed rate-limit.max to rate-limit.max-runs-per-user on line %d", i+1)
+ continue
+ }
+ } else if strings.HasPrefix(trimmedLine, "window:") {
+ replacedLine, didReplace := findAndReplaceInLine(line, "window", "max-runs-per-user-window")
+ if didReplace {
+ result[i] = replacedLine
+ modified = true
+ rateLimitFieldsCodemodLog.Printf("Renamed rate-limit.window to rate-limit.max-runs-per-user-window on line %d", i+1)
+ continue
+ }
+ }
+ }
+
+ result[i] = line
+ }
+ return result, modified
+ })
+ if applied {
+ rateLimitFieldsCodemodLog.Print("Applied rate-limit fields migration")
+ }
+ return newContent, applied, err
+ },
+ }
+}
diff --git a/pkg/cli/codemod_rate_limit_fields_test.go b/pkg/cli/codemod_rate_limit_fields_test.go
new file mode 100644
index 00000000000..bcc210cd24b
--- /dev/null
+++ b/pkg/cli/codemod_rate_limit_fields_test.go
@@ -0,0 +1,240 @@
+//go:build !integration
+
+package cli
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGetRateLimitFieldsCodemod(t *testing.T) {
+ codemod := getRateLimitFieldsCodemod()
+
+ assert.Equal(t, "rate-limit-fields-migration", codemod.ID)
+ assert.Equal(t, "Rename rate-limit fields for clarity", codemod.Name)
+ assert.NotEmpty(t, codemod.Description)
+ assert.Equal(t, "0.20.0", codemod.IntroducedIn)
+ require.NotNil(t, codemod.Apply)
+}
+
+func TestRateLimitFieldsCodemod_RenamesMaxAndWindow(t *testing.T) {
+ codemod := getRateLimitFieldsCodemod()
+
+ content := `---
+on: workflow_dispatch
+rate-limit:
+ max: 5
+ window: 60
+---
+
+# Test`
+
+ frontmatter := map[string]any{
+ "on": "workflow_dispatch",
+ "rate-limit": map[string]any{
+ "max": 5,
+ "window": 60,
+ },
+ }
+
+ result, applied, err := codemod.Apply(content, frontmatter)
+
+ require.NoError(t, err, "Should not return an error")
+ assert.True(t, applied, "Codemod should be applied")
+ assert.Contains(t, result, "max-runs-per-user: 5")
+ assert.Contains(t, result, "max-runs-per-user-window: 60")
+ assert.NotContains(t, result, " max: 5")
+ assert.NotContains(t, result, " window: 60")
+}
+
+func TestRateLimitFieldsCodemod_RenamesMaxOnly(t *testing.T) {
+ codemod := getRateLimitFieldsCodemod()
+
+ content := `---
+on: workflow_dispatch
+rate-limit:
+ max: 3
+---
+
+# Test`
+
+ frontmatter := map[string]any{
+ "on": "workflow_dispatch",
+ "rate-limit": map[string]any{
+ "max": 3,
+ },
+ }
+
+ result, applied, err := codemod.Apply(content, frontmatter)
+
+ require.NoError(t, err, "Should not return an error")
+ assert.True(t, applied, "Codemod should be applied")
+ assert.Contains(t, result, "max-runs-per-user: 3")
+ assert.NotContains(t, result, " max: 3")
+}
+
+func TestRateLimitFieldsCodemod_RenamesWindowOnly(t *testing.T) {
+ codemod := getRateLimitFieldsCodemod()
+
+ content := `---
+on: workflow_dispatch
+rate-limit:
+ window: 30
+---
+
+# Test`
+
+ frontmatter := map[string]any{
+ "on": "workflow_dispatch",
+ "rate-limit": map[string]any{
+ "window": 30,
+ },
+ }
+
+ result, applied, err := codemod.Apply(content, frontmatter)
+
+ require.NoError(t, err, "Should not return an error")
+ assert.True(t, applied, "Codemod should be applied")
+ assert.Contains(t, result, "max-runs-per-user-window: 30")
+ assert.NotContains(t, result, " window: 30")
+}
+
+func TestRateLimitFieldsCodemod_NoRateLimitBlock(t *testing.T) {
+ codemod := getRateLimitFieldsCodemod()
+
+ content := `---
+on: workflow_dispatch
+permissions:
+ contents: read
+---
+
+# Test`
+
+ frontmatter := map[string]any{
+ "on": "workflow_dispatch",
+ "permissions": map[string]any{
+ "contents": "read",
+ },
+ }
+
+ result, applied, err := codemod.Apply(content, frontmatter)
+
+ require.NoError(t, err, "Should not return an error")
+ assert.False(t, applied, "Codemod should not be applied")
+ assert.Equal(t, content, result, "Content should be unchanged")
+}
+
+func TestRateLimitFieldsCodemod_AlreadyRenamed(t *testing.T) {
+ codemod := getRateLimitFieldsCodemod()
+
+ content := `---
+on: workflow_dispatch
+rate-limit:
+ max-runs-per-user: 5
+ max-runs-per-user-window: 60
+---
+
+# Test`
+
+ frontmatter := map[string]any{
+ "on": "workflow_dispatch",
+ "rate-limit": map[string]any{
+ "max-runs-per-user": 5,
+ "max-runs-per-user-window": 60,
+ },
+ }
+
+ result, applied, err := codemod.Apply(content, frontmatter)
+
+ require.NoError(t, err, "Should not return an error")
+ assert.False(t, applied, "Codemod should not be applied when fields already renamed")
+ assert.Equal(t, content, result, "Content should be unchanged")
+}
+
+func TestRateLimitFieldsCodemod_PreservesOtherFields(t *testing.T) {
+ codemod := getRateLimitFieldsCodemod()
+
+ content := `---
+on: workflow_dispatch
+rate-limit:
+ max: 5
+ window: 60
+ events:
+ - workflow_dispatch
+ ignored-roles:
+ - admin
+---
+
+# Test`
+
+ frontmatter := map[string]any{
+ "on": "workflow_dispatch",
+ "rate-limit": map[string]any{
+ "max": 5,
+ "window": 60,
+ "events": []any{"workflow_dispatch"},
+ "ignored-roles": []any{"admin"},
+ },
+ }
+
+ result, applied, err := codemod.Apply(content, frontmatter)
+
+ require.NoError(t, err, "Should not return an error")
+ assert.True(t, applied, "Codemod should be applied")
+ assert.Contains(t, result, "max-runs-per-user: 5")
+ assert.Contains(t, result, "max-runs-per-user-window: 60")
+ assert.Contains(t, result, "events:")
+ assert.Contains(t, result, "ignored-roles:")
+}
+
+func TestRateLimitFieldsCodemod_PreservesFieldsOutsideRateLimitBlock(t *testing.T) {
+ codemod := getRateLimitFieldsCodemod()
+
+ // "max" outside the rate-limit block should NOT be renamed
+ content := `---
+on: workflow_dispatch
+safe-outputs:
+ create-issue:
+ max: 5
+rate-limit:
+ max: 3
+ window: 60
+---
+
+# Test`
+
+ frontmatter := map[string]any{
+ "on": "workflow_dispatch",
+ "safe-outputs": map[string]any{
+ "create-issue": map[string]any{"max": 5},
+ },
+ "rate-limit": map[string]any{
+ "max": 3,
+ "window": 60,
+ },
+ }
+
+ result, applied, err := codemod.Apply(content, frontmatter)
+
+ require.NoError(t, err, "Should not return an error")
+ assert.True(t, applied, "Codemod should be applied")
+ // max inside safe-outputs.create-issue is preserved
+ assert.Contains(t, result, " max: 5", "max outside rate-limit block should be unchanged")
+ // max inside rate-limit is renamed
+ assert.Contains(t, result, "max-runs-per-user: 3")
+ assert.Contains(t, result, "max-runs-per-user-window: 60")
+}
+
+func TestRateLimitFieldsCodemod_IsRegistered(t *testing.T) {
+ codemods := GetAllCodemods()
+ var found bool
+ for _, c := range codemods {
+ if c.ID == "rate-limit-fields-migration" {
+ found = true
+ break
+ }
+ }
+ assert.True(t, found, "rate-limit-fields-migration codemod should be registered in GetAllCodemods")
+}
diff --git a/pkg/cli/fix_codemods.go b/pkg/cli/fix_codemods.go
index 9bdb843c2a4..28ffacf2953 100644
--- a/pkg/cli/fix_codemods.go
+++ b/pkg/cli/fix_codemods.go
@@ -51,6 +51,7 @@ func GetAllCodemods() []Codemod {
getPluginsToDependenciesCodemod(), // Migrate plugins to dependencies (plugins removed in favour of APM)
getGitHubReposToAllowedReposCodemod(), // Rename deprecated tools.github.repos to tools.github.allowed-repos
getDIFCProxyToIntegrityProxyCodemod(), // Migrate deprecated features.difc-proxy to tools.github.integrity-proxy
+ getRateLimitFieldsCodemod(), // Rename rate-limit.max -> max-runs-per-user and rate-limit.window -> max-runs-per-user-window
}
fixCodemodsLog.Printf("Loaded codemod registry: %d codemods available", len(codemods))
return codemods
diff --git a/pkg/cli/fix_codemods_test.go b/pkg/cli/fix_codemods_test.go
index feddc2e0d78..e1799b20734 100644
--- a/pkg/cli/fix_codemods_test.go
+++ b/pkg/cli/fix_codemods_test.go
@@ -43,7 +43,7 @@ func TestGetAllCodemods_ReturnsAllCodemods(t *testing.T) {
codemods := GetAllCodemods()
// Verify we have the expected number of codemods
- expectedCount := 29
+ expectedCount := 30
assert.Len(t, codemods, expectedCount, "Should return all %d codemods", expectedCount)
// Verify all codemods have required fields
@@ -134,6 +134,7 @@ func TestGetAllCodemods_InExpectedOrder(t *testing.T) {
"plugins-to-dependencies",
"github-repos-to-allowed-repos",
"features-difc-proxy-to-tools-github",
+ "rate-limit-fields-migration",
}
require.Len(t, codemods, len(expectedOrder), "Should have expected number of codemods")
diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json
index d76b79ffd5e..6398e0dbd6a 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -8428,9 +8428,9 @@
"rate-limit": {
"type": "object",
"description": "Per-user workflow rate limit configuration to restrict how frequently individual users can trigger this workflow. Helps prevent abuse and resource exhaustion from programmatically triggered events. Optionally also limits the total number of in-progress GitHub agent tasks for the repository.",
- "required": ["max"],
+ "required": ["max-runs-per-user"],
"properties": {
- "max": {
+ "max-runs-per-user": {
"description": "Maximum number of workflow runs allowed per user within the time window. This is a per-user limit: each user is tracked independently. Required field. Supports integer or GitHub Actions expression (e.g. '${{ inputs.max }}').",
"oneOf": [
{
@@ -8445,7 +8445,7 @@
}
]
},
- "window": {
+ "max-runs-per-user-window": {
"type": "integer",
"minimum": 1,
"maximum": 180,
@@ -8479,22 +8479,22 @@
"additionalProperties": false,
"examples": [
{
- "max": 5,
- "window": 60
+ "max-runs-per-user": 5,
+ "max-runs-per-user-window": 60
},
{
- "max": 10,
- "window": 30,
+ "max-runs-per-user": 10,
+ "max-runs-per-user-window": 30,
"events": ["workflow_dispatch", "issue_comment"]
},
{
- "max": 5,
- "window": 60,
+ "max-runs-per-user": 5,
+ "max-runs-per-user-window": 60,
"ignored-roles": ["admin", "maintain"]
},
{
- "max": 5,
- "window": 60,
+ "max-runs-per-user": 5,
+ "max-runs-per-user-window": 60,
"max-in-progress": 3
}
]
diff --git a/pkg/workflow/frontmatter_types.go b/pkg/workflow/frontmatter_types.go
index ec1887f715d..41253ce2106 100644
--- a/pkg/workflow/frontmatter_types.go
+++ b/pkg/workflow/frontmatter_types.go
@@ -113,11 +113,11 @@ type PermissionsConfig struct {
// Controls how many times a user can trigger this workflow within a time window,
// and optionally limits the maximum number of in-progress GitHub agent tasks for the repo.
type RateLimitConfig struct {
- Max int `json:"max,omitempty"` // Maximum number of workflow runs allowed per user within the 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"])
- IgnoredRoles []string `json:"ignored-roles,omitempty"` // Roles that are exempt from rate limiting (e.g., ["admin", "maintainer"])
- MaxInProgress int `json:"max-in-progress,omitempty"` // Maximum number of in-progress GitHub agent tasks allowed for the repo before blocking new runs
+ Max int `json:"max-runs-per-user,omitempty"` // Maximum number of workflow runs allowed per user within the time window (default: 5)
+ Window int `json:"max-runs-per-user-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"])
+ IgnoredRoles []string `json:"ignored-roles,omitempty"` // Roles that are exempt from rate limiting (e.g., ["admin", "maintainer"])
+ MaxInProgress int `json:"max-in-progress,omitempty"` // Maximum number of in-progress GitHub agent tasks allowed for the repo before blocking new runs
}
// OTLPConfig holds configuration for OTLP (OpenTelemetry Protocol) trace export.
diff --git a/pkg/workflow/rate_limit_experimental_warning_test.go b/pkg/workflow/rate_limit_experimental_warning_test.go
index 11f295d5e17..b39cb7652e6 100644
--- a/pkg/workflow/rate_limit_experimental_warning_test.go
+++ b/pkg/workflow/rate_limit_experimental_warning_test.go
@@ -27,8 +27,8 @@ func TestRateLimitExperimentalWarning(t *testing.T) {
on: workflow_dispatch
engine: copilot
rate-limit:
- max: 5
- window: 60
+ max-runs-per-user: 5
+ max-runs-per-user-window: 60
permissions:
contents: read
issues: read
@@ -60,8 +60,8 @@ permissions:
on: workflow_dispatch
engine: copilot
rate-limit:
- max: 3
- window: 30
+ max-runs-per-user: 3
+ max-runs-per-user-window: 30
ignored-roles:
- admin
- maintain
@@ -84,8 +84,8 @@ on:
types: [created]
engine: copilot
rate-limit:
- max: 5
- window: 60
+ max-runs-per-user: 5
+ max-runs-per-user-window: 60
events: [workflow_dispatch, issue_comment]
permissions:
contents: read
diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go
index 52f87c2abea..a1796229b1f 100644
--- a/pkg/workflow/role_checks.go
+++ b/pkg/workflow/role_checks.go
@@ -177,8 +177,20 @@ func (c *Compiler) extractRateLimitConfig(frontmatter map[string]any) *RateLimit
case map[string]any:
config := &RateLimitConfig{}
- // Extract max (default: 5)
- if maxValue, ok := v["max"]; ok {
+ // Extract max-runs-per-user (previously named 'max', with backward compat fallback)
+ if maxValue, ok := v["max-runs-per-user"]; ok {
+ 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)
+ }
+ } else if maxValue, ok := v["max"]; ok {
+ // Backward compat: accept old 'max' field (codemod renames it)
switch max := maxValue.(type) {
case int:
config.Max = max
@@ -191,8 +203,20 @@ func (c *Compiler) extractRateLimitConfig(frontmatter map[string]any) *RateLimit
}
}
- // Extract window (default: 60 minutes)
- if windowValue, ok := v["window"]; ok {
+ // Extract max-runs-per-user-window (previously named 'window', with backward compat fallback)
+ if windowValue, ok := v["max-runs-per-user-window"]; ok {
+ 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)
+ }
+ } else if windowValue, ok := v["window"]; ok {
+ // Backward compat: accept old 'window' field (codemod renames it)
switch window := windowValue.(type) {
case int:
config.Window = window