diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml
index 13a0d8a8848..b3238e040fc 100644
--- a/.github/workflows/agentics-maintenance.yml
+++ b/.github/workflows/agentics-maintenance.yml
@@ -53,6 +53,7 @@ on:
- 'activity_report'
- 'close_agentic_workflows_issues'
- 'clean_cache_memories'
+ - 'update_pull_request_branches'
- '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.'
@@ -62,7 +63,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, update_pull_request_branches, validate)'
required: false
type: string
default: ''
@@ -157,7 +158,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 != 'update_pull_request_branches' && inputs.operation != 'validate' && (!(github.event.repository.fork)) }}
runs-on: ubuntu-slim
permissions:
actions: write
@@ -213,6 +214,47 @@ jobs:
id: record
run: echo "operation=${{ inputs.operation }}" >> "$GITHUB_OUTPUT"
+ update_pull_request_branches:
+ if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'update_pull_request_branches' && (!(github.event.repository.fork)) }}
+ runs-on: ubuntu-slim
+ permissions:
+ contents: write
+ pull-requests: write
+ steps:
+ - name: Checkout actions folder
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ sparse-checkout: |
+ actions
+ 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: Update pull request branches
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ 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/update_pull_request_branches.cjs');
+ await main();
+
apply_safe_outputs:
if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'safe_outputs' && (!(github.event.repository.fork)) }}
runs-on: ubuntu-slim
diff --git a/actions/setup/js/run_operation_update_upgrade.cjs b/actions/setup/js/run_operation_update_upgrade.cjs
index c0dc3768d31..1da45d2774d 100644
--- a/actions/setup/js/run_operation_update_upgrade.cjs
+++ b/actions/setup/js/run_operation_update_upgrade.cjs
@@ -45,7 +45,8 @@ function formatTimestamp(date) {
}
/**
- * Run 'gh aw update', 'gh aw upgrade', 'gh aw disable', or 'gh aw enable',
+ * Run maintenance operations handled by run_operation:
+ * - 'gh aw update', 'gh aw upgrade', 'gh aw disable', 'gh aw enable'
* creating a pull request when needed for update/upgrade operations.
*
* For update/upgrade: runs with --no-compile so lock files are not modified.
diff --git a/actions/setup/js/update_pull_request_branches.cjs b/actions/setup/js/update_pull_request_branches.cjs
new file mode 100644
index 00000000000..1a66c0f19ca
--- /dev/null
+++ b/actions/setup/js/update_pull_request_branches.cjs
@@ -0,0 +1,184 @@
+// @ts-check
+///
+
+const { getErrorMessage } = require("./error_helpers.cjs");
+const { withRetry, isTransientError, sleep } = require("./error_recovery.cjs");
+const { fetchAndLogRateLimit } = require("./github_rate_limit_logger.cjs");
+const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs");
+
+const LIST_PULL_REQUESTS_PER_PAGE = 100;
+const UPDATE_DELAY_MS = 1000;
+
+/**
+ * @param {string} owner
+ * @param {string} repo
+ * @returns {Promise}
+ */
+async function listOpenPullRequests(owner, repo) {
+ const pulls = await github.paginate(github.rest.pulls.list, {
+ owner,
+ repo,
+ state: "open",
+ per_page: LIST_PULL_REQUESTS_PER_PAGE,
+ });
+
+ return pulls.map(pr => pr.number).filter(number => Number.isInteger(number));
+}
+
+/**
+ * @param {string} owner
+ * @param {string} repo
+ * @param {number[]} pullNumbers
+ * @returns {Promise}
+ */
+async function filterMergeablePullRequests(owner, repo, pullNumbers) {
+ const mergeable = [];
+ const baseRepository = `${owner}/${repo}`.toLowerCase();
+
+ for (const pullNumber of pullNumbers) {
+ const { data: pull } = await withRetry(
+ () =>
+ github.rest.pulls.get({
+ owner,
+ repo,
+ pull_number: pullNumber,
+ }),
+ {
+ maxRetries: 2,
+ initialDelayMs: 500,
+ maxDelayMs: 2000,
+ jitterMs: 0,
+ shouldRetry: isTransientError,
+ },
+ `fetch pull request #${pullNumber}`
+ );
+
+ const headRepositoryRaw = pull?.head?.repo?.full_name;
+ const headRepository = headRepositoryRaw?.toLowerCase() ?? "";
+ const isSameRepository = headRepository === baseRepository;
+ const isMergeable = pull?.state === "open" && pull?.mergeable === true && pull?.draft !== true && isSameRepository;
+ if (isMergeable) {
+ mergeable.push(pullNumber);
+ continue;
+ }
+
+ let skipReason = "not_mergeable";
+ if (!isSameRepository) {
+ skipReason = headRepository ? "head_repository_mismatch" : "head_repository_missing";
+ }
+ core.info(`Skipping PR #${pullNumber}: reason=${skipReason}, mergeable=${String(pull?.mergeable)}, state=${pull?.state || "unknown"}, draft=${String(Boolean(pull?.draft))}, head_repo=${headRepository || "unknown"}`);
+ }
+
+ return mergeable;
+}
+
+/**
+ * @param {unknown} error
+ * @returns {boolean}
+ */
+function isNonFatalUpdateBranchError(error) {
+ if (typeof error === "object" && error !== null && "status" in error && error.status === 422) {
+ return true;
+ }
+
+ const message = getErrorMessage(error).toLowerCase();
+ return message.includes("update branch failed") || message.includes("head branch is not behind");
+}
+
+/**
+ * @param {string} owner
+ * @param {string} repo
+ * @param {number} pullNumber
+ * @returns {Promise}
+ */
+async function updatePullRequestBranch(owner, repo, pullNumber) {
+ await withRetry(
+ () =>
+ github.rest.pulls.updateBranch({
+ owner,
+ repo,
+ pull_number: pullNumber,
+ }),
+ {
+ maxRetries: 2,
+ initialDelayMs: 1000,
+ maxDelayMs: 10000,
+ shouldRetry: isTransientError,
+ },
+ `update branch for pull request #${pullNumber}`
+ );
+}
+
+/**
+ * @param {string} owner
+ * @param {string} repo
+ * @param {number} pullNumber
+ * @param {string} runUrl
+ * @returns {Promise}
+ */
+async function addMaintenanceUpdateComment(owner, repo, pullNumber, runUrl) {
+ const body = `🛠️ Agentic Maintenance updated this pull request branch.\n\n[View workflow run](${runUrl})`;
+ await github.rest.issues.createComment({
+ owner,
+ repo,
+ issue_number: pullNumber,
+ body,
+ });
+}
+
+/**
+ * Update all mergeable PR branches.
+ * @returns {Promise}
+ */
+async function main() {
+ const owner = context.repo.owner;
+ const repo = context.repo.repo;
+ const runUrl = buildWorkflowRunUrl(context, context.repo);
+
+ core.info(`Updating pull request branches in ${owner}/${repo}`);
+ core.info(`Run URL: ${runUrl}`);
+ await fetchAndLogRateLimit(github, "update_pull_request_branches_start");
+
+ const openPullRequests = await listOpenPullRequests(owner, repo);
+ core.info(`Found ${openPullRequests.length} open pull request(s)`);
+ if (openPullRequests.length === 0) return;
+
+ const mergeablePullRequests = await filterMergeablePullRequests(owner, repo, openPullRequests);
+ core.info(`Found ${mergeablePullRequests.length} mergeable pull request(s)`);
+ if (mergeablePullRequests.length === 0) return;
+
+ let updatedCount = 0;
+ let skippedCount = 0;
+ let failedCount = 0;
+
+ for (let i = 0; i < mergeablePullRequests.length; i++) {
+ const pullNumber = mergeablePullRequests[i];
+ try {
+ core.info(`Updating branch for PR #${pullNumber}`);
+ await updatePullRequestBranch(owner, repo, pullNumber);
+ await addMaintenanceUpdateComment(owner, repo, pullNumber, runUrl);
+ updatedCount++;
+ } catch (error) {
+ if (isNonFatalUpdateBranchError(error)) {
+ skippedCount++;
+ core.warning(`Skipping PR #${pullNumber}: ${getErrorMessage(error)}`);
+ } else {
+ failedCount++;
+ core.error(`Failed to update branch for PR #${pullNumber}: ${getErrorMessage(error)}`);
+ }
+ }
+
+ if (i < mergeablePullRequests.length - 1) {
+ await sleep(UPDATE_DELAY_MS);
+ }
+ }
+
+ await fetchAndLogRateLimit(github, "update_pull_request_branches_end");
+ core.notice(`update_pull_request_branches completed: updated=${updatedCount}, skipped=${skippedCount}, failed=${failedCount}`);
+}
+
+module.exports = {
+ main,
+ filterMergeablePullRequests,
+ isNonFatalUpdateBranchError,
+};
diff --git a/actions/setup/js/update_pull_request_branches.test.cjs b/actions/setup/js/update_pull_request_branches.test.cjs
new file mode 100644
index 00000000000..23a314820bf
--- /dev/null
+++ b/actions/setup/js/update_pull_request_branches.test.cjs
@@ -0,0 +1,143 @@
+// @ts-check
+import { describe, it, expect, beforeEach, vi } from "vitest";
+
+vi.mock("./github_rate_limit_logger.cjs", () => ({
+ fetchAndLogRateLimit: vi.fn().mockResolvedValue(undefined),
+}));
+
+const moduleUnderTest = await import("./update_pull_request_branches.cjs");
+
+describe("update_pull_request_branches", () => {
+ /** @type {any} */
+ let mockCore;
+ /** @type {any} */
+ let mockGithub;
+ /** @type {any} */
+ let mockContext;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+
+ mockCore = {
+ info: vi.fn(),
+ warning: vi.fn(),
+ error: vi.fn(),
+ debug: vi.fn(),
+ notice: vi.fn(),
+ };
+ mockGithub = {
+ paginate: vi.fn(),
+ graphql: vi.fn(),
+ rest: {
+ issues: {
+ createComment: vi.fn(),
+ },
+ pulls: {
+ list: vi.fn(),
+ get: vi.fn(),
+ updateBranch: vi.fn(),
+ },
+ },
+ };
+ mockContext = {
+ runId: 123,
+ serverUrl: "https://github.com",
+ repo: {
+ owner: "owner",
+ repo: "repo",
+ },
+ };
+
+ global.core = mockCore;
+ global.github = mockGithub;
+ global.context = mockContext;
+ });
+
+ it("updates only mergeable pull requests", async () => {
+ mockGithub.paginate.mockResolvedValue([{ number: 1 }, { number: 2 }, { number: 3 }]);
+ mockGithub.rest.pulls.get.mockImplementation(async ({ pull_number }) => {
+ if (pull_number === 1) return { data: { state: "open", mergeable: true, draft: false, head: { repo: { full_name: "owner/repo" } } } };
+ if (pull_number === 2) return { data: { state: "open", mergeable: false, draft: false, head: { repo: { full_name: "owner/repo" } } } };
+ return { data: { state: "open", mergeable: true, draft: false, head: { repo: { full_name: "owner/repo" } } } };
+ });
+ mockGithub.rest.pulls.updateBranch.mockResolvedValue({ data: {} });
+
+ await moduleUnderTest.main();
+
+ expect(mockGithub.rest.pulls.updateBranch).toHaveBeenCalledTimes(2);
+ expect(mockGithub.rest.pulls.updateBranch).toHaveBeenNthCalledWith(1, {
+ owner: "owner",
+ repo: "repo",
+ pull_number: 1,
+ });
+ expect(mockGithub.rest.pulls.updateBranch).toHaveBeenNthCalledWith(2, {
+ owner: "owner",
+ repo: "repo",
+ pull_number: 3,
+ });
+ expect(mockGithub.rest.issues.createComment).toHaveBeenCalledTimes(2);
+ expect(mockGithub.rest.issues.createComment).toHaveBeenNthCalledWith(1, {
+ owner: "owner",
+ repo: "repo",
+ issue_number: 1,
+ body: expect.stringContaining("[View workflow run](https://github.com/owner/repo/actions/runs/123)"),
+ });
+ expect(mockGithub.rest.issues.createComment).toHaveBeenNthCalledWith(2, {
+ owner: "owner",
+ repo: "repo",
+ issue_number: 3,
+ body: expect.stringContaining("[View workflow run](https://github.com/owner/repo/actions/runs/123)"),
+ });
+ });
+
+ it("continues on non-fatal updateBranch failures", async () => {
+ mockGithub.paginate.mockResolvedValue([{ number: 7 }]);
+ mockGithub.rest.pulls.get.mockResolvedValue({ data: { state: "open", mergeable: true, draft: false, head: { repo: { full_name: "owner/repo" } } } });
+ const err = new Error("Update branch failed");
+ // @ts-ignore
+ err.status = 422;
+ mockGithub.rest.pulls.updateBranch.mockRejectedValue(err);
+
+ await expect(moduleUnderTest.main()).resolves.not.toThrow();
+ expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Skipping PR #7"));
+ expect(mockGithub.rest.issues.createComment).not.toHaveBeenCalled();
+ });
+
+ it("ignores draft pull requests when filtering mergeable pull requests", async () => {
+ mockGithub.rest.pulls.get.mockImplementation(async ({ pull_number }) => {
+ if (pull_number === 1) return { data: { state: "open", mergeable: true, draft: true, head: { repo: { full_name: "owner/repo" } } } };
+ if (pull_number === 2) return { data: { state: "open", mergeable: true, draft: false, head: { repo: { full_name: "owner/repo" } } } };
+ return { data: { state: "open", mergeable: false, draft: false, head: { repo: { full_name: "owner/repo" } } } };
+ });
+
+ const result = await moduleUnderTest.filterMergeablePullRequests("owner", "repo", [1, 2, 3]);
+
+ expect(result).toEqual([2]);
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Skipping PR #1"));
+ });
+
+ it("ignores fork pull requests that cannot be updated by repository token", async () => {
+ mockGithub.rest.pulls.get.mockImplementation(async ({ pull_number }) => {
+ if (pull_number === 1) return { data: { state: "open", mergeable: true, draft: false, head: { repo: { full_name: "fork-owner/repo" } } } };
+ return { data: { state: "open", mergeable: true, draft: false, head: { repo: { full_name: "owner/repo" } } } };
+ });
+
+ const result = await moduleUnderTest.filterMergeablePullRequests("owner", "repo", [1, 2]);
+
+ expect(result).toEqual([2]);
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("reason=head_repository_mismatch"));
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("head_repo=fork-owner/repo"));
+ });
+
+ it("logs explicit reason when head repository is unavailable", async () => {
+ mockGithub.rest.pulls.get.mockResolvedValue({
+ data: { state: "open", mergeable: true, draft: false, head: { repo: null } },
+ });
+
+ const result = await moduleUnderTest.filterMergeablePullRequests("owner", "repo", [11]);
+
+ expect(result).toEqual([]);
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("reason=head_repository_missing"));
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("head_repo=unknown"));
+ });
+});
diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go
index 852f3fd78d3..51a3991a18e 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 != '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 != 'update_pull_request_branches' && 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'`
+ updatePullRequestBranchesCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'update_pull_request_branches'`
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'`
@@ -356,6 +357,23 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
}
}
+ // update_pull_request_branches job should be triggered when operation == 'update_pull_request_branches'
+ updatePullRequestBranchesIdx := strings.Index(yaml, "\n update_pull_request_branches:")
+ if updatePullRequestBranchesIdx == -1 {
+ t.Errorf("Job update_pull_request_branches not found in generated workflow")
+ } else {
+ updatePullRequestBranchesSection := yaml[updatePullRequestBranchesIdx : updatePullRequestBranchesIdx+runOpSectionSearchRange]
+ if !strings.Contains(updatePullRequestBranchesSection, updatePullRequestBranchesCondition) {
+ t.Errorf("Job update_pull_request_branches should have the activation condition %q in:\n%s", updatePullRequestBranchesCondition, updatePullRequestBranchesSection)
+ }
+ if !strings.Contains(updatePullRequestBranchesSection, "pull-requests: write") {
+ t.Errorf("Job update_pull_request_branches should include pull-requests: write permission in:\n%s", updatePullRequestBranchesSection)
+ }
+ if !strings.Contains(updatePullRequestBranchesSection, "contents: write") {
+ t.Errorf("Job update_pull_request_branches should include contents: write permission in:\n%s", updatePullRequestBranchesSection)
+ }
+ }
+
// validate_workflows job should be triggered when operation == 'validate'
validateCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'validate'`
validateIdx := strings.Index(yaml, "\n validate_workflows:")
@@ -454,6 +472,11 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
t.Error("workflow_dispatch operation choices should include 'clean_cache_memories'")
}
+ // Verify update_pull_request_branches is an option in the operation choices
+ if !strings.Contains(yaml, "- 'update_pull_request_branches'") {
+ t.Error("workflow_dispatch operation choices should include 'update_pull_request_branches'")
+ }
+
// Verify validate is an option in the operation choices
if !strings.Contains(yaml, "- 'validate'") {
t.Error("workflow_dispatch operation choices should include 'validate'")
diff --git a/pkg/workflow/maintenance_workflow_yaml.go b/pkg/workflow/maintenance_workflow_yaml.go
index 3dd0362056d..bda1644251e 100644
--- a/pkg/workflow/maintenance_workflow_yaml.go
+++ b/pkg/workflow/maintenance_workflow_yaml.go
@@ -62,6 +62,7 @@ on:
- 'activity_report'
- 'close_agentic_workflows_issues'
- 'clean_cache_memories'
+ - 'update_pull_request_branches'
- '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.'
@@ -71,7 +72,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, update_pull_request_branches, validate)'
required: false
type: string
default: ''
@@ -196,8 +197,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, update_pull_request_branches, validate)
+ runOperationCondition := buildRunOperationCondition("safe_outputs", "create_labels", "activity_report", "close_agentic_workflows_issues", "clean_cache_memories", "update_pull_request_branches", "validate")
yaml.WriteString(`
run_operation:
if: ${{ ` + RenderCondition(runOperationCondition) + ` }}
@@ -251,6 +252,55 @@ jobs:
run: echo "operation=${{ inputs.operation }}" >> "$GITHUB_OUTPUT"
`)
+ // Add update_pull_request_branches job for workflow_dispatch with operation == 'update_pull_request_branches'
+ yaml.WriteString(`
+ update_pull_request_branches:
+ if: ${{ ` + RenderCondition(buildDispatchOperationCondition("update_pull_request_branches")) + ` }}
+ runs-on: ` + runsOnValue + `
+ permissions:
+ contents: write
+ pull-requests: write
+ steps:
+`)
+
+ // Add checkout step only in dev/script mode (for local action paths)
+ if actionMode == ActionModeDev || actionMode == ActionModeScript {
+ yaml.WriteString(" - name: Checkout actions folder\n")
+ yaml.WriteString(" uses: " + getActionPin("actions/checkout") + "\n")
+ yaml.WriteString(" with:\n")
+ yaml.WriteString(" sparse-checkout: |\n")
+ yaml.WriteString(" actions\n")
+ yaml.WriteString(" persist-credentials: false\n\n")
+ }
+
+ yaml.WriteString(` - 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();
+
+ - name: Update pull request branches
+ uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + `
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ 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/update_pull_request_branches.cjs');
+ await main();
+`)
+
// Add apply_safe_outputs job for workflow_dispatch with operation == 'safe_outputs'
yaml.WriteString(`
apply_safe_outputs: