Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions actions/setup/js/safe_output_validator.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand All @@ -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 => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] matchesSimpleGlob is case-insensitive by default (see glob_pattern_helpers.cjs: caseSensitive = false), but the previous allowedLabels.includes(label) was case-sensitive. This is a silent behavior change: an allowed list of ["Bug"] will now accept the label "bug", which was previously rejected.

This may be intentional to align with blocked (which is also case-insensitive), but there is no test or documentation covering this. Consider adding a test:

it("should match allowed labels case-insensitively", () => {
  const result = validator.validateLabels(
    ["Bug", "Enhancement"],
    ["bug", "enhancement"],
    10
  );
  expect(result.valid).toBe(true);
  expect(result.value).toHaveLength(2);
});

And documenting that allowed patterns are matched case-insensitively.

const labelStr = String(label).trim();
return allowedLabels.some(pattern => matchesSimpleGlob(labelStr, pattern));
});
}

// Sanitize and deduplicate labels
Expand Down
15 changes: 15 additions & 0 deletions actions/setup/js/safe_output_validator.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The new test covers the happy path well, but there is no test for the blocked + allowed glob interaction — specifically a label that matches an allowed glob pattern but also matches a blocked pattern. The docs correctly state blocked takes precedence, but it is unverified for the glob-allowed scenario. Add:

it("should reject labels matching blocked pattern even if they match allowed glob", () => {
  const result = validator.validateLabels(
    ["team-backend", "team-[bot]"],
    ["team-*"],      // allowed glob
    10,
    ["*[bot]"]       // blocked pattern
  );
  expect(result.valid).toBe(true);
  expect(result.value).toContain("team-backend");
  expect(result.value).not.toContain("team-[bot]");
});

});

it("should limit labels to max count", () => {
const result = validator.validateLabels(["a", "b", "c", "d", "e"], undefined, 3);

Expand Down
20 changes: 12 additions & 8 deletions docs/src/content/docs/reference/safe-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:

Expand All @@ -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
Expand All @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/add_labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/remove_labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}

Expand Down
Loading