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
12 changes: 8 additions & 4 deletions docs/src/content/docs/reference/safe-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,20 +118,24 @@ The compiled workflow will have additional prompting describing that, to create

### Add Issue Label (`add-labels:`)

Adding `add-labels:` to the `safe-outputs:` section of your workflow declares that the workflow should conclude with adding labels to the current issue or pull request based on the coding agent's analysis.
Adding `add-labels:` to the `safe-outputs:` section of your workflow declares that the workflow should conclude with adding labels to issues or pull requests based on the coding agent's analysis. By default, labels are added to the triggering issue or pull request, but this can be configured using the `target` option.

**Basic Configuration:**
```yaml
safe-outputs:
add-labels:
```

or with further configuration:

**With Configuration:**
```yaml
safe-outputs:
add-labels:
allowed: [triage, bug, enhancement] # Optional: allowed labels for addition.
allowed: [triage, bug, enhancement] # Optional: allowed labels for addition
max: 3 # Optional: maximum number of labels to add (default: 3)
target: "*" # Optional: target for labels
# "triggering" (default) - only add labels to triggering issue/PR
# "*" - allow labels on any issue (requires issue_number in agent output)
# Explicit number - add labels to specific issue/PR (e.g., "123")
```

The agentic part of your workflow should analyze the issue content and determine appropriate labels.
Expand Down
21 changes: 21 additions & 0 deletions pkg/cli/workflows/test-add-labels-target.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
on:
workflow_dispatch:
permissions:
contents: read
actions: read
engine: claude
safe-outputs:
add-labels:
allowed: [bug, enhancement, documentation]
target: "*"
---

# Test Add Labels with Target

This workflow demonstrates the `target` field for `add-labels`.

With `target: "*"`, the workflow can add labels to any issue by specifying
the `issue_number` in the output.

Please add the label "bug" to issue #1 and the label "documentation" to issue #2.
47 changes: 35 additions & 12 deletions pkg/workflow/js/add_labels.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

207 changes: 205 additions & 2 deletions pkg/workflow/js/add_labels.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ describe("add_labels.js", () => {
});

