diff --git a/.github/aw/github-agentic-workflows.md b/.github/aw/github-agentic-workflows.md
index 4ca304d69e..9759db520c 100644
--- a/.github/aw/github-agentic-workflows.md
+++ b/.github/aw/github-agentic-workflows.md
@@ -66,10 +66,23 @@ gh aw compile --poutine # Supply chain security analyzer
# Strict mode with all scanners
gh aw compile --actionlint --zizmor --poutine
+
+# Output validation results as JSON (includes labels referenced in safe-outputs)
+gh aw compile --json --no-emit
```
**Best Practice**: Always run `gh aw compile` after every workflow change to ensure the GitHub Actions YAML is up to date.
+**Agentic Maintenance Workflow Operations:**
+
+The generated `agentics-maintenance.yml` workflow supports these `workflow_dispatch` operations:
+
+- `disable` / `enable` — Disable or re-enable all agentic workflows
+- `update` — Update workflow metadata (opens a PR for changed files)
+- `upgrade` — Upgrade gh-aw version and dependencies (opens a PR)
+- `safe_outputs` — Replay safe outputs from a previous run (provide `run_url`)
+- `create_labels` — Compile all workflows and create any labels referenced in `safe-outputs` that are missing from the repository
+
## Complete Frontmatter Schema
The YAML frontmatter supports these fields:
diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml
index de967ee673..68c22092c3 100644
--- a/.github/workflows/agentics-maintenance.yml
+++ b/.github/workflows/agentics-maintenance.yml
@@ -49,6 +49,7 @@ on:
- 'update'
- 'upgrade'
- 'safe_outputs'
+ - 'create_labels'
run_url:
description: 'Run URL or run ID to replay safe outputs from (e.g. https://github.com/owner/repo/actions/runs/12345 or 12345). Required when operation is safe_outputs.'
required: false
@@ -106,7 +107,7 @@ jobs:
await main();
run_operation:
- if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.operation != '' && github.event.inputs.operation != 'safe_outputs' && !github.event.repository.fork }}
+ if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.operation != '' && github.event.inputs.operation != 'safe_outputs' && github.event.inputs.operation != 'create_labels' && !github.event.repository.fork }}
runs-on: ubuntu-slim
permissions:
actions: write
@@ -201,6 +202,54 @@ jobs:
const { main } = require('${{ runner.temp }}/gh-aw/actions/apply_safe_outputs_replay.cjs');
await main();
+ create_labels:
+ if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.operation == 'create_labels' && !github.event.repository.fork }}
+ runs-on: ubuntu-slim
+ permissions:
+ contents: read
+ issues: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+
+ - name: Setup Scripts
+ uses: ./actions/setup
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+
+ - name: Check admin/maintainer permissions
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs');
+ await main();
+
+ - name: Setup Go
+ uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
+ with:
+ go-version-file: go.mod
+ cache: true
+
+ - name: Build gh-aw
+ run: make build
+
+ - name: Create missing labels
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_CMD_PREFIX: ./gh-aw
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/create_labels.cjs');
+ await main();
+
compile-workflows:
if: ${{ !github.event.repository.fork && (github.event_name != 'workflow_dispatch' || github.event.inputs.operation == '') }}
runs-on: ubuntu-slim
diff --git a/actions/setup/js/create_labels.cjs b/actions/setup/js/create_labels.cjs
new file mode 100644
index 0000000000..1e659bf527
--- /dev/null
+++ b/actions/setup/js/create_labels.cjs
@@ -0,0 +1,139 @@
+// @ts-check
+///
+
+const { getErrorMessage } = require("./error_helpers.cjs");
+const { ERR_SYSTEM } = require("./error_codes.cjs");
+
+/**
+ * Generate a deterministic pastel hex color string from a label name.
+ * Produces colors in the pastel range (128–191 per channel) for readability.
+ *
+ * @param {string} name
+ * @returns {string} Six-character hex color (no leading #)
+ */
+function deterministicLabelColor(name) {
+ let hash = 0;
+ for (let i = 0; i < name.length; i++) {
+ hash = (hash * 31 + name.charCodeAt(i)) >>> 0;
+ }
+ // Map to pastel range: 128–223 per channel
+ const r = 128 + (hash & 0x3f);
+ const g = 128 + ((hash >> 6) & 0x3f);
+ const b = 128 + ((hash >> 12) & 0x3f);
+ return ((r << 16) | (g << 8) | b).toString(16).padStart(6, "0");
+}
+
+/**
+ * Compile all agentic workflows, collect the labels referenced in safe-outputs
+ * configurations, and create any labels that are missing from the repository.
+ *
+ * Required environment variables:
+ * GH_AW_CMD_PREFIX - Command prefix: './gh-aw' (dev) or 'gh aw' (release)
+ *
+ * @returns {Promise}
+ */
+async function main() {
+ const cmdPrefixStr = process.env.GH_AW_CMD_PREFIX || "gh aw";
+ const [bin, ...prefixArgs] = cmdPrefixStr.split(" ").filter(Boolean);
+
+ // Run compile --json --no-emit to collect labels without writing lock files.
+ // Use ignoreReturnCode because compile exits non-zero when some workflows have errors,
+ // but still produces valid JSON output for all (valid and invalid) workflows.
+ const compileArgs = [...prefixArgs, "compile", "--json", "--no-emit"];
+ core.info(`Running: ${bin} ${compileArgs.join(" ")}`);
+
+ let compileOutput;
+ try {
+ const result = await exec.getExecOutput(bin, compileArgs, { ignoreReturnCode: true });
+ compileOutput = result.stdout;
+ // Only treat as a fatal error when the exit is non-zero AND there is no stdout at all.
+ // A non-zero exit with JSON on stdout means some workflows failed validation but we
+ // still have label data from the successfully-parsed ones — continue processing.
+ if (result.exitCode !== 0 && !compileOutput.trim()) {
+ throw new Error(`${ERR_SYSTEM}: compile exited with code ${result.exitCode}: ${result.stderr}`);
+ }
+ } catch (err) {
+ core.setFailed(`Failed to run compile: ${getErrorMessage(err)}`);
+ return;
+ }
+
+ // Parse JSON output
+ let validationResults;
+ try {
+ validationResults = JSON.parse(compileOutput);
+ } catch (err) {
+ core.setFailed(`Failed to parse compile JSON output: ${getErrorMessage(err)}`);
+ return;
+ }
+
+ // Collect all unique labels across all workflows
+ /** @type {Set} */
+ const allLabels = new Set();
+ for (const result of validationResults) {
+ if (Array.isArray(result.labels)) {
+ for (const label of result.labels) {
+ if (typeof label === "string" && label.trim()) {
+ allLabels.add(label.trim());
+ }
+ }
+ }
+ }
+
+ if (allLabels.size === 0) {
+ core.info("No labels found in safe-outputs configurations — nothing to create");
+ return;
+ }
+
+ core.info(`Found ${allLabels.size} unique label(s) in safe-outputs: ${[...allLabels].join(", ")}`);
+
+ // Fetch all existing labels from the repository
+ const { owner, repo } = context.repo;
+ let existingLabels;
+ try {
+ existingLabels = await github.paginate(github.rest.issues.listLabelsForRepo, {
+ owner,
+ repo,
+ per_page: 100,
+ });
+ } catch (err) {
+ core.setFailed(`Failed to list repository labels: ${getErrorMessage(err)}`);
+ return;
+ }
+
+ const existingLabelNames = new Set(existingLabels.map(l => l.name.toLowerCase()));
+
+ // Create missing labels
+ let created = 0;
+ let skipped = 0;
+
+ for (const labelName of allLabels) {
+ if (existingLabelNames.has(labelName.toLowerCase())) {
+ core.info(`ℹ️ Label already exists: ${labelName}`);
+ skipped++;
+ } else {
+ try {
+ await github.rest.issues.createLabel({
+ owner,
+ repo,
+ name: labelName,
+ color: deterministicLabelColor(labelName),
+ description: "",
+ });
+ core.info(`✅ Created label: ${labelName}`);
+ created++;
+ } catch (err) {
+ // 422 means label already exists (race condition) — treat as success
+ if (err && typeof err === "object" && /** @type {any} */ err.status === 422) {
+ core.info(`ℹ️ Label already exists (concurrent): ${labelName}`);
+ skipped++;
+ } else {
+ core.warning(`Failed to create label '${labelName}': ${getErrorMessage(err)}`);
+ }
+ }
+ }
+ }
+
+ core.info(`Done: ${created} label(s) created, ${skipped} already existed`);
+}
+
+module.exports = { main };
diff --git a/pkg/cli/compile_config.go b/pkg/cli/compile_config.go
index ea711b5d9a..c58c942b82 100644
--- a/pkg/cli/compile_config.go
+++ b/pkg/cli/compile_config.go
@@ -59,4 +59,5 @@ type ValidationResult struct {
Errors []CompileValidationError `json:"errors"`
Warnings []CompileValidationError `json:"warnings"`
CompiledFile string `json:"compiled_file,omitempty"`
+ Labels []string `json:"labels,omitempty"` // Labels referenced in safe-outputs configurations
}
diff --git a/pkg/cli/compile_validation.go b/pkg/cli/compile_validation.go
index 915d93ee69..4148001188 100644
--- a/pkg/cli/compile_validation.go
+++ b/pkg/cli/compile_validation.go
@@ -239,6 +239,7 @@ func sanitizeValidationResults(results []ValidationResult) []ValidationResult {
CompiledFile: result.CompiledFile,
Errors: sliceutil.Map(result.Errors, sanitizeError),
Warnings: sliceutil.Map(result.Warnings, sanitizeError),
+ Labels: result.Labels,
}
})
}
diff --git a/pkg/cli/compile_workflow_processor.go b/pkg/cli/compile_workflow_processor.go
index 7844ebd850..a425d9329e 100644
--- a/pkg/cli/compile_workflow_processor.go
+++ b/pkg/cli/compile_workflow_processor.go
@@ -142,6 +142,67 @@ func compileWorkflowFile(
if !noEmit {
result.validationResult.CompiledFile = lockFile
}
+
+ // Collect labels from safe-outputs for JSON output (used by create-labels maintenance operation)
+ result.validationResult.Labels = extractSafeOutputLabels(workflowData)
+
compileWorkflowProcessorLog.Printf("Successfully processed workflow file: %s", resolvedFile)
return result
}
+
+// extractSafeOutputLabels collects all unique labels referenced in safe-outputs configurations.
+// These are labels that should exist in the repository for the workflow to function correctly.
+// Scans: create-issue.labels/allowed-labels, create-discussion.labels/allowed-labels,
+// create-pull-request.labels/allowed-labels, and add-labels.allowed.
+func extractSafeOutputLabels(data *workflow.WorkflowData) []string {
+ if data == nil || data.SafeOutputs == nil {
+ return nil
+ }
+
+ seen := make(map[string]bool)
+ var labels []string
+
+ addLabel := func(label string) {
+ if label != "" && !seen[label] {
+ seen[label] = true
+ labels = append(labels, label)
+ }
+ }
+
+ so := data.SafeOutputs
+
+ if so.CreateIssues != nil {
+ for _, l := range so.CreateIssues.Labels {
+ addLabel(l)
+ }
+ for _, l := range so.CreateIssues.AllowedLabels {
+ addLabel(l)
+ }
+ }
+
+ if so.CreateDiscussions != nil {
+ for _, l := range so.CreateDiscussions.Labels {
+ addLabel(l)
+ }
+ for _, l := range so.CreateDiscussions.AllowedLabels {
+ addLabel(l)
+ }
+ }
+
+ if so.CreatePullRequests != nil {
+ for _, l := range so.CreatePullRequests.Labels {
+ addLabel(l)
+ }
+ for _, l := range so.CreatePullRequests.AllowedLabels {
+ addLabel(l)
+ }
+ }
+
+ if so.AddLabels != nil {
+ for _, l := range so.AddLabels.Allowed {
+ addLabel(l)
+ }
+ }
+
+ return labels
+}
diff --git a/pkg/workflow/maintenance_workflow.go b/pkg/workflow/maintenance_workflow.go
index bceef12a29..c4a4ec588d 100644
--- a/pkg/workflow/maintenance_workflow.go
+++ b/pkg/workflow/maintenance_workflow.go
@@ -220,6 +220,7 @@ on:
- 'update'
- 'upgrade'
- 'safe_outputs'
+ - 'create_labels'
run_url:
description: 'Run URL or run ID to replay safe outputs from (e.g. https://github.com/owner/repo/actions/runs/12345 or 12345). Required when operation is safe_outputs.'
required: false
@@ -300,10 +301,10 @@ jobs:
await main();
`)
- // Add unified run_operation job for all dispatch operations except safe_outputs
+ // Add unified run_operation job for all dispatch operations except safe_outputs and create_labels
yaml.WriteString(`
run_operation:
- if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.operation != '' && github.event.inputs.operation != 'safe_outputs' && !github.event.repository.fork }}
+ if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.operation != '' && github.event.inputs.operation != 'safe_outputs' && github.event.inputs.operation != 'create_labels' && !github.event.repository.fork }}
runs-on: ubuntu-slim
permissions:
actions: write
@@ -396,6 +397,51 @@ jobs:
await main();
`)
+ // Add create_labels job for workflow_dispatch with operation == 'create_labels'
+ yaml.WriteString(`
+ create_labels:
+ if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.operation == 'create_labels' && !github.event.repository.fork }}
+ runs-on: ubuntu-slim
+ permissions:
+ contents: read
+ issues: write
+ steps:
+ - name: Checkout repository
+ uses: ` + GetActionPin("actions/checkout") + `
+ with:
+ persist-credentials: false
+
+ - name: Setup Scripts
+ uses: ` + setupActionRef + `
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+
+ - name: Check admin/maintainer permissions
+ uses: ` + GetActionPin("actions/github-script") + `
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs');
+ await main();
+
+`)
+
+ yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver))
+ yaml.WriteString(` - name: Create missing labels
+ uses: ` + GetActionPin("actions/github-script") + `
+ env:
+ GH_AW_CMD_PREFIX: ` + getCLICmdPrefix(actionMode) + `
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/create_labels.cjs');
+ await main();
+`)
+
// Add compile-workflows and zizmor-scan jobs only in dev mode
// These jobs are specific to the gh-aw repository and require go.mod, make build, etc.
// User repositories won't have these dependencies, so we skip them in release mode
diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go
index 3947d0b928..8c1677d865 100644
--- a/pkg/workflow/maintenance_workflow_test.go
+++ b/pkg/workflow/maintenance_workflow_test.go
@@ -281,11 +281,12 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
yaml := string(content)
operationSkipCondition := `github.event_name != 'workflow_dispatch' || github.event.inputs.operation == ''`
- operationRunCondition := `github.event_name == 'workflow_dispatch' && github.event.inputs.operation != '' && github.event.inputs.operation != 'safe_outputs'`
+ operationRunCondition := `github.event_name == 'workflow_dispatch' && github.event.inputs.operation != '' && github.event.inputs.operation != 'safe_outputs' && github.event.inputs.operation != 'create_labels'`
applySafeOutputsCondition := `github.event_name == 'workflow_dispatch' && github.event.inputs.operation == 'safe_outputs'`
+ createLabelsCondition := `github.event_name == 'workflow_dispatch' && github.event.inputs.operation == 'create_labels'`
const jobSectionSearchRange = 300
- const runOpSectionSearchRange = 200
+ const runOpSectionSearchRange = 400
// Jobs that should be disabled when operation is set
disabledJobs := []string{"close-expired-entities:", "compile-workflows:", "zizmor-scan:", "secret-validation:"}
@@ -329,6 +330,22 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
}
}
+ // create_labels job should be triggered when operation == 'create_labels'
+ createLabelsIdx := strings.Index(yaml, "\n create_labels:")
+ if createLabelsIdx == -1 {
+ t.Errorf("Job create_labels not found in generated workflow")
+ } else {
+ createLabelsSection := yaml[createLabelsIdx : createLabelsIdx+runOpSectionSearchRange]
+ if !strings.Contains(createLabelsSection, createLabelsCondition) {
+ t.Errorf("Job create_labels should have the activation condition %q in:\n%s", createLabelsCondition, createLabelsSection)
+ }
+ }
+
+ // Verify create_labels is an option in the operation choices
+ if !strings.Contains(yaml, "- 'create_labels'") {
+ t.Error("workflow_dispatch operation choices should include 'create_labels'")
+ }
+
// Verify safe_outputs is an option in the operation choices
if !strings.Contains(yaml, "- 'safe_outputs'") {
t.Error("workflow_dispatch operation choices should include 'safe_outputs'")
@@ -572,12 +589,12 @@ func TestGenerateMaintenanceWorkflow_RunOperationCLICodegen(t *testing.T) {
t.Fatalf("Expected maintenance workflow to be generated: %v", err)
}
yaml := string(content)
- // Both run_operation and compile_workflows should use the same setup-go version
- // (both use GetActionPin, not hardcoded pins). Exactly 2 occurrences expected.
+ // run_operation, create_labels, and compile_workflows should use the same setup-go version
+ // (all use GetActionPin, not hardcoded pins). Exactly 3 occurrences expected.
setupGoPin := GetActionPin("actions/setup-go")
occurrences := strings.Count(yaml, setupGoPin)
- if occurrences != 2 {
- t.Errorf("Expected exactly 2 occurrences of pinned setup-go ref %q (run_operation + compile_workflows), got %d in:\n%s",
+ if occurrences != 3 {
+ t.Errorf("Expected exactly 3 occurrences of pinned setup-go ref %q (run_operation + create_labels + compile_workflows), got %d in:\n%s",
setupGoPin, occurrences, yaml)
}
})
diff --git a/scratchpad/labels.md b/scratchpad/labels.md
index 0f9ea020ff..6e07c1ab71 100644
--- a/scratchpad/labels.md
+++ b/scratchpad/labels.md
@@ -39,6 +39,10 @@ Labels help organize and triage issues for better project management. Use labels
- **ai-inspected** - Issue reviewed by AI workflow
- **smoke-copilot** - Smoke test results
+### Domain Labels (used by scheduled workflows)
+- **constraint-solving** - Constraint solving problems and algorithms (used by Constraint Solving POTD workflow)
+- **problem-of-the-day** - Problem of the day (used by Constraint Solving POTD workflow)
+
### Status Labels
- **good first issue** - Suitable for new contributors
- **dependencies** - Dependency updates
@@ -108,6 +112,7 @@ Type: bug, enhancement, documentation, question, testing
Priority: priority-high, priority-medium, priority-low
Component: cli, workflow, mcp, actions, engine, automation
Workflow: ai-generated, plan, ai-inspected, smoke-copilot
+Domain: constraint-solving, problem-of-the-day
Status: good first issue, dependencies
```