Skip to content
Closed
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
2 changes: 1 addition & 1 deletion pkg/workflow/approve_workflow_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ func TestApproveWorkflowRunAllowedPullRequestsExpressionEmitsJSONArray(t *testin
runtimeConfig := strings.ReplaceAll(string(configJSON), wrappedExpression, `""`)
var config map[string]map[string]any
require.NoError(t, json.Unmarshal([]byte(runtimeConfig), &config))
assert.Equal(t, "", config["approve_workflow_run"]["allowed_pull_requests"])
assert.Empty(t, config["approve_workflow_run"]["allowed_pull_requests"])
})
}

Expand Down
12 changes: 6 additions & 6 deletions pkg/workflow/config_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,13 @@ func extractStringFromMap(m map[string]any, key string, debugLog *logger.Logger)
// Returns an error (indicated by the second return value being true) if the value is "*" (wildcard),
// which is not allowed for safe output target repositories.
func parseTargetRepoWithValidation(configMap map[string]any) (string, bool) {
targetRepoSlug := extractStringFromMap(configMap, "target-repo", configHelpersLog)
// Validate that target-repo is not "*" - only definite strings are allowed
if targetRepoSlug == "*" {
configHelpersLog.Print("Invalid target-repo: wildcard '*' is not allowed")
return "", true // Return true to indicate validation error
targetConfig, isInvalid := parseSafeOutputTargetConfig(configMap, configHelpersLog, safeOutputTargetConfigOptions{
parseTargetRepo: true,
})
if isInvalid {
return "", true
}
return targetRepoSlug, false
return targetConfig.TargetRepoSlug, false
}

// NOTE: parseExpiresFromConfig and parseRelativeTimeSpec have been moved to time_delta.go
Expand Down
124 changes: 123 additions & 1 deletion pkg/workflow/config_parsing_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,8 @@ func TestParsePRReviewCommentsConfigWithHelpers(t *testing.T) {
compiler := &Compiler{}
outputMap := map[string]any{
"create-pull-request-review-comment": map[string]any{
"target-repo": "company/codebase",
"target-repo": "company/codebase",
"allowed-repos": []any{"company/codebase", "company/other"},
},
}

Expand All @@ -422,6 +423,32 @@ func TestParsePRReviewCommentsConfigWithHelpers(t *testing.T) {
if result.TargetRepoSlug != "company/codebase" {
t.Errorf("expected target-repo 'company/codebase', got %q", result.TargetRepoSlug)
}
if len(result.AllowedRepos) != 2 || result.AllowedRepos[0] != "company/codebase" || result.AllowedRepos[1] != "company/other" {
t.Errorf("expected parsed allowed-repos, got %v", result.AllowedRepos)
}
}

func TestParsePRReviewCommentsConfigPreservesAllowedReposExpression(t *testing.T) {
compiler := &Compiler{}
expr := "${{ inputs['allowed-repos'] }}"
outputMap := map[string]any{
"create-pull-request-review-comment": map[string]any{
"target-repo": "${{ inputs.target_repo }}",
"allowed-repos": expr,
},
}

result := compiler.parsePullRequestReviewCommentsConfig(outputMap)
if result == nil {
t.Fatal("expected non-nil result")
}

if result.TargetRepoSlug != "${{ inputs.target_repo }}" {
t.Errorf("expected target-repo expression, got %q", result.TargetRepoSlug)
}
if len(result.AllowedRepos) != 1 || result.AllowedRepos[0] != expr {
t.Errorf("expected allowed-repos expression to be preserved, got %v", result.AllowedRepos)
}
}

// Test wildcard target-repo is now allowed for all create/close handlers
Expand Down Expand Up @@ -565,6 +592,101 @@ func TestParseTargetRepoWithValidation(t *testing.T) {
}
}

func TestParseSafeOutputTargetConfigOptions(t *testing.T) {
t.Run("ignores target repo without option", func(t *testing.T) {
config, isInvalid := parseSafeOutputTargetConfig(map[string]any{
"target-repo": "owner/repo",
}, nil, safeOutputTargetConfigOptions{})

if isInvalid {
t.Fatal("expected config to be valid")
}
if config.TargetRepoSlug != "" {
t.Fatalf("expected target-repo to be ignored, got %q", config.TargetRepoSlug)
}
})

t.Run("parses target repo and allowed repos without target", func(t *testing.T) {
config, isInvalid := parseSafeOutputTargetConfig(map[string]any{
"target": "17",
"target-repo": "owner/repo",
"allowed-repos": []any{"owner/repo", "owner/other"},
}, nil, safeOutputTargetConfigOptions{
parseTargetRepo: true,
parseAllowedRepos: true,
})

if isInvalid {
t.Fatal("expected config to be valid")
}
if config.Target != "" {
t.Fatalf("expected target to be ignored, got %q", config.Target)
}
if config.TargetRepoSlug != "owner/repo" {
t.Fatalf("expected target-repo owner/repo, got %q", config.TargetRepoSlug)
}
if len(config.AllowedRepos) != 2 || config.AllowedRepos[0] != "owner/repo" || config.AllowedRepos[1] != "owner/other" {
t.Fatalf("expected parsed allowed-repos, got %v", config.AllowedRepos)
}
})

t.Run("rejects wildcard target repo by default", func(t *testing.T) {
_, isInvalid := parseSafeOutputTargetConfig(map[string]any{
"target-repo": "*",
}, nil, safeOutputTargetConfigOptions{
parseTargetRepo: true,
})

if !isInvalid {
t.Fatal("expected wildcard target-repo to be invalid")
}
})

t.Run("allows wildcard target repo when configured", func(t *testing.T) {
config, isInvalid := parseSafeOutputTargetConfig(map[string]any{
"target-repo": "*",
}, nil, safeOutputTargetConfigOptions{
parseTargetRepo: true,
allowTargetRepoWildcard: true,
})

if isInvalid {
t.Fatal("expected wildcard target-repo to be valid")
}
if config.TargetRepoSlug != "*" {
t.Fatalf("expected wildcard target-repo, got %q", config.TargetRepoSlug)
}
})

t.Run("parses expression allowed repos only when configured", func(t *testing.T) {
expr := "${{ inputs['allowed-repos'] }}"
baseConfig := map[string]any{
"allowed-repos": expr,
}

config, isInvalid := parseSafeOutputTargetConfig(baseConfig, nil, safeOutputTargetConfigOptions{
parseAllowedRepos: true,
})
if isInvalid {
t.Fatal("expected config to be valid")
}
if config.AllowedRepos != nil {
t.Fatalf("expected expression allowed-repos to be ignored without expression support, got %v", config.AllowedRepos)
}

config, isInvalid = parseSafeOutputTargetConfig(baseConfig, nil, safeOutputTargetConfigOptions{
parseAllowedRepos: true,
allowAllowedReposExpression: true,
})
if isInvalid {
t.Fatal("expected config to be valid")
}
if len(config.AllowedRepos) != 1 || config.AllowedRepos[0] != expr {
t.Fatalf("expected expression allowed-repos, got %v", config.AllowedRepos)
}
})
}

func TestParseBoolFromConfig(t *testing.T) {
tests := []struct {
name string
Expand Down
15 changes: 10 additions & 5 deletions pkg/workflow/create_code_scanning_alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,19 @@ func (c *Compiler) parseCodeScanningAlertsConfig(outputMap map[string]any) *Crea
}
}

// Parse target-repo
securityReportsConfig.TargetRepoSlug = extractStringFromMap(configMap, "target-repo", createCodeScanningAlertLog)
targetConfig, isInvalid := parseSafeOutputTargetConfig(configMap, createCodeScanningAlertLog, safeOutputTargetConfigOptions{
parseTargetRepo: true,
allowTargetRepoWildcard: true,
parseAllowedRepos: true,
})
if isInvalid {
return nil
}
securityReportsConfig.TargetRepoSlug = targetConfig.TargetRepoSlug
if securityReportsConfig.TargetRepoSlug != "" {
createCodeScanningAlertLog.Printf("Target repo for code scanning alerts: %s", securityReportsConfig.TargetRepoSlug)
}

// Parse allowed-repos
securityReportsConfig.AllowedRepos = ParseStringArrayFromConfig(configMap, "allowed-repos", createCodeScanningAlertLog)
securityReportsConfig.AllowedRepos = targetConfig.AllowedRepos
if len(securityReportsConfig.AllowedRepos) > 0 {
createCodeScanningAlertLog.Printf("Allowed repos for cross-repo alerts: %d configured", len(securityReportsConfig.AllowedRepos))
}
Expand Down
20 changes: 10 additions & 10 deletions pkg/workflow/create_pr_review_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,19 @@ func (c *Compiler) parsePullRequestReviewCommentsConfig(outputMap map[string]any
}
}

