From c18c4803c5f898e9b579a48e494db001803a1a07 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 19 Apr 2026 19:01:23 +0000
Subject: [PATCH 01/10] feat: add maintenance activity report operation
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/267bf5c1-a299-43c5-be20-17cdb0ab2a0c
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.github/workflows/agentics-maintenance.yml | 54 +++++++-
actions/setup/js/run_activity_report.cjs | 129 ++++++++++++++++++
actions/setup/js/run_activity_report.test.cjs | 115 ++++++++++++++++
pkg/workflow/maintenance_workflow_test.go | 40 ++++--
pkg/workflow/maintenance_workflow_yaml.go | 53 ++++++-
pkg/workflow/side_repo_maintenance.go | 50 ++++++-
.../side_repo_maintenance_integration_test.go | 4 +
7 files changed, 431 insertions(+), 14 deletions(-)
create mode 100644 actions/setup/js/run_activity_report.cjs
create mode 100644 actions/setup/js/run_activity_report.test.cjs
diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml
index 59d99c41a74..1d63f82e62b 100644
--- a/.github/workflows/agentics-maintenance.yml
+++ b/.github/workflows/agentics-maintenance.yml
@@ -50,6 +50,7 @@ on:
- 'upgrade'
- 'safe_outputs'
- 'create_labels'
+ - 'activity_report'
- 'close_agentic_workflows_issues'
- 'clean_cache_memories'
- 'validate'
@@ -61,7 +62,7 @@ on:
workflow_call:
inputs:
operation:
- description: 'Optional maintenance operation to run (disable, enable, update, upgrade, safe_outputs, create_labels, close_agentic_workflows_issues, clean_cache_memories, validate)'
+ description: 'Optional maintenance operation to run (disable, enable, update, upgrade, safe_outputs, create_labels, activity_report, close_agentic_workflows_issues, clean_cache_memories, validate)'
required: false
type: string
default: ''
@@ -156,7 +157,7 @@ jobs:
await main();
run_operation:
- if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation != '' && inputs.operation != 'safe_outputs' && inputs.operation != 'create_labels' && inputs.operation != 'close_agentic_workflows_issues' && inputs.operation != 'clean_cache_memories' && inputs.operation != 'validate' && (!(github.event.repository.fork)) }}
+ if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation != '' && inputs.operation != 'safe_outputs' && inputs.operation != 'create_labels' && inputs.operation != 'activity_report' && inputs.operation != 'close_agentic_workflows_issues' && inputs.operation != 'clean_cache_memories' && inputs.operation != 'validate' && (!(github.event.repository.fork)) }}
runs-on: ubuntu-slim
permissions:
actions: write
@@ -311,6 +312,55 @@ jobs:
const { main } = require('${{ runner.temp }}/gh-aw/actions/create_labels.cjs');
await main();
+ activity_report:
+ if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'activity_report' && (!(github.event.repository.fork)) }}
+ runs-on: ubuntu-slim
+ permissions:
+ actions: read
+ issues: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+
+ - name: Setup Scripts
+ uses: ./actions/setup
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+
+ - name: Check admin/maintainer permissions
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs');
+ await main();
+
+ - name: Setup Go
+ uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
+ with:
+ go-version-file: go.mod
+ cache: true
+
+ - name: Build gh-aw
+ run: make build
+
+ - name: Generate agentic workflow activity report
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_AW_CMD_PREFIX: ./gh-aw
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/run_activity_report.cjs');
+ await main();
+
close_agentic_workflows_issues:
if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'close_agentic_workflows_issues' && (!(github.event.repository.fork)) }}
runs-on: ubuntu-slim
diff --git a/actions/setup/js/run_activity_report.cjs b/actions/setup/js/run_activity_report.cjs
new file mode 100644
index 00000000000..85c4ea15639
--- /dev/null
+++ b/actions/setup/js/run_activity_report.cjs
@@ -0,0 +1,129 @@
+// @ts-check
+///
+
+const { getErrorMessage, isRateLimitError } = require("./error_helpers.cjs");
+const { resolveExecutionOwnerRepo } = require("./repo_helpers.cjs");
+const { sanitizeContent } = require("./sanitize_content.cjs");
+
+const ISSUE_TITLE = "[AW activity report]";
+
+/** @typedef {{ key: string, heading: string, startDate: string, optionalOnRateLimit: boolean }} ActivityRange */
+
+/** @type {ActivityRange[]} */
+const REPORT_RANGES = [
+ { key: "24h", heading: "Last 24 hours", startDate: "-1d", optionalOnRateLimit: false },
+ { key: "7d", heading: "Last 7 days", startDate: "-1w", optionalOnRateLimit: false },
+ { key: "30d", heading: "Last 30 days", startDate: "-1mo", optionalOnRateLimit: true },
+];
+
+/**
+ * @param {string} text
+ * @returns {boolean}
+ */
+function hasRateLimitText(text) {
+ return /\bapi rate limit\b|\brate limit exceeded\b|\bsecondary rate limit\b|\b429\b/i.test(text);
+}
+
+/**
+ * Run the logs command for a configured report range.
+ *
+ * @param {string} bin
+ * @param {string[]} prefixArgs
+ * @param {string} repoSlug
+ * @param {ActivityRange} range
+ * @returns {Promise<{ heading: string, body: string }>}
+ */
+async function runRangeReport(bin, prefixArgs, repoSlug, range) {
+ const args = [...prefixArgs, "logs", "--repo", repoSlug, "--start-date", range.startDate, "--format", "markdown"];
+ core.info(`Running: ${bin} ${args.join(" ")}`);
+
+ try {
+ const result = await exec.getExecOutput(bin, args, { ignoreReturnCode: true });
+ const output = `${result.stdout || ""}\n${result.stderr || ""}`.trim();
+ const rateLimited = hasRateLimitText(output);
+
+ if (result.exitCode === 0 && result.stdout.trim()) {
+ return {
+ heading: range.heading,
+ body: sanitizeContent(result.stdout.trim()),
+ };
+ }
+
+ if (rateLimited && range.optionalOnRateLimit) {
+ core.warning(`Skipping ${range.heading} report due to GitHub API rate limiting`);
+ return {
+ heading: range.heading,
+ body: "_Skipped due to GitHub API rate limiting._",
+ };
+ }
+
+ if (rateLimited) {
+ return {
+ heading: range.heading,
+ body: "_Could not generate this section due to GitHub API rate limiting._",
+ };
+ }
+
+ return {
+ heading: range.heading,
+ body: `_Report command failed (exit code ${result.exitCode})._\n\n\`\`\`\n${sanitizeContent(output || "No command output was captured.")}\n\`\`\``,
+ };
+ } catch (error) {
+ const errorMessage = getErrorMessage(error);
+ const rateLimited = isRateLimitError(error) || hasRateLimitText(errorMessage);
+
+ if (rateLimited && range.optionalOnRateLimit) {
+ core.warning(`Skipping ${range.heading} report due to GitHub API rate limiting`);
+ return {
+ heading: range.heading,
+ body: "_Skipped due to GitHub API rate limiting._",
+ };
+ }
+
+ if (rateLimited) {
+ return {
+ heading: range.heading,
+ body: "_Could not generate this section due to GitHub API rate limiting._",
+ };
+ }
+
+ return {
+ heading: range.heading,
+ body: `_Report command failed: ${sanitizeContent(errorMessage)}_`,
+ };
+ }
+}
+
+/**
+ * Generate an agentic workflow activity report issue.
+ * @returns {Promise}
+ */
+async function main() {
+ const cmdPrefixStr = process.env.GH_AW_CMD_PREFIX || "gh aw";
+ const [bin, ...prefixArgs] = cmdPrefixStr.split(" ").filter(Boolean);
+ const { owner, repo } = resolveExecutionOwnerRepo();
+ const repoSlug = `${owner}/${repo}`;
+
+ core.info(`Generating agentic workflow activity report for ${repoSlug}`);
+
+ const sections = [];
+ for (const range of REPORT_RANGES) {
+ sections.push(await runRangeReport(bin, prefixArgs, repoSlug, range));
+ }
+
+ const body = ["## Agentic workflow activity report", "", `Repository: \`${repoSlug}\``, `Generated at: ${new Date().toISOString()}`, "", ...sections.flatMap(section => ["---", "", `## ${section.heading}`, "", section.body, ""])].join(
+ "\n"
+ );
+
+ const createdIssue = await github.rest.issues.create({
+ owner,
+ repo,
+ title: ISSUE_TITLE,
+ body,
+ labels: ["agentic-workflows"],
+ });
+
+ core.info(`Created issue #${createdIssue.data.number}: ${createdIssue.data.html_url}`);
+}
+
+module.exports = { main, hasRateLimitText, runRangeReport };
diff --git a/actions/setup/js/run_activity_report.test.cjs b/actions/setup/js/run_activity_report.test.cjs
new file mode 100644
index 00000000000..2514e40161e
--- /dev/null
+++ b/actions/setup/js/run_activity_report.test.cjs
@@ -0,0 +1,115 @@
+// @ts-check
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
+
+describe("run_activity_report", () => {
+ let originalGlobals;
+ let originalEnv;
+ let mockCore;
+ let mockGithub;
+ let mockContext;
+ let mockExec;
+
+ beforeEach(() => {
+ originalEnv = { ...process.env };
+ process.env.GH_AW_CMD_PREFIX = "gh aw";
+
+ originalGlobals = {
+ core: global.core,
+ github: global.github,
+ context: global.context,
+ exec: global.exec,
+ };
+
+ mockCore = {
+ info: vi.fn(),
+ warning: vi.fn(),
+ };
+ mockGithub = {
+ rest: {
+ issues: {
+ create: vi.fn().mockResolvedValue({
+ data: { number: 42, html_url: "https://github.com/testowner/testrepo/issues/42" },
+ }),
+ },
+ },
+ };
+ mockContext = {
+ repo: {
+ owner: "testowner",
+ repo: "testrepo",
+ },
+ };
+ mockExec = {
+ getExecOutput: vi.fn(),
+ };
+
+ global.core = mockCore;
+ global.github = mockGithub;
+ global.context = mockContext;
+ global.exec = mockExec;
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ global.core = originalGlobals.core;
+ global.github = originalGlobals.github;
+ global.context = originalGlobals.context;
+ global.exec = originalGlobals.exec;
+ vi.clearAllMocks();
+ });
+
+ it("creates an activity report issue with all three time ranges", async () => {
+ mockExec.getExecOutput
+ .mockResolvedValueOnce({ stdout: "## 24h report\nok", stderr: "", exitCode: 0 })
+ .mockResolvedValueOnce({ stdout: "## 7d report\nok", stderr: "", exitCode: 0 })
+ .mockResolvedValueOnce({ stdout: "## 30d report\nok", stderr: "", exitCode: 0 });
+
+ const { main } = await import("./run_activity_report.cjs");
+ await main();
+
+ expect(mockExec.getExecOutput).toHaveBeenCalledTimes(3);
+ expect(mockExec.getExecOutput).toHaveBeenNthCalledWith(1, "gh", expect.arrayContaining(["aw", "logs", "--repo", "testowner/testrepo", "--start-date", "-1d", "--format", "markdown"]), expect.objectContaining({ ignoreReturnCode: true }));
+ expect(mockExec.getExecOutput).toHaveBeenNthCalledWith(2, "gh", expect.arrayContaining(["aw", "logs", "--repo", "testowner/testrepo", "--start-date", "-1w", "--format", "markdown"]), expect.objectContaining({ ignoreReturnCode: true }));
+ expect(mockExec.getExecOutput).toHaveBeenNthCalledWith(
+ 3,
+ "gh",
+ expect.arrayContaining(["aw", "logs", "--repo", "testowner/testrepo", "--start-date", "-1mo", "--format", "markdown"]),
+ expect.objectContaining({ ignoreReturnCode: true })
+ );
+
+ expect(mockGithub.rest.issues.create).toHaveBeenCalledWith(
+ expect.objectContaining({
+ owner: "testowner",
+ repo: "testrepo",
+ title: "[AW activity report]",
+ labels: ["agentic-workflows"],
+ })
+ );
+
+ const issueBody = mockGithub.rest.issues.create.mock.calls[0][0].body;
+ expect(issueBody).toContain("## Last 24 hours");
+ expect(issueBody).toContain("## Last 7 days");
+ expect(issueBody).toContain("## Last 30 days");
+ });
+
+ it("skips the 30-day query when rate limited", async () => {
+ mockExec.getExecOutput
+ .mockResolvedValueOnce({ stdout: "24h", stderr: "", exitCode: 0 })
+ .mockResolvedValueOnce({ stdout: "7d", stderr: "", exitCode: 0 })
+ .mockResolvedValueOnce({ stdout: "", stderr: "API rate limit exceeded", exitCode: 1 });
+
+ const { main } = await import("./run_activity_report.cjs");
+ await main();
+
+ expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Skipping Last 30 days report"));
+ const issueBody = mockGithub.rest.issues.create.mock.calls[0][0].body;
+ expect(issueBody).toContain("Skipped due to GitHub API rate limiting.");
+ });
+
+ it("detects rate limit text helper", async () => {
+ const { hasRateLimitText } = await import("./run_activity_report.cjs");
+ expect(hasRateLimitText("API rate limit exceeded")).toBe(true);
+ expect(hasRateLimitText("secondary rate limit")).toBe(true);
+ expect(hasRateLimitText("normal output")).toBe(false);
+ });
+});
diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go
index 985d9b5a813..fcdcb1a60d5 100644
--- a/pkg/workflow/maintenance_workflow_test.go
+++ b/pkg/workflow/maintenance_workflow_test.go
@@ -282,9 +282,10 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
yaml := string(content)
operationSkipCondition := `github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == ''`
- operationRunCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation != '' && inputs.operation != 'safe_outputs' && inputs.operation != 'create_labels' && inputs.operation != 'close_agentic_workflows_issues' && inputs.operation != 'clean_cache_memories' && inputs.operation != 'validate'`
+ operationRunCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation != '' && inputs.operation != 'safe_outputs' && inputs.operation != 'create_labels' && inputs.operation != 'activity_report' && inputs.operation != 'close_agentic_workflows_issues' && inputs.operation != 'clean_cache_memories' && inputs.operation != 'validate'`
applySafeOutputsCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'safe_outputs'`
createLabelsCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'create_labels'`
+ activityReportCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'activity_report'`
closeAgenticWorkflowIssuesCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'close_agentic_workflows_issues'`
cleanCacheMemoriesCondition := `github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '' || inputs.operation == 'clean_cache_memories'`
@@ -367,6 +368,17 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
}
}
+ // activity_report job should be triggered when operation == 'activity_report'
+ activityReportIdx := strings.Index(yaml, "\n activity_report:")
+ if activityReportIdx == -1 {
+ t.Errorf("Job activity_report not found in generated workflow")
+ } else {
+ activityReportSection := yaml[activityReportIdx : activityReportIdx+runOpSectionSearchRange]
+ if !strings.Contains(activityReportSection, activityReportCondition) {
+ t.Errorf("Job activity_report should have the activation condition %q in:\n%s", activityReportCondition, activityReportSection)
+ }
+ }
+
// close_agentic_workflows_issues job should be triggered when operation == 'close_agentic_workflows_issues'
closeAgenticWorkflowIssuesIdx := strings.Index(yaml, "\n close_agentic_workflows_issues:")
if closeAgenticWorkflowIssuesIdx == -1 {
@@ -398,6 +410,11 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
t.Error("workflow_dispatch operation choices should include 'validate'")
}
+ // Verify activity_report is an option in the operation choices
+ if !strings.Contains(yaml, "- 'activity_report'") {
+ t.Error("workflow_dispatch operation choices should include 'activity_report'")
+ }
+
// Verify close_agentic_workflows_issues is an option in the operation choices
if !strings.Contains(yaml, "- 'close_agentic_workflows_issues'") {
t.Error("workflow_dispatch operation choices should include 'close_agentic_workflows_issues'")
@@ -430,9 +447,10 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
// Verify run_operation job exposes outputs
runOpIdx2 := strings.Index(yaml, "\n run_operation:")
if runOpIdx2 != -1 {
- runOpSection2 := yaml[runOpIdx2 : runOpIdx2+600]
+ runOpEnd := min(runOpIdx2+1200, len(yaml))
+ runOpSection2 := yaml[runOpIdx2:runOpEnd]
if !strings.Contains(runOpSection2, "outputs:\n operation: ${{ steps.record.outputs.operation }}") {
- t.Errorf("run_operation job should declare operation output, got:\n%s", runOpSection2[:300])
+ t.Errorf("run_operation job should declare operation output, got:\n%s", runOpSection2[:min(300, len(runOpSection2))])
}
}
@@ -678,12 +696,12 @@ func TestGenerateMaintenanceWorkflow_RunOperationCLICodegen(t *testing.T) {
t.Fatalf("Expected maintenance workflow to be generated: %v", err)
}
yaml := string(content)
- // run_operation, create_labels, validate_workflows, and compile_workflows should use the same setup-go version
- // (all use getActionPin, not hardcoded pins). Exactly 4 occurrences expected.
+ // run_operation, create_labels, activity_report, validate_workflows, and compile_workflows should use the same setup-go version
+ // (all use getActionPin, not hardcoded pins). Exactly 5 occurrences expected.
setupGoPin := getActionPin("actions/setup-go")
occurrences := strings.Count(yaml, setupGoPin)
- if occurrences != 4 {
- t.Errorf("Expected exactly 4 occurrences of pinned setup-go ref %q (run_operation + create_labels + validate_workflows + compile_workflows), got %d in:\n%s",
+ if occurrences != 5 {
+ t.Errorf("Expected exactly 5 occurrences of pinned setup-go ref %q (run_operation + create_labels + activity_report + validate_workflows + compile_workflows), got %d in:\n%s",
setupGoPin, occurrences, yaml)
}
})
@@ -1143,6 +1161,9 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) {
if !strings.Contains(contentStr, "create_labels") {
t.Errorf("Side-repo maintenance should include create_labels job, got content length %d", len(contentStr))
}
+ if !strings.Contains(contentStr, "activity_report") {
+ t.Errorf("Side-repo maintenance should include activity_report job, got content length %d", len(contentStr))
+ }
})
t.Run("no side-repo file generated when no current checkout", func(t *testing.T) {
@@ -1172,7 +1193,7 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) {
}
})
- t.Run("side-repo generated without expires uses safe_outputs and create_labels only", func(t *testing.T) {
+ t.Run("side-repo generated without expires uses safe_outputs, create_labels, and activity_report", func(t *testing.T) {
tmpDir := t.TempDir()
workflowDataList := []*WorkflowData{
{
@@ -1213,6 +1234,9 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) {
if strings.Contains(contentStr, "close-expired-entities") {
t.Errorf("Side-repo maintenance should NOT include close-expired-entities when no expires, got content length %d", len(contentStr))
}
+ if !strings.Contains(contentStr, "activity_report") {
+ t.Errorf("Side-repo maintenance should include activity_report when no expires, got content length %d", len(contentStr))
+ }
})
t.Run("expression-based repository does not generate side-repo maintenance", func(t *testing.T) {
diff --git a/pkg/workflow/maintenance_workflow_yaml.go b/pkg/workflow/maintenance_workflow_yaml.go
index 0415c3de54f..d80023fb915 100644
--- a/pkg/workflow/maintenance_workflow_yaml.go
+++ b/pkg/workflow/maintenance_workflow_yaml.go
@@ -59,6 +59,7 @@ on:
- 'upgrade'
- 'safe_outputs'
- 'create_labels'
+ - 'activity_report'
- 'close_agentic_workflows_issues'
- 'clean_cache_memories'
- 'validate'
@@ -70,7 +71,7 @@ on:
workflow_call:
inputs:
operation:
- description: 'Optional maintenance operation to run (disable, enable, update, upgrade, safe_outputs, create_labels, close_agentic_workflows_issues, clean_cache_memories, validate)'
+ description: 'Optional maintenance operation to run (disable, enable, update, upgrade, safe_outputs, create_labels, activity_report, close_agentic_workflows_issues, clean_cache_memories, validate)'
required: false
type: string
default: ''
@@ -195,8 +196,8 @@ jobs:
`)
// Add unified run_operation job for all dispatch operations except those with dedicated jobs
- // (safe_outputs, create_labels, close_agentic_workflows_issues, clean_cache_memories, validate)
- runOperationCondition := buildRunOperationCondition("safe_outputs", "create_labels", "close_agentic_workflows_issues", "clean_cache_memories", "validate")
+ // (safe_outputs, create_labels, activity_report, close_agentic_workflows_issues, clean_cache_memories, validate)
+ runOperationCondition := buildRunOperationCondition("safe_outputs", "create_labels", "activity_report", "close_agentic_workflows_issues", "clean_cache_memories", "validate")
yaml.WriteString(`
run_operation:
if: ${{ ` + RenderCondition(runOperationCondition) + ` }}
@@ -349,6 +350,52 @@ jobs:
await main();
`)
+ // Add activity_report job for workflow_dispatch with operation == 'activity_report'
+ yaml.WriteString(`
+ activity_report:
+ if: ${{ ` + RenderCondition(buildDispatchOperationCondition("activity_report")) + ` }}
+ runs-on: ` + runsOnValue + `
+ permissions:
+ actions: read
+ issues: write
+ steps:
+ - name: Checkout repository
+ uses: ` + getActionPin("actions/checkout") + `
+ with:
+ persist-credentials: false
+
+ - name: Setup Scripts
+ uses: ` + setupActionRef + `
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+
+ - name: Check admin/maintainer permissions
+ uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + `
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs');
+ await main();
+
+`)
+
+ yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver))
+ yaml.WriteString(` - name: Generate agentic workflow activity report
+ uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + `
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_AW_CMD_PREFIX: ` + getCLICmdPrefix(actionMode) + `
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/run_activity_report.cjs');
+ await main();
+`)
+
// Add close_agentic_workflows_issues job for workflow_dispatch with operation == 'close_agentic_workflows_issues'
yaml.WriteString(`
close_agentic_workflows_issues:
diff --git a/pkg/workflow/side_repo_maintenance.go b/pkg/workflow/side_repo_maintenance.go
index 4e8aa64082a..36a81474ceb 100644
--- a/pkg/workflow/side_repo_maintenance.go
+++ b/pkg/workflow/side_repo_maintenance.go
@@ -216,6 +216,7 @@ on:
- ''
- 'safe_outputs'
- 'create_labels'
+ - 'activity_report'
- 'validate'
run_url:
description: 'Run URL or run ID to replay safe outputs from (e.g. https://github.com/owner/repo/actions/runs/12345 or 12345). Required when operation is safe_outputs.'
@@ -225,7 +226,7 @@ on:
workflow_call:
inputs:
operation:
- description: 'Optional maintenance operation to run (safe_outputs, create_labels, validate)'
+ description: 'Optional maintenance operation to run (safe_outputs, create_labels, activity_report, validate)'
required: false
type: string
default: ''
@@ -424,6 +425,53 @@ jobs:
await main();
`)
+ // Add activity_report job for workflow_dispatch/workflow_call with operation == 'activity_report'
+ yaml.WriteString(`
+ activity_report:
+ if: ${{ ` + RenderCondition(buildDispatchOperationCondition("activity_report")) + ` }}
+ runs-on: ` + runsOnValue + `
+ permissions:
+ actions: read
+ issues: write
+ steps:
+ - name: Checkout repository
+ uses: ` + getActionPin("actions/checkout") + `
+ with:
+ persist-credentials: false
+
+ - name: Setup Scripts
+ uses: ` + setupActionRef + `
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+
+ - name: Check admin/maintainer permissions
+ uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + `
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs');
+ await main();
+
+`)
+
+ yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver))
+ yaml.WriteString(` - name: Generate agentic workflow activity report in target repository
+ uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + `
+ env:
+ GH_TOKEN: ` + token + `
+ GH_AW_CMD_PREFIX: ` + getCLICmdPrefix(actionMode) + `
+ GH_AW_TARGET_REPO_SLUG: "` + repoSlug + `"
+ with:
+ github-token: ` + token + `
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/run_activity_report.cjs');
+ await main();
+`)
+
// Add validate_workflows job for workflow_dispatch/workflow_call with operation == 'validate'
validateRunsOnValue := FormatRunsOn(nil, "ubuntu-latest")
yaml.WriteString(`
diff --git a/pkg/workflow/side_repo_maintenance_integration_test.go b/pkg/workflow/side_repo_maintenance_integration_test.go
index dda9d21e4ef..64c4fc94952 100644
--- a/pkg/workflow/side_repo_maintenance_integration_test.go
+++ b/pkg/workflow/side_repo_maintenance_integration_test.go
@@ -87,6 +87,10 @@ This workflow operates on a separate repository.
assert.Contains(t, contentStr, "create_labels:",
"generated workflow should include create_labels job")
+ // Must have activity_report job.
+ assert.Contains(t, contentStr, "activity_report:",
+ "generated workflow should include activity_report job")
+
// GH_AW_TARGET_REPO_SLUG must be wired with the correct slug.
assert.Contains(t, contentStr, `GH_AW_TARGET_REPO_SLUG: "my-org/target-repo"`,
"GH_AW_TARGET_REPO_SLUG should be set to the target repo slug")
From 833a7c59209f8502d39afcb1f8725db5eab998c9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 19 Apr 2026 19:06:51 +0000
Subject: [PATCH 02/10] refactor: simplify activity report markdown assembly
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/267bf5c1-a299-43c5-be20-17cdb0ab2a0c
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/run_activity_report.cjs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/actions/setup/js/run_activity_report.cjs b/actions/setup/js/run_activity_report.cjs
index 85c4ea15639..2fd1eccde36 100644
--- a/actions/setup/js/run_activity_report.cjs
+++ b/actions/setup/js/run_activity_report.cjs
@@ -111,9 +111,9 @@ async function main() {
sections.push(await runRangeReport(bin, prefixArgs, repoSlug, range));
}
- const body = ["## Agentic workflow activity report", "", `Repository: \`${repoSlug}\``, `Generated at: ${new Date().toISOString()}`, "", ...sections.flatMap(section => ["---", "", `## ${section.heading}`, "", section.body, ""])].join(
- "\n"
- );
+ const headerLines = ["## Agentic workflow activity report", "", `Repository: \`${repoSlug}\``, `Generated at: ${new Date().toISOString()}`, ""];
+ const sectionLines = sections.flatMap(section => ["---", "", `## ${section.heading}`, "", section.body, ""]);
+ const body = [...headerLines, ...sectionLines].join("\n");
const createdIssue = await github.rest.issues.create({
owner,
From f18edc42d3b2f7a0f7ad33afe75aeaf3f58ae3f4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 19 Apr 2026 19:24:50 +0000
Subject: [PATCH 03/10] fix: use dashed activity-report operation and new
status issue title
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/37d80ed2-4179-4462-ac6e-bbe23dc36ad5
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.github/workflows/agentics-maintenance.yml | 8 ++++----
actions/setup/js/run_activity_report.cjs | 2 +-
actions/setup/js/run_activity_report.test.cjs | 2 +-
pkg/workflow/maintenance_workflow_test.go | 12 ++++++------
pkg/workflow/maintenance_workflow_yaml.go | 12 ++++++------
pkg/workflow/side_repo_maintenance.go | 8 ++++----
6 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml
index 1d63f82e62b..fc2007d174d 100644
--- a/.github/workflows/agentics-maintenance.yml
+++ b/.github/workflows/agentics-maintenance.yml
@@ -50,7 +50,7 @@ on:
- 'upgrade'
- 'safe_outputs'
- 'create_labels'
- - 'activity_report'
+ - 'activity-report'
- 'close_agentic_workflows_issues'
- 'clean_cache_memories'
- 'validate'
@@ -62,7 +62,7 @@ on:
workflow_call:
inputs:
operation:
- description: 'Optional maintenance operation to run (disable, enable, update, upgrade, safe_outputs, create_labels, activity_report, close_agentic_workflows_issues, clean_cache_memories, validate)'
+ description: 'Optional maintenance operation to run (disable, enable, update, upgrade, safe_outputs, create_labels, activity-report, close_agentic_workflows_issues, clean_cache_memories, validate)'
required: false
type: string
default: ''
@@ -157,7 +157,7 @@ jobs:
await main();
run_operation:
- if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation != '' && inputs.operation != 'safe_outputs' && inputs.operation != 'create_labels' && inputs.operation != 'activity_report' && inputs.operation != 'close_agentic_workflows_issues' && inputs.operation != 'clean_cache_memories' && inputs.operation != 'validate' && (!(github.event.repository.fork)) }}
+ if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation != '' && inputs.operation != 'safe_outputs' && inputs.operation != 'create_labels' && inputs.operation != 'activity-report' && inputs.operation != 'close_agentic_workflows_issues' && inputs.operation != 'clean_cache_memories' && inputs.operation != 'validate' && (!(github.event.repository.fork)) }}
runs-on: ubuntu-slim
permissions:
actions: write
@@ -313,7 +313,7 @@ jobs:
await main();
activity_report:
- if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'activity_report' && (!(github.event.repository.fork)) }}
+ if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'activity-report' && (!(github.event.repository.fork)) }}
runs-on: ubuntu-slim
permissions:
actions: read
diff --git a/actions/setup/js/run_activity_report.cjs b/actions/setup/js/run_activity_report.cjs
index 2fd1eccde36..09a5766fc49 100644
--- a/actions/setup/js/run_activity_report.cjs
+++ b/actions/setup/js/run_activity_report.cjs
@@ -5,7 +5,7 @@ const { getErrorMessage, isRateLimitError } = require("./error_helpers.cjs");
const { resolveExecutionOwnerRepo } = require("./repo_helpers.cjs");
const { sanitizeContent } = require("./sanitize_content.cjs");
-const ISSUE_TITLE = "[AW activity report]";
+const ISSUE_TITLE = "[aw] agentic status report";
/** @typedef {{ key: string, heading: string, startDate: string, optionalOnRateLimit: boolean }} ActivityRange */
diff --git a/actions/setup/js/run_activity_report.test.cjs b/actions/setup/js/run_activity_report.test.cjs
index 2514e40161e..857357f9380 100644
--- a/actions/setup/js/run_activity_report.test.cjs
+++ b/actions/setup/js/run_activity_report.test.cjs
@@ -81,7 +81,7 @@ describe("run_activity_report", () => {
expect.objectContaining({
owner: "testowner",
repo: "testrepo",
- title: "[AW activity report]",
+ title: "[aw] agentic status report",
labels: ["agentic-workflows"],
})
);
diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go
index fcdcb1a60d5..b1b33b02a34 100644
--- a/pkg/workflow/maintenance_workflow_test.go
+++ b/pkg/workflow/maintenance_workflow_test.go
@@ -282,10 +282,10 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
yaml := string(content)
operationSkipCondition := `github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == ''`
- operationRunCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation != '' && inputs.operation != 'safe_outputs' && inputs.operation != 'create_labels' && inputs.operation != 'activity_report' && inputs.operation != 'close_agentic_workflows_issues' && inputs.operation != 'clean_cache_memories' && inputs.operation != 'validate'`
+ operationRunCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation != '' && inputs.operation != 'safe_outputs' && inputs.operation != 'create_labels' && inputs.operation != 'activity-report' && inputs.operation != 'close_agentic_workflows_issues' && inputs.operation != 'clean_cache_memories' && inputs.operation != 'validate'`
applySafeOutputsCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'safe_outputs'`
createLabelsCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'create_labels'`
- activityReportCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'activity_report'`
+ activityReportCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'activity-report'`
closeAgenticWorkflowIssuesCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'close_agentic_workflows_issues'`
cleanCacheMemoriesCondition := `github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '' || inputs.operation == 'clean_cache_memories'`
@@ -368,7 +368,7 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
}
}
- // activity_report job should be triggered when operation == 'activity_report'
+ // activity_report job should be triggered when operation == 'activity-report'
activityReportIdx := strings.Index(yaml, "\n activity_report:")
if activityReportIdx == -1 {
t.Errorf("Job activity_report not found in generated workflow")
@@ -410,9 +410,9 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
t.Error("workflow_dispatch operation choices should include 'validate'")
}
- // Verify activity_report is an option in the operation choices
- if !strings.Contains(yaml, "- 'activity_report'") {
- t.Error("workflow_dispatch operation choices should include 'activity_report'")
+ // Verify activity-report is an option in the operation choices
+ if !strings.Contains(yaml, "- 'activity-report'") {
+ t.Error("workflow_dispatch operation choices should include 'activity-report'")
}
// Verify close_agentic_workflows_issues is an option in the operation choices
diff --git a/pkg/workflow/maintenance_workflow_yaml.go b/pkg/workflow/maintenance_workflow_yaml.go
index d80023fb915..ae529a96275 100644
--- a/pkg/workflow/maintenance_workflow_yaml.go
+++ b/pkg/workflow/maintenance_workflow_yaml.go
@@ -59,7 +59,7 @@ on:
- 'upgrade'
- 'safe_outputs'
- 'create_labels'
- - 'activity_report'
+ - 'activity-report'
- 'close_agentic_workflows_issues'
- 'clean_cache_memories'
- 'validate'
@@ -71,7 +71,7 @@ on:
workflow_call:
inputs:
operation:
- description: 'Optional maintenance operation to run (disable, enable, update, upgrade, safe_outputs, create_labels, activity_report, close_agentic_workflows_issues, clean_cache_memories, validate)'
+ description: 'Optional maintenance operation to run (disable, enable, update, upgrade, safe_outputs, create_labels, activity-report, close_agentic_workflows_issues, clean_cache_memories, validate)'
required: false
type: string
default: ''
@@ -196,8 +196,8 @@ jobs:
`)
// Add unified run_operation job for all dispatch operations except those with dedicated jobs
- // (safe_outputs, create_labels, activity_report, close_agentic_workflows_issues, clean_cache_memories, validate)
- runOperationCondition := buildRunOperationCondition("safe_outputs", "create_labels", "activity_report", "close_agentic_workflows_issues", "clean_cache_memories", "validate")
+ // (safe_outputs, create_labels, activity-report, close_agentic_workflows_issues, clean_cache_memories, validate)
+ runOperationCondition := buildRunOperationCondition("safe_outputs", "create_labels", "activity-report", "close_agentic_workflows_issues", "clean_cache_memories", "validate")
yaml.WriteString(`
run_operation:
if: ${{ ` + RenderCondition(runOperationCondition) + ` }}
@@ -350,10 +350,10 @@ jobs:
await main();
`)
- // Add activity_report job for workflow_dispatch with operation == 'activity_report'
+ // Add activity_report job for workflow_dispatch with operation == 'activity-report'
yaml.WriteString(`
activity_report:
- if: ${{ ` + RenderCondition(buildDispatchOperationCondition("activity_report")) + ` }}
+ if: ${{ ` + RenderCondition(buildDispatchOperationCondition("activity-report")) + ` }}
runs-on: ` + runsOnValue + `
permissions:
actions: read
diff --git a/pkg/workflow/side_repo_maintenance.go b/pkg/workflow/side_repo_maintenance.go
index 36a81474ceb..06007252ebc 100644
--- a/pkg/workflow/side_repo_maintenance.go
+++ b/pkg/workflow/side_repo_maintenance.go
@@ -216,7 +216,7 @@ on:
- ''
- 'safe_outputs'
- 'create_labels'
- - 'activity_report'
+ - 'activity-report'
- 'validate'
run_url:
description: 'Run URL or run ID to replay safe outputs from (e.g. https://github.com/owner/repo/actions/runs/12345 or 12345). Required when operation is safe_outputs.'
@@ -226,7 +226,7 @@ on:
workflow_call:
inputs:
operation:
- description: 'Optional maintenance operation to run (safe_outputs, create_labels, activity_report, validate)'
+ description: 'Optional maintenance operation to run (safe_outputs, create_labels, activity-report, validate)'
required: false
type: string
default: ''
@@ -425,10 +425,10 @@ jobs:
await main();
`)
- // Add activity_report job for workflow_dispatch/workflow_call with operation == 'activity_report'
+ // Add activity_report job for workflow_dispatch/workflow_call with operation == 'activity-report'
yaml.WriteString(`
activity_report:
- if: ${{ ` + RenderCondition(buildDispatchOperationCondition("activity_report")) + ` }}
+ if: ${{ ` + RenderCondition(buildDispatchOperationCondition("activity-report")) + ` }}
runs-on: ` + runsOnValue + `
permissions:
actions: read
From 3552a55ae8cf52d734b207f227505be2c679b76a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 19 Apr 2026 19:51:47 +0000
Subject: [PATCH 04/10] feat: add fallback pull request path for
push-to-pr-branch non-fast-forward failures
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/9f0ad012-0f23-4022-ad35-c955d90fc310
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.../setup/js/push_to_pull_request_branch.cjs | 54 +++++++++++++++++++
.../js/push_to_pull_request_branch.test.cjs | 41 +++++++++++++-
.../setup/js/safe_output_handler_manager.cjs | 30 ++++++++---
.../js/safe_output_handler_manager.test.cjs | 28 ++++++++++
actions/setup/js/safe_output_summary.cjs | 29 ++++++----
actions/setup/js/safe_output_summary.test.cjs | 26 +++++++++
actions/setup/js/types/handler-factory.d.ts | 2 +
.../docs/reference/frontmatter-full.md | 7 +++
.../reference/safe-outputs-pull-requests.md | 3 ++
pkg/parser/schemas/main_workflow_schema.json | 5 ++
pkg/workflow/push_to_pull_request_branch.go | 8 +++
.../push_to_pull_request_branch_test.go | 37 +++++++++++++
pkg/workflow/safe_outputs_permissions.go | 17 +++++-
pkg/workflow/safe_outputs_permissions_test.go | 14 ++++-
14 files changed, 281 insertions(+), 20 deletions(-)
diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs
index 7869cf329f7..2337566f7c4 100644
--- a/actions/setup/js/push_to_pull_request_branch.cjs
+++ b/actions/setup/js/push_to_pull_request_branch.cjs
@@ -56,6 +56,7 @@ async function main(config = {}) {
const envLabels = config.labels ? (Array.isArray(config.labels) ? config.labels : config.labels.split(",")).map(label => String(label).trim()).filter(label => label) : [];
const ifNoChanges = config.if_no_changes || "warn";
const ignoreMissingBranchFailure = config.ignore_missing_branch_failure === true;
+ const fallbackAsPullRequest = config.fallback_as_pull_request !== false;
const commitTitleSuffix = config.commit_title_suffix || "";
const maxSizeKb = config.max_patch_size ? parseInt(String(config.max_patch_size), 10) : 1024;
const maxCount = config.max || 0; // 0 means no limit
@@ -91,6 +92,7 @@ async function main(config = {}) {
}
core.info(`If no changes: ${ifNoChanges}`);
core.info(`Ignore missing branch failure: ${ignoreMissingBranchFailure}`);
+ core.info(`Fallback as pull request: ${fallbackAsPullRequest}`);
if (commitTitleSuffix) {
core.info(`Commit title suffix: ${commitTitleSuffix}`);
}
@@ -770,6 +772,58 @@ async function main(config = {}) {
core.warning(`Push failed and branch existence re-check errored for ${branchName}: ${getErrorMessage(diagnosisError)}`);
}
+ // Fallback path for diverged branches: create a new pull request so changes
+ // can still be reviewed and merged into the original PR branch.
+ if (isNonFastForward && fallbackAsPullRequest) {
+ const fallbackBranchName = normalizeBranchName(`${branchName}-fallback`, String(Date.now()));
+ core.warning(`Non-fast-forward push detected; creating fallback pull request from '${fallbackBranchName}' to '${branchName}'`);
+ try {
+ await exec.exec("git", ["checkout", "-b", fallbackBranchName]);
+ await exec.exec("git", ["push", "origin", fallbackBranchName], {
+ env: { ...process.env, ...gitAuthEnv },
+ });
+
+ const fallbackBody = [
+ "> [!NOTE]",
+ "> Direct push to the original pull request branch failed because the branch diverged (non-fast-forward).",
+ `> Original PR branch: \`${branchName}\``,
+ "",
+ `This fallback PR contains the prepared changes for PR #${pullNumber}.`,
+ "Merge this fallback PR into the original PR branch to apply them.",
+ "",
+ `Workflow run: ${buildWorkflowRunUrl(context, context.repo)}`,
+ ].join("\n");
+
+ const { data: fallbackPR } = await githubClient.rest.pulls.create({
+ owner: repoParts.owner,
+ repo: repoParts.repo,
+ title: `[fallback] ${prTitle || `Changes for #${pullNumber}`}`,
+ body: fallbackBody,
+ head: fallbackBranchName,
+ base: branchName,
+ });
+
+ core.info(`Created fallback pull request #${fallbackPR.number}: ${fallbackPR.html_url}`);
+ await updateActivationComment(github, context, core, fallbackPR.html_url, fallbackPR.number, "pull_request");
+
+ return {
+ success: true,
+ fallback_used: true,
+ fallback_type: "pull_request",
+ pull_request_number: fallbackPR.number,
+ pull_request_url: fallbackPR.html_url,
+ branch_name: fallbackBranchName,
+ repo: itemRepo,
+ number: fallbackPR.number,
+ url: fallbackPR.html_url,
+ };
+ } catch (fallbackError) {
+ const fallbackErrorMessage = getErrorMessage(fallbackError);
+ core.error(`Failed to create fallback pull request: ${fallbackErrorMessage}`);
+ userMessage = `${userMessage} Fallback pull request creation also failed: ${fallbackErrorMessage}`;
+ }
+ }
+
return { success: false, error_type: "push_failed", error: userMessage };
}
diff --git a/actions/setup/js/push_to_pull_request_branch.test.cjs b/actions/setup/js/push_to_pull_request_branch.test.cjs
index 4fdc66c4d4d..664451be648 100644
--- a/actions/setup/js/push_to_pull_request_branch.test.cjs
+++ b/actions/setup/js/push_to_pull_request_branch.test.cjs
@@ -144,6 +144,12 @@ describe("push_to_pull_request_branch.cjs", () => {
labels: [],
},
}),
+ create: vi.fn().mockResolvedValue({
+ data: {
+ number: 999,
+ html_url: "https://github.com/test-owner/test-repo/pull/999",
+ },
+ }),
},
repos: {
get: vi.fn().mockResolvedValue({
@@ -767,7 +773,7 @@ index 0000000..abc1234
expect(mockCore.info).toHaveBeenCalledWith("Investigating patch failure...");
});
- it("should handle git push rejection (concurrent changes)", async () => {
+ it("should create fallback pull request on non-fast-forward push rejection by default", async () => {
const patchPath = createPatchFile();
// Set up successful operations until push
@@ -798,8 +804,39 @@ index 0000000..abc1234
const handler = await module.main({});
const result = await handler({ patch_path: patchPath }, {});
- // The error happens during push
+ expect(result.success).toBe(true);
+ expect(result.fallback_used).toBe(true);
+ expect(result.fallback_type).toBe("pull_request");
+ expect(result.pull_request_number).toBe(999);
+ expect(mockGithub.rest.pulls.create).toHaveBeenCalled();
+ });
+
+ it("should not create fallback pull request when fallback-as-pull-request is disabled", async () => {
+ const patchPath = createPatchFile();
+
+ mockExec.exec.mockResolvedValueOnce(0); // fetch
+ mockExec.exec.mockResolvedValueOnce(0); // rev-parse
+ mockExec.exec.mockResolvedValueOnce(0); // checkout
+
+ mockExec.getExecOutput.mockResolvedValueOnce({ exitCode: 0, stdout: "before-sha\n", stderr: "" }); // git rev-parse HEAD (before patch)
+
+ mockExec.exec.mockResolvedValueOnce(0); // git am
+
+ mockExec.getExecOutput.mockResolvedValueOnce({ exitCode: 0, stdout: "abc123\n", stderr: "" }); // git rev-list
+ mockExec.getExecOutput.mockResolvedValueOnce({ exitCode: 0, stdout: "remote-oid\trefs/heads/feature-branch\n", stderr: "" }); // git ls-remote
+ mockExec.getExecOutput.mockResolvedValueOnce({ exitCode: 0, stdout: "Test commit\n", stderr: "" }); // git log -1
+ mockExec.getExecOutput.mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" }); // git diff --name-status
+
+ mockGithub.graphql.mockRejectedValueOnce(new Error("GraphQL error: branch protection"));
+ mockExec.exec.mockRejectedValueOnce(new Error("! [rejected] feature-branch -> feature-branch (non-fast-forward)"));
+
+ const module = await loadModule();
+ const handler = await module.main({ fallback_as_pull_request: false });
+ const result = await handler({ patch_path: patchPath }, {});
+
expect(result.success).toBe(false);
+ expect(result.error_type).toBe("push_failed");
+ expect(mockGithub.rest.pulls.create).not.toHaveBeenCalled();
});
it("should diagnose deleted branch when push fails", async () => {
diff --git a/actions/setup/js/safe_output_handler_manager.cjs b/actions/setup/js/safe_output_handler_manager.cjs
index 52bfc6417de..5d4bab0f453 100644
--- a/actions/setup/js/safe_output_handler_manager.cjs
+++ b/actions/setup/js/safe_output_handler_manager.cjs
@@ -363,7 +363,7 @@ async function processMessages(messageHandlers, messages, onItemCreated = null)
// Track when a code-push operation falls back to creating a review issue instead.
// When set, subsequent add_comment messages will receive a correction note prepended
// to their body so the posted comment accurately reflects the actual outcome.
- /** @type {{type: string, issueNumber: number, issueUrl: string}|null} */
+ /** @type {{type: string, fallbackTargetType: "issue" | "pull_request", number: number, url: string}|null} */
let codePushFallbackInfo = null;
// Load custom safe output job types (from GH_AW_SAFE_OUTPUT_JOBS env var)
@@ -481,9 +481,12 @@ async function processMessages(messageHandlers, messages, onItemCreated = null)
// If a previous code-push operation fell back to a review issue, prepend a correction note
// so the posted comment accurately reflects the outcome.
if (codePushFallbackInfo) {
- const fallbackNote = `\n\n---\n> [!NOTE]\n> The pull request was not created — a fallback review issue was created instead due to protected file changes: [#${codePushFallbackInfo.issueNumber}](${codePushFallbackInfo.issueUrl})\n\n`;
+ const fallbackNote =
+ codePushFallbackInfo.fallbackTargetType === "pull_request"
+ ? `\n\n---\n> [!NOTE]\n> Direct push to the original pull request branch was not possible (diverged/non-fast-forward). A fallback pull request was created instead: [#${codePushFallbackInfo.number}](${codePushFallbackInfo.url})\n\n`
+ : `\n\n---\n> [!NOTE]\n> The pull request was not created — a fallback review issue was created instead due to protected file changes: [#${codePushFallbackInfo.number}](${codePushFallbackInfo.url})\n\n`;
effectiveMessage = { ...effectiveMessage, body: fallbackNote + (effectiveMessage.body || "") };
- core.info(`Prepending fallback correction note to add_comment body (fallback issue: #${codePushFallbackInfo.issueNumber})`);
+ core.info(`Prepending fallback correction note to add_comment body (fallback ${codePushFallbackInfo.fallbackTargetType}: #${codePushFallbackInfo.number})`);
}
// If a previous code-push operation failed outright (e.g. patch application error),
// prepend a failure warning so the status comment accurately reflects that the
@@ -587,9 +590,24 @@ async function processMessages(messageHandlers, messages, onItemCreated = null)
// Track when a code-push operation falls back to a review issue so subsequent
// add_comment messages can include a correction note.
- if (CODE_PUSH_TYPES.has(messageType) && result && result.fallback_used === true && result.issue_number != null && result.issue_url) {
- codePushFallbackInfo = { type: messageType, issueNumber: result.issue_number, issueUrl: result.issue_url };
- core.info(`Code push '${messageType}' fell back to review issue #${result.issue_number} — add_comment messages will be annotated`);
+ if (CODE_PUSH_TYPES.has(messageType) && result && result.fallback_used === true) {
+ if (result.issue_number != null && result.issue_url) {
+ codePushFallbackInfo = {
+ type: messageType,
+ fallbackTargetType: "issue",
+ number: result.issue_number,
+ url: result.issue_url,
+ };
+ core.info(`Code push '${messageType}' fell back to review issue #${result.issue_number} — add_comment messages will be annotated`);
+ } else if (result.pull_request_number != null && result.pull_request_url) {
+ codePushFallbackInfo = {
+ type: messageType,
+ fallbackTargetType: "pull_request",
+ number: result.pull_request_number,
+ url: result.pull_request_url,
+ };
+ core.info(`Code push '${messageType}' fell back to pull request #${result.pull_request_number} — add_comment messages will be annotated`);
+ }
}
// Check if this output was created with unresolved temporary IDs
diff --git a/actions/setup/js/safe_output_handler_manager.test.cjs b/actions/setup/js/safe_output_handler_manager.test.cjs
index 50b85111096..89e7b73b386 100644
--- a/actions/setup/js/safe_output_handler_manager.test.cjs
+++ b/actions/setup/js/safe_output_handler_manager.test.cjs
@@ -1333,6 +1333,34 @@ describe("Safe Output Handler Manager", () => {
expect(calledMessage.body).toContain("#7");
});
+ it("should prepend fallback note to add_comment body when push_to_pull_request_branch falls back to pull request", async () => {
+ const messages = [
+ { type: "push_to_pull_request_branch", branch: "fix-branch" },
+ { type: "add_comment", body: "Changes pushed." },
+ ];
+
+ const pushHandler = vi.fn().mockResolvedValue({
+ success: true,
+ fallback_used: true,
+ fallback_type: "pull_request",
+ pull_request_number: 71,
+ pull_request_url: "https://github.com/owner/repo/pull/71",
+ });
+ const commentHandler = vi.fn().mockResolvedValue([{ _tracking: null }]);
+
+ const handlers = new Map([
+ ["push_to_pull_request_branch", pushHandler],
+ ["add_comment", commentHandler],
+ ]);
+
+ await processMessages(handlers, messages);
+
+ const calledMessage = commentHandler.mock.calls[0][0];
+ expect(calledMessage.body).toContain("Direct push to the original pull request branch was not possible");
+ expect(calledMessage.body).toContain("#71");
+ expect(calledMessage.body).toContain("https://github.com/owner/repo/pull/71");
+ });
+
it("should NOT prepend fallback note when create_pull_request succeeds normally", async () => {
const messages = [
{ type: "create_pull_request", title: "My Fix PR" },
diff --git a/actions/setup/js/safe_output_summary.cjs b/actions/setup/js/safe_output_summary.cjs
index 94b0ef776e7..fa2d2143c43 100644
--- a/actions/setup/js/safe_output_summary.cjs
+++ b/actions/setup/js/safe_output_summary.cjs
@@ -30,12 +30,13 @@ function generateSafeOutputSummary(options) {
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
- // Detect fallback-to-issue outcome for code-push types
+ // Detect fallback outcomes for code-push types
const isFallback = success && result && result.fallback_used === true;
+ const fallbackType = isFallback && (result.pull_request_url || result.pull_request_number != null) ? "pull_request" : "issue";
// Choose emoji and status based on success and fallback
const emoji = isFallback ? "⚠️" : success ? "✅" : "❌";
- const status = isFallback ? "Fallback Issue Created" : success ? "Success" : "Failed";
+ const status = isFallback ? (fallbackType === "pull_request" ? "Fallback Pull Request Created" : "Fallback Issue Created") : success ? "Success" : "Failed";
// Start building the summary
let summary = `\n${emoji} ${displayType} - ${status} (Message ${messageIndex})
\n\n`;
@@ -45,13 +46,23 @@ function generateSafeOutputSummary(options) {
summary += sectionTitle;
if (isFallback) {
- // Explain why the fallback occurred and show the created issue
- summary += `> ℹ️ Pull request creation was blocked due to protected file changes. A review issue was created instead.\n\n`;
- if (result.issue_url) {
- summary += `**Fallback Issue:** ${result.issue_url}\n\n`;
- }
- if (result.issue_number != null && result.repo) {
- summary += `**Location:** ${result.repo}#${result.issue_number}\n\n`;
+ // Explain why the fallback occurred and show the created fallback target
+ if (fallbackType === "pull_request") {
+ summary += `> ℹ️ Direct push to the original pull request branch was not possible (diverged/non-fast-forward). A fallback pull request was created instead.\n\n`;
+ if (result.pull_request_url) {
+ summary += `**Fallback Pull Request:** ${result.pull_request_url}\n\n`;
+ }
+ if (result.pull_request_number != null && result.repo) {
+ summary += `**Location:** ${result.repo}#${result.pull_request_number}\n\n`;
+ }
+ } else {
+ summary += `> ℹ️ Pull request creation was blocked due to protected file changes. A review issue was created instead.\n\n`;
+ if (result.issue_url) {
+ summary += `**Fallback Issue:** ${result.issue_url}\n\n`;
+ }
+ if (result.issue_number != null && result.repo) {
+ summary += `**Location:** ${result.repo}#${result.issue_number}\n\n`;
+ }
}
if (result.branch_name) {
summary += `**Branch:** \`${result.branch_name}\`\n\n`;
diff --git a/actions/setup/js/safe_output_summary.test.cjs b/actions/setup/js/safe_output_summary.test.cjs
index bce462f9e43..5abfd1cb354 100644
--- a/actions/setup/js/safe_output_summary.test.cjs
+++ b/actions/setup/js/safe_output_summary.test.cjs
@@ -360,6 +360,32 @@ describe("safe_output_summary", () => {
expect(summary).not.toContain("⚠️");
expect(summary).not.toContain("Fallback");
});
+
+ it("should show fallback pull request status when push_to_pull_request_branch falls back to pull request", () => {
+ const options = {
+ type: "push_to_pull_request_branch",
+ messageIndex: 3,
+ success: true,
+ result: {
+ fallback_used: true,
+ fallback_type: "pull_request",
+ pull_request_number: 71,
+ pull_request_url: "https://github.com/owner/repo/pull/71",
+ repo: "owner/repo",
+ },
+ message: {
+ body: "Pushing to PR branch.",
+ },
+ };
+
+ const summary = generateSafeOutputSummary(options);
+
+ expect(summary).toContain("⚠️");
+ expect(summary).toContain("Fallback Pull Request Created");
+ expect(summary).toContain("https://github.com/owner/repo/pull/71");
+ expect(summary).toContain("owner/repo#71");
+ expect(summary).toContain("non-fast-forward");
+ });
});
describe("writeSafeOutputSummaries", () => {
diff --git a/actions/setup/js/types/handler-factory.d.ts b/actions/setup/js/types/handler-factory.d.ts
index 848817b2a20..f5ef66af9f3 100644
--- a/actions/setup/js/types/handler-factory.d.ts
+++ b/actions/setup/js/types/handler-factory.d.ts
@@ -18,6 +18,8 @@ interface HandlerConfig {
protected_path_prefixes?: string[];
/** Policy for how protected file matches are handled: "blocked" (default), "fallback-to-issue", or "allowed" */
protected_files_policy?: string;
+ /** When true (default), create a fallback pull request if direct push to PR branch fails with non-fast-forward/diverged branch. */
+ fallback_as_pull_request?: boolean;
/** Additional handler-specific configuration properties */
[key: string]: any;
}
diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md
index e29e729d7a6..69b3930eb56 100644
--- a/docs/src/content/docs/reference/frontmatter-full.md
+++ b/docs/src/content/docs/reference/frontmatter-full.md
@@ -4599,6 +4599,13 @@ safe-outputs:
# (optional)
github-token-for-extra-empty-commit: "example-value"
+ # When true (default), if pushing to the PR branch fails due to a
+ # non-fast-forward/diverged branch, create a fallback pull request that targets
+ # the original PR branch. Set to false to disable this behavior and avoid
+ # requiring pull-requests: write permission.
+ # (optional)
+ fallback-as-pull-request: true
+
# Target repository in format 'owner/repo' for cross-repository push to pull
# request branch. Takes precedence over trial target repo settings.
# (optional)
diff --git a/docs/src/content/docs/reference/safe-outputs-pull-requests.md b/docs/src/content/docs/reference/safe-outputs-pull-requests.md
index 6d339a5a146..fcd57dd4350 100644
--- a/docs/src/content/docs/reference/safe-outputs-pull-requests.md
+++ b/docs/src/content/docs/reference/safe-outputs-pull-requests.md
@@ -234,6 +234,7 @@ safe-outputs:
- "**/*.lock"
github-token: ${{ secrets.SOME_CUSTOM_TOKEN }} # optional custom token for permissions
github-token-for-extra-empty-commit: ${{ secrets.CI_TOKEN }} # optional token to push empty commit triggering CI
+ fallback-as-pull-request: true # default: on non-fast-forward push failure, create fallback PR to original PR branch
protected-files: fallback-to-issue # create review issue if protected files modified
```
@@ -283,6 +284,8 @@ checkout:
If `push-to-pull-request-branch` (or `create-pull-request`) fails, the safe-output pipeline cancels all remaining non-code-push outputs. Each cancelled output is marked with an explicit reason such as "Cancelled: code push operation failed". The failure details appear in the agent failure issue or comment generated by the conclusion job.
+When `fallback-as-pull-request` is enabled (default), non-fast-forward push failures trigger a fallback pull request that targets the original PR branch. Set `fallback-as-pull-request: false` to disable this fallback behavior.
+
## Protected Files
Both `create-pull-request` and `push-to-pull-request-branch` enforce protected file protection by default. Patches that modify package manifests, agent instruction files, or repository security configuration are refused unless you explicitly configure a policy.
diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json
index 23994195e59..c41bd8294c6 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -7096,6 +7096,11 @@
"type": "string",
"description": "Token used to push an empty commit after pushing changes to trigger CI events. Works around the GITHUB_TOKEN limitation where pushes don't trigger workflow runs. Defaults to the magic secret GH_AW_CI_TRIGGER_TOKEN if set in the repository. Use a secret expression (e.g. '${{ secrets.CI_TOKEN }}') for a custom token, or 'app' for GitHub App auth."
},
+ "fallback-as-pull-request": {
+ "type": "boolean",
+ "description": "When true (default), if pushing to the PR branch fails due to a non-fast-forward/diverged branch, create a fallback pull request that targets the original PR branch. Set to false to disable this behavior and avoid requiring pull-requests: write permission.",
+ "default": true
+ },
"target-repo": {
"type": "string",
"description": "Target repository in format 'owner/repo' for cross-repository push to pull request branch. Takes precedence over trial target repo settings."
diff --git a/pkg/workflow/push_to_pull_request_branch.go b/pkg/workflow/push_to_pull_request_branch.go
index e3a31c1538d..4530a332a20 100644
--- a/pkg/workflow/push_to_pull_request_branch.go
+++ b/pkg/workflow/push_to_pull_request_branch.go
@@ -26,6 +26,7 @@ type PushToPullRequestBranchConfig struct {
AllowedFiles []string `yaml:"allowed-files,omitempty"` // Strict allowlist of glob patterns for files eligible for push. Checked independently of protected-files; both checks must pass.
ExcludedFiles []string `yaml:"excluded-files,omitempty"` // List of glob patterns for files to exclude from the patch using git :(exclude) pathspecs. Matching files are stripped by git at generation time and will not appear in the commit or be subject to allowed-files or protected-files checks.
PatchFormat string `yaml:"patch-format,omitempty"` // Transport format for packaging changes: "am" (default, uses git format-patch) or "bundle" (uses git bundle, preserves merge topology and per-commit metadata).
+ FallbackAsPullRequest *bool `yaml:"fallback-as-pull-request,omitempty"` // When true (default), creates a fallback pull request if direct push fails due to diverged/non-fast-forward branch. When false, fallback is disabled and pull-requests: write is not requested.
AllowWorkflows bool `yaml:"allow-workflows,omitempty"` // When true, adds workflows: write to the GitHub App token. Requires safe-outputs.github-app to be configured.
}
@@ -173,6 +174,13 @@ func (c *Compiler) parsePushToPullRequestBranchConfig(outputMap map[string]any)
}
}
+ // Parse fallback-as-pull-request (optional, defaults to true)
+ if fallbackAsPullRequest, exists := configMap["fallback-as-pull-request"]; exists {
+ if fallbackAsPullRequestBool, ok := fallbackAsPullRequest.(bool); ok {
+ pushToBranchConfig.FallbackAsPullRequest = &fallbackAsPullRequestBool
+ }
+ }
+
// Parse allow-workflows: when true, adds workflows: write to the GitHub App token
if allowWorkflows, exists := configMap["allow-workflows"]; exists {
if allowWorkflowsBool, ok := allowWorkflows.(bool); ok {
diff --git a/pkg/workflow/push_to_pull_request_branch_test.go b/pkg/workflow/push_to_pull_request_branch_test.go
index d42c6ef7397..0487036e7dc 100644
--- a/pkg/workflow/push_to_pull_request_branch_test.go
+++ b/pkg/workflow/push_to_pull_request_branch_test.go
@@ -130,6 +130,43 @@ safe-outputs:
}
}
+func TestPushToPullRequestBranchFallbackAsPullRequestDisabled(t *testing.T) {
+ tmpDir := testutil.TempDir(t, "test-*")
+
+ testMarkdown := `---
+on:
+ pull_request:
+ types: [opened, synchronize]
+safe-outputs:
+ push-to-pull-request-branch:
+ fallback-as-pull-request: false
+---
+
+# Test Push to PR Branch Fallback Disabled
+`
+
+ mdFile := filepath.Join(tmpDir, "test-push-to-pull-request-branch-fallback-disabled.md")
+ if err := os.WriteFile(mdFile, []byte(testMarkdown), 0644); err != nil {
+ t.Fatalf("Failed to write test markdown file: %v", err)
+ }
+
+ compiler := NewCompiler()
+ if err := compiler.CompileWorkflow(mdFile); err != nil {
+ t.Fatalf("Failed to compile workflow: %v", err)
+ }
+
+ lockFile := stringutil.MarkdownToLockFile(mdFile)
+ lockContent, err := os.ReadFile(lockFile)
+ if err != nil {
+ t.Fatalf("Failed to read lock file: %v", err)
+ }
+
+ lockContentStr := string(lockContent)
+ if strings.Contains(lockContentStr, "pull-requests: write") {
+ t.Errorf("Generated workflow should NOT have pull-requests: write permission when fallback-as-pull-request is false")
+ }
+}
+
func TestPushToPullRequestBranchWithTargetAsterisk(t *testing.T) {
// Create a temporary directory for the test
tmpDir := testutil.TempDir(t, "test-*")
diff --git a/pkg/workflow/safe_outputs_permissions.go b/pkg/workflow/safe_outputs_permissions.go
index 74ff34d27cc..940ea72f2e6 100644
--- a/pkg/workflow/safe_outputs_permissions.go
+++ b/pkg/workflow/safe_outputs_permissions.go
@@ -51,6 +51,14 @@ func isHandlerStaged(globalStaged, handlerStaged bool) bool {
return globalStaged || handlerStaged
}
+// getPushFallbackAsPullRequest returns the effective fallback-as-pull-request setting (defaults to true).
+func getPushFallbackAsPullRequest(config *PushToPullRequestBranchConfig) bool {
+ if config == nil || config.FallbackAsPullRequest == nil {
+ return true // Default
+ }
+ return *config.FallbackAsPullRequest
+}
+
// ComputePermissionsForSafeOutputs computes the minimal required permissions
// based on the configured safe-outputs. This function is used by both the
// consolidated safe outputs job and the conclusion job to ensure they only
@@ -137,8 +145,13 @@ func ComputePermissionsForSafeOutputs(safeOutputs *SafeOutputsConfig) *Permissio
}
}
if safeOutputs.PushToPullRequestBranch != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.PushToPullRequestBranch.Staged) {
- safeOutputsPermissionsLog.Print("Adding permissions for push-to-pull-request-branch")
- permissions.Merge(NewPermissionsContentsWritePRWrite())
+ if getPushFallbackAsPullRequest(safeOutputs.PushToPullRequestBranch) {
+ safeOutputsPermissionsLog.Print("Adding permissions for push-to-pull-request-branch with fallback-as-pull-request")
+ permissions.Merge(NewPermissionsContentsWritePRWrite())
+ } else {
+ safeOutputsPermissionsLog.Print("Adding permissions for push-to-pull-request-branch without fallback-as-pull-request")
+ permissions.Merge(NewPermissionsContentsWrite())
+ }
// Add workflows: write when allow-workflows is true (GitHub App-only permission)
if safeOutputs.PushToPullRequestBranch.AllowWorkflows {
safeOutputsPermissionsLog.Print("Adding workflows: write for push-to-pull-request-branch (allow-workflows: true)")
diff --git a/pkg/workflow/safe_outputs_permissions_test.go b/pkg/workflow/safe_outputs_permissions_test.go
index f1ffc6bb765..a4533a2856b 100644
--- a/pkg/workflow/safe_outputs_permissions_test.go
+++ b/pkg/workflow/safe_outputs_permissions_test.go
@@ -245,7 +245,7 @@ func TestComputePermissionsForSafeOutputs(t *testing.T) {
},
},
{
- name: "push-to-pull-request-branch - no issues permission",
+ name: "push-to-pull-request-branch default fallback - requires pull-requests write",
safeOutputs: &SafeOutputsConfig{
PushToPullRequestBranch: &PushToPullRequestBranchConfig{
BaseSafeOutputConfig: BaseSafeOutputConfig{},
@@ -256,6 +256,18 @@ func TestComputePermissionsForSafeOutputs(t *testing.T) {
PermissionPullRequests: PermissionWrite,
},
},
+ {
+ name: "push-to-pull-request-branch with fallback-as-pull-request false - no pull-requests permission",
+ safeOutputs: &SafeOutputsConfig{
+ PushToPullRequestBranch: &PushToPullRequestBranchConfig{
+ BaseSafeOutputConfig: BaseSafeOutputConfig{},
+ FallbackAsPullRequest: boolPtr(false),
+ },
+ },
+ expected: map[PermissionScope]PermissionLevel{
+ PermissionContents: PermissionWrite,
+ },
+ },
{
name: "multiple safe outputs without discussions - no discussions permission",
safeOutputs: &SafeOutputsConfig{
From 86cf23f714b5a3a8893d0492c26ceabe55524eba Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 19 Apr 2026 19:54:00 +0000
Subject: [PATCH 05/10] test: assert non-fast-forward error when fallback PR is
disabled
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/9f0ad012-0f23-4022-ad35-c955d90fc310
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/push_to_pull_request_branch.test.cjs | 1 +
1 file changed, 1 insertion(+)
diff --git a/actions/setup/js/push_to_pull_request_branch.test.cjs b/actions/setup/js/push_to_pull_request_branch.test.cjs
index 664451be648..b7f94059c63 100644
--- a/actions/setup/js/push_to_pull_request_branch.test.cjs
+++ b/actions/setup/js/push_to_pull_request_branch.test.cjs
@@ -836,6 +836,7 @@ index 0000000..abc1234
expect(result.success).toBe(false);
expect(result.error_type).toBe("push_failed");
+ expect(result.error).toContain("non-fast-forward");
expect(mockGithub.rest.pulls.create).not.toHaveBeenCalled();
});
From 198379571a4c0ed729dcd20f012b2211d17c5b0c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 19 Apr 2026 20:51:39 +0000
Subject: [PATCH 06/10] fix: address PR review comments for fallback type and
handler config propagation
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/f35b039c-a302-4ce9-a90d-a41ec5468b27
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.../setup/js/safe_output_handler_manager.cjs | 6 ++---
actions/setup/js/safe_output_summary.cjs | 13 ++++++---
actions/setup/js/safe_output_summary.test.cjs | 27 +++++++++++++++++++
.../compiler_safe_outputs_handlers.go | 1 +
.../push_to_pull_request_branch_test.go | 3 +++
5 files changed, 44 insertions(+), 6 deletions(-)
diff --git a/actions/setup/js/safe_output_handler_manager.cjs b/actions/setup/js/safe_output_handler_manager.cjs
index 5d4bab0f453..96226a8c6fe 100644
--- a/actions/setup/js/safe_output_handler_manager.cjs
+++ b/actions/setup/js/safe_output_handler_manager.cjs
@@ -360,9 +360,9 @@ async function processMessages(messageHandlers, messages, onItemCreated = null)
/** @type {Array<{type: string, error: string}>} */
const codePushFailures = [];
- // Track when a code-push operation falls back to creating a review issue instead.
+ // Track when a code-push operation falls back to creating an issue or pull request instead.
// When set, subsequent add_comment messages will receive a correction note prepended
- // to their body so the posted comment accurately reflects the actual outcome.
+ // to their body so the posted comment accurately reflects the actual fallback target.
/** @type {{type: string, fallbackTargetType: "issue" | "pull_request", number: number, url: string}|null} */
let codePushFallbackInfo = null;
@@ -588,7 +588,7 @@ async function processMessages(messageHandlers, messages, onItemCreated = null)
}
}
- // Track when a code-push operation falls back to a review issue so subsequent
+ // Track when a code-push operation falls back to an issue or pull request so subsequent
// add_comment messages can include a correction note.
if (CODE_PUSH_TYPES.has(messageType) && result && result.fallback_used === true) {
if (result.issue_number != null && result.issue_url) {
diff --git a/actions/setup/js/safe_output_summary.cjs b/actions/setup/js/safe_output_summary.cjs
index fa2d2143c43..86b3ffcf5e6 100644
--- a/actions/setup/js/safe_output_summary.cjs
+++ b/actions/setup/js/safe_output_summary.cjs
@@ -30,9 +30,16 @@ function generateSafeOutputSummary(options) {
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
- // Detect fallback outcomes for code-push types
+ // Detect fallback outcomes for code-push types.
+ // Prefer explicit fallback_type when available; infer only for backward compatibility.
const isFallback = success && result && result.fallback_used === true;
- const fallbackType = isFallback && (result.pull_request_url || result.pull_request_number != null) ? "pull_request" : "issue";
+ const inferredFallbackType = isFallback && (result.pull_request_url || result.pull_request_number != null) ? "pull_request" : "issue";
+ let fallbackType = inferredFallbackType;
+ if (isFallback && result?.fallback_type === "pull_request") {
+ fallbackType = "pull_request";
+ } else if (isFallback && result?.fallback_type === "issue") {
+ fallbackType = "issue";
+ }
// Choose emoji and status based on success and fallback
const emoji = isFallback ? "⚠️" : success ? "✅" : "❌";
@@ -42,7 +49,7 @@ function generateSafeOutputSummary(options) {
let summary = `\n${emoji} ${displayType} - ${status} (Message ${messageIndex})
\n\n`;
// Add message details
- const sectionTitle = isFallback ? `### ${displayType} — Fallback Issue\n\n` : `### ${displayType}\n\n`;
+ const sectionTitle = isFallback ? `### ${displayType} — ${fallbackType === "pull_request" ? "Fallback Pull Request" : "Fallback Issue"}\n\n` : `### ${displayType}\n\n`;
summary += sectionTitle;
if (isFallback) {
diff --git a/actions/setup/js/safe_output_summary.test.cjs b/actions/setup/js/safe_output_summary.test.cjs
index 5abfd1cb354..e37f78cdf08 100644
--- a/actions/setup/js/safe_output_summary.test.cjs
+++ b/actions/setup/js/safe_output_summary.test.cjs
@@ -386,6 +386,33 @@ describe("safe_output_summary", () => {
expect(summary).toContain("owner/repo#71");
expect(summary).toContain("non-fast-forward");
});
+
+ it("should prefer explicit fallback_type over inferred shape for backward compatibility", () => {
+ const options = {
+ type: "push_to_pull_request_branch",
+ messageIndex: 4,
+ success: true,
+ result: {
+ fallback_used: true,
+ fallback_type: "issue",
+ // pull_request_url present by shape, but explicit fallback_type should win
+ pull_request_url: "https://github.com/owner/repo/pull/72",
+ issue_number: 123,
+ issue_url: "https://github.com/owner/repo/issues/123",
+ repo: "owner/repo",
+ },
+ message: {
+ body: "Pushing to PR branch.",
+ },
+ };
+
+ const summary = generateSafeOutputSummary(options);
+
+ expect(summary).toContain("Fallback Issue Created");
+ expect(summary).toContain("Fallback Issue:");
+ expect(summary).toContain("https://github.com/owner/repo/issues/123");
+ expect(summary).not.toContain("Fallback Pull Request Created");
+ });
});
describe("writeSafeOutputSummaries", () => {
diff --git a/pkg/workflow/compiler_safe_outputs_handlers.go b/pkg/workflow/compiler_safe_outputs_handlers.go
index c42bb7abc61..1386b9aef3b 100644
--- a/pkg/workflow/compiler_safe_outputs_handlers.go
+++ b/pkg/workflow/compiler_safe_outputs_handlers.go
@@ -425,6 +425,7 @@ var handlerRegistry = map[string]handlerBuilder{
AddStringSlice("allowed_files", c.AllowedFiles).
AddStringSlice("excluded_files", c.ExcludedFiles).
AddIfNotEmpty("patch_format", c.PatchFormat).
+ AddBoolPtr("fallback_as_pull_request", c.FallbackAsPullRequest).
Build()
},
"update_pull_request": func(cfg *SafeOutputsConfig) map[string]any {
diff --git a/pkg/workflow/push_to_pull_request_branch_test.go b/pkg/workflow/push_to_pull_request_branch_test.go
index 0487036e7dc..870e6d3fee4 100644
--- a/pkg/workflow/push_to_pull_request_branch_test.go
+++ b/pkg/workflow/push_to_pull_request_branch_test.go
@@ -162,6 +162,9 @@ safe-outputs:
}
lockContentStr := string(lockContent)
+ if !strings.Contains(lockContentStr, `"fallback_as_pull_request":false`) && !strings.Contains(lockContentStr, `"fallback_as_pull_request": false`) {
+ t.Errorf("Generated workflow should contain fallback_as_pull_request in handler config JSON")
+ }
if strings.Contains(lockContentStr, "pull-requests: write") {
t.Errorf("Generated workflow should NOT have pull-requests: write permission when fallback-as-pull-request is false")
}
From e1ce37aa5aadba533a98fa5943e21bcb9e372c7f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 19 Apr 2026 20:52:34 +0000
Subject: [PATCH 07/10] refactor: simplify fallback type selection in safe
output summary
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/f35b039c-a302-4ce9-a90d-a41ec5468b27
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/safe_output_summary.cjs | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/actions/setup/js/safe_output_summary.cjs b/actions/setup/js/safe_output_summary.cjs
index 86b3ffcf5e6..1e17ed89b29 100644
--- a/actions/setup/js/safe_output_summary.cjs
+++ b/actions/setup/js/safe_output_summary.cjs
@@ -34,12 +34,7 @@ function generateSafeOutputSummary(options) {
// Prefer explicit fallback_type when available; infer only for backward compatibility.
const isFallback = success && result && result.fallback_used === true;
const inferredFallbackType = isFallback && (result.pull_request_url || result.pull_request_number != null) ? "pull_request" : "issue";
- let fallbackType = inferredFallbackType;
- if (isFallback && result?.fallback_type === "pull_request") {
- fallbackType = "pull_request";
- } else if (isFallback && result?.fallback_type === "issue") {
- fallbackType = "issue";
- }
+ const fallbackType = isFallback && result?.fallback_type ? result.fallback_type : inferredFallbackType;
// Choose emoji and status based on success and fallback
const emoji = isFallback ? "⚠️" : success ? "✅" : "❌";
From a76669258a85e4e56109c4ac5f2a95b7cf83fcf3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 19 Apr 2026 22:21:46 +0000
Subject: [PATCH 08/10] test: harden fallback_as_pull_request compile
assertions
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/892e3d78-093f-4f10-9bd4-3d91ae48774e
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/safe_output_summary.cjs | 2 +-
.../push_to_pull_request_branch_test.go | 111 +++++++++++++++++-
2 files changed, 111 insertions(+), 2 deletions(-)
diff --git a/actions/setup/js/safe_output_summary.cjs b/actions/setup/js/safe_output_summary.cjs
index 1e17ed89b29..6355e68436b 100644
--- a/actions/setup/js/safe_output_summary.cjs
+++ b/actions/setup/js/safe_output_summary.cjs
@@ -34,7 +34,7 @@ function generateSafeOutputSummary(options) {
// Prefer explicit fallback_type when available; infer only for backward compatibility.
const isFallback = success && result && result.fallback_used === true;
const inferredFallbackType = isFallback && (result.pull_request_url || result.pull_request_number != null) ? "pull_request" : "issue";
- const fallbackType = isFallback && result?.fallback_type ? result.fallback_type : inferredFallbackType;
+ const fallbackType = result?.fallback_type || inferredFallbackType;
// Choose emoji and status based on success and fallback
const emoji = isFallback ? "⚠️" : success ? "✅" : "❌";
diff --git a/pkg/workflow/push_to_pull_request_branch_test.go b/pkg/workflow/push_to_pull_request_branch_test.go
index 870e6d3fee4..20a28de8c83 100644
--- a/pkg/workflow/push_to_pull_request_branch_test.go
+++ b/pkg/workflow/push_to_pull_request_branch_test.go
@@ -3,6 +3,7 @@
package workflow
import (
+ "encoding/json"
"os"
"path/filepath"
"strings"
@@ -11,8 +12,66 @@ import (
"github.com/github/gh-aw/pkg/stringutil"
"github.com/github/gh-aw/pkg/testutil"
+ "go.yaml.in/yaml/v3"
)
+func extractPushToPullRequestBranchHandlerConfigFromLockContent(t *testing.T, lockContent []byte) map[string]any {
+ t.Helper()
+
+ var workflowDoc map[string]any
+ if err := yaml.Unmarshal(lockContent, &workflowDoc); err != nil {
+ t.Fatalf("Failed to unmarshal lock workflow YAML: %v", err)
+ }
+
+ jobsRaw, ok := workflowDoc["jobs"].(map[string]any)
+ if !ok {
+ t.Fatalf("Generated workflow should contain jobs map")
+ }
+
+ safeOutputsJobRaw, ok := jobsRaw["safe_outputs"].(map[string]any)
+ if !ok {
+ t.Fatalf("Generated workflow should contain safe_outputs job")
+ }
+
+ stepsRaw, ok := safeOutputsJobRaw["steps"].([]any)
+ if !ok {
+ t.Fatalf("Generated workflow safe_outputs job should contain steps array")
+ }
+
+ var handlerConfigJSON string
+ for _, step := range stepsRaw {
+ stepMap, ok := step.(map[string]any)
+ if !ok {
+ continue
+ }
+ envMap, ok := stepMap["env"].(map[string]any)
+ if !ok {
+ continue
+ }
+ rawConfig, ok := envMap["GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG"].(string)
+ if ok && rawConfig != "" {
+ handlerConfigJSON = rawConfig
+ break
+ }
+ }
+
+ if handlerConfigJSON == "" {
+ t.Fatalf("Generated workflow should contain GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG env var")
+ }
+
+ var handlerConfig map[string]any
+ if err := json.Unmarshal([]byte(handlerConfigJSON), &handlerConfig); err != nil {
+ t.Fatalf("Failed to unmarshal GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG JSON: %v", err)
+ }
+
+ pushCfgRaw, ok := handlerConfig["push_to_pull_request_branch"].(map[string]any)
+ if !ok {
+ t.Fatalf("Handler config should contain push_to_pull_request_branch object")
+ }
+
+ return pushCfgRaw
+}
+
func TestPushToPullRequestBranchConfigParsing(t *testing.T) {
// Create a temporary directory for the test
tmpDir := testutil.TempDir(t, "test-*")
@@ -162,14 +221,64 @@ safe-outputs:
}
lockContentStr := string(lockContent)
- if !strings.Contains(lockContentStr, `"fallback_as_pull_request":false`) && !strings.Contains(lockContentStr, `"fallback_as_pull_request": false`) {
+ pushConfig := extractPushToPullRequestBranchHandlerConfigFromLockContent(t, lockContent)
+ fallbackAsPullRequest, exists := pushConfig["fallback_as_pull_request"]
+ if !exists {
t.Errorf("Generated workflow should contain fallback_as_pull_request in handler config JSON")
}
+ if fallbackAsPullRequest != false {
+ t.Errorf("Expected fallback_as_pull_request=false, got %#v", fallbackAsPullRequest)
+ }
if strings.Contains(lockContentStr, "pull-requests: write") {
t.Errorf("Generated workflow should NOT have pull-requests: write permission when fallback-as-pull-request is false")
}
}
+func TestPushToPullRequestBranchFallbackAsPullRequestEnabled(t *testing.T) {
+ tmpDir := testutil.TempDir(t, "test-*")
+
+ testMarkdown := `---
+on:
+ pull_request:
+ types: [opened, synchronize]
+safe-outputs:
+ push-to-pull-request-branch:
+ fallback-as-pull-request: true
+---
+
+# Test Push to PR Branch Fallback Enabled
+`
+
+ mdFile := filepath.Join(tmpDir, "test-push-to-pull-request-branch-fallback-enabled.md")
+ if err := os.WriteFile(mdFile, []byte(testMarkdown), 0644); err != nil {
+ t.Fatalf("Failed to write test markdown file: %v", err)
+ }
+
+ compiler := NewCompiler()
+ if err := compiler.CompileWorkflow(mdFile); err != nil {
+ t.Fatalf("Failed to compile workflow: %v", err)
+ }
+
+ lockFile := stringutil.MarkdownToLockFile(mdFile)
+ lockContent, err := os.ReadFile(lockFile)
+ if err != nil {
+ t.Fatalf("Failed to read lock file: %v", err)
+ }
+
+ lockContentStr := string(lockContent)
+ pushConfig := extractPushToPullRequestBranchHandlerConfigFromLockContent(t, lockContent)
+ fallbackAsPullRequest, exists := pushConfig["fallback_as_pull_request"]
+ if !exists {
+ t.Errorf("Generated workflow should contain fallback_as_pull_request in handler config JSON")
+ }
+ if fallbackAsPullRequest != true {
+ t.Errorf("Expected fallback_as_pull_request=true, got %#v", fallbackAsPullRequest)
+ }
+ if !strings.Contains(lockContentStr, "pull-requests: write") {
+ t.Errorf("Generated workflow should have pull-requests: write permission when fallback-as-pull-request is true")
+ }
+}
+
func TestPushToPullRequestBranchWithTargetAsterisk(t *testing.T) {
// Create a temporary directory for the test
tmpDir := testutil.TempDir(t, "test-*")
From 3bd003c49f6bbeeca1eec71ec81145ada928b86d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 19 Apr 2026 22:26:11 +0000
Subject: [PATCH 09/10] refactor: polish fallback_as_pull_request compile tests
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/892e3d78-093f-4f10-9bd4-3d91ae48774e
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.../push_to_pull_request_branch_test.go | 22 +++++++++++++------
1 file changed, 15 insertions(+), 7 deletions(-)
diff --git a/pkg/workflow/push_to_pull_request_branch_test.go b/pkg/workflow/push_to_pull_request_branch_test.go
index 20a28de8c83..ab28bd369c9 100644
--- a/pkg/workflow/push_to_pull_request_branch_test.go
+++ b/pkg/workflow/push_to_pull_request_branch_test.go
@@ -15,7 +15,7 @@ import (
"go.yaml.in/yaml/v3"
)
-func extractPushToPullRequestBranchHandlerConfigFromLockContent(t *testing.T, lockContent []byte) map[string]any {
+func extractPushToPRBranchHandlerConfig(t *testing.T, lockContent []byte) map[string]any {
t.Helper()
var workflowDoc map[string]any
@@ -221,13 +221,17 @@ safe-outputs:
}
lockContentStr := string(lockContent)
- pushConfig := extractPushToPullRequestBranchHandlerConfigFromLockContent(t, lockContent)
+ pushConfig := extractPushToPRBranchHandlerConfig(t, lockContent)
fallbackAsPullRequest, exists := pushConfig["fallback_as_pull_request"]
if !exists {
t.Errorf("Generated workflow should contain fallback_as_pull_request in handler config JSON")
}
- if fallbackAsPullRequest != false {
- t.Errorf("Expected fallback_as_pull_request=false, got %#v", fallbackAsPullRequest)
+ fallbackAsPullRequestBool, isBool := fallbackAsPullRequest.(bool)
+ if !isBool {
+ t.Errorf("Expected fallback_as_pull_request to be a bool, got %#v", fallbackAsPullRequest)
+ }
+ if fallbackAsPullRequestBool {
+ t.Errorf("Expected fallback_as_pull_request=false, got %#v", fallbackAsPullRequestBool)
}
if strings.Contains(lockContentStr, "pull-requests: write") {
t.Errorf("Generated workflow should NOT have pull-requests: write permission when fallback-as-pull-request is false")
@@ -266,13 +270,17 @@ safe-outputs:
}
lockContentStr := string(lockContent)
- pushConfig := extractPushToPullRequestBranchHandlerConfigFromLockContent(t, lockContent)
+ pushConfig := extractPushToPRBranchHandlerConfig(t, lockContent)
fallbackAsPullRequest, exists := pushConfig["fallback_as_pull_request"]
if !exists {
t.Errorf("Generated workflow should contain fallback_as_pull_request in handler config JSON")
}
- if fallbackAsPullRequest != true {
- t.Errorf("Expected fallback_as_pull_request=true, got %#v", fallbackAsPullRequest)
+ fallbackAsPullRequestBool, isBool := fallbackAsPullRequest.(bool)
+ if !isBool {
+ t.Errorf("Expected fallback_as_pull_request to be a bool, got %#v", fallbackAsPullRequest)
+ }
+ if !fallbackAsPullRequestBool {
+ t.Errorf("Expected fallback_as_pull_request=true, got %#v", fallbackAsPullRequestBool)
}
if !strings.Contains(lockContentStr, "pull-requests: write") {
t.Errorf("Generated workflow should have pull-requests: write permission when fallback-as-pull-request is true")
From 6db0ad6e0ec3951d7402217b8dad6a09a870002a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 19 Apr 2026 22:30:27 +0000
Subject: [PATCH 10/10] refactor: align fallback helper naming and fallback
type guard
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/892e3d78-093f-4f10-9bd4-3d91ae48774e
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/safe_output_summary.cjs | 2 +-
pkg/workflow/push_to_pull_request_branch_test.go | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/actions/setup/js/safe_output_summary.cjs b/actions/setup/js/safe_output_summary.cjs
index 6355e68436b..1e17ed89b29 100644
--- a/actions/setup/js/safe_output_summary.cjs
+++ b/actions/setup/js/safe_output_summary.cjs
@@ -34,7 +34,7 @@ function generateSafeOutputSummary(options) {
// Prefer explicit fallback_type when available; infer only for backward compatibility.
const isFallback = success && result && result.fallback_used === true;
const inferredFallbackType = isFallback && (result.pull_request_url || result.pull_request_number != null) ? "pull_request" : "issue";
- const fallbackType = result?.fallback_type || inferredFallbackType;
+ const fallbackType = isFallback && result?.fallback_type ? result.fallback_type : inferredFallbackType;
// Choose emoji and status based on success and fallback
const emoji = isFallback ? "⚠️" : success ? "✅" : "❌";
diff --git a/pkg/workflow/push_to_pull_request_branch_test.go b/pkg/workflow/push_to_pull_request_branch_test.go
index ab28bd369c9..989d018b4a2 100644
--- a/pkg/workflow/push_to_pull_request_branch_test.go
+++ b/pkg/workflow/push_to_pull_request_branch_test.go
@@ -15,7 +15,7 @@ import (
"go.yaml.in/yaml/v3"
)
-func extractPushToPRBranchHandlerConfig(t *testing.T, lockContent []byte) map[string]any {
+func extractPushToPullRequestBranchHandlerConfig(t *testing.T, lockContent []byte) map[string]any {
t.Helper()
var workflowDoc map[string]any
@@ -221,7 +221,7 @@ safe-outputs:
}
lockContentStr := string(lockContent)
- pushConfig := extractPushToPRBranchHandlerConfig(t, lockContent)
+ pushConfig := extractPushToPullRequestBranchHandlerConfig(t, lockContent)
fallbackAsPullRequest, exists := pushConfig["fallback_as_pull_request"]
if !exists {
t.Errorf("Generated workflow should contain fallback_as_pull_request in handler config JSON")
@@ -270,7 +270,7 @@ safe-outputs:
}
lockContentStr := string(lockContent)
- pushConfig := extractPushToPRBranchHandlerConfig(t, lockContent)
+ pushConfig := extractPushToPullRequestBranchHandlerConfig(t, lockContent)
fallbackAsPullRequest, exists := pushConfig["fallback_as_pull_request"]
if !exists {
t.Errorf("Generated workflow should contain fallback_as_pull_request in handler config JSON")