From 71e159780763612db914545e3b801aa468bf11bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:03:53 +0000 Subject: [PATCH 01/12] Initial plan From 05f152fda9acd508ef5eb161b1310cc967cb98dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:24:00 +0000 Subject: [PATCH 02/12] Fix user rate limit schema aliases and fallback events Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/check_rate_limit.cjs | 2 +- actions/setup/js/check_rate_limit.test.cjs | 10 +- docs/public/editor/autocomplete-data.json | 14 + .../docs/reference/frontmatter-full.md | 5 +- .../src/content/docs/reference/frontmatter.md | 4 +- .../docs/reference/rate-limiting-controls.md | 2 +- pkg/parser/schema_test.go | 63 ++++ pkg/parser/schemas/main_workflow_schema.json | 38 ++- pkg/workflow/role_checks.go | 269 ++++++++---------- pkg/workflow/role_checks_test.go | 43 ++- 10 files changed, 289 insertions(+), 161 deletions(-) diff --git a/actions/setup/js/check_rate_limit.cjs b/actions/setup/js/check_rate_limit.cjs index 00003165f12..bc2844ee9a3 100644 --- a/actions/setup/js/check_rate_limit.cjs +++ b/actions/setup/js/check_rate_limit.cjs @@ -9,7 +9,7 @@ const { fetchAndLogRateLimit } = require("./github_rate_limit_logger.cjs"); * Prevents users from triggering workflows too frequently */ -const PROGRAMMATIC_EVENTS = ["workflow_dispatch", "repository_dispatch", "issue_comment", "pull_request_review", "pull_request_review_comment", "discussion_comment"]; +const PROGRAMMATIC_EVENTS = ["discussion", "discussion_comment", "issue_comment", "issues", "pull_request", "pull_request_review", "pull_request_review_comment", "repository_dispatch", "workflow_dispatch"]; async function main() { const { diff --git a/actions/setup/js/check_rate_limit.test.cjs b/actions/setup/js/check_rate_limit.test.cjs index 6f909788b94..4658004421d 100644 --- a/actions/setup/js/check_rate_limit.test.cjs +++ b/actions/setup/js/check_rate_limit.test.cjs @@ -751,14 +751,18 @@ describe("check_rate_limit", () => { expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true"); }); - it("should skip non-programmatic events like pull_request by default", async () => { + it("should apply rate limiting to pull_request events by default", async () => { mockContext.eventName = "pull_request"; + 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("Event 'pull_request' is not a programmatic trigger")); - expect(mockGithub.rest.actions.listWorkflowRuns).not.toHaveBeenCalled(); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Rate limiting applies to programmatic events")); + expect(mockGithub.rest.actions.listWorkflowRuns).toHaveBeenCalled(); }); it("should log stack trace for errors that have one", async () => { diff --git a/docs/public/editor/autocomplete-data.json b/docs/public/editor/autocomplete-data.json index 903a45cc7c9..677dd6836a2 100644 --- a/docs/public/editor/autocomplete-data.json +++ b/docs/public/editor/autocomplete-data.json @@ -2760,6 +2760,20 @@ "desc": "Maximum number of workflow runs allowed per user within the time window.", "leaf": true }, + "max-runs": { + "type": "integer|string", + "desc": "Deprecated alias for max-runs-per-window.", + "deprecated": true, + "x-deprecation-message": "Use user-rate-limit.max-runs-per-window instead.", + "leaf": true + }, + "max": { + "type": "integer|string", + "desc": "Deprecated alias for max-runs-per-window.", + "deprecated": true, + "x-deprecation-message": "Use user-rate-limit.max-runs-per-window instead.", + "leaf": true + }, "window": { "type": "integer", "desc": "Time window in minutes for rate limiting.", diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index 8ff5c9e02d4..271868e0c3c 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -21976,6 +21976,7 @@ user-rate-limit: # Maximum number of workflow runs allowed per user within the time window. # Required field. Supports integer or GitHub Actions expression (e.g. '${{ # inputs.max }}'). + # (optional) # Accepted formats: # Format 1: integer @@ -21990,8 +21991,8 @@ user-rate-limit: 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). + # limiting is inferred from the workflow triggers; if no supported programmatic + # triggers are found, it falls back to all supported programmatic events. # (optional) events: [] # Array of strings diff --git a/docs/src/content/docs/reference/frontmatter.md b/docs/src/content/docs/reference/frontmatter.md index e69d3c9b50c..a8b1c363e2a 100644 --- a/docs/src/content/docs/reference/frontmatter.md +++ b/docs/src/content/docs/reference/frontmatter.md @@ -499,13 +499,13 @@ max-daily-ai-credits: -1 ### Per-User Rate Limiting (`user-rate-limit:`) -Limits how frequently a single user can trigger the workflow. When the limit is exceeded, the pre-activation job cancels the run before the agent executes. Rate limiting applies to programmatically triggered events (such as `workflow_dispatch`, `issue_comment`, and `pull_request_review`); when `events` is omitted, the applicable events are inferred from the `on:` section. +Limits how frequently a single user can trigger the workflow. When the limit is exceeded, the pre-activation job cancels the run before the agent executes. Rate limiting applies to programmatically triggered events (such as `workflow_dispatch`, `issue_comment`, and `pull_request_review`); when `events` is omitted, the applicable events are inferred from the `on:` section, falling back to all supported programmatic events if no supported triggers are found. ```yaml wrap user-rate-limit: max-runs-per-window: 5 # Required: maximum runs per user per window (1-10) window: 60 # Optional: window in minutes (default: 60, max: 180) - events: [workflow_dispatch, issue_comment] # Optional: events to rate limit (inferred from `on:` when omitted) + events: [workflow_dispatch, issue_comment] # Optional: events to rate limit (inferred from `on:` when omitted; fallback to all supported programmatic events) ignored-roles: [admin, maintain] # Optional: exempt roles (default: [admin, maintain, write]) ``` diff --git a/docs/src/content/docs/reference/rate-limiting-controls.md b/docs/src/content/docs/reference/rate-limiting-controls.md index 28bad3eb3a9..1319fd99cfa 100644 --- a/docs/src/content/docs/reference/rate-limiting-controls.md +++ b/docs/src/content/docs/reference/rate-limiting-controls.md @@ -117,7 +117,7 @@ The `user-rate-limit` frontmatter field prevents users from triggering workflows user-rate-limit: max-runs-per-window: 5 # Required: Maximum runs per window (1-10) window: 60 # Optional: Time window in minutes (default: 60, max: 180) - events: [workflow_dispatch, issue_comment] # Optional: Specific events (auto-inferred if omitted) + events: [workflow_dispatch, issue_comment] # Optional: Specific events (inferred from `on:` when omitted; fallback to all supported programmatic events) ignored-roles: [admin, maintain] # Optional: Roles exempt from rate limiting (default: [admin, maintain, write]) ``` diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index 40748f9b058..094480f6f6e 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -112,6 +112,69 @@ func TestValidateMainWorkflowFrontmatter_Plugins(t *testing.T) { } } +func TestValidateMainWorkflowFrontmatter_UserRateLimitMaxAliases(t *testing.T) { + t.Parallel() + + for _, tt := range []struct { + name string + rateLimit map[string]any + wantErr bool + errContains string + }{ + { + name: "canonical max-runs-per-window", + rateLimit: map[string]any{ + "max-runs-per-window": 5, + }, + }, + { + name: "legacy max-runs alias", + rateLimit: map[string]any{ + "max-runs": 5, + }, + }, + { + name: "legacy max alias", + rateLimit: map[string]any{ + "max": 5, + }, + }, + { + name: "legacy max-runs expression alias", + rateLimit: map[string]any{ + "max-runs": "${{ inputs.max_runs }}", + }, + }, + { + name: "missing max field", + rateLimit: map[string]any{"window": 60}, + wantErr: true, + errContains: "missing property 'max-runs-per-window'", + }, + { + name: "unknown nested field", + rateLimit: map[string]any{"max-runs-per-window": 5, "limit": 5}, + wantErr: true, + errContains: "Unknown property: limit", + }, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(map[string]any{ + "on": "workflow_dispatch", + "user-rate-limit": tt.rateLimit, + }, "workflow.md") + if (err != nil) != tt.wantErr { + t.Fatalf("validation error = %v, wantErr %t", err, tt.wantErr) + } + if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) { + t.Fatalf("validation error = %v, want substring %q", err, tt.errContains) + } + }) + } +} + func TestValidateMainWorkflowFrontmatterEnclaves(t *testing.T) { valid := map[string]any{ "on": "workflow_dispatch", diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 55fe76043c6..7291731c31e 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -12337,7 +12337,7 @@ "user-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-runs-per-window"], + "anyOf": [{ "required": ["max-runs-per-window"] }, { "required": ["max-runs"] }, { "required": ["max"] }], "properties": { "max-runs-per-window": { "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 }}').", @@ -12354,6 +12354,40 @@ } ] }, + "max-runs": { + "description": "Deprecated alias for max-runs-per-window. Use max-runs-per-window in new workflows.", + "deprecated": true, + "x-deprecation-message": "Use user-rate-limit.max-runs-per-window instead.", + "oneOf": [ + { + "type": "integer", + "minimum": 1, + "maximum": 10 + }, + { + "type": "string", + "pattern": "^\\$\\{\\{.*\\}\\}$", + "description": "GitHub Actions expression that resolves to an integer at runtime" + } + ] + }, + "max": { + "description": "Deprecated alias for max-runs-per-window. Use max-runs-per-window in new workflows.", + "deprecated": true, + "x-deprecation-message": "Use user-rate-limit.max-runs-per-window instead.", + "oneOf": [ + { + "type": "integer", + "minimum": 1, + "maximum": 10 + }, + { + "type": "string", + "pattern": "^\\$\\{\\{.*\\}\\}$", + "description": "GitHub Actions expression that resolves to an integer at runtime" + } + ] + }, "window": { "type": "integer", "minimum": 1, @@ -12363,7 +12397,7 @@ }, "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 rate limiting to. If not specified, rate limiting is inferred from the workflow triggers; if no supported programmatic triggers are found, it falls back to all supported programmatic events.", "items": { "type": "string", "enum": ["workflow_dispatch", "issue_comment", "pull_request_review", "pull_request_review_comment", "issues", "pull_request", "discussion_comment", "discussion", "repository_dispatch"] diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go index 3571aca403d..a6a1119806d 100644 --- a/pkg/workflow/role_checks.go +++ b/pkg/workflow/role_checks.go @@ -15,6 +15,18 @@ import ( var roleLog = logger.New("workflow:role_checks") +var rateLimitProgrammaticEvents = []string{ + "discussion", + "discussion_comment", + "issue_comment", + "issues", + "pull_request", + "pull_request_review", + "pull_request_review_comment", + "repository_dispatch", + "workflow_dispatch", +} + // generateMembershipCheck generates steps for the check_membership job that only sets outputs func (c *Compiler) generateMembershipCheck(data *WorkflowData, steps []string) []string { if len(data.Command) > 0 { @@ -221,86 +233,76 @@ func parseOptionalStringSliceField(value any, fieldName string) []string { // extractRateLimitConfig extracts the user-rate-limit config from frontmatter. func (c *Compiler) extractRateLimitConfig(frontmatter map[string]any) *RateLimitConfig { rateLimitValue, exists := frontmatter["user-rate-limit"] + if !exists || rateLimitValue == nil { + roleLog.Print("No user-rate-limit configuration specified") + return nil + } - if exists && rateLimitValue != nil { - switch v := rateLimitValue.(type) { - case map[string]any: - config := &RateLimitConfig{} + rateLimitMap, ok := rateLimitValue.(map[string]any) + if !ok { + roleLog.Print("No user-rate-limit configuration specified") + return nil + } - // Extract max-runs-per-window (default: 5) - maxValue, ok := v["max-runs-per-window"] - if !ok { - maxValue, ok = v["max-runs"] - } - if !ok { - maxValue, ok = v["max"] // legacy compatibility - } - if 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) - } - } + config := &RateLimitConfig{ + Max: extractRateLimitInt(rateLimitMap, "max-runs-per-window", "max-runs", "max"), + Window: extractRateLimitInt(rateLimitMap, "window"), + Events: c.extractRateLimitEvents(rateLimitMap, frontmatter), + IgnoredRoles: extractRateLimitIgnoredRoles(rateLimitMap), + } - // Extract window (default: 60 minutes) - if windowValue, ok := v["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) - } - } + roleLog.Printf("Extracted user-rate-limit config: max=%d, window=%d, events=%v, ignored-roles=%v", config.Max, config.Window, config.Events, config.IgnoredRoles) + return config +} - // Extract events - if eventsValue, ok := v["events"]; ok { - switch events := eventsValue.(type) { - case []any: - config.Events = parseStringSliceAny(events, nil) - case []string: - config.Events = events - 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) - } +func extractRateLimitInt(config map[string]any, keys ...string) int { + for _, key := range keys { + if value, ok := config[key]; ok { + switch typedValue := value.(type) { + case int: + return typedValue + case int64: + return int(typedValue) + case uint64: + return int(typedValue) + case float64: + return int(typedValue) } + } + } + return 0 +} - // Extract ignored-roles - if ignoredRolesValue, ok := v["ignored-roles"]; ok { - switch ignoredRoles := ignoredRolesValue.(type) { - case []any: - config.IgnoredRoles = parseStringSliceAny(ignoredRoles, nil) - case []string: - config.IgnoredRoles = ignoredRoles - case string: - config.IgnoredRoles = []string{ignoredRoles} - } - } else { - // Default: admin, maintain, and write roles are exempt from rate limiting - config.IgnoredRoles = []string{"admin", "maintain", "write"} - roleLog.Print("No ignored-roles specified, using defaults: admin, maintain, write") - } +func (c *Compiler) extractRateLimitEvents(config map[string]any, frontmatter map[string]any) []string { + if eventsValue, ok := config["events"]; ok { + return extractRateLimitStringSlice(eventsValue) + } - roleLog.Printf("Extracted user-rate-limit config: max=%d, window=%d, events=%v, ignored-roles=%v", config.Max, config.Window, config.Events, config.IgnoredRoles) - return config - } + events := c.inferEventsFromTriggers(frontmatter) + if len(events) > 0 { + roleLog.Printf("Inferred events from workflow triggers: %v", events) + } + return events +} + +func extractRateLimitIgnoredRoles(config map[string]any) []string { + if ignoredRolesValue, ok := config["ignored-roles"]; ok { + return extractRateLimitStringSlice(ignoredRolesValue) + } + + roleLog.Print("No ignored-roles specified, using defaults: admin, maintain, write") + return []string{"admin", "maintain", "write"} +} + +func extractRateLimitStringSlice(value any) []string { + switch typedValue := value.(type) { + case []any: + return parseStringSliceAny(typedValue, nil) + case []string: + return typedValue + case string: + return []string{typedValue} } - roleLog.Print("No user-rate-limit configuration specified") return nil } @@ -312,16 +314,9 @@ func (c *Compiler) inferEventsFromTriggers(frontmatter map[string]any) []string } var events []string - programmaticTriggers := map[string]string{ - "discussion": "discussion", - "discussion_comment": "discussion_comment", - "issue_comment": "issue_comment", - "issues": "issues", - "pull_request": "pull_request", - "pull_request_review": "pull_request_review", - "pull_request_review_comment": "pull_request_review_comment", - "repository_dispatch": "repository_dispatch", - "workflow_dispatch": "workflow_dispatch", + programmaticTriggers := make(map[string]string, len(rateLimitProgrammaticEvents)) + for _, event := range rateLimitProgrammaticEvents { + programmaticTriggers[event] = event } switch on := onValue.(type) { @@ -347,6 +342,9 @@ func (c *Compiler) inferEventsFromTriggers(frontmatter map[string]any) []string // Sort events alphabetically for consistent output sort.Strings(events) + if len(events) == 0 { + return append([]string(nil), rateLimitProgrammaticEvents...) + } return events } @@ -385,78 +383,55 @@ func (c *Compiler) hasSafeEventsOnly(data *WorkflowData, frontmatter map[string] // Parse the "on" section to determine events if onValue, exists := frontmatter["on"]; exists { if onMap, ok := onValue.(map[string]any); ok { - // Check if only safe events are present - hasUnsafeEvents := false - hasWorkflowDispatch := false - - for eventName := range onMap { - // Skip command events as they are handled separately - // Skip stop-after and reaction as they are not event types - // Skip roles, bots, labels, and other configuration keys as they are not event types - if eventName == "command" || eventName == "stop-after" || eventName == "reaction" || eventName == "roles" || eventName == "bots" || eventName == "labels" || eventName == "allow-bot-authored-trigger-comment" { - continue - } + return hasOnlySafeOnMapEvents(onMap, data.Roles) + } + } - // Track if workflow_dispatch is present - if eventName == "workflow_dispatch" { - hasWorkflowDispatch = true - } + // If no "on" section or it's a string, check for default command trigger + // For command workflows, they are not considered "safe only" + return false +} - // Check if this event is in the safe list - isSafe := slices.Contains(constants.SafeWorkflowEvents, eventName) - if !isSafe { - hasUnsafeEvents = true - break - } - } +func hasOnlySafeOnMapEvents(onMap map[string]any, roles []string) bool { + hasUnsafeEvents := false + hasWorkflowDispatch := false - // If there are events and none are unsafe, then it's safe - eventCount := len(onMap) - // Subtract non-event entries - if _, hasSlashCommand := onMap["slash_command"]; hasSlashCommand { - eventCount-- - } - if _, hasCommand := onMap["command"]; hasCommand { - eventCount-- - } - if _, hasStopAfter := onMap["stop-after"]; hasStopAfter { - eventCount-- - } - if _, hasReaction := onMap["reaction"]; hasReaction { - eventCount-- - } - if _, hasRoles := onMap["roles"]; hasRoles { - eventCount-- - } - if _, hasBots := onMap["bots"]; hasBots { - eventCount-- - } - if _, hasLabelNames := onMap["labels"]; hasLabelNames { - eventCount-- - } - if _, hasAllowBotAuthored := onMap["allow-bot-authored-trigger-comment"]; hasAllowBotAuthored { - eventCount-- - } + for eventName := range onMap { + if isRoleCheckConfigKey(eventName) { + continue + } + if eventName == "workflow_dispatch" { + hasWorkflowDispatch = true + } + if !slices.Contains(constants.SafeWorkflowEvents, eventName) { + hasUnsafeEvents = true + break + } + } - // Special handling for workflow_dispatch: - // workflow_dispatch can be triggered by users with "write" access, - // so it's only considered "safe" if "write" is in the allowed roles - if hasWorkflowDispatch && !hasUnsafeEvents { - // Check if "write" is in the allowed roles - hasWriteRole := slices.Contains(data.Roles, "write") - // If write is not in the allowed roles, workflow_dispatch needs permission checks - if !hasWriteRole { - return false - } - } + if hasWorkflowDispatch && !hasUnsafeEvents && !slices.Contains(roles, "write") { + return false + } + return countWorkflowEvents(onMap) > 0 && !hasUnsafeEvents +} - return eventCount > 0 && !hasUnsafeEvents - } +func isRoleCheckConfigKey(key string) bool { + switch key { + case "allow-bot-authored-trigger-comment", "bots", "command", "labels", "reaction", "roles", "stop-after": + return true + default: + return false } +} - // If no "on" section or it's a string, check for default command trigger - // For command workflows, they are not considered "safe only" - return false +func countWorkflowEvents(onMap map[string]any) int { + eventCount := 0 + for eventName := range onMap { + if !isRoleCheckConfigKey(eventName) && eventName != "slash_command" { + eventCount++ + } + } + return eventCount } // hasWorkflowRunTrigger checks if the agentic workflow's frontmatter declares a workflow_run trigger diff --git a/pkg/workflow/role_checks_test.go b/pkg/workflow/role_checks_test.go index 13cf2db2950..f6d5fe914fe 100644 --- a/pkg/workflow/role_checks_test.go +++ b/pkg/workflow/role_checks_test.go @@ -221,11 +221,24 @@ func TestInferEventsFromTriggers(t *testing.T) { expected: []string{"issues"}, }, { - name: "no triggers", + name: "no recognized triggers falls back to all programmatic triggers", frontmatter: map[string]any{ - "on": map[string]any{}, + "on": map[string]any{ + "push": map[string]any{}, + "schedule": "daily", + }, + }, + expected: []string{ + "discussion", + "discussion_comment", + "issue_comment", + "issues", + "pull_request", + "pull_request_review", + "pull_request_review_comment", + "repository_dispatch", + "workflow_dispatch", }, - expected: nil, }, { name: "missing on section", @@ -291,6 +304,30 @@ func TestExtractRateLimitConfig(t *testing.T) { } }) + t.Run("extracts legacy max-runs alias", func(t *testing.T) { + cfg := c.extractRateLimitConfig(map[string]any{ + "user-rate-limit": map[string]any{ + "max-runs": 4, + }, + }) + + if assert.NotNil(t, cfg) { + assert.Equal(t, 4, cfg.Max) + } + }) + + t.Run("extracts legacy max alias", func(t *testing.T) { + cfg := c.extractRateLimitConfig(map[string]any{ + "user-rate-limit": map[string]any{ + "max": 2, + }, + }) + + if assert.NotNil(t, cfg) { + assert.Equal(t, 2, cfg.Max) + } + }) + t.Run("legacy rate-limit key is ignored", func(t *testing.T) { cfg := c.extractRateLimitConfig(map[string]any{ "rate-limit": map[string]any{ From f30e5fe6daee9dc5fd49ee3bce900cf90d555da7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:30:26 +0000 Subject: [PATCH 03/12] Clarify user rate limit required field docs Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- docs/src/content/docs/reference/frontmatter-full.md | 5 ++--- docs/src/content/docs/reference/frontmatter.md | 2 +- pkg/parser/schemas/main_workflow_schema.json | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index 271868e0c3c..7ebc00705e6 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -21974,9 +21974,8 @@ observability: # (optional) user-rate-limit: # Maximum number of workflow runs allowed per user within the time window. - # Required field. Supports integer or GitHub Actions expression (e.g. '${{ - # inputs.max }}'). - # (optional) + # Required field unless a deprecated alias is used. Supports integer or GitHub + # Actions expression (e.g. '${{ inputs.max }}'). # Accepted formats: # Format 1: integer diff --git a/docs/src/content/docs/reference/frontmatter.md b/docs/src/content/docs/reference/frontmatter.md index a8b1c363e2a..c915453588f 100644 --- a/docs/src/content/docs/reference/frontmatter.md +++ b/docs/src/content/docs/reference/frontmatter.md @@ -503,7 +503,7 @@ Limits how frequently a single user can trigger the workflow. When the limit is ```yaml wrap user-rate-limit: - max-runs-per-window: 5 # Required: maximum runs per user per window (1-10) + max-runs-per-window: 5 # Required unless using a deprecated alias: maximum runs per user per window (1-10) window: 60 # Optional: window in minutes (default: 60, max: 180) events: [workflow_dispatch, issue_comment] # Optional: events to rate limit (inferred from `on:` when omitted; fallback to all supported programmatic events) ignored-roles: [admin, maintain] # Optional: exempt roles (default: [admin, maintain, write]) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 7291731c31e..8273fd06e6d 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -12340,7 +12340,7 @@ "anyOf": [{ "required": ["max-runs-per-window"] }, { "required": ["max-runs"] }, { "required": ["max"] }], "properties": { "max-runs-per-window": { - "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. Required field unless a deprecated alias is used. Supports integer or GitHub Actions expression (e.g. '${{ inputs.max }}').", "oneOf": [ { "type": "integer", From 6b569fb357e3cef9c52e97176e63eb4d068d5418 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:34:09 +0000 Subject: [PATCH 04/12] Handle missing trigger rate limit fallback Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schema_test.go | 2 +- pkg/workflow/role_checks.go | 2 +- pkg/workflow/role_checks_test.go | 12 +++++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index 094480f6f6e..bfaf3062b0c 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -149,7 +149,7 @@ func TestValidateMainWorkflowFrontmatter_UserRateLimitMaxAliases(t *testing.T) { name: "missing max field", rateLimit: map[string]any{"window": 60}, wantErr: true, - errContains: "missing property 'max-runs-per-window'", + errContains: "'anyOf' failed", }, { name: "unknown nested field", diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go index a6a1119806d..781a3667011 100644 --- a/pkg/workflow/role_checks.go +++ b/pkg/workflow/role_checks.go @@ -310,7 +310,7 @@ func extractRateLimitStringSlice(value any) []string { func (c *Compiler) inferEventsFromTriggers(frontmatter map[string]any) []string { onValue, exists := frontmatter["on"] if !exists || onValue == nil { - return nil + return append([]string(nil), rateLimitProgrammaticEvents...) } var events []string diff --git a/pkg/workflow/role_checks_test.go b/pkg/workflow/role_checks_test.go index f6d5fe914fe..66c9ab9f3d8 100644 --- a/pkg/workflow/role_checks_test.go +++ b/pkg/workflow/role_checks_test.go @@ -243,7 +243,17 @@ func TestInferEventsFromTriggers(t *testing.T) { { name: "missing on section", frontmatter: map[string]any{}, - expected: nil, + expected: []string{ + "discussion", + "discussion_comment", + "issue_comment", + "issues", + "pull_request", + "pull_request_review", + "pull_request_review_comment", + "repository_dispatch", + "workflow_dispatch", + }, }, { name: "all programmatic triggers", From 6b13df693cae10e4af544f34eafc1a5e79293bef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:37:22 +0000 Subject: [PATCH 05/12] Clarify invalid user rate limit log Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/role_checks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go index 781a3667011..c7f36a1b18c 100644 --- a/pkg/workflow/role_checks.go +++ b/pkg/workflow/role_checks.go @@ -240,7 +240,7 @@ func (c *Compiler) extractRateLimitConfig(frontmatter map[string]any) *RateLimit rateLimitMap, ok := rateLimitValue.(map[string]any) if !ok { - roleLog.Print("No user-rate-limit configuration specified") + roleLog.Printf("user-rate-limit value is not an object, ignoring configuration: %T", rateLimitValue) return nil } From 622c6480fc86c2c54a25eb680f83fb3debffc71d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:40:35 +0000 Subject: [PATCH 06/12] Document rate limit fallback branch Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/role_checks.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go index c7f36a1b18c..1b9639c4eab 100644 --- a/pkg/workflow/role_checks.go +++ b/pkg/workflow/role_checks.go @@ -343,6 +343,7 @@ func (c *Compiler) inferEventsFromTriggers(frontmatter map[string]any) []string // Sort events alphabetically for consistent output sort.Strings(events) if len(events) == 0 { + // If "on" exists but has no supported rate-limit triggers, keep the omitted-events fallback broad. return append([]string(nil), rateLimitProgrammaticEvents...) } return events From 379a8e463f6f5085044aab918e08ff728b282b10 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:45:36 +0000 Subject: [PATCH 07/12] Tighten user rate limit alias schema Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schema_test.go | 8 +++++++- pkg/parser/schemas/main_workflow_schema.json | 2 +- pkg/workflow/role_checks.go | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index bfaf3062b0c..2d4bfc9438e 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -149,7 +149,13 @@ func TestValidateMainWorkflowFrontmatter_UserRateLimitMaxAliases(t *testing.T) { name: "missing max field", rateLimit: map[string]any{"window": 60}, wantErr: true, - errContains: "'anyOf' failed", + errContains: "missing property 'max-runs-per-window'", + }, + { + name: "multiple max aliases", + rateLimit: map[string]any{"max-runs-per-window": 5, "max-runs": 4}, + wantErr: true, + errContains: "'oneOf' failed", }, { name: "unknown nested field", diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 8273fd06e6d..a27505099e6 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -12337,7 +12337,7 @@ "user-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.", - "anyOf": [{ "required": ["max-runs-per-window"] }, { "required": ["max-runs"] }, { "required": ["max"] }], + "oneOf": [{ "required": ["max-runs-per-window"] }, { "required": ["max-runs"] }, { "required": ["max"] }], "properties": { "max-runs-per-window": { "description": "Maximum number of workflow runs allowed per user within the time window. Required field unless a deprecated alias is used. Supports integer or GitHub Actions expression (e.g. '${{ inputs.max }}').", diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go index 1b9639c4eab..cc56f946968 100644 --- a/pkg/workflow/role_checks.go +++ b/pkg/workflow/role_checks.go @@ -428,6 +428,7 @@ func isRoleCheckConfigKey(key string) bool { func countWorkflowEvents(onMap map[string]any) int { eventCount := 0 for eventName := range onMap { + // slash_command is not a standalone GitHub event, but the safety scan still treats it as unsafe. if !isRoleCheckConfigKey(eventName) && eventName != "slash_command" { eventCount++ } From cb26312c4bb912271beb0add9fa58c8d6d6bcb63 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:47:49 +0000 Subject: [PATCH 08/12] Cover ambiguous user rate limit aliases Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schema_test.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index 2d4bfc9438e..30e2db7e0be 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -146,10 +146,9 @@ func TestValidateMainWorkflowFrontmatter_UserRateLimitMaxAliases(t *testing.T) { }, }, { - name: "missing max field", - rateLimit: map[string]any{"window": 60}, - wantErr: true, - errContains: "missing property 'max-runs-per-window'", + name: "missing max field", + rateLimit: map[string]any{"window": 60}, + wantErr: true, }, { name: "multiple max aliases", @@ -157,6 +156,18 @@ func TestValidateMainWorkflowFrontmatter_UserRateLimitMaxAliases(t *testing.T) { wantErr: true, errContains: "'oneOf' failed", }, + { + name: "all max aliases", + rateLimit: map[string]any{"max-runs-per-window": 5, "max-runs": 4, "max": 3}, + wantErr: true, + errContains: "'oneOf' failed", + }, + { + name: "legacy max-runs and max aliases", + rateLimit: map[string]any{"max-runs": 4, "max": 3}, + wantErr: true, + errContains: "'oneOf' failed", + }, { name: "unknown nested field", rateLimit: map[string]any{"max-runs-per-window": 5, "limit": 5}, From e72cd05ef6a37c771a1e6e80cebdfa90213a58b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:51:38 +0000 Subject: [PATCH 09/12] Note rate limit event list coupling Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/check_rate_limit.cjs | 1 + pkg/workflow/role_checks.go | 1 + 2 files changed, 2 insertions(+) diff --git a/actions/setup/js/check_rate_limit.cjs b/actions/setup/js/check_rate_limit.cjs index bc2844ee9a3..44d1d62e640 100644 --- a/actions/setup/js/check_rate_limit.cjs +++ b/actions/setup/js/check_rate_limit.cjs @@ -9,6 +9,7 @@ const { fetchAndLogRateLimit } = require("./github_rate_limit_logger.cjs"); * Prevents users from triggering workflows too frequently */ +// Keep in sync with pkg/workflow/role_checks.go and the user-rate-limit.events schema enum. const PROGRAMMATIC_EVENTS = ["discussion", "discussion_comment", "issue_comment", "issues", "pull_request", "pull_request_review", "pull_request_review_comment", "repository_dispatch", "workflow_dispatch"]; async function main() { diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go index cc56f946968..0abfa967966 100644 --- a/pkg/workflow/role_checks.go +++ b/pkg/workflow/role_checks.go @@ -15,6 +15,7 @@ import ( var roleLog = logger.New("workflow:role_checks") +// Keep in sync with actions/setup/js/check_rate_limit.cjs and the user-rate-limit.events schema enum. var rateLimitProgrammaticEvents = []string{ "discussion", "discussion_comment", From 2eb1cce9ad55e69dfe5a4725820ce7fd8b56f64c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:01:58 +0000 Subject: [PATCH 10/12] Remove user rate limit legacy aliases Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- docs/public/editor/autocomplete-data.json | 14 ------ .../docs/reference/frontmatter-full.md | 5 ++- .../src/content/docs/reference/frontmatter.md | 4 +- pkg/parser/schema_test.go | 44 ++++++------------- pkg/parser/schemas/main_workflow_schema.json | 38 +--------------- pkg/workflow/role_checks.go | 2 +- pkg/workflow/role_checks_test.go | 8 ++-- 7 files changed, 25 insertions(+), 90 deletions(-) diff --git a/docs/public/editor/autocomplete-data.json b/docs/public/editor/autocomplete-data.json index 677dd6836a2..903a45cc7c9 100644 --- a/docs/public/editor/autocomplete-data.json +++ b/docs/public/editor/autocomplete-data.json @@ -2760,20 +2760,6 @@ "desc": "Maximum number of workflow runs allowed per user within the time window.", "leaf": true }, - "max-runs": { - "type": "integer|string", - "desc": "Deprecated alias for max-runs-per-window.", - "deprecated": true, - "x-deprecation-message": "Use user-rate-limit.max-runs-per-window instead.", - "leaf": true - }, - "max": { - "type": "integer|string", - "desc": "Deprecated alias for max-runs-per-window.", - "deprecated": true, - "x-deprecation-message": "Use user-rate-limit.max-runs-per-window instead.", - "leaf": true - }, "window": { "type": "integer", "desc": "Time window in minutes for rate limiting.", diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index 7ebc00705e6..271868e0c3c 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -21974,8 +21974,9 @@ observability: # (optional) user-rate-limit: # Maximum number of workflow runs allowed per user within the time window. - # Required field unless a deprecated alias is used. Supports integer or GitHub - # Actions expression (e.g. '${{ inputs.max }}'). + # Required field. Supports integer or GitHub Actions expression (e.g. '${{ + # inputs.max }}'). + # (optional) # Accepted formats: # Format 1: integer diff --git a/docs/src/content/docs/reference/frontmatter.md b/docs/src/content/docs/reference/frontmatter.md index c915453588f..112ab4f3a93 100644 --- a/docs/src/content/docs/reference/frontmatter.md +++ b/docs/src/content/docs/reference/frontmatter.md @@ -503,7 +503,7 @@ Limits how frequently a single user can trigger the workflow. When the limit is ```yaml wrap user-rate-limit: - max-runs-per-window: 5 # Required unless using a deprecated alias: maximum runs per user per window (1-10) + max-runs-per-window: 5 # Required: maximum runs per user per window (1-10) window: 60 # Optional: window in minutes (default: 60, max: 180) events: [workflow_dispatch, issue_comment] # Optional: events to rate limit (inferred from `on:` when omitted; fallback to all supported programmatic events) ignored-roles: [admin, maintain] # Optional: exempt roles (default: [admin, maintain, write]) @@ -513,7 +513,7 @@ user-rate-limit: Users with any of the `ignored-roles` are not rate limited. The default exemptions are `admin`, `maintain`, and `write`; set `ignored-roles: []` to rate limit every user, including administrators. -Legacy frontmatter that used a top-level `rate-limit:` section, or `max:`/`max-runs:` instead of `max-runs-per-window:`, is migrated automatically by `gh aw fix`. +Legacy frontmatter that used a top-level `rate-limit:` section, or `max:`/`max-runs:` instead of `max-runs-per-window:`, must be migrated with `gh aw fix`. See [Rate Limiting and Controls](/gh-aw/reference/rate-limiting-controls/) for more details. diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index 30e2db7e0be..9f5043344b4 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -112,7 +112,7 @@ func TestValidateMainWorkflowFrontmatter_Plugins(t *testing.T) { } } -func TestValidateMainWorkflowFrontmatter_UserRateLimitMaxAliases(t *testing.T) { +func TestValidateMainWorkflowFrontmatter_UserRateLimitMaxField(t *testing.T) { t.Parallel() for _, tt := range []struct { @@ -128,46 +128,28 @@ func TestValidateMainWorkflowFrontmatter_UserRateLimitMaxAliases(t *testing.T) { }, }, { - name: "legacy max-runs alias", - rateLimit: map[string]any{ - "max-runs": 5, - }, + name: "legacy max-runs alias", + rateLimit: map[string]any{"max-runs": 5}, + wantErr: true, + errContains: "Unknown property: max-runs", }, { - name: "legacy max alias", - rateLimit: map[string]any{ - "max": 5, - }, + name: "legacy max alias", + rateLimit: map[string]any{"max": 5}, + wantErr: true, + errContains: "Unknown property: max", }, { - name: "legacy max-runs expression alias", - rateLimit: map[string]any{ - "max-runs": "${{ inputs.max_runs }}", - }, + name: "legacy max-runs expression alias", + rateLimit: map[string]any{"max-runs": "${{ inputs.max_runs }}"}, + wantErr: true, + errContains: "Unknown property: max-runs", }, { name: "missing max field", rateLimit: map[string]any{"window": 60}, wantErr: true, }, - { - name: "multiple max aliases", - rateLimit: map[string]any{"max-runs-per-window": 5, "max-runs": 4}, - wantErr: true, - errContains: "'oneOf' failed", - }, - { - name: "all max aliases", - rateLimit: map[string]any{"max-runs-per-window": 5, "max-runs": 4, "max": 3}, - wantErr: true, - errContains: "'oneOf' failed", - }, - { - name: "legacy max-runs and max aliases", - rateLimit: map[string]any{"max-runs": 4, "max": 3}, - wantErr: true, - errContains: "'oneOf' failed", - }, { name: "unknown nested field", rateLimit: map[string]any{"max-runs-per-window": 5, "limit": 5}, diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index a27505099e6..692b41f9e72 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -12337,44 +12337,10 @@ "user-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.", - "oneOf": [{ "required": ["max-runs-per-window"] }, { "required": ["max-runs"] }, { "required": ["max"] }], + "required": ["max-runs-per-window"], "properties": { "max-runs-per-window": { - "description": "Maximum number of workflow runs allowed per user within the time window. Required field unless a deprecated alias is used. Supports integer or GitHub Actions expression (e.g. '${{ inputs.max }}').", - "oneOf": [ - { - "type": "integer", - "minimum": 1, - "maximum": 10 - }, - { - "type": "string", - "pattern": "^\\$\\{\\{.*\\}\\}$", - "description": "GitHub Actions expression that resolves to an integer at runtime" - } - ] - }, - "max-runs": { - "description": "Deprecated alias for max-runs-per-window. Use max-runs-per-window in new workflows.", - "deprecated": true, - "x-deprecation-message": "Use user-rate-limit.max-runs-per-window instead.", - "oneOf": [ - { - "type": "integer", - "minimum": 1, - "maximum": 10 - }, - { - "type": "string", - "pattern": "^\\$\\{\\{.*\\}\\}$", - "description": "GitHub Actions expression that resolves to an integer at runtime" - } - ] - }, - "max": { - "description": "Deprecated alias for max-runs-per-window. Use max-runs-per-window in new workflows.", - "deprecated": true, - "x-deprecation-message": "Use user-rate-limit.max-runs-per-window instead.", + "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 }}').", "oneOf": [ { "type": "integer", diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go index 0abfa967966..cd804cf4496 100644 --- a/pkg/workflow/role_checks.go +++ b/pkg/workflow/role_checks.go @@ -246,7 +246,7 @@ func (c *Compiler) extractRateLimitConfig(frontmatter map[string]any) *RateLimit } config := &RateLimitConfig{ - Max: extractRateLimitInt(rateLimitMap, "max-runs-per-window", "max-runs", "max"), + Max: extractRateLimitInt(rateLimitMap, "max-runs-per-window"), Window: extractRateLimitInt(rateLimitMap, "window"), Events: c.extractRateLimitEvents(rateLimitMap, frontmatter), IgnoredRoles: extractRateLimitIgnoredRoles(rateLimitMap), diff --git a/pkg/workflow/role_checks_test.go b/pkg/workflow/role_checks_test.go index 66c9ab9f3d8..9ca07ecb173 100644 --- a/pkg/workflow/role_checks_test.go +++ b/pkg/workflow/role_checks_test.go @@ -314,7 +314,7 @@ func TestExtractRateLimitConfig(t *testing.T) { } }) - t.Run("extracts legacy max-runs alias", func(t *testing.T) { + t.Run("ignores legacy max-runs alias", func(t *testing.T) { cfg := c.extractRateLimitConfig(map[string]any{ "user-rate-limit": map[string]any{ "max-runs": 4, @@ -322,11 +322,11 @@ func TestExtractRateLimitConfig(t *testing.T) { }) if assert.NotNil(t, cfg) { - assert.Equal(t, 4, cfg.Max) + assert.Zero(t, cfg.Max) } }) - t.Run("extracts legacy max alias", func(t *testing.T) { + t.Run("ignores legacy max alias", func(t *testing.T) { cfg := c.extractRateLimitConfig(map[string]any{ "user-rate-limit": map[string]any{ "max": 2, @@ -334,7 +334,7 @@ func TestExtractRateLimitConfig(t *testing.T) { }) if assert.NotNil(t, cfg) { - assert.Equal(t, 2, cfg.Max) + assert.Zero(t, cfg.Max) } }) From 9c379f3bbddd4889c132c72c621eda2c05957f1c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:03:38 +0000 Subject: [PATCH 11/12] Fix user rate limit required docs Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- docs/src/content/docs/reference/frontmatter-full.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index 271868e0c3c..c7e152f054f 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -21976,7 +21976,6 @@ user-rate-limit: # Maximum number of workflow runs allowed per user within the time window. # Required field. Supports integer or GitHub Actions expression (e.g. '${{ # inputs.max }}'). - # (optional) # Accepted formats: # Format 1: integer From 20770ce73f19c31b965298e50196a9b243741522 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:16:41 +0000 Subject: [PATCH 12/12] Test rate limit event list parity Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/role_checks_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pkg/workflow/role_checks_test.go b/pkg/workflow/role_checks_test.go index 9ca07ecb173..f044f1fc286 100644 --- a/pkg/workflow/role_checks_test.go +++ b/pkg/workflow/role_checks_test.go @@ -3,8 +3,10 @@ package workflow import ( + "encoding/json" "os" "path/filepath" + "regexp" "strings" "testing" @@ -12,6 +14,24 @@ import ( "github.com/stretchr/testify/assert" ) +func TestRateLimitProgrammaticEventsMatchJavaScript(t *testing.T) { + source, err := os.ReadFile(filepath.Join("..", "..", "actions", "setup", "js", "check_rate_limit.cjs")) + if err != nil { + t.Fatal(err) + } + + match := regexp.MustCompile(`const PROGRAMMATIC_EVENTS = (\[[^;]+\]);`).FindSubmatch(source) + if len(match) != 2 { + t.Fatal("PROGRAMMATIC_EVENTS not found in check_rate_limit.cjs") + } + + var javascriptEvents []string + if err := json.Unmarshal(match[1], &javascriptEvents); err != nil { + t.Fatal(err) + } + assert.Equal(t, rateLimitProgrammaticEvents, javascriptEvents) +} + // TestRoleMembershipUsesGitHubToken tests that the role membership check // explicitly uses the GitHub Actions token (GITHUB_TOKEN) and not any other secret func TestRoleMembershipUsesGitHubToken(t *testing.T) {