From c3fc1387c9b8d2861e4a2ccf87c17c4bb6b2786e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 21:01:42 +0000 Subject: [PATCH 1/3] Initial plan From f5ee855978bc268fa59625b0b08c774b8523fbf4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 21:35:18 +0000 Subject: [PATCH 2/3] fix: use native GitHub Actions label filter for issues/pull_request When users specify `names: label-name` in their `on.issues` or `on.pull_request` triggers with `types: [labeled]`, gh-aw was converting this to a job-level `if` condition. This caused the workflow to trigger for ALL label events and be immediately "skipped" when the label didn't match, resulting in noisy workflow runs. GitHub Actions supports native `labels:` filtering in the `on:` section for issues and pull_request events. For `types: [labeled]`, the filter applies specifically to the label being applied (not existing labels). This change adds `convertNamesToNativeLabelFilter` which renames `names:` -> `labels:` in the frontmatter before YAML generation for these event types. The generated lock file now uses the native filter, preventing the workflow from triggering entirely for non-matching labels. Discussion events are not converted because GitHub Actions does not support `labels:` filter for discussions. Recompiled .github/workflows/cloclo.lock.yml which had `names: cloclo`. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/cloclo.lock.yml | 14 ++- .../compiler_orchestrator_workflow.go | 4 + pkg/workflow/filters.go | 94 +++++++++++++++++++ pkg/workflow/label_filter_test.go | 90 +++++++++++------- pkg/workflow/label_trigger_parser.go | 14 +-- 5 files changed, 165 insertions(+), 51 deletions(-) diff --git a/.github/workflows/cloclo.lock.yml b/.github/workflows/cloclo.lock.yml index a1b53d7fd4c..cc6b945fc0a 100644 --- a/.github/workflows/cloclo.lock.yml +++ b/.github/workflows/cloclo.lock.yml @@ -44,8 +44,8 @@ name: "/cloclo" - created - edited issues: - # names: # Label filtering applied via job conditions - # - cloclo # Label filtering applied via job conditions + labels: + - cloclo types: - labeled pull_request: @@ -70,7 +70,7 @@ jobs: activation: needs: pre_activation if: > - (needs.pre_activation.outputs.activated == 'true') && ((((github.event_name == 'issues' || github.event_name == 'issue_comment' || + (needs.pre_activation.outputs.activated == 'true') && (((github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment') && ((github.event_name == 'issues') && ((startsWith(github.event.issue.body, '/cloclo ')) || (github.event.issue.body == '/cloclo')) || (github.event_name == 'issue_comment') && (((startsWith(github.event.comment.body, '/cloclo ')) || @@ -84,8 +84,7 @@ jobs: (github.event_name == 'discussion_comment') && ((startsWith(github.event.comment.body, '/cloclo ')) || (github.event.comment.body == '/cloclo')))) || (!(github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || - github.event_name == 'discussion_comment'))) && ((github.event_name != 'issues') || ((github.event.action != 'labeled') || - (github.event.label.name == 'cloclo')))) + github.event_name == 'discussion_comment'))) runs-on: ubuntu-slim permissions: contents: read @@ -1435,7 +1434,7 @@ jobs: pre_activation: if: > - (((github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request' || + ((github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment') && ((github.event_name == 'issues') && ((startsWith(github.event.issue.body, '/cloclo ')) || (github.event.issue.body == '/cloclo')) || (github.event_name == 'issue_comment') && (((startsWith(github.event.comment.body, '/cloclo ')) || (github.event.comment.body == '/cloclo')) && @@ -1448,8 +1447,7 @@ jobs: (github.event_name == 'discussion_comment') && ((startsWith(github.event.comment.body, '/cloclo ')) || (github.event.comment.body == '/cloclo')))) || (!(github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || - github.event_name == 'discussion_comment'))) && ((github.event_name != 'issues') || ((github.event.action != 'labeled') || - (github.event.label.name == 'cloclo'))) + github.event_name == 'discussion_comment')) runs-on: ubuntu-slim permissions: contents: read diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go index aa1f1720d00..89a22797d27 100644 --- a/pkg/workflow/compiler_orchestrator_workflow.go +++ b/pkg/workflow/compiler_orchestrator_workflow.go @@ -79,6 +79,10 @@ func (c *Compiler) ParseWorkflowFile(markdownPath string) (*WorkflowData, error) workflowData.ActionResolver = actionResolver workflowData.ActionPinWarnings = c.actionPinWarnings + // Convert names: to labels: for issues/pull_request with labeled/unlabeled types + // This must run before extractYAMLSections so the generated on: YAML uses native label filtering + c.convertNamesToNativeLabelFilter(result.Frontmatter, workflowData) + // Extract YAML configuration sections from frontmatter c.extractYAMLSections(result.Frontmatter, workflowData) diff --git a/pkg/workflow/filters.go b/pkg/workflow/filters.go index 2410cf29ada..0f425d9dd8d 100644 --- a/pkg/workflow/filters.go +++ b/pkg/workflow/filters.go @@ -8,6 +8,100 @@ import ( var filtersLog = logger.New("workflow:filters") +// convertNamesToNativeLabelFilter converts the `names:` field to the native GitHub Actions `labels:` field +// for issues and pull_request event sections that include labeled or unlabeled activity types. +// GitHub Actions supports native label filtering for these event types, which prevents the workflow +// from triggering at all for non-matching labels (instead of triggering and then being skipped). +// This must be called before extractYAMLSections so the generated on: YAML uses the labels: field. +// Discussion events are not converted because GitHub Actions does not support labels: filter for discussions. +func (c *Compiler) convertNamesToNativeLabelFilter(frontmatter map[string]any, workflowData *WorkflowData) { + onValue, hasOn := frontmatter["on"] + if !hasOn { + return + } + onMap, isOnMap := onValue.(map[string]any) + if !isOnMap { + return + } + + renameNamesToLabelsInOnSection(onMap) + + // Also update ParsedFrontmatter.On cache so applyLabelFilter sees the updated data + if workflowData != nil && workflowData.ParsedFrontmatter != nil && workflowData.ParsedFrontmatter.On != nil { + renameNamesToLabelsInOnSection(workflowData.ParsedFrontmatter.On) + } +} + +// renameNamesToLabelsInOnSection renames names: to labels: in issues/pull_request sections +// that include labeled or unlabeled types. Modifies the map in-place. +func renameNamesToLabelsInOnSection(onMap map[string]any) { + for _, sectionKey := range []string{"issues", "pull_request"} { + sectionValue, hasSec := onMap[sectionKey] + if !hasSec { + continue + } + sectionMap, isSectionMap := sectionValue.(map[string]any) + if !isSectionMap { + continue + } + + // Only convert when types includes labeled or unlabeled + typesValue, hasTypes := sectionMap["types"] + if !hasTypes { + continue + } + var hasLabeledType bool + switch v := typesValue.(type) { + case []any: + for _, t := range v { + if s, ok := t.(string); ok && (s == "labeled" || s == "unlabeled") { + hasLabeledType = true + break + } + } + case []string: + for _, s := range v { + if s == "labeled" || s == "unlabeled" { + hasLabeledType = true + break + } + } + } + if !hasLabeledType { + continue + } + + // Check for names field and convert to labels: + namesValue, hasNames := sectionMap["names"] + if !hasNames { + continue + } + + // Convert names value to []any for YAML compatibility + var labelNames []any + switch v := namesValue.(type) { + case string: + labelNames = []any{v} + case []string: + for _, s := range v { + labelNames = append(labelNames, s) + } + case []any: + labelNames = v + default: + continue + } + if len(labelNames) == 0 { + continue + } + + // Rename names: to labels: — uses GitHub Actions native label filtering + filtersLog.Printf("Converting names: to labels: for %s section (native label filtering)", sectionKey) + delete(sectionMap, "names") + sectionMap["labels"] = labelNames + } +} + // applyPullRequestDraftFilter applies draft filter conditions for pull_request triggers func (c *Compiler) applyPullRequestDraftFilter(data *WorkflowData, frontmatter map[string]any) { filtersLog.Print("Applying pull request draft filter") diff --git a/pkg/workflow/label_filter_test.go b/pkg/workflow/label_filter_test.go index daaebb37197..d86c95b7398 100644 --- a/pkg/workflow/label_filter_test.go +++ b/pkg/workflow/label_filter_test.go @@ -20,10 +20,10 @@ func TestLabelFilter(t *testing.T) { compiler := NewCompiler() tests := []struct { - name string - frontmatter string - expectedIf string // Expected if condition in the generated lock file - shouldHaveIf bool // Whether an if condition should be present + name string + frontmatter string + expectedLabels []string // Expected labels in the on: section (native filter) + shouldHaveJobCond bool // Whether an activation job if: condition for label.name should be present }{ { name: "issues with labeled and single label name", @@ -45,8 +45,8 @@ tools: github: allowed: [issue_read] ---`, - expectedIf: "github.event.label.name == 'bug'", - shouldHaveIf: true, + expectedLabels: []string{"bug"}, + shouldHaveJobCond: false, }, { name: "issues with labeled and multiple label names", @@ -68,8 +68,8 @@ tools: github: allowed: [issue_read] ---`, - expectedIf: "github.event.label.name == 'bug'", - shouldHaveIf: true, + expectedLabels: []string{"bug", "enhancement", "feature"}, + shouldHaveJobCond: false, }, { name: "issues with unlabeled and label names", @@ -91,8 +91,8 @@ tools: github: allowed: [issue_read] ---`, - expectedIf: "github.event.label.name == 'wontfix'", - shouldHaveIf: true, + expectedLabels: []string{"wontfix", "duplicate"}, + shouldHaveJobCond: false, }, { name: "issues with both labeled and unlabeled", @@ -114,8 +114,8 @@ tools: github: allowed: [issue_read] ---`, - expectedIf: "github.event.label.name == 'priority'", - shouldHaveIf: true, + expectedLabels: []string{"priority", "urgent"}, + shouldHaveJobCond: false, }, { name: "pull_request with labeled and label names", @@ -137,8 +137,8 @@ tools: github: allowed: [get_pull_request] ---`, - expectedIf: "github.event.label.name == 'ready-for-review'", - shouldHaveIf: true, + expectedLabels: []string{"ready-for-review"}, + shouldHaveJobCond: false, }, { name: "issues without labeled/unlabeled types", @@ -160,8 +160,8 @@ tools: github: allowed: [issue_read] ---`, - expectedIf: "", - shouldHaveIf: false, + expectedLabels: nil, + shouldHaveJobCond: false, }, { name: "issues with labeled but no names field", @@ -182,8 +182,8 @@ tools: github: allowed: [issue_read] ---`, - expectedIf: "", - shouldHaveIf: false, + expectedLabels: nil, + shouldHaveJobCond: false, }, } @@ -210,15 +210,22 @@ tools: } lockContent := string(lockBytes) - // Check if the condition is present - if tt.shouldHaveIf { - if !strings.Contains(lockContent, "if:") { - t.Errorf("Expected 'if:' condition to be present in generated workflow") + // Check that native labels: appear in the on: section + for _, label := range tt.expectedLabels { + if !strings.Contains(lockContent, "- "+label) { + t.Errorf("Expected label '%s' to appear under labels: in generated workflow, got:\n%s", label, lockContent) } + } - // Check if the expected condition fragment is present - if tt.expectedIf != "" && !strings.Contains(lockContent, tt.expectedIf) { - t.Errorf("Expected condition to contain '%s', got:\n%s", tt.expectedIf, lockContent) + // Check that no label.name job condition is generated for native-filtered events + if tt.shouldHaveJobCond { + if !strings.Contains(lockContent, "github.event.label.name") { + t.Errorf("Expected 'github.event.label.name' condition to be present in generated workflow") + } + } else if len(tt.expectedLabels) > 0 { + // When using native filtering, label.name should NOT appear in job conditions + if strings.Contains(lockContent, "github.event.label.name") { + t.Errorf("Expected no 'github.event.label.name' job condition when native label filtering is used, got:\n%s", lockContent) } } @@ -229,9 +236,10 @@ tools: } } -// TestLabelFilterCommentedOut tests that the names field is commented out in the final YAML -func TestLabelFilterCommentedOut(t *testing.T) { - tmpDir := testutil.TempDir(t, "label-filter-comment-test") +// TestLabelFilterNative tests that the names field is converted to labels: in the on: section +// for issues/pull_request with labeled/unlabeled types (native GitHub Actions label filtering) +func TestLabelFilterNative(t *testing.T) { + tmpDir := testutil.TempDir(t, "label-filter-native-test") compiler := NewCompiler() @@ -254,8 +262,8 @@ tools: allowed: [issue_read] ---` - testFile := tmpDir + "/test-comment.md" - content := frontmatter + "\n\n# Test Workflow\n\nTest comment." + testFile := tmpDir + "/test-native.md" + content := frontmatter + "\n\n# Test Workflow\n\nTest native label filter." if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { t.Fatal(err) } @@ -271,13 +279,23 @@ tools: } lockContent := string(lockBytes) - // Check that the names field is commented out - if !strings.Contains(lockContent, "# names:") || !strings.Contains(lockContent, "Label filtering applied") { - t.Error("Expected 'names:' field to be commented out with 'Label filtering applied' note") + // Check that labels: field is present (native GitHub Actions filter) + if !strings.Contains(lockContent, "labels:") { + t.Error("Expected 'labels:' field to be present in generated workflow on: section") + } + + // Check that label names appear as list items + if !strings.Contains(lockContent, "- bug") || !strings.Contains(lockContent, "- enhancement") { + t.Error("Expected label names to appear as list items under labels:") + } + + // Check that names: is NOT commented out (it's been replaced by labels:) + if strings.Contains(lockContent, "# names:") { + t.Error("Expected 'names:' to be replaced by 'labels:', not commented out") } - // Check that the names array items are commented out - if !strings.Contains(lockContent, "# - bug") || !strings.Contains(lockContent, "# - enhancement") { - t.Error("Expected names array items to be commented out") + // Check that no job condition for label.name is generated (native filter handles it) + if strings.Contains(lockContent, "github.event.label.name") { + t.Error("Expected no 'github.event.label.name' job condition when native label filtering is used") } } diff --git a/pkg/workflow/label_trigger_parser.go b/pkg/workflow/label_trigger_parser.go index 2f2579568de..5f075da3b4b 100644 --- a/pkg/workflow/label_trigger_parser.go +++ b/pkg/workflow/label_trigger_parser.go @@ -80,8 +80,9 @@ func parseLabelTriggerShorthand(input string) (entityType string, labelNames []s // expandLabelTriggerShorthand takes an entity type and label names and returns a map that represents // the expanded label trigger + workflow_dispatch configuration with item_number input. -// Note: GitHub Actions doesn't support native label filtering for any event type, -// so all labels are filtered via job conditions using the internal `names` field. +// For issues and pull_request, the names field will later be converted to native GitHub Actions +// labels: filter by convertNamesToNativeLabelFilter, preventing unnecessary workflow triggers. +// For discussion, names is used for job-level condition filtering (native labels: not supported). func expandLabelTriggerShorthand(entityType string, labelNames []string) map[string]any { labelTriggerParserLog.Printf("Expanding label trigger shorthand: entity=%s, labels=%v", entityType, labelNames) @@ -100,16 +101,15 @@ func expandLabelTriggerShorthand(entityType string, labelNames []string) map[str } // Build the trigger configuration - // GitHub Actions doesn't support native label filtering for any event type, - // so we use the `names` field (internal representation) for job condition filtering + // For issues and pull_request: convertNamesToNativeLabelFilter will later convert names: to labels: + // For discussion: names: is used for job-level condition filtering (native labels: not supported) triggerConfig := map[string]any{ "types": []any{"labeled"}, } // Add label names for filtering - // All event types use `names` field for job condition filtering - // The `names` field is an internal representation for job condition generation - // and won't be rendered in the final GitHub Actions YAML for these event types + // For issues and pull_request: convertNamesToNativeLabelFilter will later convert names: to labels: + // The names field is an internal representation that gets converted before YAML generation triggerConfig["names"] = labelNames // Create workflow_dispatch with item_number input From e6b0af56fbc1a9e05c1c21720d668ca26876ebdb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 21:59:26 +0000 Subject: [PATCH 3/3] revert: remove invalid labels: native filter change GitHub Actions does not support a `labels:` filter in the `on.issues` or `on.pull_request` trigger syntax. The previous change introduced invalid YAML that would break GitHub Actions processing. The original job-level `if` condition approach is correct: - Workflows trigger on all `issues: labeled` events (unavoidable) - Job conditions correctly skip when the label doesn't match - Skipped runs for non-matching labels are expected behavior Reverts changes to: - pkg/workflow/filters.go - pkg/workflow/compiler_orchestrator_workflow.go - pkg/workflow/label_filter_test.go - pkg/workflow/label_trigger_parser.go - .github/workflows/cloclo.lock.yml Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/cloclo.lock.yml | 14 +-- .../compiler_orchestrator_workflow.go | 4 - pkg/workflow/filters.go | 94 ------------------- pkg/workflow/label_filter_test.go | 90 +++++++----------- pkg/workflow/label_trigger_parser.go | 14 +-- 5 files changed, 51 insertions(+), 165 deletions(-) diff --git a/.github/workflows/cloclo.lock.yml b/.github/workflows/cloclo.lock.yml index cc6b945fc0a..a1b53d7fd4c 100644 --- a/.github/workflows/cloclo.lock.yml +++ b/.github/workflows/cloclo.lock.yml @@ -44,8 +44,8 @@ name: "/cloclo" - created - edited issues: - labels: - - cloclo + # names: # Label filtering applied via job conditions + # - cloclo # Label filtering applied via job conditions types: - labeled pull_request: @@ -70,7 +70,7 @@ jobs: activation: needs: pre_activation if: > - (needs.pre_activation.outputs.activated == 'true') && (((github.event_name == 'issues' || github.event_name == 'issue_comment' || + (needs.pre_activation.outputs.activated == 'true') && ((((github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment') && ((github.event_name == 'issues') && ((startsWith(github.event.issue.body, '/cloclo ')) || (github.event.issue.body == '/cloclo')) || (github.event_name == 'issue_comment') && (((startsWith(github.event.comment.body, '/cloclo ')) || @@ -84,7 +84,8 @@ jobs: (github.event_name == 'discussion_comment') && ((startsWith(github.event.comment.body, '/cloclo ')) || (github.event.comment.body == '/cloclo')))) || (!(github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || - github.event_name == 'discussion_comment'))) + github.event_name == 'discussion_comment'))) && ((github.event_name != 'issues') || ((github.event.action != 'labeled') || + (github.event.label.name == 'cloclo')))) runs-on: ubuntu-slim permissions: contents: read @@ -1434,7 +1435,7 @@ jobs: pre_activation: if: > - ((github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request' || + (((github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment') && ((github.event_name == 'issues') && ((startsWith(github.event.issue.body, '/cloclo ')) || (github.event.issue.body == '/cloclo')) || (github.event_name == 'issue_comment') && (((startsWith(github.event.comment.body, '/cloclo ')) || (github.event.comment.body == '/cloclo')) && @@ -1447,7 +1448,8 @@ jobs: (github.event_name == 'discussion_comment') && ((startsWith(github.event.comment.body, '/cloclo ')) || (github.event.comment.body == '/cloclo')))) || (!(github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || - github.event_name == 'discussion_comment')) + github.event_name == 'discussion_comment'))) && ((github.event_name != 'issues') || ((github.event.action != 'labeled') || + (github.event.label.name == 'cloclo'))) runs-on: ubuntu-slim permissions: contents: read diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go index 89a22797d27..aa1f1720d00 100644 --- a/pkg/workflow/compiler_orchestrator_workflow.go +++ b/pkg/workflow/compiler_orchestrator_workflow.go @@ -79,10 +79,6 @@ func (c *Compiler) ParseWorkflowFile(markdownPath string) (*WorkflowData, error) workflowData.ActionResolver = actionResolver workflowData.ActionPinWarnings = c.actionPinWarnings - // Convert names: to labels: for issues/pull_request with labeled/unlabeled types - // This must run before extractYAMLSections so the generated on: YAML uses native label filtering - c.convertNamesToNativeLabelFilter(result.Frontmatter, workflowData) - // Extract YAML configuration sections from frontmatter c.extractYAMLSections(result.Frontmatter, workflowData) diff --git a/pkg/workflow/filters.go b/pkg/workflow/filters.go index 0f425d9dd8d..2410cf29ada 100644 --- a/pkg/workflow/filters.go +++ b/pkg/workflow/filters.go @@ -8,100 +8,6 @@ import ( var filtersLog = logger.New("workflow:filters") -// convertNamesToNativeLabelFilter converts the `names:` field to the native GitHub Actions `labels:` field -// for issues and pull_request event sections that include labeled or unlabeled activity types. -// GitHub Actions supports native label filtering for these event types, which prevents the workflow -// from triggering at all for non-matching labels (instead of triggering and then being skipped). -// This must be called before extractYAMLSections so the generated on: YAML uses the labels: field. -// Discussion events are not converted because GitHub Actions does not support labels: filter for discussions. -func (c *Compiler) convertNamesToNativeLabelFilter(frontmatter map[string]any, workflowData *WorkflowData) { - onValue, hasOn := frontmatter["on"] - if !hasOn { - return - } - onMap, isOnMap := onValue.(map[string]any) - if !isOnMap { - return - } - - renameNamesToLabelsInOnSection(onMap) - - // Also update ParsedFrontmatter.On cache so applyLabelFilter sees the updated data - if workflowData != nil && workflowData.ParsedFrontmatter != nil && workflowData.ParsedFrontmatter.On != nil { - renameNamesToLabelsInOnSection(workflowData.ParsedFrontmatter.On) - } -} - -// renameNamesToLabelsInOnSection renames names: to labels: in issues/pull_request sections -// that include labeled or unlabeled types. Modifies the map in-place. -func renameNamesToLabelsInOnSection(onMap map[string]any) { - for _, sectionKey := range []string{"issues", "pull_request"} { - sectionValue, hasSec := onMap[sectionKey] - if !hasSec { - continue - } - sectionMap, isSectionMap := sectionValue.(map[string]any) - if !isSectionMap { - continue - } - - // Only convert when types includes labeled or unlabeled - typesValue, hasTypes := sectionMap["types"] - if !hasTypes { - continue - } - var hasLabeledType bool - switch v := typesValue.(type) { - case []any: - for _, t := range v { - if s, ok := t.(string); ok && (s == "labeled" || s == "unlabeled") { - hasLabeledType = true - break - } - } - case []string: - for _, s := range v { - if s == "labeled" || s == "unlabeled" { - hasLabeledType = true - break - } - } - } - if !hasLabeledType { - continue - } - - // Check for names field and convert to labels: - namesValue, hasNames := sectionMap["names"] - if !hasNames { - continue - } - - // Convert names value to []any for YAML compatibility - var labelNames []any - switch v := namesValue.(type) { - case string: - labelNames = []any{v} - case []string: - for _, s := range v { - labelNames = append(labelNames, s) - } - case []any: - labelNames = v - default: - continue - } - if len(labelNames) == 0 { - continue - } - - // Rename names: to labels: — uses GitHub Actions native label filtering - filtersLog.Printf("Converting names: to labels: for %s section (native label filtering)", sectionKey) - delete(sectionMap, "names") - sectionMap["labels"] = labelNames - } -} - // applyPullRequestDraftFilter applies draft filter conditions for pull_request triggers func (c *Compiler) applyPullRequestDraftFilter(data *WorkflowData, frontmatter map[string]any) { filtersLog.Print("Applying pull request draft filter") diff --git a/pkg/workflow/label_filter_test.go b/pkg/workflow/label_filter_test.go index d86c95b7398..daaebb37197 100644 --- a/pkg/workflow/label_filter_test.go +++ b/pkg/workflow/label_filter_test.go @@ -20,10 +20,10 @@ func TestLabelFilter(t *testing.T) { compiler := NewCompiler() tests := []struct { - name string - frontmatter string - expectedLabels []string // Expected labels in the on: section (native filter) - shouldHaveJobCond bool // Whether an activation job if: condition for label.name should be present + name string + frontmatter string + expectedIf string // Expected if condition in the generated lock file + shouldHaveIf bool // Whether an if condition should be present }{ { name: "issues with labeled and single label name", @@ -45,8 +45,8 @@ tools: github: allowed: [issue_read] ---`, - expectedLabels: []string{"bug"}, - shouldHaveJobCond: false, + expectedIf: "github.event.label.name == 'bug'", + shouldHaveIf: true, }, { name: "issues with labeled and multiple label names", @@ -68,8 +68,8 @@ tools: github: allowed: [issue_read] ---`, - expectedLabels: []string{"bug", "enhancement", "feature"}, - shouldHaveJobCond: false, + expectedIf: "github.event.label.name == 'bug'", + shouldHaveIf: true, }, { name: "issues with unlabeled and label names", @@ -91,8 +91,8 @@ tools: github: allowed: [issue_read] ---`, - expectedLabels: []string{"wontfix", "duplicate"}, - shouldHaveJobCond: false, + expectedIf: "github.event.label.name == 'wontfix'", + shouldHaveIf: true, }, { name: "issues with both labeled and unlabeled", @@ -114,8 +114,8 @@ tools: github: allowed: [issue_read] ---`, - expectedLabels: []string{"priority", "urgent"}, - shouldHaveJobCond: false, + expectedIf: "github.event.label.name == 'priority'", + shouldHaveIf: true, }, { name: "pull_request with labeled and label names", @@ -137,8 +137,8 @@ tools: github: allowed: [get_pull_request] ---`, - expectedLabels: []string{"ready-for-review"}, - shouldHaveJobCond: false, + expectedIf: "github.event.label.name == 'ready-for-review'", + shouldHaveIf: true, }, { name: "issues without labeled/unlabeled types", @@ -160,8 +160,8 @@ tools: github: allowed: [issue_read] ---`, - expectedLabels: nil, - shouldHaveJobCond: false, + expectedIf: "", + shouldHaveIf: false, }, { name: "issues with labeled but no names field", @@ -182,8 +182,8 @@ tools: github: allowed: [issue_read] ---`, - expectedLabels: nil, - shouldHaveJobCond: false, + expectedIf: "", + shouldHaveIf: false, }, } @@ -210,22 +210,15 @@ tools: } lockContent := string(lockBytes) - // Check that native labels: appear in the on: section - for _, label := range tt.expectedLabels { - if !strings.Contains(lockContent, "- "+label) { - t.Errorf("Expected label '%s' to appear under labels: in generated workflow, got:\n%s", label, lockContent) + // Check if the condition is present + if tt.shouldHaveIf { + if !strings.Contains(lockContent, "if:") { + t.Errorf("Expected 'if:' condition to be present in generated workflow") } - } - // Check that no label.name job condition is generated for native-filtered events - if tt.shouldHaveJobCond { - if !strings.Contains(lockContent, "github.event.label.name") { - t.Errorf("Expected 'github.event.label.name' condition to be present in generated workflow") - } - } else if len(tt.expectedLabels) > 0 { - // When using native filtering, label.name should NOT appear in job conditions - if strings.Contains(lockContent, "github.event.label.name") { - t.Errorf("Expected no 'github.event.label.name' job condition when native label filtering is used, got:\n%s", lockContent) + // Check if the expected condition fragment is present + if tt.expectedIf != "" && !strings.Contains(lockContent, tt.expectedIf) { + t.Errorf("Expected condition to contain '%s', got:\n%s", tt.expectedIf, lockContent) } } @@ -236,10 +229,9 @@ tools: } } -// TestLabelFilterNative tests that the names field is converted to labels: in the on: section -// for issues/pull_request with labeled/unlabeled types (native GitHub Actions label filtering) -func TestLabelFilterNative(t *testing.T) { - tmpDir := testutil.TempDir(t, "label-filter-native-test") +// TestLabelFilterCommentedOut tests that the names field is commented out in the final YAML +func TestLabelFilterCommentedOut(t *testing.T) { + tmpDir := testutil.TempDir(t, "label-filter-comment-test") compiler := NewCompiler() @@ -262,8 +254,8 @@ tools: allowed: [issue_read] ---` - testFile := tmpDir + "/test-native.md" - content := frontmatter + "\n\n# Test Workflow\n\nTest native label filter." + testFile := tmpDir + "/test-comment.md" + content := frontmatter + "\n\n# Test Workflow\n\nTest comment." if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { t.Fatal(err) } @@ -279,23 +271,13 @@ tools: } lockContent := string(lockBytes) - // Check that labels: field is present (native GitHub Actions filter) - if !strings.Contains(lockContent, "labels:") { - t.Error("Expected 'labels:' field to be present in generated workflow on: section") - } - - // Check that label names appear as list items - if !strings.Contains(lockContent, "- bug") || !strings.Contains(lockContent, "- enhancement") { - t.Error("Expected label names to appear as list items under labels:") - } - - // Check that names: is NOT commented out (it's been replaced by labels:) - if strings.Contains(lockContent, "# names:") { - t.Error("Expected 'names:' to be replaced by 'labels:', not commented out") + // Check that the names field is commented out + if !strings.Contains(lockContent, "# names:") || !strings.Contains(lockContent, "Label filtering applied") { + t.Error("Expected 'names:' field to be commented out with 'Label filtering applied' note") } - // Check that no job condition for label.name is generated (native filter handles it) - if strings.Contains(lockContent, "github.event.label.name") { - t.Error("Expected no 'github.event.label.name' job condition when native label filtering is used") + // Check that the names array items are commented out + if !strings.Contains(lockContent, "# - bug") || !strings.Contains(lockContent, "# - enhancement") { + t.Error("Expected names array items to be commented out") } } diff --git a/pkg/workflow/label_trigger_parser.go b/pkg/workflow/label_trigger_parser.go index 5f075da3b4b..2f2579568de 100644 --- a/pkg/workflow/label_trigger_parser.go +++ b/pkg/workflow/label_trigger_parser.go @@ -80,9 +80,8 @@ func parseLabelTriggerShorthand(input string) (entityType string, labelNames []s // expandLabelTriggerShorthand takes an entity type and label names and returns a map that represents // the expanded label trigger + workflow_dispatch configuration with item_number input. -// For issues and pull_request, the names field will later be converted to native GitHub Actions -// labels: filter by convertNamesToNativeLabelFilter, preventing unnecessary workflow triggers. -// For discussion, names is used for job-level condition filtering (native labels: not supported). +// Note: GitHub Actions doesn't support native label filtering for any event type, +// so all labels are filtered via job conditions using the internal `names` field. func expandLabelTriggerShorthand(entityType string, labelNames []string) map[string]any { labelTriggerParserLog.Printf("Expanding label trigger shorthand: entity=%s, labels=%v", entityType, labelNames) @@ -101,15 +100,16 @@ func expandLabelTriggerShorthand(entityType string, labelNames []string) map[str } // Build the trigger configuration - // For issues and pull_request: convertNamesToNativeLabelFilter will later convert names: to labels: - // For discussion: names: is used for job-level condition filtering (native labels: not supported) + // GitHub Actions doesn't support native label filtering for any event type, + // so we use the `names` field (internal representation) for job condition filtering triggerConfig := map[string]any{ "types": []any{"labeled"}, } // Add label names for filtering - // For issues and pull_request: convertNamesToNativeLabelFilter will later convert names: to labels: - // The names field is an internal representation that gets converted before YAML generation + // All event types use `names` field for job condition filtering + // The `names` field is an internal representation for job condition generation + // and won't be rendered in the final GitHub Actions YAML for these event types triggerConfig["names"] = labelNames // Create workflow_dispatch with item_number input