diff --git a/.github/workflows/smoke-copilot-arm.lock.yml b/.github/workflows/smoke-copilot-arm.lock.yml index e91adec39d8..06c72279d46 100644 --- a/.github/workflows/smoke-copilot-arm.lock.yml +++ b/.github/workflows/smoke-copilot-arm.lock.yml @@ -2181,6 +2181,7 @@ jobs: GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_DETECTION_CONCLUSION: ${{ needs.agent.outputs.detection_conclusion }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e 📰 *BREAKING: Report filed by [{workflow_name}]({run_url})*{history_link}\",\"appendOnlyComments\":true,\"runStarted\":\"📰 BREAKING: [{workflow_name}]({run_url}) is now investigating this {event_type}. Sources say the story is developing...\",\"runSuccess\":\"📰 VERDICT: [{workflow_name}]({run_url}) has concluded. All systems operational. This is a developing story. 🎤\",\"runFailure\":\"📰 DEVELOPING STORY: [{workflow_name}]({run_url}) reports {status}. Our correspondents are investigating the incident...\"}" + GH_AW_SAFE_OUTPUT_JOBS: "{\"send_slack_message\":\"\"}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index ceb342a2643..acacb9d0aa0 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -2296,6 +2296,7 @@ jobs: GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_DETECTION_CONCLUSION: ${{ needs.agent.outputs.detection_conclusion }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e 📰 *BREAKING: Report filed by [{workflow_name}]({run_url})*{history_link}\",\"appendOnlyComments\":true,\"runStarted\":\"📰 BREAKING: [{workflow_name}]({run_url}) is now investigating this {event_type}. Sources say the story is developing...\",\"runSuccess\":\"📰 VERDICT: [{workflow_name}]({run_url}) has concluded. All systems operational. This is a developing story. 🎤\",\"runFailure\":\"📰 DEVELOPING STORY: [{workflow_name}]({run_url}) reports {status}. Our correspondents are investigating the incident...\"}" + GH_AW_SAFE_OUTPUT_JOBS: "{\"send_slack_message\":\"\"}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/actions/setup/js/create_pr_review_comment.cjs b/actions/setup/js/create_pr_review_comment.cjs index 902286028f5..401a058b4c2 100644 --- a/actions/setup/js/create_pr_review_comment.cjs +++ b/actions/setup/js/create_pr_review_comment.cjs @@ -118,6 +118,7 @@ async function main(config = {}) { return { success: false, error: "Not in pull request context", + skipped: true, }; } diff --git a/actions/setup/js/create_pr_review_comment.test.cjs b/actions/setup/js/create_pr_review_comment.test.cjs index 02ea89436ec..65b543fe55b 100644 --- a/actions/setup/js/create_pr_review_comment.test.cjs +++ b/actions/setup/js/create_pr_review_comment.test.cjs @@ -234,6 +234,7 @@ describe("create_pr_review_comment.cjs", () => { expect(result.success).toBe(false); expect(result.error).toContain("Not in pull request context"); + expect(result.skipped).toBe(true); expect(buffer.getBufferedCount()).toBe(0); }); @@ -409,6 +410,7 @@ describe("create_pr_review_comment.cjs", () => { expect(result.success).toBe(false); expect(result.error).toContain("Not in pull request context"); + expect(result.skipped).toBe(true); expect(buffer.getBufferedCount()).toBe(0); }); diff --git a/actions/setup/js/reply_to_pr_review_comment.cjs b/actions/setup/js/reply_to_pr_review_comment.cjs index beeda671549..ef668e34f3c 100644 --- a/actions/setup/js/reply_to_pr_review_comment.cjs +++ b/actions/setup/js/reply_to_pr_review_comment.cjs @@ -120,6 +120,7 @@ async function main(config = {}) { return { success: false, error: "Cannot reply to review comments outside of a pull request context", + skipped: true, }; } targetPRNumber = triggeringPRNumber; diff --git a/actions/setup/js/reply_to_pr_review_comment.test.cjs b/actions/setup/js/reply_to_pr_review_comment.test.cjs index 6a346fc6cea..41d48c3d6f2 100644 --- a/actions/setup/js/reply_to_pr_review_comment.test.cjs +++ b/actions/setup/js/reply_to_pr_review_comment.test.cjs @@ -130,6 +130,7 @@ describe("reply_to_pr_review_comment", () => { expect(result.success).toBe(false); expect(result.error).toContain("pull request context"); + expect(result.skipped).toBe(true); }); it("should work when triggered from issue_comment on a PR", async () => { diff --git a/pkg/workflow/action_pins_test.go b/pkg/workflow/action_pins_test.go index 415a53d1400..a14b3fd1885 100644 --- a/pkg/workflow/action_pins_test.go +++ b/pkg/workflow/action_pins_test.go @@ -297,9 +297,9 @@ func TestApplyActionPinToStep(t *testing.T) { func TestGetActionPinsSorting(t *testing.T) { pins := getActionPins() - // Verify we got all the pins (38 as of March 2026) - if len(pins) != 38 { - t.Errorf("getActionPins() returned %d pins, expected 38", len(pins)) + // Verify we got all the pins (39 as of March 2026) + if len(pins) != 39 { + t.Errorf("getActionPins() returned %d pins, expected 39", len(pins)) } // Verify they are sorted by version (descending) then by repository name (ascending) diff --git a/pkg/workflow/notify_comment.go b/pkg/workflow/notify_comment.go index 40492f176ba..652faf2751d 100644 --- a/pkg/workflow/notify_comment.go +++ b/pkg/workflow/notify_comment.go @@ -431,8 +431,23 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa return job, nil } +// systemSafeOutputJobNames contains job names that are built-in system jobs and should not be +// treated as custom safe output job types in the GH_AW_SAFE_OUTPUT_JOBS mapping. +// The safe output handler manager uses this mapping to determine which message types are +// handled by custom job steps (and therefore should be silently skipped rather than flagged +// as "no handler loaded"). +var systemSafeOutputJobNames = map[string]bool{ + "safe_outputs": true, // consolidated safe outputs job + "upload_assets": true, // upload assets job +} + // buildSafeOutputJobsEnvVars creates environment variables for safe output job URLs -// Returns both a JSON mapping and the actual environment variable declarations +// Returns both a JSON mapping and the actual environment variable declarations. +// The mapping includes: +// - Built-in jobs with known URL outputs (e.g., create_issue → issue_url) +// - Custom safe-output jobs (from safe-outputs.jobs) with an empty URL key, so the handler +// manager knows those message types are handled by a dedicated job step and should be +// skipped gracefully rather than reported as "No handler loaded". func buildSafeOutputJobsEnvVars(jobNames []string) (string, []string) { // Map job names to their expected URL output keys jobOutputMapping := make(map[string]string) @@ -462,7 +477,13 @@ func buildSafeOutputJobsEnvVars(jobNames []string) (string, []string) { case "push_to_pull_request_branch": urlKey = "commit_url" default: - // Skip jobs that don't have URL outputs + if systemSafeOutputJobNames[jobName] { + // Skip known system jobs — they are not custom safe output types + continue + } + // Custom safe-output job: include in the mapping with an empty URL key so the + // handler manager can silently skip messages of this type. + jobOutputMapping[jobName] = "" continue } diff --git a/pkg/workflow/notify_comment_test.go b/pkg/workflow/notify_comment_test.go index aac7f055906..4b60d81a9bb 100644 --- a/pkg/workflow/notify_comment_test.go +++ b/pkg/workflow/notify_comment_test.go @@ -608,14 +608,27 @@ func TestBuildSafeOutputJobsEnvVars(t *testing.T) { checkJSONKeys: []string{"push_to_pull_request_branch"}, }, { - name: "skips jobs without URL outputs", - jobNames: []string{"create_issue", "detection", "some_custom_job"}, + name: "includes custom jobs and skips system jobs", + jobNames: []string{"create_issue", "safe_outputs", "some_custom_job"}, expectJSON: true, expectEnvVars: 1, checkEnvVars: []string{ "GH_AW_OUTPUT_CREATE_ISSUE_ISSUE_URL: ${{ needs.create_issue.outputs.issue_url }}", }, - checkJSONKeys: []string{"create_issue"}, + checkJSONKeys: []string{"create_issue", "some_custom_job"}, + }, + { + name: "custom-only jobs produce JSON without URL env vars", + jobNames: []string{"safe_outputs", "send_slack_message"}, + expectJSON: true, + expectEnvVars: 0, + checkJSONKeys: []string{"send_slack_message"}, + }, + { + name: "system jobs are excluded from JSON", + jobNames: []string{"safe_outputs", "upload_assets"}, + expectJSON: false, + expectEnvVars: 0, }, { name: "handles empty job list",