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
2,868 changes: 2,868 additions & 0 deletions examples/label-trigger-comma-no-spaces.lock.yml

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions examples/label-trigger-comma-no-spaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
name: Label Trigger Example - Comma No Spaces
description: Example workflow demonstrating comma-separated labels without spaces
on: issue labeled bug,enhancement,priority-high
engine:
id: codex
model: gpt-5-mini
strict: true
---

# Label Trigger Example - Comma No Spaces

This workflow tests the syntax without spaces after commas:

```yaml
on: issue labeled bug,enhancement,priority-high
```

## Task

Acknowledge the trigger.
2,879 changes: 2,879 additions & 0 deletions examples/label-trigger-comma-separated.lock.yml

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions examples/label-trigger-comma-separated.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
name: Label Trigger Example - Comma Separated
description: Example workflow demonstrating the comma-separated label trigger syntax
on: issue labeled bug, enhancement, priority-high
engine:
id: codex
model: gpt-5-mini
strict: true
---

# Label Trigger Example - Comma Separated

This workflow demonstrates the comma-separated label trigger shorthand syntax:

```yaml
on: issue labeled bug, enhancement, priority-high
```

This short syntax automatically expands to:
- Issues labeled with any of the specified labels (bug, enhancement, or priority-high)
- Workflow dispatch trigger with an item_number input parameter

## Task

When this workflow is triggered, acknowledge that it was triggered and provide a brief summary.

Example output:
```
✅ Workflow triggered!
📋 This workflow responds to label events on issues
🏷️ Triggered by one of: bug, enhancement, priority-high
```
33 changes: 21 additions & 12 deletions pkg/workflow/label_trigger_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,22 @@ func parseLabelTriggerShorthand(input string) (entityType string, labelNames []s
}

// Check for different patterns:
// 1. "issue labeled label1 label2 ..."
// 2. "pull_request labeled label1 label2 ..." or "pull-request labeled label1 label2 ..."
// 3. "discussion labeled label1 label2 ..."
// 1. "issue labeled label1 label2 ..." or "issue labeled label1, label2, ..."
// 2. "pull_request labeled label1 label2 ..." or "pull-request labeled label1, label2, ..."
// 3. "discussion labeled label1 label2 ..." or "discussion labeled label1, label2, ..."

var startIdx int