// Parse target
if target, exists := configMap["target"]; exists {
if targetStr, ok := target.(string); ok {
prReviewCommentsConfig.Target = targetStr
}
}

// Parse target-repo using shared helper with validation
targetRepoSlug, isInvalid := parseTargetRepoWithValidation(configMap)
// Parse target config (target, target-repo, allowed-repos)
targetConfig, isInvalid := parseSafeOutputTargetConfig(configMap, createPRReviewCommentLog, safeOutputTargetConfigOptions{
parseTarget: true,
parseTargetRepo: true,
parseAllowedRepos: true,
allowAllowedReposExpression: true,
})
if isInvalid {
return nil // Invalid configuration, return nil to cause validation error
}
prReviewCommentsConfig.TargetRepoSlug = targetRepoSlug
prReviewCommentsConfig.Target = targetConfig.Target
prReviewCommentsConfig.TargetRepoSlug = targetConfig.TargetRepoSlug
prReviewCommentsConfig.AllowedRepos = targetConfig.AllowedRepos

if commitId, exists := configMap["commit-id"]; exists {
if commitIdStr, ok := commitId.(string); ok {
Expand Down
13 changes: 11 additions & 2 deletions pkg/workflow/dispatch_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,17 @@ func (c *Compiler) parseDispatchWorkflowConfig(outputMap map[string]any) *Dispat
}

// Parse target-repo (optional cross-repo dispatch target)
dispatchWorkflowConfig.TargetRepoSlug = extractStringFromMap(configMap, "target-repo", dispatchWorkflowLog)
dispatchWorkflowConfig.AllowedRepos = ParseStringArrayOrExprFromConfig(configMap, "allowed-repos", dispatchWorkflowLog)
targetConfig, isInvalid := parseSafeOutputTargetConfig(configMap, dispatchWorkflowLog, safeOutputTargetConfigOptions{
parseTargetRepo: true,
allowTargetRepoWildcard: true,
parseAllowedRepos: true,
allowAllowedReposExpression: true,
})
if isInvalid {
return nil
}
dispatchWorkflowConfig.TargetRepoSlug = targetConfig.TargetRepoSlug
dispatchWorkflowConfig.AllowedRepos = targetConfig.AllowedRepos
dispatchWorkflowConfig.AllowedRefs = ParseStringArrayOrExprFromConfig(configMap, "allowed-refs", dispatchWorkflowLog)
if dispatchWorkflowConfig.AllowedRefs == nil {
dispatchWorkflowConfig.AllowedRefs = []string{defaultDispatchWorkflowAllowedRef}
Expand Down
12 changes: 8 additions & 4 deletions pkg/workflow/link_sub_issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@ func (c *Compiler) parseLinkSubIssueConfig(outputMap map[string]any) *LinkSubIss
linkSubIssueLog.Print("Found link-sub-issue config map")

// Parse target config (target-repo) with validation
targetConfig, isInvalid := ParseTargetConfig(configMap)
targetConfig, isInvalid := parseSafeOutputTargetConfig(configMap, linkSubIssueLog, safeOutputTargetConfigOptions{
parseTarget: true,
parseTargetRepo: true,
allowTargetRepoWildcard: true,
parseAllowedRepos: true,
allowAllowedReposExpression: true,
})
if isInvalid {
return nil // Invalid configuration (e.g., wildcard target-repo), return nil to cause validation error
return nil // Invalid configuration, return nil to cause validation error
}
linkSubIssueConfig.SafeOutputTargetConfig = targetConfig
// Override AllowedRepos with expression-aware parsing (supports GitHub Actions expressions)
linkSubIssueConfig.AllowedRepos = ParseStringArrayOrExprFromConfig(configMap, "allowed-repos", linkSubIssueLog)

// Parse common base fields with default max of 5
c.parseBaseSafeOutputConfig(configMap, &linkSubIssueConfig.BaseSafeOutputConfig, 5)
Expand Down
22 changes: 13 additions & 9 deletions pkg/workflow/push_to_pull_request_branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,6 @@ func (c *Compiler) parsePushToPullRequestBranchConfig(outputMap map[string]any)
}

if configMap, ok := configData.(map[string]any); ok {
// Parse target (optional, similar to add-comment)
if target, exists := configMap["target"]; exists {
if targetStr, ok := target.(string); ok {
pushToBranchConfig.Target = targetStr
}
}

// Parse if-no-changes (optional, defaults to "warn")
if ifNoChanges, exists := configMap["if-no-changes"]; exists {
if ifNoChangesStr, ok := ifNoChanges.(string); ok {
Expand Down Expand Up @@ -159,7 +152,18 @@ func (c *Compiler) parsePushToPullRequestBranchConfig(outputMap map[string]any)
}

// Parse target-repo for cross-repository push
pushToBranchConfig.TargetRepoSlug = extractStringFromMap(configMap, "target-repo", pushToPullRequestBranchLog)
targetConfig, isInvalid := parseSafeOutputTargetConfig(configMap, pushToPullRequestBranchLog, safeOutputTargetConfigOptions{
parseTarget: true,
parseTargetRepo: true,
allowTargetRepoWildcard: true,
parseAllowedRepos: true,
allowAllowedReposExpression: true,
})
if isInvalid {
return nil
}
pushToBranchConfig.Target = targetConfig.Target
pushToBranchConfig.TargetRepoSlug = targetConfig.TargetRepoSlug
pushToBranchConfig.HeadRepoSlug = extractStringFromMap(configMap, "head-repo", pushToPullRequestBranchLog)
pushToBranchConfig.HeadGitHubToken = extractStringFromMap(configMap, "head-github-token", pushToPullRequestBranchLog)

Expand All @@ -179,7 +183,7 @@ func (c *Compiler) parsePushToPullRequestBranchConfig(outputMap map[string]any)
pushToBranchConfig.BaseBranch = extractStringFromMap(configMap, "base-branch", pushToPullRequestBranchLog)

// Parse allowed-repos for cross-repository push (expression-aware)
pushToBranchConfig.AllowedRepos = ParseStringArrayOrExprFromConfig(configMap, "allowed-repos", pushToPullRequestBranchLog)
pushToBranchConfig.AllowedRepos = targetConfig.AllowedRepos

// Parse protected-files: supports string enum OR object form {policy, exclude}.
exclude := preprocessProtectedFilesField(configMap, pushToPullRequestBranchLog)
Expand Down
28 changes: 7 additions & 21 deletions pkg/workflow/reply_to_pr_review_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,30 +27,16 @@ func (c *Compiler) parseReplyToPullRequestReviewCommentConfig(outputMap map[stri
// Parse common base fields with default max of 10
c.parseBaseSafeOutputConfig(configMap, &config.BaseSafeOutputConfig, 10)

// Parse target
if target, exists := configMap["target"]; exists {
if targetStr, ok := target.(string); ok {
config.Target = targetStr
}
}

// Parse target-repo using shared helper with validation
targetRepoSlug, isInvalid := parseTargetRepoWithValidation(configMap)
// Parse target config (target, target-repo, allowed-repos)
targetConfig, isInvalid := parseSafeOutputTargetConfig(configMap, replyToPRReviewCommentLog, safeOutputTargetConfigOptions{
parseTarget: true,
parseTargetRepo: true,
parseAllowedRepos: true,
})
if isInvalid {
return nil // Invalid configuration, return nil to cause validation error
}
config.TargetRepoSlug = targetRepoSlug

// Parse allowed-repos
if allowedRepos, exists := configMap["allowed-repos"]; exists {
if repos, ok := allowedRepos.([]any); ok {
for _, repo := range repos {
if repoStr, ok := repo.(string); ok {
config.AllowedRepos = append(config.AllowedRepos, repoStr)
}
}
}
}
config.SafeOutputTargetConfig = targetConfig

// Parse footer as templatable bool
if err := preprocessBoolFieldAsString(configMap, "footer", replyToPRReviewCommentLog); err != nil {
Expand Down
11 changes: 11 additions & 0 deletions pkg/workflow/safe_outputs_cross_repo_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,17 @@ func TestCreateCodeScanningAlertConfigTargetRepo(t *testing.T) {
expectedToken: "",
expectedDriver: "My Scanner",
},
{
name: "wildcard target-repo remains allowed",
configMap: map[string]any{
"create-code-scanning-alert": map[string]any{
"target-repo": "*",
},
},
expectedRepo: "*",
expectedRepos: nil,
expectedToken: "",
},
{
name: "no cross-repo config",
configMap: map[string]any{
Expand Down
Loading
Loading