From ac4d62a7b3ee189026b2a3490c501626751fd3e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:39:29 +0000 Subject: [PATCH 1/3] Initial plan From 55415c06e5f809f05174696ee47c19aa9003fdde Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:05:22 +0000 Subject: [PATCH 2/3] Add allowed-events filter to submit-pull-request-review safe-output Agent-Logs-Url: https://github.com/github/gh-aw/sessions/43196536-353b-438f-a615-21c245bcd453 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/submit_pr_review.cjs | 16 ++++ actions/setup/js/submit_pr_review.test.cjs | 77 +++++++++++++++++++ .../setup/js/types/safe-outputs-config.d.ts | 1 + .../docs/reference/frontmatter-full.md | 7 ++ .../content/docs/reference/safe-outputs.md | 3 + pkg/parser/schemas/main_workflow_schema.json | 18 ++++- pkg/workflow/compiler_safe_outputs_config.go | 1 + pkg/workflow/submit_pr_review.go | 24 +++++- pkg/workflow/submit_pr_review_footer_test.go | 69 +++++++++++++++++ 9 files changed, 211 insertions(+), 5 deletions(-) diff --git a/actions/setup/js/submit_pr_review.cjs b/actions/setup/js/submit_pr_review.cjs index 2858c695d51..6769fa4f4ae 100644 --- a/actions/setup/js/submit_pr_review.cjs +++ b/actions/setup/js/submit_pr_review.cjs @@ -33,6 +33,9 @@ async function main(config = {}) { const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config); const githubClient = await createAuthenticatedGitHubClient(config); + // Build the allowed events set from config (empty set means all events are allowed) + const allowedEvents = new Set(Array.isArray(config.allowed_events) && config.allowed_events.length > 0 ? config.allowed_events.map(e => String(e).toUpperCase()) : []); + if (!buffer) { core.warning("submit_pull_request_review: No PR review buffer provided in config"); return async function handleSubmitPRReview() { @@ -45,6 +48,9 @@ async function main(config = {}) { if (allowedRepos.size > 0) { core.info(`Allowed repos: ${Array.from(allowedRepos).join(", ")}`); } + if (allowedEvents.size > 0) { + core.info(`Allowed review events: ${Array.from(allowedEvents).join(", ")}`); + } // Propagate per-handler staged flag to the shared PR review buffer if (config.staged === true) { @@ -81,6 +87,16 @@ async function main(config = {}) { }; } + // Enforce allowed-events filter (infrastructure-level enforcement) + if (allowedEvents.size > 0 && !allowedEvents.has(event)) { + const allowedList = Array.from(allowedEvents).join(", "); + core.warning(`Review event '${event}' is not allowed. Allowed events: ${allowedList}`); + return { + success: false, + error: `Review event '${event}' is not allowed by safe-outputs configuration. Allowed events: ${allowedList}`, + }; + } + // Body is required for REQUEST_CHANGES per GitHub API docs; // optional for APPROVE and COMMENT const body = message.body || ""; diff --git a/actions/setup/js/submit_pr_review.test.cjs b/actions/setup/js/submit_pr_review.test.cjs index 2aa35116b48..e39f925bef1 100644 --- a/actions/setup/js/submit_pr_review.test.cjs +++ b/actions/setup/js/submit_pr_review.test.cjs @@ -173,6 +173,83 @@ describe("submit_pr_review (Handler Factory Architecture)", () => { expect(result.error).toContain("Invalid review event"); }); + it("should allow all events when allowed_events not configured", async () => { + const { main } = require("./submit_pr_review.cjs"); + + for (const event of ["APPROVE", "COMMENT", "REQUEST_CHANGES"]) { + const localBuffer = createReviewBuffer(); + const h = await main({ max: 5, _prReviewBuffer: localBuffer }); + const result = await h({ event, body: event === "REQUEST_CHANGES" ? "body" : "" }, {}); + expect(result.success).toBe(true); + expect(result.event).toBe(event); + } + }); + + it("should block APPROVE when allowed_events is [COMMENT, REQUEST_CHANGES]", async () => { + const { main } = require("./submit_pr_review.cjs"); + const localBuffer = createReviewBuffer(); + const localHandler = await main({ + max: 5, + _prReviewBuffer: localBuffer, + allowed_events: ["COMMENT", "REQUEST_CHANGES"], + }); + + const result = await localHandler({ event: "APPROVE", body: "" }, {}); + + expect(result.success).toBe(false); + expect(result.error).toContain("not allowed"); + expect(result.error).toContain("APPROVE"); + }); + + it("should allow COMMENT when allowed_events is [COMMENT, REQUEST_CHANGES]", async () => { + const { main } = require("./submit_pr_review.cjs"); + const localBuffer = createReviewBuffer(); + const localHandler = await main({ + max: 5, + _prReviewBuffer: localBuffer, + allowed_events: ["COMMENT", "REQUEST_CHANGES"], + }); + + const result = await localHandler({ event: "COMMENT", body: "some feedback" }, {}); + + expect(result.success).toBe(true); + expect(result.event).toBe("COMMENT"); + }); + + it("should normalize event case before checking allowed_events", async () => { + const { main } = require("./submit_pr_review.cjs"); + const localBuffer = createReviewBuffer(); + const localHandler = await main({ + max: 5, + _prReviewBuffer: localBuffer, + allowed_events: ["COMMENT"], + }); + + const result = await localHandler({ event: "comment", body: "feedback" }, {}); + + expect(result.success).toBe(true); + expect(result.event).toBe("COMMENT"); + }); + + it("should not consume max count slot when event is blocked by allowed_events", async () => { + const { main } = require("./submit_pr_review.cjs"); + const localBuffer = createReviewBuffer(); + const localHandler = await main({ + max: 1, + _prReviewBuffer: localBuffer, + allowed_events: ["COMMENT"], + }); + + // Blocked event should not consume a slot + const blocked = await localHandler({ event: "APPROVE", body: "" }, {}); + expect(blocked.success).toBe(false); + + // Allowed event should still succeed since blocked one didn't count + const allowed = await localHandler({ event: "COMMENT", body: "feedback" }, {}); + expect(allowed.success).toBe(true); + expect(allowed.event).toBe("COMMENT"); + }); + it("should allow empty body for APPROVE event", async () => { const message = { type: "submit_pull_request_review", diff --git a/actions/setup/js/types/safe-outputs-config.d.ts b/actions/setup/js/types/safe-outputs-config.d.ts index 3a789faa40c..fe05260545d 100644 --- a/actions/setup/js/types/safe-outputs-config.d.ts +++ b/actions/setup/js/types/safe-outputs-config.d.ts @@ -109,6 +109,7 @@ interface SubmitPullRequestReviewConfig extends SafeOutputConfig { target?: string; "target-repo"?: string; "allowed-repos"?: string[]; + "allowed-events"?: Array<"APPROVE" | "COMMENT" | "REQUEST_CHANGES">; footer?: boolean | "always" | "none" | "if-body"; } diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index 07613eb29f9..6818b18c7f7 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -4039,6 +4039,13 @@ safe-outputs: allowed-repos: [] # Array of strings + # Optional list of allowed review event types. If omitted, all event types + # (APPROVE, COMMENT, REQUEST_CHANGES) are allowed. Use this to restrict the agent + # to specific event types at the infrastructure level. + # (optional) + allowed-events: [] + # Array of strings (APPROVE, COMMENT, REQUEST_CHANGES) + # GitHub token to use for this specific output type. Overrides global github-token # if specified. # (optional) diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index 39858b5c51d..471cda75e6d 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -734,9 +734,12 @@ safe-outputs: target: "triggering" # or "*", or e.g. ${{ github.event.inputs.pr_number }} when not in pull_request trigger target-repo: "owner/repo" # cross-repository: submit review on PR in another repo allowed-repos: ["org/repo1", "org/repo2"] # additional allowed repositories + allowed-events: [COMMENT, REQUEST_CHANGES] # restrict allowed review event types (default: all allowed) footer: false # omit AI-generated footer from review body (default: true) ``` +Use `allowed-events` to restrict which review event types the agent can submit. This provides infrastructure-level enforcement — for example, `allowed-events: [COMMENT, REQUEST_CHANGES]` prevents the agent from submitting APPROVE reviews regardless of what the agent attempts to output. If omitted, all event types (APPROVE, COMMENT, REQUEST_CHANGES) are allowed. + ### Resolve PR Review Thread (`resolve-pull-request-review-thread:`) Resolves review threads on pull requests. Allows AI agents to mark review conversations as resolved after addressing the feedback. Uses the GitHub GraphQL API with the `resolveReviewThread` mutation. diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 8a64c16cc14..35534577b20 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -1887,7 +1887,7 @@ }, "status-comment": { "type": "boolean", - "description": "Whether to post status comments (started/completed) on the triggering item. When true, adds a comment with workflow run link and updates it on completion. When false or not specified, no status comments are posted. Automatically enabled for slash_command and label_command triggers — manual configuration is only needed for other trigger types.", + "description": "Whether to post status comments (started/completed) on the triggering item. When true, adds a comment with workflow run link and updates it on completion. When false or not specified, no status comments are posted. Automatically enabled for slash_command and label_command triggers \u2014 manual configuration is only needed for other trigger types.", "examples": [true, false] }, "github-token": { @@ -5907,6 +5907,14 @@ }, "description": "List of additional repositories in format 'owner/repo' that PR reviews can be submitted in. When specified, the agent can use a 'repo' field in the output to specify which repository to submit the review in. The target repository (current or target-repo) is always implicitly allowed." }, + "allowed-events": { + "type": "array", + "items": { + "type": "string", + "enum": ["APPROVE", "COMMENT", "REQUEST_CHANGES"] + }, + "description": "Optional list of allowed review event types. If omitted, all event types (APPROVE, COMMENT, REQUEST_CHANGES) are allowed. Use this to restrict the agent to specific event types, e.g. [COMMENT, REQUEST_CHANGES] to prevent approvals." + }, "github-token": { "$ref": "#/$defs/github_token", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." @@ -7553,12 +7561,16 @@ "properties": { "include": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "Glob patterns for files to include" }, "exclude": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "Glob patterns for files to exclude" } }, diff --git a/pkg/workflow/compiler_safe_outputs_config.go b/pkg/workflow/compiler_safe_outputs_config.go index 85461fea7d7..12b747fba4a 100644 --- a/pkg/workflow/compiler_safe_outputs_config.go +++ b/pkg/workflow/compiler_safe_outputs_config.go @@ -461,6 +461,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target", c.Target). AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). + AddStringSlice("allowed_events", c.AllowedEvents). AddIfNotEmpty("github-token", c.GitHubToken). AddStringPtr("footer", getEffectiveFooterString(c.Footer, cfg.Footer)). AddIfTrue("staged", c.Staged). diff --git a/pkg/workflow/submit_pr_review.go b/pkg/workflow/submit_pr_review.go index 96ea51ed3bb..100aea7698c 100644 --- a/pkg/workflow/submit_pr_review.go +++ b/pkg/workflow/submit_pr_review.go @@ -1,6 +1,8 @@ package workflow import ( + "strings" + "github.com/github/gh-aw/pkg/logger" ) @@ -13,7 +15,8 @@ var submitPRReviewLog = logger.New("workflow:submit_pr_review") type SubmitPullRequestReviewConfig struct { BaseSafeOutputConfig `yaml:",inline"` SafeOutputTargetConfig `yaml:",inline"` - Footer *string `yaml:"footer,omitempty"` // Controls when to show footer in PR review body: "always" (default), "none", or "if-body" (only when review has body text) + Footer *string `yaml:"footer,omitempty"` // Controls when to show footer in PR review body: "always" (default), "none", or "if-body" (only when review has body text) + AllowedEvents []string `yaml:"allowed-events,omitempty"` // Optional list of allowed review event types: APPROVE, COMMENT, REQUEST_CHANGES. If omitted, all event types are allowed. } // parseSubmitPullRequestReviewConfig handles submit-pull-request-review configuration @@ -71,7 +74,24 @@ func (c *Compiler) parseSubmitPullRequestReviewConfig(outputMap map[string]any) } } - submitPRReviewLog.Printf("Parsed submit-pull-request-review config: max=%d, target=%s, target_repo=%s", templatableIntValue(config.Max), config.Target, config.TargetRepoSlug) + // Parse allowed-events configuration + if allowedEvents, exists := configMap["allowed-events"]; exists { + if eventsSlice, ok := allowedEvents.([]any); ok { + validEvents := map[string]bool{"APPROVE": true, "COMMENT": true, "REQUEST_CHANGES": true} + for _, e := range eventsSlice { + if eventStr, ok := e.(string); ok { + upper := strings.ToUpper(eventStr) + if validEvents[upper] { + config.AllowedEvents = append(config.AllowedEvents, upper) + } else { + submitPRReviewLog.Printf("Ignoring invalid allowed-events value: %s", eventStr) + } + } + } + } + } + + submitPRReviewLog.Printf("Parsed submit-pull-request-review config: max=%d, target=%s, target_repo=%s, allowed_events=%v", templatableIntValue(config.Max), config.Target, config.TargetRepoSlug, config.AllowedEvents) } else { // If configData is nil or not a map, set the default max config.Max = defaultIntStr(1) diff --git a/pkg/workflow/submit_pr_review_footer_test.go b/pkg/workflow/submit_pr_review_footer_test.go index aa1c4b986af..9af4d544c59 100644 --- a/pkg/workflow/submit_pr_review_footer_test.go +++ b/pkg/workflow/submit_pr_review_footer_test.go @@ -198,6 +198,75 @@ func TestSubmitPRReviewFooterConfig(t *testing.T) { assert.Equal(t, []string{"consumer-org/other-repo", "consumer-org/another-repo"}, config.AllowedRepos, "AllowedRepos should be parsed") }) + t.Run("parses allowed-events field", func(t *testing.T) { + compiler := NewCompiler() + outputMap := map[string]any{ + "submit-pull-request-review": map[string]any{ + "max": 1, + "allowed-events": []any{"COMMENT", "REQUEST_CHANGES"}, + }, + } + + config := compiler.parseSubmitPullRequestReviewConfig(outputMap) + require.NotNil(t, config, "Config should be parsed") + assert.Equal(t, []string{"COMMENT", "REQUEST_CHANGES"}, config.AllowedEvents, "AllowedEvents should be parsed") + }) + + t.Run("parses allowed-events and normalizes to uppercase", func(t *testing.T) { + compiler := NewCompiler() + outputMap := map[string]any{ + "submit-pull-request-review": map[string]any{ + "max": 1, + "allowed-events": []any{"comment", "approve"}, + }, + } + + config := compiler.parseSubmitPullRequestReviewConfig(outputMap) + require.NotNil(t, config, "Config should be parsed") + assert.Equal(t, []string{"COMMENT", "APPROVE"}, config.AllowedEvents, "AllowedEvents should be normalized to uppercase") + }) + + t.Run("ignores invalid values in allowed-events", func(t *testing.T) { + compiler := NewCompiler() + outputMap := map[string]any{ + "submit-pull-request-review": map[string]any{ + "max": 1, + "allowed-events": []any{"COMMENT", "INVALID_EVENT", "APPROVE"}, + }, + } + + config := compiler.parseSubmitPullRequestReviewConfig(outputMap) + require.NotNil(t, config, "Config should be parsed") + assert.Equal(t, []string{"COMMENT", "APPROVE"}, config.AllowedEvents, "Invalid events should be ignored") + }) + + t.Run("allowed-events empty when omitted", func(t *testing.T) { + compiler := NewCompiler() + outputMap := map[string]any{ + "submit-pull-request-review": map[string]any{ + "max": 1, + }, + } + + config := compiler.parseSubmitPullRequestReviewConfig(outputMap) + require.NotNil(t, config, "Config should be parsed") + assert.Empty(t, config.AllowedEvents, "AllowedEvents should be empty when not configured") + }) + + t.Run("parses all three valid event types in allowed-events", func(t *testing.T) { + compiler := NewCompiler() + outputMap := map[string]any{ + "submit-pull-request-review": map[string]any{ + "max": 1, + "allowed-events": []any{"APPROVE", "COMMENT", "REQUEST_CHANGES"}, + }, + } + + config := compiler.parseSubmitPullRequestReviewConfig(outputMap) + require.NotNil(t, config, "Config should be parsed") + assert.Equal(t, []string{"APPROVE", "COMMENT", "REQUEST_CHANGES"}, config.AllowedEvents, "All three event types should be parsed") + }) + t.Run("returns nil for wildcard target-repo", func(t *testing.T) { compiler := NewCompiler() outputMap := map[string]any{ From 5b475a77e8a0e1edf8d322e035a6e340f78d086b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:15:11 +0000 Subject: [PATCH 3/3] Address review feedback: fail-closed allowed-events, minItems:1 schema constraint, compiler test Agent-Logs-Url: https://github.com/github/gh-aw/sessions/f51da69c-737b-4d35-b3dc-c9ce3c172474 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schemas/main_workflow_schema.json | 3 +- pkg/workflow/submit_pr_review.go | 29 +++-- pkg/workflow/submit_pr_review_footer_test.go | 112 ++++++++++++++++++- 3 files changed, 130 insertions(+), 14 deletions(-) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 35534577b20..8fb5729f7a0 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -5913,7 +5913,8 @@ "type": "string", "enum": ["APPROVE", "COMMENT", "REQUEST_CHANGES"] }, - "description": "Optional list of allowed review event types. If omitted, all event types (APPROVE, COMMENT, REQUEST_CHANGES) are allowed. Use this to restrict the agent to specific event types, e.g. [COMMENT, REQUEST_CHANGES] to prevent approvals." + "description": "Optional list of allowed review event types. If omitted, all event types (APPROVE, COMMENT, REQUEST_CHANGES) are allowed. Use this to restrict the agent to specific event types, e.g. [COMMENT, REQUEST_CHANGES] to prevent approvals.", + "minItems": 1 }, "github-token": { "$ref": "#/$defs/github_token", diff --git a/pkg/workflow/submit_pr_review.go b/pkg/workflow/submit_pr_review.go index 100aea7698c..fc75fc40749 100644 --- a/pkg/workflow/submit_pr_review.go +++ b/pkg/workflow/submit_pr_review.go @@ -76,19 +76,28 @@ func (c *Compiler) parseSubmitPullRequestReviewConfig(outputMap map[string]any) // Parse allowed-events configuration if allowedEvents, exists := configMap["allowed-events"]; exists { - if eventsSlice, ok := allowedEvents.([]any); ok { - validEvents := map[string]bool{"APPROVE": true, "COMMENT": true, "REQUEST_CHANGES": true} - for _, e := range eventsSlice { - if eventStr, ok := e.(string); ok { - upper := strings.ToUpper(eventStr) - if validEvents[upper] { - config.AllowedEvents = append(config.AllowedEvents, upper) - } else { - submitPRReviewLog.Printf("Ignoring invalid allowed-events value: %s", eventStr) - } + eventsSlice, ok := allowedEvents.([]any) + if !ok { + submitPRReviewLog.Printf("Invalid allowed-events configuration: must be a list of review event types") + return nil + } + + validEvents := map[string]bool{"APPROVE": true, "COMMENT": true, "REQUEST_CHANGES": true} + for _, e := range eventsSlice { + if eventStr, ok := e.(string); ok { + upper := strings.ToUpper(eventStr) + if validEvents[upper] { + config.AllowedEvents = append(config.AllowedEvents, upper) + } else { + submitPRReviewLog.Printf("Ignoring invalid allowed-events value: %s", eventStr) } } } + + if len(config.AllowedEvents) == 0 { + submitPRReviewLog.Printf("Invalid allowed-events configuration: at least one valid event type is required when allowed-events is specified") + return nil + } } submitPRReviewLog.Printf("Parsed submit-pull-request-review config: max=%d, target=%s, target_repo=%s, allowed_events=%v", templatableIntValue(config.Max), config.Target, config.TargetRepoSlug, config.AllowedEvents) diff --git a/pkg/workflow/submit_pr_review_footer_test.go b/pkg/workflow/submit_pr_review_footer_test.go index 9af4d544c59..32422c1a455 100644 --- a/pkg/workflow/submit_pr_review_footer_test.go +++ b/pkg/workflow/submit_pr_review_footer_test.go @@ -226,7 +226,7 @@ func TestSubmitPRReviewFooterConfig(t *testing.T) { assert.Equal(t, []string{"COMMENT", "APPROVE"}, config.AllowedEvents, "AllowedEvents should be normalized to uppercase") }) - t.Run("ignores invalid values in allowed-events", func(t *testing.T) { + t.Run("ignores invalid values in allowed-events when mixed with valid values", func(t *testing.T) { compiler := NewCompiler() outputMap := map[string]any{ "submit-pull-request-review": map[string]any{ @@ -236,8 +236,34 @@ func TestSubmitPRReviewFooterConfig(t *testing.T) { } config := compiler.parseSubmitPullRequestReviewConfig(outputMap) - require.NotNil(t, config, "Config should be parsed") - assert.Equal(t, []string{"COMMENT", "APPROVE"}, config.AllowedEvents, "Invalid events should be ignored") + require.NotNil(t, config, "Config should be parsed when at least one valid event remains") + assert.Equal(t, []string{"COMMENT", "APPROVE"}, config.AllowedEvents, "Invalid events should be ignored while valid ones remain") + }) + + t.Run("returns nil when allowed-events contains only invalid values (fail closed)", func(t *testing.T) { + compiler := NewCompiler() + outputMap := map[string]any{ + "submit-pull-request-review": map[string]any{ + "max": 1, + "allowed-events": []any{"INVALID_EVENT", "ANOTHER_BAD_VALUE"}, + }, + } + + config := compiler.parseSubmitPullRequestReviewConfig(outputMap) + assert.Nil(t, config, "Config should be nil when all allowed-events values are invalid (fail closed)") + }) + + t.Run("returns nil when allowed-events is not a list (fail closed)", func(t *testing.T) { + compiler := NewCompiler() + outputMap := map[string]any{ + "submit-pull-request-review": map[string]any{ + "max": 1, + "allowed-events": "COMMENT", + }, + } + + config := compiler.parseSubmitPullRequestReviewConfig(outputMap) + assert.Nil(t, config, "Config should be nil when allowed-events is not a list (fail closed)") }) t.Run("allowed-events empty when omitted", func(t *testing.T) { @@ -461,4 +487,84 @@ func TestSubmitPRReviewFooterInHandlerConfig(t *testing.T) { } } }) + + t.Run("allowed_events included in submit_pull_request_review handler config when set", func(t *testing.T) { + compiler := NewCompiler() + workflowData := &WorkflowData{ + Name: "Test", + SafeOutputs: &SafeOutputsConfig{ + SubmitPullRequestReview: &SubmitPullRequestReviewConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{Max: strPtr("1")}, + AllowedEvents: []string{"COMMENT", "REQUEST_CHANGES"}, + }, + }, + } + + var steps []string + compiler.addHandlerManagerConfigEnvVar(&steps, workflowData) + require.NotEmpty(t, steps, "Steps should not be empty") + + stepsContent := strings.Join(steps, "") + require.Contains(t, stepsContent, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") + + 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 handlerConfig map[string]any + err := json.Unmarshal([]byte(jsonStr), &handlerConfig) + require.NoError(t, err, "Should unmarshal handler config") + + submitConfig, ok := handlerConfig["submit_pull_request_review"].(map[string]any) + require.True(t, ok, "submit_pull_request_review config should exist") + allowedEvents, ok := submitConfig["allowed_events"].([]any) + require.True(t, ok, "allowed_events should be present in handler config") + require.Len(t, allowedEvents, 2, "allowed_events should have 2 entries") + assert.Equal(t, "COMMENT", allowedEvents[0], "First allowed event should be COMMENT") + assert.Equal(t, "REQUEST_CHANGES", allowedEvents[1], "Second allowed event should be REQUEST_CHANGES") + } + } + } + }) + + t.Run("allowed_events not in handler config when not set", func(t *testing.T) { + compiler := NewCompiler() + workflowData := &WorkflowData{ + Name: "Test", + SafeOutputs: &SafeOutputsConfig{ + SubmitPullRequestReview: &SubmitPullRequestReviewConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{Max: strPtr("1")}, + }, + }, + } + + var steps []string + compiler.addHandlerManagerConfigEnvVar(&steps, workflowData) + require.NotEmpty(t, steps, "Steps should not be empty") + + stepsContent := strings.Join(steps, "") + require.Contains(t, stepsContent, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") + + 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 handlerConfig map[string]any + err := json.Unmarshal([]byte(jsonStr), &handlerConfig) + require.NoError(t, err, "Should unmarshal handler config") + + submitConfig, ok := handlerConfig["submit_pull_request_review"].(map[string]any) + require.True(t, ok, "submit_pull_request_review config should exist") + _, hasAllowedEvents := submitConfig["allowed_events"] + assert.False(t, hasAllowedEvents, "allowed_events should not be in handler config when not set") + } + } + } + }) }