From 3efce1ddb479ae1a0e0f2ae0367d287b8096c1ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 05:24:20 +0000 Subject: [PATCH 1/3] Initial plan From af3876a2aa1af4e5de389b392be633c6cac48a02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 05:35:22 +0000 Subject: [PATCH 2/3] Add base-branch field for create-pull-request safe output Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schemas/main_workflow_schema.json | 4 + pkg/workflow/compiler_safe_outputs_config.go | 13 +- .../compiler_safe_outputs_config_test.go | 72 +++++++ pkg/workflow/create_pull_request.go | 9 +- ...ll_request_base_branch_integration_test.go | 180 ++++++++++++++++++ 5 files changed, 272 insertions(+), 6 deletions(-) create mode 100644 pkg/workflow/create_pull_request_base_branch_integration_test.go diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 10ec36bd20..d1ba93264c 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -4835,6 +4835,10 @@ "type": "boolean", "description": "Enable auto-merge for the pull request. When enabled, the PR will be automatically merged once all required checks pass and required approvals are met. Defaults to false.", "default": false + }, + "base-branch": { + "type": "string", + "description": "Base branch for the pull request. Defaults to the workflow's branch (github.ref_name) if not specified. Useful for cross-repository PRs targeting non-default branches (e.g., 'vnext', 'release/v1.0')." } }, "additionalProperties": false, diff --git a/pkg/workflow/compiler_safe_outputs_config.go b/pkg/workflow/compiler_safe_outputs_config.go index 0fbc23757a..7eb3f73ed4 100644 --- a/pkg/workflow/compiler_safe_outputs_config.go +++ b/pkg/workflow/compiler_safe_outputs_config.go @@ -300,7 +300,7 @@ var handlerRegistry = map[string]handlerBuilder{ if cfg.MaximumPatchSize > 0 { maxPatchSize = cfg.MaximumPatchSize } - return newHandlerConfigBuilder(). + builder := newHandlerConfigBuilder(). AddIfPositive("max", c.Max). AddIfNotEmpty("title_prefix", c.TitlePrefix). AddStringSlice("labels", c.Labels). @@ -311,9 +311,14 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfPositive("expires", c.Expires). AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). - AddDefault("base_branch", "${{ github.ref_name }}"). - AddDefault("max_patch_size", maxPatchSize). - Build() + AddDefault("max_patch_size", maxPatchSize) + // Add base_branch - use custom value if specified, otherwise use github.ref_name + if c.BaseBranch != "" { + builder.AddDefault("base_branch", c.BaseBranch) + } else { + builder.AddDefault("base_branch", "${{ github.ref_name }}") + } + return builder.Build() }, "push_to_pull_request_branch": func(cfg *SafeOutputsConfig) map[string]any { if cfg.PushToPullRequestBranch == nil { diff --git a/pkg/workflow/compiler_safe_outputs_config_test.go b/pkg/workflow/compiler_safe_outputs_config_test.go index c8fb54f496..1fdbb838d3 100644 --- a/pkg/workflow/compiler_safe_outputs_config_test.go +++ b/pkg/workflow/compiler_safe_outputs_config_test.go @@ -734,3 +734,75 @@ func TestAutoEnabledHandlers(t *testing.T) { }) } } + +// TestCreatePullRequestBaseBranch tests the base-branch field configuration +func TestCreatePullRequestBaseBranch(t *testing.T) { + tests := []struct { + name string + baseBranch string + expectedBaseBranch string + }{ + { + name: "custom base branch", + baseBranch: "vnext", + expectedBaseBranch: "vnext", + }, + { + name: "default base branch", + baseBranch: "", + expectedBaseBranch: "${{ github.ref_name }}", + }, + { + name: "branch with slash", + baseBranch: "release/v1.0", + expectedBaseBranch: "release/v1.0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + compiler := NewCompiler() + + workflowData := &WorkflowData{ + Name: "Test Workflow", + SafeOutputs: &SafeOutputsConfig{ + CreatePullRequests: &CreatePullRequestsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Max: 1, + }, + BaseBranch: tt.baseBranch, + }, + }, + } + + var steps []string + compiler.addHandlerManagerConfigEnvVar(&steps, workflowData) + + require.NotEmpty(t, steps, "Steps should be generated") + + // Extract and validate JSON + for _, step := range steps { + if strings.Contains(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") { + parts := strings.Split(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ") + if len(parts) == 2 { + jsonStr := strings.TrimSpace(parts[1]) + jsonStr = strings.Trim(jsonStr, "\"") + jsonStr = strings.ReplaceAll(jsonStr, "\\\"", "\"") + + var config map[string]map[string]any + err := json.Unmarshal([]byte(jsonStr), &config) + require.NoError(t, err, "Config JSON should be valid") + + prConfig, ok := config["create_pull_request"] + require.True(t, ok, "create_pull_request config should exist") + + baseBranch, ok := prConfig["base_branch"] + require.True(t, ok, "base_branch should be in config") + + assert.Equal(t, tt.expectedBaseBranch, baseBranch, "base_branch should match expected value") + } + } + } + }) + } +} diff --git a/pkg/workflow/create_pull_request.go b/pkg/workflow/create_pull_request.go index 6dc109edf1..c24b51b483 100644 --- a/pkg/workflow/create_pull_request.go +++ b/pkg/workflow/create_pull_request.go @@ -23,6 +23,7 @@ type CreatePullRequestsConfig struct { AllowedRepos []string `yaml:"allowed-repos,omitempty"` // List of additional repositories that pull requests can be created in (additionally to the target-repo) Expires int `yaml:"expires,omitempty"` // Hours until the pull request expires and should be automatically closed (only for same-repo PRs) AutoMerge bool `yaml:"auto-merge,omitempty"` // Enable auto-merge for the pull request when all required checks pass + BaseBranch string `yaml:"base-branch,omitempty"` // Base branch for the pull request (defaults to github.ref_name if not specified) } // buildCreateOutputPullRequestJob creates the create_pull_request job @@ -61,8 +62,12 @@ func (c *Compiler) buildCreateOutputPullRequestJob(data *WorkflowData, mainJobNa var customEnvVars []string // Pass the workflow ID for branch naming customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_WORKFLOW_ID: %q\n", mainJobName)) - // Pass the base branch from GitHub context - customEnvVars = append(customEnvVars, " GH_AW_BASE_BRANCH: ${{ github.ref_name }}\n") + // Pass the base branch - use custom value if specified, otherwise default to github.ref_name + if data.SafeOutputs.CreatePullRequests.BaseBranch != "" { + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_BASE_BRANCH: %q\n", data.SafeOutputs.CreatePullRequests.BaseBranch)) + } else { + customEnvVars = append(customEnvVars, " GH_AW_BASE_BRANCH: ${{ github.ref_name }}\n") + } customEnvVars = append(customEnvVars, buildTitlePrefixEnvVar("GH_AW_PR_TITLE_PREFIX", data.SafeOutputs.CreatePullRequests.TitlePrefix)...) customEnvVars = append(customEnvVars, buildLabelsEnvVar("GH_AW_PR_LABELS", data.SafeOutputs.CreatePullRequests.Labels)...) customEnvVars = append(customEnvVars, buildLabelsEnvVar("GH_AW_PR_ALLOWED_LABELS", data.SafeOutputs.CreatePullRequests.AllowedLabels)...) diff --git a/pkg/workflow/create_pull_request_base_branch_integration_test.go b/pkg/workflow/create_pull_request_base_branch_integration_test.go new file mode 100644 index 0000000000..477a4c30ef --- /dev/null +++ b/pkg/workflow/create_pull_request_base_branch_integration_test.go @@ -0,0 +1,180 @@ +//go:build integration + +package workflow + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// TestCreatePullRequestWithCustomBaseBranch tests end-to-end workflow compilation with custom base-branch +func TestCreatePullRequestWithCustomBaseBranch(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "base-branch-test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create test workflow with custom base-branch + workflowContent := `--- +on: push +permissions: + contents: read + actions: read + issues: read + pull-requests: read +engine: copilot +safe-outputs: + create-pull-request: + target-repo: "microsoft/vscode-docs" + base-branch: vnext + draft: true +--- + +# Test Workflow + +Create a pull request targeting vnext branch in cross-repo. +` + + workflowPath := filepath.Join(tmpDir, "test-workflow.md") + if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + // Compile the workflow + compiler := NewCompiler() + if err := compiler.CompileWorkflow(workflowPath); err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the compiled output + outputFile := filepath.Join(tmpDir, "test-workflow.lock.yml") + compiledBytes, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("Failed to read compiled output: %v", err) + } + + compiledContent := string(compiledBytes) + + // Verify GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG contains base_branch set to "vnext" + // The JSON is escaped in YAML, so we need to look for the escaped version + if !strings.Contains(compiledContent, `\"base_branch\":\"vnext\"`) { + t.Error("Expected handler config to contain base_branch set to vnext in compiled workflow") + } + + // Verify it does NOT contain the default github.ref_name expression + if strings.Contains(compiledContent, `\"base_branch\":\"${{ github.ref_name }}\"`) { + t.Error("Did not expect handler config to use github.ref_name when base-branch is explicitly set") + } +} + +// TestCreatePullRequestWithDefaultBaseBranch tests workflow compilation with default base-branch +func TestCreatePullRequestWithDefaultBaseBranch(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "default-base-branch-test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create test workflow without base-branch field + workflowContent := `--- +on: push +permissions: + contents: read + actions: read + issues: read + pull-requests: read +engine: copilot +safe-outputs: + create-pull-request: + draft: true +--- + +# Test Workflow + +Create a pull request with default base branch. +` + + workflowPath := filepath.Join(tmpDir, "test-default.md") + if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + // Compile the workflow + compiler := NewCompiler() + if err := compiler.CompileWorkflow(workflowPath); err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the compiled output + outputFile := filepath.Join(tmpDir, "test-default.lock.yml") + compiledBytes, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("Failed to read compiled output: %v", err) + } + + compiledContent := string(compiledBytes) + + // Verify GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG uses github.ref_name by default + // The JSON is escaped in YAML, so we need to look for the escaped version + if !strings.Contains(compiledContent, `\"base_branch\":\"${{ github.ref_name }}\"`) { + t.Error("Expected handler config to use github.ref_name when base-branch is not specified") + } +} + +// TestCreatePullRequestWithBranchSlash tests workflow compilation with branch containing slash +func TestCreatePullRequestWithBranchSlash(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "branch-slash-test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create test workflow with base-branch containing slash + workflowContent := `--- +on: push +permissions: + contents: read + actions: read + issues: read + pull-requests: read +engine: copilot +safe-outputs: + create-pull-request: + base-branch: release/v1.0 + draft: true +--- + +# Test Workflow + +Create a pull request targeting release/v1.0 branch. +` + + workflowPath := filepath.Join(tmpDir, "test-slash.md") + if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + // Compile the workflow + compiler := NewCompiler() + if err := compiler.CompileWorkflow(workflowPath); err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the compiled output + outputFile := filepath.Join(tmpDir, "test-slash.lock.yml") + compiledBytes, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("Failed to read compiled output: %v", err) + } + + compiledContent := string(compiledBytes) + + // Verify GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG contains base_branch set to "release/v1.0" + // The JSON is escaped in YAML, so we need to look for the escaped version + if !strings.Contains(compiledContent, `\"base_branch\":\"release/v1.0\"`) { + t.Error("Expected handler config to contain base_branch set to release/v1.0 in compiled workflow") + } +} From 9a0c21e65f8874374960fb828a8de00e16ea0d65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 05:42:11 +0000 Subject: [PATCH 3/3] Update documentation with base-branch field --- docs/src/content/docs/reference/frontmatter-full.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index b0be7dc601..f21b185658 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -2581,6 +2581,12 @@ safe-outputs: # (optional) auto-merge: true + # Base branch for the pull request. Defaults to the workflow's branch + # (github.ref_name) if not specified. Useful for cross-repository PRs targeting + # non-default branches (e.g., 'vnext', 'release/v1.0'). + # (optional) + base-branch: "example-value" + # Option 2: Enable pull request creation with default configuration create-pull-request: null