From e987baf334fbc1d16060928fcbb4edbe59d748ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 10:21:54 +0000 Subject: [PATCH 1/2] Initial plan From aee007966003e9a55db046e06f3343fd74ec7901 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 10:38:56 +0000 Subject: [PATCH 2/2] Add project-github-token support for campaigns - Add ProjectGitHubToken field to CampaignSpec struct - Pass github-token through BuildOrchestrator to UpdateProjectConfig - Serialize github-token in generated .g.campaign.md files - Add tests for github-token configuration and serialization - Update existing campaign specs with token configuration Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- ...ze-reduction-project64.campaign.g.lock.yml | 2 +- ...ile-size-reduction-project64.campaign.g.md | 1 + ...-file-size-reduction-project64.campaign.md | 1 + ...go-file-size-reduction.campaign.g.lock.yml | 2 +- .../go-file-size-reduction.campaign.g.md | 1 + .../go-file-size-reduction.campaign.md | 1 + pkg/campaign/orchestrator.go | 9 +- pkg/campaign/orchestrator_test.go | 73 ++++++++++++ pkg/campaign/spec.go | 6 + pkg/cli/compile_campaign_orchestrator_test.go | 110 ++++++++++++++++++ pkg/cli/compile_orchestrator.go | 7 +- 11 files changed, 209 insertions(+), 4 deletions(-) diff --git a/.github/workflows/go-file-size-reduction-project64.campaign.g.lock.yml b/.github/workflows/go-file-size-reduction-project64.campaign.g.lock.yml index 88dbd77277..7f9d683617 100644 --- a/.github/workflows/go-file-size-reduction-project64.campaign.g.lock.yml +++ b/.github/workflows/go-file-size-reduction-project64.campaign.g.lock.yml @@ -7706,7 +7706,7 @@ jobs: GH_AW_WORKFLOW_NAME: "Go File Size Reduction Campaign (Project 64)" GH_AW_ENGINE_ID: "copilot" with: - github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + github-token: ${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }} script: | globalThis.github = github; globalThis.context = context; diff --git a/.github/workflows/go-file-size-reduction-project64.campaign.g.md b/.github/workflows/go-file-size-reduction-project64.campaign.g.md index 2b606493a1..9daa5e85ec 100644 --- a/.github/workflows/go-file-size-reduction-project64.campaign.g.md +++ b/.github/workflows/go-file-size-reduction-project64.campaign.g.md @@ -10,6 +10,7 @@ safe-outputs: add-comment: max: 10 update-project: + github-token: ${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }} max: 10 runs-on: ubuntu-latest roles: diff --git a/.github/workflows/go-file-size-reduction-project64.campaign.md b/.github/workflows/go-file-size-reduction-project64.campaign.md index 694e067058..5d6b1278d6 100644 --- a/.github/workflows/go-file-size-reduction-project64.campaign.md +++ b/.github/workflows/go-file-size-reduction-project64.campaign.md @@ -5,6 +5,7 @@ name: "Go File Size Reduction Campaign (Project 64)" description: "Systematically reduce oversized Go files to improve maintainability. Success: all files ≤800 LOC, maintain coverage, no regressions." project-url: "https://github.com/orgs/githubnext/projects/64" +project-github-token: "${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}" workflows: - daily-file-diet diff --git a/.github/workflows/go-file-size-reduction.campaign.g.lock.yml b/.github/workflows/go-file-size-reduction.campaign.g.lock.yml index 6a355f3577..08ade5b31f 100644 --- a/.github/workflows/go-file-size-reduction.campaign.g.lock.yml +++ b/.github/workflows/go-file-size-reduction.campaign.g.lock.yml @@ -7706,7 +7706,7 @@ jobs: GH_AW_WORKFLOW_NAME: "Go File Size Reduction Campaign" GH_AW_ENGINE_ID: "copilot" with: - github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + github-token: ${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }} script: | globalThis.github = github; globalThis.context = context; diff --git a/.github/workflows/go-file-size-reduction.campaign.g.md b/.github/workflows/go-file-size-reduction.campaign.g.md index d54d71337b..64e5c0b71a 100644 --- a/.github/workflows/go-file-size-reduction.campaign.g.md +++ b/.github/workflows/go-file-size-reduction.campaign.g.md @@ -10,6 +10,7 @@ safe-outputs: add-comment: max: 10 update-project: + github-token: ${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }} max: 10 runs-on: ubuntu-latest roles: diff --git a/.github/workflows/go-file-size-reduction.campaign.md b/.github/workflows/go-file-size-reduction.campaign.md index 47f8ca57b1..eb796e1575 100644 --- a/.github/workflows/go-file-size-reduction.campaign.md +++ b/.github/workflows/go-file-size-reduction.campaign.md @@ -5,6 +5,7 @@ name: "Go File Size Reduction Campaign" description: "Reduce oversized non-test Go files under pkg/ to ≤800 LOC via tracked refactors, with daily metrics snapshots and a GitHub Projects dashboard." project-url: "https://github.com/orgs/githubnext/projects/60" +project-github-token: "${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}" workflows: - daily-file-diet diff --git a/pkg/campaign/orchestrator.go b/pkg/campaign/orchestrator.go index f683938134..30ad6b8e54 100644 --- a/pkg/campaign/orchestrator.go +++ b/pkg/campaign/orchestrator.go @@ -104,7 +104,14 @@ func BuildOrchestrator(spec *CampaignSpec, campaignFilePath string) (*workflow.W // Always allow commenting on tracker issues (or other issues/PRs if needed). safeOutputs.AddComments = &workflow.AddCommentsConfig{BaseSafeOutputConfig: workflow.BaseSafeOutputConfig{Max: 10}} // Allow updating the campaign's GitHub Project dashboard. - safeOutputs.UpdateProjects = &workflow.UpdateProjectConfig{BaseSafeOutputConfig: workflow.BaseSafeOutputConfig{Max: 10}} + updateProjectConfig := &workflow.UpdateProjectConfig{BaseSafeOutputConfig: workflow.BaseSafeOutputConfig{Max: 10}} + // If the campaign spec specifies a custom GitHub token for Projects v2 operations, + // pass it to the update-project configuration. + if strings.TrimSpace(spec.ProjectGitHubToken) != "" { + updateProjectConfig.GitHubToken = strings.TrimSpace(spec.ProjectGitHubToken) + orchestratorLog.Printf("Campaign orchestrator '%s' configured with custom GitHub token for update-project", spec.ID) + } + safeOutputs.UpdateProjects = updateProjectConfig orchestratorLog.Printf("Campaign orchestrator '%s' built successfully with safe outputs enabled", spec.ID) diff --git a/pkg/campaign/orchestrator_test.go b/pkg/campaign/orchestrator_test.go index 7de6fb8448..e69462b89f 100644 --- a/pkg/campaign/orchestrator_test.go +++ b/pkg/campaign/orchestrator_test.go @@ -160,3 +160,76 @@ func TestBuildOrchestrator_TrackerIDMonitoring(t *testing.T) { t.Errorf("expected markdown to contain Phase 4: Report, got: %q", data.MarkdownContent) } } + +func TestBuildOrchestrator_GitHubToken(t *testing.T) { + t.Run("with custom github token", func(t *testing.T) { + spec := &CampaignSpec{ + ID: "test-campaign-with-token", + Name: "Test Campaign", + Description: "A test campaign with custom GitHub token", + ProjectURL: "https://github.com/orgs/test/projects/1", + Workflows: []string{"test-workflow"}, + TrackerLabel: "campaign:test", + ProjectGitHubToken: "${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}", + } + + mdPath := ".github/workflows/test-campaign.campaign.md" + data, _ := BuildOrchestrator(spec, mdPath) + + if data == nil { + t.Fatalf("expected non-nil WorkflowData") + } + + // Verify that SafeOutputs is configured + if data.SafeOutputs == nil { + t.Fatalf("expected SafeOutputs to be configured") + } + + // Verify that UpdateProjects is configured + if data.SafeOutputs.UpdateProjects == nil { + t.Fatalf("expected UpdateProjects to be configured") + } + + // Verify that the GitHubToken is set + if data.SafeOutputs.UpdateProjects.GitHubToken != "${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}" { + t.Errorf("expected GitHubToken to be %q, got %q", + "${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}", + data.SafeOutputs.UpdateProjects.GitHubToken) + } + }) + + t.Run("without custom github token", func(t *testing.T) { + spec := &CampaignSpec{ + ID: "test-campaign-no-token", + Name: "Test Campaign", + Description: "A test campaign without custom GitHub token", + ProjectURL: "https://github.com/orgs/test/projects/1", + Workflows: []string{"test-workflow"}, + TrackerLabel: "campaign:test", + // ProjectGitHubToken is intentionally omitted + } + + mdPath := ".github/workflows/test-campaign.campaign.md" + data, _ := BuildOrchestrator(spec, mdPath) + + if data == nil { + t.Fatalf("expected non-nil WorkflowData") + } + + // Verify that SafeOutputs is configured + if data.SafeOutputs == nil { + t.Fatalf("expected SafeOutputs to be configured") + } + + // Verify that UpdateProjects is configured + if data.SafeOutputs.UpdateProjects == nil { + t.Fatalf("expected UpdateProjects to be configured") + } + + // Verify that the GitHubToken is empty when not specified + if data.SafeOutputs.UpdateProjects.GitHubToken != "" { + t.Errorf("expected GitHubToken to be empty when not specified, got %q", + data.SafeOutputs.UpdateProjects.GitHubToken) + } + }) +} diff --git a/pkg/campaign/spec.go b/pkg/campaign/spec.go index f3d35208dc..089eea75a7 100644 --- a/pkg/campaign/spec.go +++ b/pkg/campaign/spec.go @@ -65,6 +65,12 @@ type CampaignSpec struct { // enforced by validation in the future. AllowedSafeOutputs []string `yaml:"allowed-safe-outputs,omitempty" json:"allowed_safe_outputs,omitempty" console:"header:Allowed Safe Outputs,omitempty,maxlen:30"` + // ProjectGitHubToken is an optional GitHub token expression (e.g., + // ${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}) used for GitHub Projects v2 + // operations. When specified, this token is passed to the update-project + // safe output configuration in the generated orchestrator workflow. + ProjectGitHubToken string `yaml:"project-github-token,omitempty" json:"project_github_token,omitempty" console:"header:Project Token,omitempty,maxlen:30"` + // ApprovalPolicy describes high-level approval expectations for this // campaign (for example: number of approvals and required roles). ApprovalPolicy *CampaignApprovalPolicy `yaml:"approval-policy,omitempty" json:"approval-policy,omitempty"` diff --git a/pkg/cli/compile_campaign_orchestrator_test.go b/pkg/cli/compile_campaign_orchestrator_test.go index b926485df3..ecff7b1f53 100644 --- a/pkg/cli/compile_campaign_orchestrator_test.go +++ b/pkg/cli/compile_campaign_orchestrator_test.go @@ -234,3 +234,113 @@ func extractSourcePath(t *testing.T, content string) string { return strings.TrimSpace(content[startIdx : startIdx+endIdx]) } + +// TestCampaignOrchestratorGitHubToken verifies that when a campaign spec includes +// a project-github-token field, it is properly serialized into the generated +// .g.campaign.md file's safe-outputs configuration +func TestCampaignOrchestratorGitHubToken(t *testing.T) { + tmpDir := t.TempDir() + campaignSpecPath := filepath.Join(tmpDir, "test-campaign-with-token.campaign.md") + + // Test case 1: Campaign with custom GitHub token + t.Run("with custom token", func(t *testing.T) { + spec := &campaign.CampaignSpec{ + ID: "test-campaign-with-token", + Name: "Test Campaign With Token", + Description: "A test campaign with custom GitHub token", + Workflows: []string{"example-workflow"}, + TrackerLabel: "campaign:test-campaign-with-token", + MemoryPaths: []string{"memory/campaigns/test-campaign-with-token-*/**"}, + ProjectGitHubToken: "${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}", + } + + compiler := workflow.NewCompiler(false, "", GetVersion()) + compiler.SetSkipValidation(true) + compiler.SetNoEmit(false) + compiler.SetStrictMode(false) + + orchestratorPath, err := generateAndCompileCampaignOrchestrator( + compiler, + spec, + campaignSpecPath, + false, false, false, false, false, false, false, + ) + if err != nil { + t.Fatalf("generateAndCompileCampaignOrchestrator() error: %v", err) + } + + // Read the generated markdown file + mdContent, err := os.ReadFile(orchestratorPath) + if err != nil { + t.Fatalf("failed to read generated markdown: %v", err) + } + mdStr := string(mdContent) + + // Verify the github-token is present in the safe-outputs configuration + if !strings.Contains(mdStr, "github-token:") { + t.Errorf("expected generated markdown to contain 'github-token:' field") + } + + if !strings.Contains(mdStr, "${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}") { + t.Errorf("expected generated markdown to contain the token expression") + } + + // Verify the safe-outputs structure + if !strings.Contains(mdStr, "safe-outputs:") { + t.Errorf("expected generated markdown to contain 'safe-outputs:' section") + } + + if !strings.Contains(mdStr, "update-project:") { + t.Errorf("expected generated markdown to contain 'update-project:' section") + } + }) + + // Test case 2: Campaign without custom GitHub token + t.Run("without custom token", func(t *testing.T) { + spec := &campaign.CampaignSpec{ + ID: "test-campaign-no-token", + Name: "Test Campaign Without Token", + Description: "A test campaign without custom GitHub token", + Workflows: []string{"example-workflow"}, + TrackerLabel: "campaign:test-campaign-no-token", + MemoryPaths: []string{"memory/campaigns/test-campaign-no-token-*/**"}, + // ProjectGitHubToken is intentionally omitted + } + + compiler := workflow.NewCompiler(false, "", GetVersion()) + compiler.SetSkipValidation(true) + compiler.SetNoEmit(false) + compiler.SetStrictMode(false) + + orchestratorPath, err := generateAndCompileCampaignOrchestrator( + compiler, + spec, + filepath.Join(tmpDir, "test-campaign-no-token.campaign.md"), + false, false, false, false, false, false, false, + ) + if err != nil { + t.Fatalf("generateAndCompileCampaignOrchestrator() error: %v", err) + } + + // Read the generated markdown file + mdContent, err := os.ReadFile(orchestratorPath) + if err != nil { + t.Fatalf("failed to read generated markdown: %v", err) + } + mdStr := string(mdContent) + + // Verify the github-token is NOT present when not configured + if strings.Contains(mdStr, "github-token:") { + t.Errorf("expected generated markdown to NOT contain 'github-token:' field when not configured") + } + + // But safe-outputs and update-project should still be present + if !strings.Contains(mdStr, "safe-outputs:") { + t.Errorf("expected generated markdown to contain 'safe-outputs:' section") + } + + if !strings.Contains(mdStr, "update-project:") { + t.Errorf("expected generated markdown to contain 'update-project:' section") + } + }) +} diff --git a/pkg/cli/compile_orchestrator.go b/pkg/cli/compile_orchestrator.go index b5dbdc2aa4..a19c228ae4 100644 --- a/pkg/cli/compile_orchestrator.go +++ b/pkg/cli/compile_orchestrator.go @@ -79,9 +79,14 @@ func renderGeneratedCampaignOrchestratorMarkdown(data *workflow.WorkflowData, so } } if data.SafeOutputs.UpdateProjects != nil { - outputs["update-project"] = map[string]any{ + updateProjectConfig := map[string]any{ "max": data.SafeOutputs.UpdateProjects.Max, } + // Include github-token if specified + if strings.TrimSpace(data.SafeOutputs.UpdateProjects.GitHubToken) != "" { + updateProjectConfig["github-token"] = data.SafeOutputs.UpdateProjects.GitHubToken + } + outputs["update-project"] = updateProjectConfig } if len(outputs) > 0 { payload := map[string]any{"safe-outputs": outputs}