diff --git a/actions/setup/js/safe_output_validator.cjs b/actions/setup/js/safe_output_validator.cjs index ff0053ca62d..736041db6ba 100644 --- a/actions/setup/js/safe_output_validator.cjs +++ b/actions/setup/js/safe_output_validator.cjs @@ -4,6 +4,7 @@ const fs = require("fs"); const { sanitizeLabelContent } = require("./sanitize_label_content.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { matchesSimpleGlob } = require("./glob_pattern_helpers.cjs"); /** * Load and parse the safe outputs configuration from config.json @@ -82,7 +83,7 @@ function validateBody(body, fieldName = "body", required = false) { /** * Validate and sanitize an array of labels * @param {any} labels - The labels to validate - * @param {string[]|undefined} allowedLabels - Optional list of allowed labels + * @param {string[]|undefined} allowedLabels - Optional list of allowed label patterns (supports glob patterns like "team-*", "area/*") * @param {number} maxCount - Maximum number of labels allowed * @param {string[]|undefined} blockedPatterns - Optional list of blocked label patterns (supports glob patterns like "~*", "*[bot]") * @returns {{valid: boolean, value?: string[], error?: string}} Validation result @@ -102,7 +103,6 @@ function validateLabels(labels, allowedLabels = undefined, maxCount = 3, blocked // Filter out blocked labels first (security boundary) let validLabels = labels; if (blockedPatterns && blockedPatterns.length > 0) { - const { matchesSimpleGlob } = require("./glob_pattern_helpers.cjs"); const blockedLabels = []; validLabels = labels.filter(label => { const labelStr = String(label).trim(); @@ -119,7 +119,10 @@ function validateLabels(labels, allowedLabels = undefined, maxCount = 3, blocked // Filter labels based on allowed list if provided if (allowedLabels && allowedLabels.length > 0) { - validLabels = validLabels.filter(label => allowedLabels.includes(label)); + validLabels = validLabels.filter(label => { + const labelStr = String(label).trim(); + return allowedLabels.some(pattern => matchesSimpleGlob(labelStr, pattern)); + }); } // Sanitize and deduplicate labels diff --git a/actions/setup/js/safe_output_validator.test.cjs b/actions/setup/js/safe_output_validator.test.cjs index 772cdc0b395..1f50bf9fc89 100644 --- a/actions/setup/js/safe_output_validator.test.cjs +++ b/actions/setup/js/safe_output_validator.test.cjs @@ -192,6 +192,21 @@ describe("safe_output_validator.cjs", () => { expect(result.value).not.toContain("custom"); }); + it("should filter labels based on allowed glob patterns", () => { + const result = validator.validateLabels( + ["team-backend", "priority-high", "area/ui", "bug"], + ["team-*", "priority-*", "area/*"], + 10 + ); + + expect(result.valid).toBe(true); + expect(result.value).toHaveLength(3); + expect(result.value).toContain("team-backend"); + expect(result.value).toContain("priority-high"); + expect(result.value).toContain("area/ui"); + expect(result.value).not.toContain("bug"); + }); + it("should limit labels to max count", () => { const result = validator.validateLabels(["a", "b", "c", "d", "e"], undefined, 3); diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index 068fad92b05..9346881519d 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -301,12 +301,12 @@ safe-outputs: ### Add Labels (`add-labels:`) -Adds labels to issues or PRs. Specify `allowed` to restrict to specific labels, or `blocked` to deny specific label patterns regardless of the allow list. +Adds labels to issues or PRs. Specify `allowed` to restrict to specific labels or glob patterns, or `blocked` to deny specific label patterns regardless of the allow list. ```yaml wrap safe-outputs: add-labels: - allowed: [bug, enhancement] # restrict to specific labels + allowed: [bug, team-*, area/*] # restrict to specific labels or glob patterns blocked: ["~*", "*[bot]"] # deny labels matching these glob patterns max: 3 # max labels (default: 3) target: "*" # "triggering" (default), "*", or number @@ -316,7 +316,11 @@ safe-outputs: #### Blocked Label Patterns -The `blocked` field accepts glob patterns that are evaluated before the `allowed` list. Any label matching a blocked pattern is rejected, even if it also appears in the allowed list. This provides infrastructure-level protection against prompt injection attacks in repositories with many labels where maintaining an exhaustive allowlist is impractical. +Both `allowed` and `blocked` accept glob patterns and are evaluated in this order: +1. `blocked` patterns first (security boundary) +2. `allowed` patterns second (if provided) + +Any label matching a blocked pattern is rejected, even if it also matches an allowed pattern. This provides infrastructure-level protection against prompt injection attacks in repositories with many labels where maintaining an exhaustive allowlist is impractical. Common patterns: @@ -329,19 +333,19 @@ Common patterns: ```yaml wrap safe-outputs: add-labels: - blocked: ["~*", "*[bot]"] # Blocked patterns evaluated first - allowed: [bug, enhancement] # Allowed list applied after blocked check + blocked: ["~*", "*[bot]"] # Blocked patterns evaluated first + allowed: [bug, team-*, area/*] # Allowed patterns applied after blocked check max: 5 ``` ### Remove Labels (`remove-labels:`) -Removes labels from issues or PRs. Specify `allowed` to restrict which labels can be removed, or `blocked` to prevent removal of specific label patterns. If a label is not present on the item, it will be silently skipped. +Removes labels from issues or PRs. Specify `allowed` to restrict which labels can be removed (specific labels or glob patterns), or `blocked` to prevent removal of specific label patterns. If a label is not present on the item, it will be silently skipped. ```yaml wrap safe-outputs: remove-labels: - allowed: [automated, stale] # restrict to specific labels (optional) + allowed: [automated, team-*] # restrict to specific labels or glob patterns (optional) blocked: ["~*"] # deny removal of labels matching these glob patterns max: 3 # max operations (default: 3) target: "*" # "triggering" (default), "*", or number @@ -351,7 +355,7 @@ safe-outputs: **Target**: `"triggering"` (requires issue/PR event), `"*"` (any issue/PR), or number (specific issue/PR). -When `allowed` is omitted or set to `null`, any labels can be removed. Use `allowed` to restrict removal to specific labels only, providing control over which labels agents can manipulate. The `blocked` field takes precedence over `allowed`. +When `allowed` is omitted or set to `null`, any labels can be removed. Use `allowed` to restrict removal to specific labels or glob patterns, providing control over which labels agents can manipulate. The `blocked` field takes precedence over `allowed`. **Example use case**: Label lifecycle management where agents add temporary labels during triage and remove them once processed. diff --git a/pkg/workflow/add_labels.go b/pkg/workflow/add_labels.go index 74b95d4fce1..f9c3a5cdecf 100644 --- a/pkg/workflow/add_labels.go +++ b/pkg/workflow/add_labels.go @@ -10,7 +10,7 @@ var addLabelsLog = logger.New("workflow:add_labels") type AddLabelsConfig struct { BaseSafeOutputConfig `yaml:",inline"` SafeOutputTargetConfig `yaml:",inline"` - Allowed []string `yaml:"allowed,omitempty"` // Optional list of allowed labels. Labels will be created if they don't already exist in the repository. If omitted, any labels are allowed (including creating new ones). + Allowed []string `yaml:"allowed,omitempty"` // Optional list of allowed label patterns (supports glob patterns like "team-*", "area/*"). Labels will be created if they don't already exist in the repository. If omitted, any labels are allowed (including creating new ones). Blocked []string `yaml:"blocked,omitempty"` // Optional list of blocked label patterns (supports glob patterns like "~*", "*[bot]"). Labels matching these patterns will be rejected. } diff --git a/pkg/workflow/remove_labels.go b/pkg/workflow/remove_labels.go index 1369d893ae7..70daf294306 100644 --- a/pkg/workflow/remove_labels.go +++ b/pkg/workflow/remove_labels.go @@ -10,7 +10,7 @@ var removeLabelsLog = logger.New("workflow:remove_labels") type RemoveLabelsConfig struct { BaseSafeOutputConfig `yaml:",inline"` SafeOutputTargetConfig `yaml:",inline"` - Allowed []string `yaml:"allowed,omitempty"` // Optional list of allowed labels to remove. If omitted, any labels can be removed. + Allowed []string `yaml:"allowed,omitempty"` // Optional list of allowed label patterns to remove (supports glob patterns like "team-*", "area/*"). If omitted, any labels can be removed. Blocked []string `yaml:"blocked,omitempty"` // Optional list of blocked label patterns (supports glob patterns like "~*", "*[bot]"). Labels matching these patterns will be rejected. }