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
177 changes: 177 additions & 0 deletions .github/workflows/test-claude.lock.yml

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

2 changes: 2 additions & 0 deletions .github/workflows/test-claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ permissions:
actions: read
contents: read
output:
labels:
allowed: ["bug", "feature"]
issue:
title-prefix: "[claude-test] "
labels: [claude, automation, haiku]
Expand Down
68 changes: 68 additions & 0 deletions docs/frontmatter.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@ output:
title-prefix: "[ai] " # Optional: prefix for PR titles
labels: [automation, ai-agent] # Optional: labels to attach to PRs
draft: true # Optional: create as draft PR (defaults to true)
labels:
allowed: [triage, bug, enhancement] # Mandatory: allowed labels for addition
Copy link
Contributor

@dsyme dsyme Aug 22, 2025

Choose a reason for hiding this comment

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

It feels like if this allowed: is not specified, then the agentic part should be able to leave the chosen labels as data.

Copy link
Contributor

Choose a reason for hiding this comment

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

This applies to all the labels: features.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll defer this to another PR.

max-count: 3 # Optional: maximum number of labels to add (default: 3)
```

### Issue Creation (`output.issue`)
Expand Down Expand Up @@ -391,6 +394,71 @@ Write a summary to ${{ env.GITHUB_AW_OUTPUT }} with title and description.
**Required Patch Format:**
The agent must create git patches in `/tmp/aw.patch` for the changes to be applied. The pull request creation job validates patch existence and content before proceeding.

### Label Addition (`output.labels`)

**Behavior:**
- When `output.labels` is configured, the compiler automatically generates a separate `add_labels` job
- This job runs after the main AI agent job completes
- The agent's output content flows from the main job to the label addition job via job output variables
- The job parses labels from the agent output (one per line), validates them against the allowed list, and adds them to the current issue or pull request
- **Important**: Only **label addition** is supported; label removal is strictly prohibited and will cause the job to fail
- **Security**: The `allowed` list is mandatory and enforced at runtime - only labels from this list can be added

**Generated Job Properties:**
- **Job Name**: `add_labels`
- **Dependencies**: Runs after the main agent job (`needs: [main-job-name]`)
- **Permissions**: Only the label addition job has `issues: write` and `pull-requests: write` permissions
- **Timeout**: 10-minute timeout to prevent hanging
- **Conditional Execution**: Only runs when `github.event.issue.number` or `github.event.pull_request.number` is available
- **Environment Variables**: Configuration passed via `GITHUB_AW_LABELS_ALLOWED`
- **Outputs**: Returns `labels_added` as a newline-separated list of labels that were successfully added

**Configuration:**
```yaml
output:
labels:
allowed: [triage, bug, enhancement] # Mandatory: list of allowed labels (must be non-empty)
max-count: 3 # Optional: maximum number of labels to add (default: 3)
```

**Agent Output Format:**
The agent should write labels to add, one per line, to the `${{ env.GITHUB_AW_OUTPUT }}` file:
```
triage
bug
needs-review
```

**Safety Features:**
- Empty lines in agent output are ignored
- Lines starting with `-` are rejected (no removal operations allowed)
- Duplicate labels are automatically removed
- All requested labels must be in the `allowed` list or the job fails with a clear error message
- Label count is limited by `max-count` setting (default: 3) - exceeding this limit causes job failure
- Only GitHub's `issues.addLabels` API endpoint is used (no removal endpoints)

**Example workflow using label addition:**
```yaml
---
on:
issues:
types: [opened]
permissions:
contents: read
actions: read # Main job only needs minimal permissions
engine: claude
output:
labels:
allowed: [triage, bug, enhancement, documentation, needs-review]
---

# Issue Labeling Agent

