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
13 changes: 13 additions & 0 deletions .github/aw/github-agentic-workflows.md

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

51 changes: 50 additions & 1 deletion .github/workflows/agentics-maintenance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
139 changes: 139 additions & 0 deletions actions/setup/js/create_labels.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// @ts-check
/// <reference types="@actions/github-script" />

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");
Comment on lines +8 to +23
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

The deterministic color docs are internally inconsistent: the function comment says pastel range is 128–191 per channel, but the inline comment says 128–223. The code uses 0x3f (0–63), so the actual range is 128–191. Please align the comment(s) with the actual range to avoid confusion.

Copilot uses AI. Check for mistakes.
}

/**
* 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<void>}
*/
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<string>} */
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(", ")}`);
Comment on lines +35 to +87
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

There are extensive unit tests for other actions/setup/js/*.cjs handlers, but create_labels.cjs is currently untested. Adding a focused test file (e.g., for label extraction from compile JSON, deterministic color output, and error handling paths like 422 already-exists vs other 422 validation failures) would reduce regression risk for this new maintenance operation.

Copilot uses AI. Check for mistakes.

// 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)}`);
}
Comment on lines +124 to +131
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

Treating any HTTP 422 from issues.createLabel as “label already exists (race)” is too broad: 422 is also returned for other validation failures (invalid name/color, etc.). Consider checking the error payload/message for an “already exists” condition (e.g., response data errors/code) and only then downgrading to info; otherwise, surface it as a real warning/failure.

Copilot uses AI. Check for mistakes.
}
}
}

core.info(`Done: ${created} label(s) created, ${skipped} already existed`);
}

module.exports = { main };
1 change: 1 addition & 0 deletions pkg/cli/compile_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
1 change: 1 addition & 0 deletions pkg/cli/compile_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
})
}
61 changes: 61 additions & 0 deletions pkg/cli/compile_workflow_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Comment on lines +146 to +148
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

Labels are only extracted after a successful compilation. If parsing succeeds but compilation fails (e.g., other validation errors), the JSON output for that workflow will omit labels, which defeats the purpose of compile --json as an inventory for create_labels. Consider extracting labels immediately after parsing (once workflowData is available) so they are present regardless of later compilation/validation failures.

Copilot uses AI. Check for mistakes.
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 {
Comment on lines +153 to +157
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

extractSafeOutputLabels currently scans only create-issue/create-discussion/create-pull-request and add-labels, but there are additional safe-output configs that reference labels (e.g., remove-labels.allowed in pkg/workflow/remove_labels.go:13, update-discussion allowed-labels, missing-tool/missing-data labels, push-to-pull-request-branch labels). If the intent is “all labels referenced in safe-outputs”, this will miss some and create_labels won’t fully eliminate label-missing warnings. Either broaden the scan to all label-bearing safe-output configs or narrow/update the stated scope accordingly.

Copilot uses AI. Check for mistakes.
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
}
50 changes: 48 additions & 2 deletions pkg/workflow/maintenance_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading