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
48 changes: 46 additions & 2 deletions .github/workflows/file-size-reduction-project71.campaign.lock.yml

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

14 changes: 14 additions & 0 deletions pkg/campaign/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"strings"

"github.com/githubnext/gh-aw/pkg/constants"
"github.com/githubnext/gh-aw/pkg/logger"
"github.com/githubnext/gh-aw/pkg/workflow"
"github.com/goccy/go-yaml"
Expand Down Expand Up @@ -355,6 +356,19 @@ func BuildOrchestrator(spec *CampaignSpec, campaignFilePath string) (*workflow.W
}
safeOutputs.CreateProjectStatusUpdates = statusUpdateConfig

// Allow copying GitHub Projects for campaign setup (max: 1, typically used once).
// Default source project is the githubnext "[TEMPLATE: Agentic Campaign]" project.
copyProjectConfig := &workflow.CopyProjectsConfig{
BaseSafeOutputConfig: workflow.BaseSafeOutputConfig{Max: 1},
SourceProject: string(constants.DefaultCampaignTemplateProjectURL),
}
// Use the same custom GitHub token for copy-project as for other project operations.
if strings.TrimSpace(spec.ProjectGitHubToken) != "" {
copyProjectConfig.GitHubToken = strings.TrimSpace(spec.ProjectGitHubToken)
orchestratorLog.Printf("Campaign orchestrator '%s' configured with custom GitHub token for copy-project", spec.ID)
}
safeOutputs.CopyProjects = copyProjectConfig

orchestratorLog.Printf("Campaign orchestrator '%s' built successfully with safe outputs enabled", spec.ID)

// Extract file-glob patterns from memory-paths or metrics-glob to support
Expand Down
76 changes: 76 additions & 0 deletions pkg/campaign/orchestrator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,82 @@ func TestBuildOrchestrator_GovernanceOverridesSafeOutputMaxima(t *testing.T) {
}
}

func TestBuildOrchestrator_CopyProjectSafeOutput(t *testing.T) {
t.Run("copy-project enabled by default with template source", func(t *testing.T) {
spec := &CampaignSpec{
ID: "test-campaign",
Name: "Test Campaign",
Description: "A test campaign",
ProjectURL: "https://github.com/orgs/test/projects/1",
Workflows: []string{"test-workflow"},
}

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 CopyProjects is configured
if data.SafeOutputs.CopyProjects == nil {
t.Fatalf("expected CopyProjects to be configured")
}

// Verify that the max is 1
if data.SafeOutputs.CopyProjects.Max != 1 {
t.Errorf("expected CopyProjects max to be 1, got %d", data.SafeOutputs.CopyProjects.Max)
}

// Verify that the default source project is set to the template project
expectedSourceProject := "https://github.com/orgs/githubnext/projects/74"
if data.SafeOutputs.CopyProjects.SourceProject != expectedSourceProject {
t.Errorf("expected CopyProjects source-project to be %q, got %q",
expectedSourceProject, data.SafeOutputs.CopyProjects.SourceProject)
}
})

t.Run("copy-project 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"},
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 CopyProjects is configured
if data.SafeOutputs.CopyProjects == nil {
t.Fatalf("expected CopyProjects to be configured")
}

// Verify that the GitHubToken is set
if data.SafeOutputs.CopyProjects.GitHubToken != "${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}" {
t.Errorf("expected CopyProjects GitHubToken to be %q, got %q",
"${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}",
data.SafeOutputs.CopyProjects.GitHubToken)
}
})
}

func TestExtractFileGlobPatterns(t *testing.T) {
tests := []struct {
name string
Expand Down
18 changes: 18 additions & 0 deletions pkg/cli/compile_orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,24 @@ func renderGeneratedCampaignOrchestratorMarkdown(data *workflow.WorkflowData, so
}
outputs["create-project-status-update"] = statusUpdateConfig
}
if data.SafeOutputs.CopyProjects != nil {
copyProjectConfig := map[string]any{
"max": data.SafeOutputs.CopyProjects.Max,
}
// Include source-project if specified
if strings.TrimSpace(data.SafeOutputs.CopyProjects.SourceProject) != "" {
copyProjectConfig["source-project"] = data.SafeOutputs.CopyProjects.SourceProject
}
// Include target-owner if specified
if strings.TrimSpace(data.SafeOutputs.CopyProjects.TargetOwner) != "" {
copyProjectConfig["target-owner"] = data.SafeOutputs.CopyProjects.TargetOwner
}
// Include github-token if specified
if strings.TrimSpace(data.SafeOutputs.CopyProjects.GitHubToken) != "" {
copyProjectConfig["github-token"] = data.SafeOutputs.CopyProjects.GitHubToken
}
outputs["copy-project"] = copyProjectConfig
}
if len(outputs) > 0 {
payload := map[string]any{"safe-outputs": outputs}
if out, err := yaml.Marshal(payload); err == nil {
Expand Down
4 changes: 4 additions & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ const ExpressionBreakThreshold LineLength = 100
// DefaultMCPRegistryURL is the default MCP registry URL.
const DefaultMCPRegistryURL URL = "https://api.mcp.github.com/v0"

// DefaultCampaignTemplateProjectURL is the default source project URL for copying campaign templates.
// This points to the githubnext "[TEMPLATE: Agentic Campaign]" project (Project 74).
const DefaultCampaignTemplateProjectURL URL = "https://github.com/orgs/githubnext/projects/74"

// DefaultClaudeCodeVersion is the default version of the Claude Code CLI.
const DefaultClaudeCodeVersion Version = "2.0.76"

Expand Down
9 changes: 9 additions & 0 deletions pkg/workflow/safe_outputs_config_generation.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,12 @@ func generateSafeOutputsConfig(data *WorkflowData) string {
10, // default max
)
}
if data.SafeOutputs.CopyProjects != nil {
safeOutputsConfig["copy_project"] = generateMaxConfig(
data.SafeOutputs.CopyProjects.Max,
1, // default max
)
}
if data.SafeOutputs.UpdateRelease != nil {
safeOutputsConfig["update_release"] = generateMaxConfig(
data.SafeOutputs.UpdateRelease.Max,
Expand Down Expand Up @@ -525,6 +531,9 @@ func generateFilteredToolsJSON(data *WorkflowData, markdownPath string) (string,
if data.SafeOutputs.CreateProjectStatusUpdates != nil {
enabledTools["create_project_status_update"] = true
}
if data.SafeOutputs.CopyProjects != nil {
enabledTools["copy_project"] = true
}
// Note: dispatch_workflow tools are generated dynamically below, not from the static tools list

// Filter tools to only include enabled ones and enhance descriptions
Expand Down
Loading