Analyze the issue content and add appropriate labels.
Write the labels you want to add (one per line) to ${{ env.GITHUB_AW_OUTPUT }}.
Only use labels from the allowed list: triage, bug, enhancement, documentation, needs-review.
```

## Cache Configuration (`cache:`)

Cache configuration using GitHub Actions `actions/cache` syntax:
Expand Down
21 changes: 21 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,27 @@
}
},
"additionalProperties": false
},
"labels": {
"type": "object",
"description": "Configuration for adding labels to issues/PRs from agent output",
"properties": {
"allowed": {
"type": "array",
"description": "Mandatory list of allowed labels that can be added",
"items": {
"type": "string"
},
"minItems": 1
},
"max-count": {
"type": "integer",
"description": "Optional maximum number of labels to add (default: 3)",
"minimum": 1
}
},
"required": ["allowed"],
"additionalProperties": false
}
},
"additionalProperties": false
Expand Down
64 changes: 64 additions & 0 deletions pkg/workflow/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ type OutputConfig struct {
Issue *IssueConfig `yaml:"issue,omitempty"`
Comment *CommentConfig `yaml:"comment,omitempty"`
PullRequest *PullRequestConfig `yaml:"pull-request,omitempty"`
Labels *LabelConfig `yaml:"labels,omitempty"`
}

// IssueConfig holds configuration for creating GitHub issues from agent output
Expand All @@ -170,6 +171,12 @@ type PullRequestConfig struct {
Draft *bool `yaml:"draft,omitempty"` // Pointer to distinguish between unset (nil) and explicitly false
}

// LabelConfig holds configuration for adding labels to issues/PRs from agent output
type LabelConfig struct {
Allowed []string `yaml:"allowed"` // Mandatory list of allowed labels
MaxCount *int `yaml:"max-count,omitempty"` // Optional maximum number of labels to add (default: 3)
}

// CompileWorkflow converts a markdown workflow to GitHub Actions YAML
func (c *Compiler) CompileWorkflow(markdownPath string) error {

Expand Down Expand Up @@ -1534,6 +1541,17 @@ func (c *Compiler) buildJobs(data *WorkflowData) error {
}
}

// Build add_labels job if output.labels is configured
if data.Output != nil && data.Output.Labels != nil {
addLabelsJob, err := c.buildCreateOutputLabelJob(data, jobName)
if err != nil {
return fmt.Errorf("failed to build add_labels job: %w", err)
}
if err := c.jobManager.AddJob(addLabelsJob); err != nil {
return fmt.Errorf("failed to add add_labels job: %w", err)
}
}

// Build additional custom jobs from frontmatter jobs section
if err := c.buildCustomJobs(data); err != nil {
return fmt.Errorf("failed to build custom jobs: %w", err)
Expand Down Expand Up @@ -2284,6 +2302,52 @@ func (c *Compiler) extractOutputConfig(frontmatter map[string]any) *OutputConfig
}
}

// Parse labels configuration
if labels, exists := outputMap["labels"]; exists {
if labelsMap, ok := labels.(map[string]any); ok {
labelConfig := &LabelConfig{}

// Parse allowed labels (mandatory)
if allowed, exists := labelsMap["allowed"]; exists {
if allowedArray, ok := allowed.([]any); ok {
var allowedStrings []string
for _, label := range allowedArray {
if labelStr, ok := label.(string); ok {
allowedStrings = append(allowedStrings, labelStr)
}
}
labelConfig.Allowed = allowedStrings
}
}

// Parse max-count (optional)
if maxCount, exists := labelsMap["max-count"]; exists {
// Handle different numeric types that YAML parsers might return
var maxCountInt int
var validMaxCount bool
switch v := maxCount.(type) {
case int:
maxCountInt = v
validMaxCount = true
case int64:
maxCountInt = int(v)
validMaxCount = true
case uint64:
maxCountInt = int(v)
validMaxCount = true
case float64:
maxCountInt = int(v)
validMaxCount = true
}
if validMaxCount {
labelConfig.MaxCount = &maxCountInt
}
}

config.Labels = labelConfig
}
}

return config
}
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/workflow/js.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ var createIssueScript string

//go:embed js/create_comment.cjs
var createCommentScript string

//go:embed js/add_labels.cjs
var addLabelsScript string
Loading