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
50 changes: 50 additions & 0 deletions pkg/cli/compile_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,56 @@ Verify staged safe-outputs with create-issue.
}
}

func TestCompileSafeOutputsCreatePullRequestAllowedBaseBranches(t *testing.T) {
setup := setupIntegrationTest(t)
defer setup.cleanup()

testWorkflow := `---
name: Allowed Base Branches
on:
workflow_dispatch:
permissions: read-all
engine: copilot
safe-outputs:
create-pull-request:
allowed-base-branches:
- main
- release/*
---

Verify allowed base branches in create-pull-request safe output.
`
testWorkflowPath := filepath.Join(setup.workflowsDir, "allowed-base-branches.md")
if err := os.WriteFile(testWorkflowPath, []byte(testWorkflow), 0644); err != nil {
t.Fatalf("Failed to write test workflow file: %v", err)
}

cmd := exec.Command(setup.binaryPath, "compile", testWorkflowPath)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("CLI compile command failed: %v\nOutput: %s", err, string(output))
}

lockFilePath := filepath.Join(setup.workflowsDir, "allowed-base-branches.lock.yml")
lockContent, err := os.ReadFile(lockFilePath)
if err != nil {
t.Fatalf("Failed to read lock file: %v", err)
}
lockContentStr := string(lockContent)

if !strings.Contains(lockContentStr, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") {
t.Fatalf("Expected handler config env var in lock file, got:\n%s", lockContentStr)
}

if !strings.Contains(lockContentStr, "allowed_base_branches") {
t.Fatalf("Expected allowed_base_branches in handler config, got:\n%s", lockContentStr)
}

if !strings.Contains(lockContentStr, "release/*") {
t.Fatalf("Expected release/* pattern in lock file handler config, got:\n%s", lockContentStr)
}
}

// TestCompileStagedSafeOutputsAddComment verifies that a workflow with staged: true
// and an add-comment handler compiles and emits GH_AW_SAFE_OUTPUTS_STAGED.
// Prior to the schema fix, staged was not listed in the add-comment handler schema.
Expand Down
33 changes: 33 additions & 0 deletions pkg/parser/schema_location_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,36 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AdditionalProperti
})
}
}

func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsAllowedBaseBranchesInCreatePullRequest(t *testing.T) {
frontmatter := map[string]any{
"on": map[string]any{
"workflow_dispatch": map[string]any{},
},
"permissions": map[string]any{
"contents": "read",
"pull-requests": "read",
},
"engine": map[string]any{
"id": "copilot",
"model": "gpt-5.4",
},
"network": map[string]any{
"allowed": []any{"defaults"},
},
"tools": map[string]any{
"edit": map[string]any{},
"bash": true,
},
Comment on lines +283 to +297
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This regression test includes several unrelated schema fields (permissions/engine/network/tools). That makes the test more likely to fail due to future schema changes in those areas rather than the intended safe-outputs.create-pull-request.allowed-base-branches validation. Consider trimming frontmatter to only the minimal fields required for a valid workflow plus the safe-outputs.create-pull-request.allowed-base-branches shape (similar to the smaller fixtures used in other tests in this file) so the test isolates the regression being covered.

Suggested change
"permissions": map[string]any{
"contents": "read",
"pull-requests": "read",
},
"engine": map[string]any{
"id": "copilot",
"model": "gpt-5.4",
},
"network": map[string]any{
"allowed": []any{"defaults"},
},
"tools": map[string]any{
"edit": map[string]any{},
"bash": true,
},

Copilot uses AI. Check for mistakes.
"safe-outputs": map[string]any{
"create-pull-request": map[string]any{
"allowed-base-branches": []any{"main", "release/*"},
},
},
}

err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/test/workflow.md")
if err != nil {
t.Fatalf("expected allowed-base-branches to be accepted under safe-outputs.create-pull-request, got error: %v", err)
}
}
77 changes: 77 additions & 0 deletions pkg/parser/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,83 @@ func TestMainWorkflowSchema_WorkflowDispatchNumberTypeDocumentation(t *testing.T
}
}

func TestMainWorkflowSchema_CreatePullRequestAllowedBaseBranches(t *testing.T) {
t.Parallel()

schemaPath := "schemas/main_workflow_schema.json"
schemaContent, err := os.ReadFile(schemaPath)
if err != nil {
t.Fatalf("failed to read schema: %v", err)
}

var schema map[string]any
if err := json.Unmarshal(schemaContent, &schema); err != nil {
t.Fatalf("failed to parse schema json: %v", err)
}

properties, ok := schema["properties"].(map[string]any)
if !ok {
t.Fatal("schema properties section not found")
}

safeOutputs, ok := properties["safe-outputs"].(map[string]any)
if !ok {
t.Fatal("'safe-outputs' field not found in schema")
}

safeOutputsProperties, ok := safeOutputs["properties"].(map[string]any)
if !ok {
t.Fatal("'safe-outputs.properties' not found in schema")
}

createPullRequest, ok := safeOutputsProperties["create-pull-request"].(map[string]any)
if !ok {
t.Fatal("'safe-outputs.create-pull-request' not found in schema")
}

createPullRequestOneOf, ok := createPullRequest["oneOf"].([]any)
if !ok {
t.Fatal("'safe-outputs.create-pull-request.oneOf' not found in schema")
}

var createPullRequestProperties map[string]any
for _, candidate := range createPullRequestOneOf {
candidateMap, ok := candidate.(map[string]any)
if !ok {
continue
}

properties, ok := candidateMap["properties"].(map[string]any)
if !ok {
continue
}

createPullRequestProperties = properties
break
}
if createPullRequestProperties == nil {
t.Fatal("'safe-outputs.create-pull-request' object schema with properties not found")
}

allowedBaseBranches, ok := createPullRequestProperties["allowed-base-branches"].(map[string]any)
if !ok {
t.Fatal("'allowed-base-branches' not found under safe-outputs.create-pull-request")
}

if gotType, _ := allowedBaseBranches["type"].(string); gotType != "array" {
t.Fatalf("'allowed-base-branches' should be type array, got: %v", allowedBaseBranches["type"])
}

items, ok := allowedBaseBranches["items"].(map[string]any)
if !ok {
t.Fatal("'allowed-base-branches.items' not found in schema")
}

if gotItemType, _ := items["type"].(string); gotItemType != "string" {
t.Fatalf("'allowed-base-branches.items' should be type string, got: %v", items["type"])
}
}

func TestGetSafeOutputTypeKeys(t *testing.T) {
keys, err := GetSafeOutputTypeKeys()
if err != nil {
Expand Down
Loading