From db01d4ece1c404215122f446c2f929fda482242f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:53:44 +0000 Subject: [PATCH 1/3] Initial plan From eb6b439bcb7fbccc59fcd976529ee7e4fadd16c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:08:43 +0000 Subject: [PATCH 2/3] Add workflow to create missing repository labels: constraint-solving, problem-of-the-day Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d6e9d52e-eb6a-46af-a3fd-d3250276d028 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/create-labels.yml | 54 +++++++++++++++++++++++++++++ scratchpad/labels.md | 5 +++ 2 files changed, 59 insertions(+) create mode 100644 .github/workflows/create-labels.yml diff --git a/.github/workflows/create-labels.yml b/.github/workflows/create-labels.yml new file mode 100644 index 00000000000..bf89a14201a --- /dev/null +++ b/.github/workflows/create-labels.yml @@ -0,0 +1,54 @@ +name: Create Repository Labels + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - '.github/workflows/create-labels.yml' + +permissions: + issues: write + +jobs: + create-labels: + name: Create Missing Labels + runs-on: ubuntu-latest + if: github.repository == 'github/gh-aw' + steps: + - name: Create repository labels + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const labels = [ + { + name: 'constraint-solving', + color: '0075ca', + description: 'Constraint solving problems and algorithms', + }, + { + name: 'problem-of-the-day', + color: 'e4e669', + description: 'Problem of the day', + }, + ]; + + for (const label of labels) { + try { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label.name, + color: label.color, + description: label.description, + }); + core.info(`✅ Created label: ${label.name}`); + } catch (error) { + if (error.status === 422) { + core.info(`ℹ️ Label already exists: ${label.name}`); + } else { + core.setFailed(`Failed to create label '${label.name}': ${error.message}`); + } + } + } diff --git a/scratchpad/labels.md b/scratchpad/labels.md index 0f9ea020ff4..6e07c1ab711 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 ``` From b347244ada7a6501b5f35472745ecacbf522d718 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:47:29 +0000 Subject: [PATCH 3/3] Replace standalone create-labels workflow with compile --json label reporting and agentics-maintenance create_labels operation Agent-Logs-Url: https://github.com/github/gh-aw/sessions/55ecc334-0656-4098-9246-3d7b1c15f4c0 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/aw/github-agentic-workflows.md | 13 ++ .github/workflows/agentics-maintenance.yml | 51 +++++++- .github/workflows/create-labels.yml | 54 -------- actions/setup/js/create_labels.cjs | 139 +++++++++++++++++++++ pkg/cli/compile_config.go | 1 + pkg/cli/compile_validation.go | 1 + pkg/cli/compile_workflow_processor.go | 61 +++++++++ pkg/workflow/maintenance_workflow.go | 50 +++++++- pkg/workflow/maintenance_workflow_test.go | 29 ++++- 9 files changed, 336 insertions(+), 63 deletions(-) delete mode 100644 .github/workflows/create-labels.yml create mode 100644 actions/setup/js/create_labels.cjs diff --git a/.github/aw/github-agentic-workflows.md b/.github/aw/github-agentic-workflows.md index 4ca304d69e4..9759db520c5 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 de967ee6732..68c22092c3b 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/.github/workflows/create-labels.yml b/.github/workflows/create-labels.yml deleted file mode 100644 index bf89a14201a..00000000000 --- a/.github/workflows/create-labels.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Create Repository Labels - -on: - workflow_dispatch: - push: - branches: - - main - paths: - - '.github/workflows/create-labels.yml' - -permissions: - issues: write - -jobs: - create-labels: - name: Create Missing Labels - runs-on: ubuntu-latest - if: github.repository == 'github/gh-aw' - steps: - - name: Create repository labels - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const labels = [ - { - name: 'constraint-solving', - color: '0075ca', - description: 'Constraint solving problems and algorithms', - }, - { - name: 'problem-of-the-day', - color: 'e4e669', - description: 'Problem of the day', - }, - ]; - - for (const label of labels) { - try { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - color: label.color, - description: label.description, - }); - core.info(`✅ Created label: ${label.name}`); - } catch (error) { - if (error.status === 422) { - core.info(`ℹ️ Label already exists: ${label.name}`); - } else { - core.setFailed(`Failed to create label '${label.name}': ${error.message}`); - } - } - } diff --git a/actions/setup/js/create_labels.cjs b/actions/setup/js/create_labels.cjs new file mode 100644 index 00000000000..1e659bf527f --- /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 ea711b5d9a2..c58c942b825 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 915d93ee69b..4148001188c 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 7844ebd8506..a425d9329ec 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 bceef12a29c..c4a4ec588d7 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 3947d0b928c..8c1677d8659 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) } })