describe("Context validation", () => {
it("should fail when not in issue or PR context", async () => {
it("should skip when not in issue or PR context (with default target)", async () => {
process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify({
items: [
{
Expand All @@ -270,7 +270,9 @@ describe("add_labels.js", () => {
// Execute the script
await eval(`(async () => { ${addLabelsScript} })()`);

expect(mockCore.setFailed).toHaveBeenCalledWith("Not running in issue or pull request context, skipping label addition");
expect(mockCore.info).toHaveBeenCalledWith(
'Target is "triggering" but not running in issue or pull request context, skipping label addition'
);
expect(mockGithub.rest.issues.addLabels).not.toHaveBeenCalled();
});

Expand Down Expand Up @@ -857,4 +859,205 @@ describe("add_labels.js", () => {
});
});
});

describe("Target configuration", () => {
beforeEach(() => {
// Reset environment variables
delete process.env.GITHUB_AW_LABELS_TARGET;
});

it("should use triggering issue when target is not specified (default behavior)", async () => {
process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify({
items: [
{
type: "add-labels",
labels: ["bug"],
},
],
});

global.context.eventName = "issues";
global.context.payload.issue = { number: 456 };

mockGithub.rest.issues.addLabels.mockResolvedValue({});

// Execute the script
await eval(`(async () => { ${addLabelsScript} })()`);

expect(mockCore.info).toHaveBeenCalledWith("Labels target configuration: triggering");
expect(mockGithub.rest.issues.addLabels).toHaveBeenCalledWith({
owner: "testowner",
repo: "testrepo",
issue_number: 456,
labels: ["bug"],
});
});

it("should use triggering issue when target is explicitly set to 'triggering'", async () => {
process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify({
items: [
{
type: "add-labels",
labels: ["enhancement"],
},
],
});
process.env.GITHUB_AW_LABELS_TARGET = "triggering";

global.context.eventName = "issues";
global.context.payload.issue = { number: 789 };

mockGithub.rest.issues.addLabels.mockResolvedValue({});

// Execute the script
await eval(`(async () => { ${addLabelsScript} })()`);

expect(mockCore.info).toHaveBeenCalledWith("Labels target configuration: triggering");
expect(mockGithub.rest.issues.addLabels).toHaveBeenCalledWith({
owner: "testowner",
repo: "testrepo",
issue_number: 789,
labels: ["enhancement"],
});
});

it("should skip when target is 'triggering' but not in issue context", async () => {
process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify({
items: [
{
type: "add-labels",
labels: ["bug"],
},
],
});
process.env.GITHUB_AW_LABELS_TARGET = "triggering";

// Set context to something other than issues or PR
global.context.eventName = "push";
delete global.context.payload.issue;
delete global.context.payload.pull_request;

// Execute the script
await eval(`(async () => { ${addLabelsScript} })()`);

expect(mockCore.info).toHaveBeenCalledWith(
'Target is "triggering" but not running in issue or pull request context, skipping label addition'
);
expect(mockGithub.rest.issues.addLabels).not.toHaveBeenCalled();
});

it("should use explicit issue number from target configuration", async () => {
process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify({
items: [
{
type: "add-labels",
labels: ["bug", "urgent"],
},
],
});
process.env.GITHUB_AW_LABELS_TARGET = "999";

// Context doesn't matter when explicit issue number is provided
global.context.eventName = "push";
delete global.context.payload.issue;

mockGithub.rest.issues.addLabels.mockResolvedValue({});

// Execute the script
await eval(`(async () => { ${addLabelsScript} })()`);

expect(mockCore.info).toHaveBeenCalledWith("Labels target configuration: 999");
expect(mockGithub.rest.issues.addLabels).toHaveBeenCalledWith({
owner: "testowner",
repo: "testrepo",
issue_number: 999,
labels: ["bug", "urgent"],
});
});

it("should use issue_number from labels item when target is '*'", async () => {
process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify({
items: [
{
type: "add-labels",
labels: ["documentation"],
issue_number: 555,
},
],
});
process.env.GITHUB_AW_LABELS_TARGET = "*";

// Context doesn't matter when issue_number is provided in the item
global.context.eventName = "push";
delete global.context.payload.issue;

mockGithub.rest.issues.addLabels.mockResolvedValue({});

// Execute the script
await eval(`(async () => { ${addLabelsScript} })()`);

expect(mockCore.info).toHaveBeenCalledWith("Labels target configuration: *");
expect(mockGithub.rest.issues.addLabels).toHaveBeenCalledWith({
owner: "testowner",
repo: "testrepo",
issue_number: 555,
labels: ["documentation"],
});
});

it("should fail when target is '*' but no issue_number in labels item", async () => {
process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify({
items: [
{
type: "add-labels",
labels: ["bug"],
},
],
});
process.env.GITHUB_AW_LABELS_TARGET = "*";

// Execute the script
await eval(`(async () => { ${addLabelsScript} })()`);

expect(mockCore.setFailed).toHaveBeenCalledWith('Target is "*" but no issue_number specified in labels item');
expect(mockGithub.rest.issues.addLabels).not.toHaveBeenCalled();
});

it("should fail when target has invalid issue number", async () => {
process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify({
items: [
{
type: "add-labels",
labels: ["bug"],
},
],
});
process.env.GITHUB_AW_LABELS_TARGET = "invalid";

// Execute the script
await eval(`(async () => { ${addLabelsScript} })()`);

expect(mockCore.setFailed).toHaveBeenCalledWith("Invalid issue number in target configuration: invalid");
expect(mockGithub.rest.issues.addLabels).not.toHaveBeenCalled();
});

it("should fail when target is '*' and issue_number in item is invalid", async () => {
process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify({
items: [
{
type: "add-labels",
labels: ["bug"],
issue_number: -5,
},
],
});
process.env.GITHUB_AW_LABELS_TARGET = "*";

// Execute the script
await eval(`(async () => { ${addLabelsScript} })()`);

expect(mockCore.setFailed).toHaveBeenCalledWith("Invalid issue number specified: -5");
expect(mockGithub.rest.issues.addLabels).not.toHaveBeenCalled();
});
});
});
Loading