Skip to content
Closed
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
1 change: 1 addition & 0 deletions .github/workflows/cloclo.lock.yml

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

1 change: 1 addition & 0 deletions .github/workflows/smoke-copilot.lock.yml

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

13 changes: 13 additions & 0 deletions actions/setup/js/remove_trigger_label.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ const { ERR_API, ERR_CONFIG } = require("./error_codes.cjs");
*
* Supported events: issues (labeled), pull_request (labeled), discussion (labeled).
* For workflow_dispatch, the step emits an empty label_name output and exits without error.
*
* When GH_AW_REMOVE_LABEL is set to "false", the label is NOT removed but the label_name
* output is still set. This is useful when you want to keep the label on the item.
*/
async function main() {
const labelNamesJSON = process.env.GH_AW_LABEL_NAMES;
const removeLabelEnv = process.env.GH_AW_REMOVE_LABEL;

const { getErrorMessage } = require("./error_helpers.cjs");

Expand All @@ -32,6 +36,9 @@ async function main() {
return;
}

// When GH_AW_REMOVE_LABEL is "false", skip label removal but still output the label name.
const shouldRemoveLabel = removeLabelEnv !== "false";

const eventName = context.eventName;

// For workflow_dispatch and other non-labeled events, nothing to remove.
Expand All @@ -56,6 +63,12 @@ async function main() {
return;
}

if (!shouldRemoveLabel) {
core.info(`Label removal disabled (remove-label: false) – keeping label '${triggerLabel}' on the item.`);
core.setOutput("label_name", triggerLabel);
return;
}

core.info(`Removing trigger label '${triggerLabel}' (event: ${eventName})`);

const owner = context.repo?.owner;
Expand Down
25 changes: 25 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,27 @@
],
"description": "Alternative to 'name': label name(s) that trigger the workflow."
},
"labels": {
"oneOf": [
{
"type": "string",
"minLength": 1,
"description": "Single label name."
},
{
"type": "array",
"minItems": 1,
"description": "Array of label names \u2014 any of these labels will trigger the workflow.",
"items": {
"type": "string",
"minLength": 1,
"description": "A label name"
},
"maxItems": 25
}
],
"description": "Label name(s) that trigger the workflow. Preferred alternative to 'name'/'names'."
},
"events": {
"description": "Item types where the label-command trigger should be active. Default is all supported types: issues, pull_request, discussion.",
"oneOf": [
Expand All @@ -374,6 +395,10 @@
"maxItems": 3
}
]
},
"remove-label": {
"type": "boolean",
"description": "Whether to remove the triggering label when the workflow starts. Default is true. Set to false to keep the label on the item and skip the label removal step (also removes the need for issues:write or discussions:write permissions)."
}
},
"additionalProperties": false
Expand Down
11 changes: 10 additions & 1 deletion pkg/workflow/compiler_activation_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ func (c *Compiler) buildActivationJob(data *WorkflowData, preActivationJobCreate
// Add label removal step and label_command output for label-command workflows.
// When a label-command trigger fires, the triggering label is immediately removed
// so that the same label can be applied again to trigger the workflow in the future.
// When remove-label is false, the step still runs to identify and output the label name
// but skips the actual removal operation.
if len(data.LabelCommand) > 0 {
// The removal step only makes sense for actual "labeled" events; for
// workflow_dispatch we skip it silently via the env-based label check.
Expand All @@ -322,6 +324,12 @@ func (c *Compiler) buildActivationJob(data *WorkflowData, preActivationJobCreate
return nil, fmt.Errorf("failed to marshal label-command names: %w", err)
}
steps = append(steps, fmt.Sprintf(" GH_AW_LABEL_NAMES: '%s'\n", string(labelNamesJSON)))
// Pass whether to remove the label (default true; false skips removal but still outputs label name)
removeLabelStr := "true"
if !data.LabelCommandRemoveLabel {
removeLabelStr = "false"
}
steps = append(steps, fmt.Sprintf(" GH_AW_REMOVE_LABEL: '%s'\n", removeLabelStr))
steps = append(steps, " with:\n")
// Use GitHub App or custom token if configured (avoids needing elevated GITHUB_TOKEN permissions)
labelToken := c.resolveActivationToken(data)
Expand Down Expand Up @@ -502,7 +510,8 @@ func (c *Compiler) buildActivationJob(data *WorkflowData, preActivationJobCreate
// - discussion events need discussions:write
// When a github-app token is configured, the GITHUB_TOKEN permissions are irrelevant
// for the label removal step (it uses the app token instead), so we skip them.
if hasLabelCommand && data.ActivationGitHubApp == nil {
// When remove-label is false, the step does not remove labels so no write permissions are needed.
if hasLabelCommand && data.ActivationGitHubApp == nil && data.LabelCommandRemoveLabel {
if sliceutil.Contains(filteredLabelEvents, "issues") || sliceutil.Contains(filteredLabelEvents, "pull_request") {
permsMap[PermissionIssues] = PermissionWrite
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/compiler_orchestrator_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,7 @@ func (c *Compiler) extractAdditionalConfigurations(

// Extract and process mcp-scripts and safe-outputs
workflowData.Command, workflowData.CommandEvents = c.extractCommandConfig(frontmatter)
workflowData.LabelCommand, workflowData.LabelCommandEvents = c.extractLabelCommandConfig(frontmatter)
workflowData.LabelCommand, workflowData.LabelCommandEvents, workflowData.LabelCommandRemoveLabel = c.extractLabelCommandConfig(frontmatter)
workflowData.Jobs = c.extractJobsFromFrontmatter(frontmatter)

// Merge jobs from imported YAML workflows
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/compiler_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ type WorkflowData struct {
LabelCommand []string // for label-command trigger support - label names that act as commands
LabelCommandEvents []string // events where label-command should be active (nil = all: issues, pull_request, discussion)
LabelCommandOtherEvents map[string]any // for merging label-command with other events
LabelCommandRemoveLabel bool // whether to remove the triggering label when the workflow starts (default: true)
AIReaction string // AI reaction type like "eyes", "heart", etc.
StatusComment *bool // whether to post status comments (default: true when ai-reaction is set, false otherwise)
ActivationGitHubToken string // custom github token from on.github-token for reactions/comments
Expand Down
43 changes: 31 additions & 12 deletions pkg/workflow/frontmatter_extraction_yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -743,37 +743,39 @@ func (c *Compiler) extractCommandConfig(frontmatter map[string]any) (commandName
}

// extractLabelCommandConfig extracts the label-command configuration from frontmatter
// including label name(s) and the events field.
// including label name(s), the events field, and the remove-label option.
// It reads on.label_command which can be:
// - a string: label name directly (e.g. label_command: "deploy")
// - a map with "name" or "names" and optional "events" fields
// - a map with "name", "names", or "labels" and optional "events" and "remove-label" fields
//
// Returns (labelNames, labelEvents) where labelEvents is nil for default (all events).
func (c *Compiler) extractLabelCommandConfig(frontmatter map[string]any) (labelNames []string, labelEvents []string) {
// Returns (labelNames, labelEvents, removeLabelEnabled) where labelEvents is nil for default
// (all events) and removeLabelEnabled defaults to true.
func (c *Compiler) extractLabelCommandConfig(frontmatter map[string]any) (labelNames []string, labelEvents []string, removeLabelEnabled bool) {
frontmatterLog.Print("Extracting label-command configuration from frontmatter")
onValue, exists := frontmatter["on"]
if !exists {
return nil, nil
return nil, nil, true
}
onMap, ok := onValue.(map[string]any)
if !ok {
return nil, nil
return nil, nil, true
}
labelCommandValue, hasLabelCommand := onMap["label_command"]
if !hasLabelCommand {
return nil, nil
return nil, nil, true
}

// Simple string form: label_command: "my-label"
if nameStr, ok := labelCommandValue.(string); ok {
frontmatterLog.Printf("Extracted label-command name (shorthand): %s", nameStr)
return []string{nameStr}, nil
return []string{nameStr}, nil, true
}

// Map form: label_command: {name: "...", names: [...], events: [...]}
// Map form: label_command: {name: "...", names: [...], labels: [...], events: [...], remove-label: bool}
if lcMap, ok := labelCommandValue.(map[string]any); ok {
var names []string
var events []string
removeLabel := true

if nameVal, hasName := lcMap["name"]; hasName {
if nameStr, ok := nameVal.(string); ok {
Expand All @@ -797,16 +799,33 @@ func (c *Compiler) extractLabelCommandConfig(frontmatter map[string]any) (labelN
names = append(names, namesStr)
}
}
if labelsVal, hasLabels := lcMap["labels"]; hasLabels {
if labelsArray, ok := labelsVal.([]any); ok {
for _, item := range labelsArray {
if s, ok := item.(string); ok {
names = append(names, s)
}
}
} else if labelsStr, ok := labelsVal.(string); ok {
names = append(names, labelsStr)
}
}

if eventsVal, hasEvents := lcMap["events"]; hasEvents {
events = ParseCommandEvents(eventsVal)
}

frontmatterLog.Printf("Extracted label-command config: names=%v, events=%v", names, events)
return names, events
if removeLabelVal, hasRemoveLabel := lcMap["remove-label"]; hasRemoveLabel {
if removeLabelBool, ok := removeLabelVal.(bool); ok {
removeLabel = removeLabelBool
}
}

frontmatterLog.Printf("Extracted label-command config: names=%v, events=%v, removeLabel=%v", names, events, removeLabel)
return names, events, removeLabel
}

return nil, nil
return nil, nil, true
}

// isGitHubAppNestedField returns true if the trimmed YAML line represents a known
Expand Down
Loading