if tokens[0] == "issue" && tokens[1] == "labeled" {
// Pattern 1: "issue labeled label1 label2 ..."
// Pattern 1: "issue labeled label1 label2 ..." or "issue labeled label1, label2, ..."
entityType = "issues"
startIdx = 2
} else if (tokens[0] == "pull_request" || tokens[0] == "pull-request") && tokens[1] == "labeled" {
// Pattern 2: "pull_request labeled label1 label2 ..." or "pull-request labeled label1 label2 ..."
// Pattern 2: "pull_request labeled label1 label2 ..." or "pull-request labeled label1, label2, ..."
entityType = "pull_request"
startIdx = 2
} else if tokens[0] == "discussion" && tokens[1] == "labeled" {
// Pattern 3: "discussion labeled label1 label2 ..."
// Pattern 3: "discussion labeled label1 label2 ..." or "discussion labeled label1, label2, ..."
entityType = "discussion"
startIdx = 2
} else {
Expand All @@ -54,15 +54,24 @@ func parseLabelTriggerShorthand(input string) (entityType string, labelNames []s
return "", nil, true, fmt.Errorf("label trigger shorthand requires at least one label name")
}

labelNames = tokens[startIdx:]

// Validate label names are not empty
for _, label := range labelNames {
if strings.TrimSpace(label) == "" {
return "", nil, true, fmt.Errorf("label names cannot be empty in label trigger shorthand")
// Process label names: handle both space-separated and comma-separated formats
rawLabels := tokens[startIdx:]
for _, token := range rawLabels {
// Split on commas to handle "label1,label2,label3" format
parts := strings.Split(token, ",")
for _, part := range parts {
cleanLabel := strings.TrimSpace(part)
if cleanLabel != "" {
labelNames = append(labelNames, cleanLabel)
}
}
}

// Validate we have at least one label after processing
if len(labelNames) == 0 {
return "", nil, true, fmt.Errorf("label trigger shorthand requires at least one label name")
}

labelTriggerParserLog.Printf("Parsed label trigger shorthand: %s -> entity: %s, labels: %v", input, entityType, labelNames)

return entityType, labelNames, true, nil
Expand Down
21 changes: 14 additions & 7 deletions pkg/workflow/label_trigger_parser_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,20 @@ func FuzzExpandLabelTriggerShorthand(f *testing.F) {
t.Errorf("types array is empty for entityType=%q", entityType)
}

// Check for names field
if names, hasNames := triggerMap["names"]; !hasNames {
t.Errorf("trigger missing names field for entityType=%q", entityType)
} else if namesArray, ok := names.([]string); !ok {
t.Errorf("names is not a string array for entityType=%q", entityType)
} else if len(namesArray) != len(labelNames) {
t.Errorf("names array length mismatch: got %d, want %d for entityType=%q", len(namesArray), len(labelNames), entityType)
// Check for names field (only for issues and pull_request, not discussion)
if entityType == "issues" || entityType == "pull_request" {
if names, hasNames := triggerMap["names"]; !hasNames {
t.Errorf("trigger missing names field for entityType=%q", entityType)
} else if namesArray, ok := names.([]string); !ok {
t.Errorf("names is not a string array for entityType=%q", entityType)
} else if len(namesArray) != len(labelNames) {
t.Errorf("names array length mismatch: got %d, want %d for entityType=%q", len(namesArray), len(labelNames), entityType)
}
} else if entityType == "discussion" {
// Discussion should not have names field (GitHub Actions doesn't support it)
if _, hasNames := triggerMap["names"]; hasNames {
t.Errorf("discussion trigger should not have names field, but it does")
}
}
}
}
Expand Down
57 changes: 57 additions & 0 deletions pkg/workflow/label_trigger_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,63 @@ func TestParseLabelTriggerShorthand(t *testing.T) {
wantIsLabel: true,
wantErr: false,
},
// Comma-separated label tests
{
name: "issue labeled with comma-separated labels",
input: "issue labeled bug, enhancement, priority-high",
wantEntityType: "issues",
wantLabelNames: []string{"bug", "enhancement", "priority-high"},
wantIsLabel: true,
wantErr: false,
},
{
name: "issue labeled with comma-separated single label",
input: "issue labeled bug,",
wantEntityType: "issues",
wantLabelNames: []string{"bug"},
wantIsLabel: true,
wantErr: false,
},
{
name: "pull_request labeled with comma-separated labels",
input: "pull_request labeled needs-review, approved, ready-to-merge",
wantEntityType: "pull_request",
wantLabelNames: []string{"needs-review", "approved", "ready-to-merge"},
wantIsLabel: true,
wantErr: false,
},
{
name: "pull-request (hyphen) labeled with comma-separated labels",
input: "pull-request labeled needs-review, approved",
wantEntityType: "pull_request",
wantLabelNames: []string{"needs-review", "approved"},
wantIsLabel: true,
wantErr: false,
},
{
name: "discussion labeled with comma-separated labels",
input: "discussion labeled question, announcement, help-wanted",
wantEntityType: "discussion",
wantLabelNames: []string{"question", "announcement", "help-wanted"},
wantIsLabel: true,
wantErr: false,
},
{
name: "issue labeled with mixed comma and space separation",
input: "issue labeled bug, enhancement priority-high",
wantEntityType: "issues",
wantLabelNames: []string{"bug", "enhancement", "priority-high"},
wantIsLabel: true,
wantErr: false,
},
{
name: "issue labeled with commas but no spaces",
input: "issue labeled bug,enhancement,priority-high",
wantEntityType: "issues",
wantLabelNames: []string{"bug", "enhancement", "priority-high"},
wantIsLabel: true,
wantErr: false,
},
}

for _, tt := range tests {
Expand Down
20 changes: 10 additions & 10 deletions pkg/workflow/schedule_preprocessing.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,17 +345,17 @@ func (c *Compiler) preprocessScheduleFields(frontmatter map[string]any, markdown
// createTriggerParseError creates a detailed error for trigger parsing issues with source location
func (c *Compiler) createTriggerParseError(filePath, content, triggerStr string, err error) error {
schedulePreprocessingLog.Printf("Creating trigger parse error for: %s", triggerStr)

lines := strings.Split(content, "\n")

// Find the line where "on:" appears in the frontmatter
var onLine int
var onColumn int
inFrontmatter := false

for i, line := range lines {
lineNum := i + 1

// Check for frontmatter delimiter
if strings.TrimSpace(line) == "---" {
if !inFrontmatter {
Expand All @@ -366,7 +366,7 @@ func (c *Compiler) createTriggerParseError(filePath, content, triggerStr string,
}
continue
}

if inFrontmatter {
// Look for "on:" field
trimmed := strings.TrimSpace(line)
Expand All @@ -378,20 +378,20 @@ func (c *Compiler) createTriggerParseError(filePath, content, triggerStr string,
}
}
}

// If we found the line, create a formatted error
if onLine > 0 {
// Create context lines around the error
var context []string
startLine := max(1, onLine-2)
endLine := min(len(lines), onLine+2)

for i := startLine; i <= endLine; i++ {
if i-1 < len(lines) {
context = append(context, lines[i-1])
}
}

compilerErr := console.CompilerError{
Position: console.ErrorPosition{
File: filePath,
Expand All @@ -402,12 +402,12 @@ func (c *Compiler) createTriggerParseError(filePath, content, triggerStr string,
Message: fmt.Sprintf("trigger syntax error: %s", err.Error()),
Context: context,
}

// Format and return the error
formattedErr := console.FormatError(compilerErr)
return errors.New(formattedErr)
}

// Fallback to original error if we can't find the line
schedulePreprocessingLog.Printf("Could not find 'on:' line in frontmatter, using fallback error")
return fmt.Errorf("trigger syntax error: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/trigger_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type TriggerIR struct {
func ParseTriggerShorthand(input string) (*TriggerIR, error) {
input = strings.TrimSpace(input)
if input == "" {
return nil, fmt.Errorf("trigger shorthand cannot be empty. Expected format: 'push to <branch>', 'issue opened', 'pull_request merged', etc.")
return nil, fmt.Errorf("trigger shorthand cannot be empty")
}

triggerParserLog.Printf("Parsing trigger shorthand: %s", input)
Expand Down
26 changes: 13 additions & 13 deletions pkg/workflow/trigger_parser_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,18 +301,18 @@ func FuzzTriggerIRToYAMLMap(f *testing.F) {
func validateTriggerIR(t *testing.T, ir *TriggerIR, input string) {
// Event should be from the expected set of GitHub Actions events
validEvents := map[string]bool{
"push": true,
"pull_request": true,
"issues": true,
"discussion": true,
"release": true,
"watch": true,
"fork": true,
"issue_comment": true,
"workflow_run": true,
"repository_dispatch": true,
"code_scanning_alert": true,
"": true, // Empty is valid for manual-only triggers
"push": true,
"pull_request": true,
"issues": true,
"discussion": true,
"release": true,
"watch": true,
"fork": true,
"issue_comment": true,
"workflow_run": true,
"repository_dispatch": true,
"code_scanning_alert": true,
"": true, // Empty is valid for manual-only triggers
}

if !validEvents[ir.Event] {
Expand Down Expand Up @@ -355,7 +355,7 @@ func validateTriggerIR(t *testing.T, ir *TriggerIR, input string) {
}

// Validate that if Event is empty, AdditionalEvents should have something
if ir.Event == "" && (ir.AdditionalEvents == nil || len(ir.AdditionalEvents) == 0) {
if ir.Event == "" && len(ir.AdditionalEvents) == 0 {
t.Errorf("TriggerIR has empty Event but no AdditionalEvents for input: %q", input)
}
}
Expand Down