From 92bda5a15a0b5f3c82cfb4b64815bc1bb7175148 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:06:14 +0000 Subject: [PATCH 1/4] wip: start secret validation org billing fix Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/changeset.lock.yml | 5 +++++ .github/workflows/craft.lock.yml | 5 +++++ .github/workflows/daily-safeoutputs-git-simulator.lock.yml | 1 - .github/workflows/design-decision-gate.lock.yml | 5 +++++ .github/workflows/mergefest.lock.yml | 5 +++++ .github/workflows/necromancer.lock.yml | 5 +++++ .github/workflows/poem-bot.lock.yml | 5 +++++ .github/workflows/pr-sous-chef.lock.yml | 5 +++++ .github/workflows/smoke-claude.lock.yml | 5 +++++ .github/workflows/smoke-update-cross-repo-pr.lock.yml | 5 +++++ .github/workflows/tidy.lock.yml | 5 +++++ 11 files changed, 50 insertions(+), 1 deletion(-) diff --git a/.github/workflows/changeset.lock.yml b/.github/workflows/changeset.lock.yml index 297ca263df7..d4cc0e16a72 100644 --- a/.github/workflows/changeset.lock.yml +++ b/.github/workflows/changeset.lock.yml @@ -642,6 +642,11 @@ jobs: "push_to_pull_request_branch": { "defaultMax": 1, "fields": { + "branch": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, "message": { "required": true, "type": "string", diff --git a/.github/workflows/craft.lock.yml b/.github/workflows/craft.lock.yml index 03c9cd64a8e..3ca3964be3f 100644 --- a/.github/workflows/craft.lock.yml +++ b/.github/workflows/craft.lock.yml @@ -668,6 +668,11 @@ jobs: "push_to_pull_request_branch": { "defaultMax": 1, "fields": { + "branch": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, "message": { "required": true, "type": "string", diff --git a/.github/workflows/daily-safeoutputs-git-simulator.lock.yml b/.github/workflows/daily-safeoutputs-git-simulator.lock.yml index 6d7eb70fee9..725c85f3011 100644 --- a/.github/workflows/daily-safeoutputs-git-simulator.lock.yml +++ b/.github/workflows/daily-safeoutputs-git-simulator.lock.yml @@ -691,7 +691,6 @@ jobs: "defaultMax": 1, "fields": { "branch": { - "required": true, "type": "string", "sanitize": true, "maxLength": 256 diff --git a/.github/workflows/design-decision-gate.lock.yml b/.github/workflows/design-decision-gate.lock.yml index c9ae9c15040..c29289f8c36 100644 --- a/.github/workflows/design-decision-gate.lock.yml +++ b/.github/workflows/design-decision-gate.lock.yml @@ -707,6 +707,11 @@ jobs: "push_to_pull_request_branch": { "defaultMax": 1, "fields": { + "branch": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, "message": { "required": true, "type": "string", diff --git a/.github/workflows/mergefest.lock.yml b/.github/workflows/mergefest.lock.yml index 3606604b78c..04d3887eafe 100644 --- a/.github/workflows/mergefest.lock.yml +++ b/.github/workflows/mergefest.lock.yml @@ -645,6 +645,11 @@ jobs: "push_to_pull_request_branch": { "defaultMax": 1, "fields": { + "branch": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, "message": { "required": true, "type": "string", diff --git a/.github/workflows/necromancer.lock.yml b/.github/workflows/necromancer.lock.yml index 7654e145301..b24e144ea3f 100644 --- a/.github/workflows/necromancer.lock.yml +++ b/.github/workflows/necromancer.lock.yml @@ -681,6 +681,11 @@ jobs: "push_to_pull_request_branch": { "defaultMax": 1, "fields": { + "branch": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, "message": { "required": true, "type": "string", diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index 9cd8daecaea..71daf2ab9da 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -924,6 +924,11 @@ jobs: "push_to_pull_request_branch": { "defaultMax": 1, "fields": { + "branch": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, "message": { "required": true, "type": "string", diff --git a/.github/workflows/pr-sous-chef.lock.yml b/.github/workflows/pr-sous-chef.lock.yml index 36e2f8d58d1..9c15ba75e8e 100644 --- a/.github/workflows/pr-sous-chef.lock.yml +++ b/.github/workflows/pr-sous-chef.lock.yml @@ -664,6 +664,11 @@ jobs: "push_to_pull_request_branch": { "defaultMax": 1, "fields": { + "branch": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, "message": { "required": true, "type": "string", diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 90e281a79ec..42adfa5773a 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -1164,6 +1164,11 @@ jobs: "push_to_pull_request_branch": { "defaultMax": 1, "fields": { + "branch": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, "message": { "required": true, "type": "string", diff --git a/.github/workflows/smoke-update-cross-repo-pr.lock.yml b/.github/workflows/smoke-update-cross-repo-pr.lock.yml index ce024421e68..13eb2d2c98b 100644 --- a/.github/workflows/smoke-update-cross-repo-pr.lock.yml +++ b/.github/workflows/smoke-update-cross-repo-pr.lock.yml @@ -767,6 +767,11 @@ jobs: "push_to_pull_request_branch": { "defaultMax": 1, "fields": { + "branch": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, "message": { "required": true, "type": "string", diff --git a/.github/workflows/tidy.lock.yml b/.github/workflows/tidy.lock.yml index 019a2096f05..f87b9a75a39 100644 --- a/.github/workflows/tidy.lock.yml +++ b/.github/workflows/tidy.lock.yml @@ -704,6 +704,11 @@ jobs: "push_to_pull_request_branch": { "defaultMax": 1, "fields": { + "branch": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, "message": { "required": true, "type": "string", From 9462da4052df283aebf6a4fc6ec206f2b2aeadcb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:17:54 +0000 Subject: [PATCH 2/4] Account for copilot org billing mode in secret validation Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/validate_secrets.cjs | 28 +++- actions/setup/js/validate_secrets.test.cjs | 33 +++++ pkg/workflow/maintenance_workflow.go | 27 ++++ pkg/workflow/maintenance_workflow_test.go | 153 +++++++++++++++++++++ pkg/workflow/maintenance_workflow_yaml.go | 16 ++- 5 files changed, 253 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/validate_secrets.cjs b/actions/setup/js/validate_secrets.cjs index d9e0bb421d1..684bb381059 100644 --- a/actions/setup/js/validate_secrets.cjs +++ b/actions/setup/js/validate_secrets.cjs @@ -244,6 +244,30 @@ async function testGitHubGraphQLAPI(token, owner, repo) { } } +/** + * Test Copilot token availability, accounting for copilot org billing mode. + * + * When all repository workflows use `copilot-requests: write`, the built-in + * GITHUB_TOKEN is used for Copilot authentication and no separate + * COPILOT_GITHUB_TOKEN secret is required. In that case the maintenance + * workflow sets GH_AW_COPILOT_ORG_BILLING="true" and validation is skipped + * rather than flagging the missing token as unconfigured. + * + * @param {string | undefined} token - Value of GH_AW_COPILOT_TOKEN + * @param {boolean} orgBilling - Whether copilot org billing mode is active (GH_AW_COPILOT_ORG_BILLING="true") + * @returns {Promise<{status: string, message: string, details?: any}>} + */ +async function testCopilotToken(token, orgBilling) { + if (!token && orgBilling) { + return { + status: Status.SKIPPED, + message: "Copilot org billing mode — GITHUB_TOKEN is used for Copilot authentication; COPILOT_GITHUB_TOKEN is not required", + details: { note: "Set copilot-requests: write in your workflow permissions to use the built-in GITHUB_TOKEN" }, + }; + } + return testCopilotCLI(token); +} + /** * Test Copilot CLI availability * @param {string | undefined} token @@ -665,7 +689,8 @@ async function main() { // Test GH_AW_COPILOT_TOKEN core.info("Testing GH_AW_COPILOT_TOKEN..."); const copilotToken = process.env.GH_AW_COPILOT_TOKEN; - const copilotResult = await testCopilotCLI(copilotToken); + const copilotOrgBilling = process.env.GH_AW_COPILOT_ORG_BILLING === "true"; + const copilotResult = await testCopilotToken(copilotToken, copilotOrgBilling); results.push({ secret: "GH_AW_COPILOT_TOKEN", test: "Copilot CLI Availability", @@ -756,6 +781,7 @@ module.exports = { testGitHubRESTAPI, testGitHubGraphQLAPI, testCopilotCLI, + testCopilotToken, testAnthropicAPI, testOpenAIAPI, testBraveSearchAPI, diff --git a/actions/setup/js/validate_secrets.test.cjs b/actions/setup/js/validate_secrets.test.cjs index 64fff8daf1b..d3dada3c9fe 100644 --- a/actions/setup/js/validate_secrets.test.cjs +++ b/actions/setup/js/validate_secrets.test.cjs @@ -8,6 +8,7 @@ import { testGitHubRESTAPI, testGitHubGraphQLAPI, testCopilotCLI, + testCopilotToken, testAnthropicAPI, testOpenAIAPI, testBraveSearchAPI, @@ -55,6 +56,38 @@ describe("validate_secrets", () => { }); }); + describe("testCopilotToken", () => { + it("should return SKIPPED when token is not set and org billing is active", async () => { + const result = await testCopilotToken("", true); + expect(result.status).toBe("skipped"); + expect(result.message).toContain("org billing"); + }); + + it("should return SKIPPED when token is undefined and org billing is active", async () => { + const result = await testCopilotToken(undefined, true); + expect(result.status).toBe("skipped"); + expect(result.message).toContain("GITHUB_TOKEN"); + }); + + it("should return NOT_SET when token is not set and org billing is not active", async () => { + const result = await testCopilotToken("", false); + expect(result.status).toBe("not_set"); + expect(result.message).toBe("Token not set"); + }); + + it("should delegate to testCopilotCLI when token is set regardless of org billing", async () => { + // testCopilotCLI with a non-empty token checks CLI availability (skipped if not installed) + const result = await testCopilotToken("some-token", true); + // Result should be skipped or success depending on environment, but NOT the org billing skip + expect(result.message).not.toContain("org billing"); + }); + + it("should not suppress warning when token is missing and org billing is false", async () => { + const result = await testCopilotToken(undefined, false); + expect(result.status).toBe("not_set"); + }); + }); + describe("testAnthropicAPI", () => { it("should return NOT_SET when API key is not provided", async () => { const result = await testAnthropicAPI(""); diff --git a/pkg/workflow/maintenance_workflow.go b/pkg/workflow/maintenance_workflow.go index 1ecd4adc59e..7717b387662 100644 --- a/pkg/workflow/maintenance_workflow.go +++ b/pkg/workflow/maintenance_workflow.go @@ -232,6 +232,7 @@ func GenerateMaintenanceWorkflow(ctx context.Context, opts GenerateMaintenanceWo enableCompileCreatePullRequest, strings.TrimSpace(compileGitHubTokenSecret) != "", ) + copilotOrgBilling := allCopilotWorkflowsUseOrgBilling(workflowDataList) content := buildMaintenanceWorkflowYAML(ctx, buildMaintenanceWorkflowYAMLOptions{ cronSchedule: cronSchedule, scheduleDesc: scheduleDesc, @@ -246,6 +247,7 @@ func GenerateMaintenanceWorkflow(ctx context.Context, opts GenerateMaintenanceWo disableLabelTrigger: disableLabelTrigger, compileGitHubToken: getEffectiveMaintenanceGitHubToken(compileGitHubTokenSecret), createCompilePR: enableCompileCreatePullRequest, + copilotOrgBilling: copilotOrgBilling, }) // Write the maintenance workflow file @@ -310,6 +312,31 @@ func handleMaintenanceDisabled(workflowDataList []*WorkflowData, workflowDir str return nil } +// allCopilotWorkflowsUseOrgBilling reports whether all Copilot-engine workflows +// in the list have copilot-requests: write set. This indicates org billing mode, +// where the GITHUB_TOKEN is used for Copilot authentication and the +// COPILOT_GITHUB_TOKEN secret is not required. +// Returns false if no Copilot workflows are found (billing mode is indeterminate) +// or if any Copilot workflow does not have copilot-requests: write set. +func allCopilotWorkflowsUseOrgBilling(workflowDataList []*WorkflowData) bool { + copilotCount := 0 + for _, data := range workflowDataList { + if data == nil { + continue + } + engineID := ResolveEngineID(data) + // Default engine (empty string) is Copilot, as is an explicit "copilot" ID. + if engineID != "" && engineID != string(constants.CopilotEngine) { + continue + } + copilotCount++ + if !hasCopilotRequestsWritePermission(data) { + return false + } + } + return copilotCount > 0 +} + // scanWorkflowsForExpires checks all workflow data for expires fields and returns // whether any expires fields are set and the minimum expires value in hours. func scanWorkflowsForExpires(workflowDataList []*WorkflowData) (bool, int) { diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go index 18fb7cdf937..36a44bc701e 100644 --- a/pkg/workflow/maintenance_workflow_test.go +++ b/pkg/workflow/maintenance_workflow_test.go @@ -2305,3 +2305,156 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) { } }) } + +func TestAllCopilotWorkflowsUseOrgBilling(t *testing.T) { + orgBillingPermission := "permissions:\n copilot-requests: write" + + t.Run("empty list returns false", func(t *testing.T) { + result := allCopilotWorkflowsUseOrgBilling([]*WorkflowData{}) + if result { + t.Error("Expected false for empty list") + } + }) + + t.Run("nil entries are skipped", func(t *testing.T) { + result := allCopilotWorkflowsUseOrgBilling([]*WorkflowData{nil, nil}) + if result { + t.Error("Expected false when all entries are nil") + } + }) + + t.Run("single copilot workflow with org billing returns true", func(t *testing.T) { + data := []*WorkflowData{ + {Name: "wf1", Permissions: orgBillingPermission}, + } + if !allCopilotWorkflowsUseOrgBilling(data) { + t.Error("Expected true when single Copilot workflow has copilot-requests: write") + } + }) + + t.Run("single copilot workflow without org billing returns false", func(t *testing.T) { + data := []*WorkflowData{ + {Name: "wf1"}, + } + if allCopilotWorkflowsUseOrgBilling(data) { + t.Error("Expected false when Copilot workflow lacks copilot-requests: write") + } + }) + + t.Run("all copilot workflows with org billing returns true", func(t *testing.T) { + data := []*WorkflowData{ + {Name: "wf1", Permissions: orgBillingPermission}, + {Name: "wf2", Permissions: orgBillingPermission}, + } + if !allCopilotWorkflowsUseOrgBilling(data) { + t.Error("Expected true when all Copilot workflows have copilot-requests: write") + } + }) + + t.Run("mixed copilot workflows returns false", func(t *testing.T) { + data := []*WorkflowData{ + {Name: "wf1", Permissions: orgBillingPermission}, + {Name: "wf2"}, + } + if allCopilotWorkflowsUseOrgBilling(data) { + t.Error("Expected false when only some Copilot workflows have copilot-requests: write") + } + }) + + t.Run("non-copilot engines are ignored", func(t *testing.T) { + data := []*WorkflowData{ + {Name: "wf1", Permissions: orgBillingPermission}, + {Name: "wf2", EngineConfig: &EngineConfig{ID: "openai"}}, + } + if !allCopilotWorkflowsUseOrgBilling(data) { + t.Error("Expected true when non-Copilot workflows are ignored and all Copilot workflows have org billing") + } + }) + + t.Run("only non-copilot engines returns false", func(t *testing.T) { + data := []*WorkflowData{ + {Name: "wf1", EngineConfig: &EngineConfig{ID: "openai"}}, + } + if allCopilotWorkflowsUseOrgBilling(data) { + t.Error("Expected false when no Copilot workflows found") + } + }) +} + +func TestGenerateMaintenanceWorkflow_CopilotOrgBilling(t *testing.T) { + orgBillingPermission := "permissions:\n copilot-requests: write" + // SafeOutputs with Expires is required for the maintenance workflow to be generated. + safeOutputsWithExpires := &SafeOutputsConfig{ + CreateIssues: &CreateIssuesConfig{Expires: 48}, + } + + t.Run("org billing mode sets GH_AW_COPILOT_ORG_BILLING in secret-validation step", func(t *testing.T) { + tmpDir := t.TempDir() + workflowDataList := []*WorkflowData{ + {Name: "wf1", Permissions: orgBillingPermission, SafeOutputs: safeOutputsWithExpires}, + } + err := GenerateMaintenanceWorkflow(context.Background(), GenerateMaintenanceWorkflowOptions{ + WorkflowDataList: workflowDataList, + WorkflowDir: tmpDir, + Version: "v1.0.0", + ActionMode: ActionModeDev, + }) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + content, err := os.ReadFile(filepath.Join(tmpDir, "agentics-maintenance.yml")) + if err != nil { + t.Fatalf("Expected maintenance workflow to be generated: %v", err) + } + if !strings.Contains(string(content), `GH_AW_COPILOT_ORG_BILLING: "true"`) { + t.Errorf("Expected GH_AW_COPILOT_ORG_BILLING to be set in org billing mode, got:\n%s", string(content)) + } + }) + + t.Run("non-org billing mode does not set GH_AW_COPILOT_ORG_BILLING", func(t *testing.T) { + tmpDir := t.TempDir() + workflowDataList := []*WorkflowData{ + {Name: "wf1", SafeOutputs: safeOutputsWithExpires}, + } + err := GenerateMaintenanceWorkflow(context.Background(), GenerateMaintenanceWorkflowOptions{ + WorkflowDataList: workflowDataList, + WorkflowDir: tmpDir, + Version: "v1.0.0", + ActionMode: ActionModeDev, + }) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + content, err := os.ReadFile(filepath.Join(tmpDir, "agentics-maintenance.yml")) + if err != nil { + t.Fatalf("Expected maintenance workflow to be generated: %v", err) + } + if strings.Contains(string(content), "GH_AW_COPILOT_ORG_BILLING") { + t.Errorf("Expected GH_AW_COPILOT_ORG_BILLING to be absent in non-org billing mode, got:\n%s", string(content)) + } + }) + + t.Run("mixed billing mode does not set GH_AW_COPILOT_ORG_BILLING", func(t *testing.T) { + tmpDir := t.TempDir() + workflowDataList := []*WorkflowData{ + {Name: "wf1", Permissions: orgBillingPermission, SafeOutputs: safeOutputsWithExpires}, + {Name: "wf2", SafeOutputs: safeOutputsWithExpires}, + } + err := GenerateMaintenanceWorkflow(context.Background(), GenerateMaintenanceWorkflowOptions{ + WorkflowDataList: workflowDataList, + WorkflowDir: tmpDir, + Version: "v1.0.0", + ActionMode: ActionModeDev, + }) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + content, err := os.ReadFile(filepath.Join(tmpDir, "agentics-maintenance.yml")) + if err != nil { + t.Fatalf("Expected maintenance workflow to be generated: %v", err) + } + if strings.Contains(string(content), "GH_AW_COPILOT_ORG_BILLING") { + t.Errorf("Expected GH_AW_COPILOT_ORG_BILLING to be absent in mixed billing mode, got:\n%s", string(content)) + } + }) +} diff --git a/pkg/workflow/maintenance_workflow_yaml.go b/pkg/workflow/maintenance_workflow_yaml.go index 699362fbf5a..fef7520b2d1 100644 --- a/pkg/workflow/maintenance_workflow_yaml.go +++ b/pkg/workflow/maintenance_workflow_yaml.go @@ -25,6 +25,7 @@ type buildMaintenanceWorkflowYAMLOptions struct { disableLabelTrigger bool compileGitHubToken string createCompilePR bool + copilotOrgBilling bool // all Copilot workflows use copilot-requests: write (GITHUB_TOKEN); COPILOT_GITHUB_TOKEN is not required } // buildMaintenanceWorkflowYAML generates the complete YAML content for the @@ -47,7 +48,8 @@ func buildMaintenanceWorkflowYAML( disableLabelTrigger := opts.disableLabelTrigger compileGitHubToken := opts.compileGitHubToken createCompilePR := opts.createCompilePR - maintenanceWorkflowYAMLLog.Printf("Building maintenance workflow YAML: actionMode=%s minExpiresDays=%d cronSchedule=%q defaultBranch=%q disableLabelTrigger=%v createCompilePR=%v", actionMode, minExpiresDays, cronSchedule, defaultBranch, disableLabelTrigger, createCompilePR) + copilotOrgBilling := opts.copilotOrgBilling + maintenanceWorkflowYAMLLog.Printf("Building maintenance workflow YAML: actionMode=%s minExpiresDays=%d cronSchedule=%q defaultBranch=%q disableLabelTrigger=%v createCompilePR=%v copilotOrgBilling=%v", actionMode, minExpiresDays, cronSchedule, defaultBranch, disableLabelTrigger, createCompilePR, copilotOrgBilling) var yaml strings.Builder @@ -945,7 +947,15 @@ jobs: with: destination: ${{ runner.temp }}/gh-aw/actions - - name: Validate Secrets +`) + // Build the Validate Secrets step, conditionally including the org billing flag. + copilotOrgBillingLine := "" + if copilotOrgBilling { + maintenanceWorkflowYAMLLog.Print("Copilot org billing mode detected: adding GH_AW_COPILOT_ORG_BILLING=true to secret-validation step") + copilotOrgBillingLine = ` GH_AW_COPILOT_ORG_BILLING: "true" +` + } + yaml.WriteString(` - name: Validate Secrets uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + ` env: # GitHub tokens @@ -953,7 +963,7 @@ jobs: GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} GH_AW_PROJECT_GITHUB_TOKEN: ${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }} GH_AW_COPILOT_TOKEN: ${{ secrets.GH_AW_COPILOT_TOKEN }} - # AI Engine API keys +` + copilotOrgBillingLine + ` # AI Engine API keys ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} From 2fcbf80ffabc6b788524a6a1b3e8574a770e4838 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:21:53 +0000 Subject: [PATCH 3/4] Address code review: clarify org billing note message and add indentation comment Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/validate_secrets.cjs | 2 +- pkg/workflow/maintenance_workflow_yaml.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/validate_secrets.cjs b/actions/setup/js/validate_secrets.cjs index 684bb381059..f4e27de1eae 100644 --- a/actions/setup/js/validate_secrets.cjs +++ b/actions/setup/js/validate_secrets.cjs @@ -262,7 +262,7 @@ async function testCopilotToken(token, orgBilling) { return { status: Status.SKIPPED, message: "Copilot org billing mode — GITHUB_TOKEN is used for Copilot authentication; COPILOT_GITHUB_TOKEN is not required", - details: { note: "Set copilot-requests: write in your workflow permissions to use the built-in GITHUB_TOKEN" }, + details: { note: "copilot-requests: write is set in the workflow permissions, so the built-in GITHUB_TOKEN handles Copilot authentication" }, }; } return testCopilotCLI(token); diff --git a/pkg/workflow/maintenance_workflow_yaml.go b/pkg/workflow/maintenance_workflow_yaml.go index fef7520b2d1..f86fc6ba427 100644 --- a/pkg/workflow/maintenance_workflow_yaml.go +++ b/pkg/workflow/maintenance_workflow_yaml.go @@ -949,6 +949,7 @@ jobs: `) // Build the Validate Secrets step, conditionally including the org billing flag. + // The line uses 10-space indentation to match the surrounding env block structure. copilotOrgBillingLine := "" if copilotOrgBilling { maintenanceWorkflowYAMLLog.Print("Copilot org billing mode detected: adding GH_AW_COPILOT_ORG_BILLING=true to secret-validation step") From a5c9974df43d5aeba585ee5190f72a9efe2c41e7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:33:43 +0000 Subject: [PATCH 4/4] docs(adr): add draft ADR-38459 for Copilot org billing detection Co-Authored-By: Claude Opus 4.8 (1M context) --- ...ect-copilot-org-billing-at-compile-time.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/adr/38459-detect-copilot-org-billing-at-compile-time.md diff --git a/docs/adr/38459-detect-copilot-org-billing-at-compile-time.md b/docs/adr/38459-detect-copilot-org-billing-at-compile-time.md new file mode 100644 index 00000000000..55f0ed9ecfb --- /dev/null +++ b/docs/adr/38459-detect-copilot-org-billing-at-compile-time.md @@ -0,0 +1,40 @@ +# ADR-38459: Detect Copilot Org Billing at Compile Time to Skip Secret Validation + +**Date**: 2026-06-10 +**Status**: Draft + +## Context + +When a repository uses Copilot *org billing* (every Copilot-engine workflow declares `copilot-requests: write`), the built-in `GITHUB_TOKEN` serves as the Copilot authentication token and no separate `COPILOT_GITHUB_TOKEN` secret exists. The generated maintenance workflow's "Validate Secrets" step nonetheless tested for `GH_AW_COPILOT_TOKEN` and reported it as `NOT_SET`, producing a false warning on every maintenance run for org-billing repos. The compiler already has full visibility into all workflows and their permissions at generation time, so the billing mode can be determined statically rather than guessed at runtime. + +## Decision + +We decided to detect org billing mode at compile time and propagate it to the runtime validator. A new Go helper `allCopilotWorkflowsUseOrgBilling()` returns `true` only when every Copilot-engine workflow in the repo sets `copilot-requests: write` (non-Copilot engines are ignored; empty or mixed sets return `false`). When `true`, the generated maintenance YAML injects `GH_AW_COPILOT_ORG_BILLING: "true"` into the Validate Secrets step's env block. The JS validator gains `testCopilotToken(token, orgBilling)`, which returns `SKIPPED` (not `NOT_SET`) when the token is absent and org billing is active, and otherwise delegates to the existing `testCopilotCLI`. + +## Alternatives Considered + +### Alternative 1: Runtime-only detection in the validator +The JS validator could inspect the live token/permissions at runtime to infer billing mode, avoiding a generated env flag. Rejected because the validator runs inside a single maintenance workflow and cannot reliably observe the permissions of *all* Copilot workflows in the repo; the compiler already has that complete view, making compile-time detection both simpler and more accurate. + +### Alternative 2: Unconditionally suppress the missing-Copilot-token warning +We could drop the `NOT_SET` warning whenever `GH_AW_COPILOT_TOKEN` is absent. Rejected because it would mask genuine misconfiguration in non-org-billing repos, where the missing secret is a real problem the warning is meant to surface. + +## Consequences + +### Positive +- Eliminates the false `NOT_SET` warning for org-billing repos, replacing it with an explanatory `SKIPPED` status. +- The "all Copilot workflows" gate prevents false positives: mixed or empty configurations correctly fall back to the existing warning behavior. +- Decision is fully covered by unit tests on both the Go helper and the JS `testCopilotToken` function. + +### Negative +- Introduces compile-time coupling: the maintenance YAML must be regenerated when a repo's billing mode changes, or the flag will be stale. +- Mixed-billing repos (some workflows with, some without `copilot-requests: write`) still emit the warning, which may be unexpected for partial-migration scenarios. +- Adds a new environment variable (`GH_AW_COPILOT_ORG_BILLING`) to the secret-validation contract that must be kept in sync between the Go generator and the JS validator. + +### Neutral +- The flag is emitted only in the maintenance workflow's Validate Secrets step; other generated steps are unaffected. +- `ResolveEngineID` treats the empty/default engine as Copilot, so default-engine workflows participate in the org-billing determination. + +--- + +*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/27312938315) workflow. The PR author must review, complete, and finalize this document before the PR can merge.*