From ddd9b1d61b88759ef72a0a62ac37376ad6cc3506 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Jun 2026 03:07:14 +0000 Subject: [PATCH 1/2] Apply remaining changes Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/route_slash_command.cjs | 90 +++++++++++++++++++ actions/setup/js/route_slash_command.test.cjs | 26 ++++++ 2 files changed, 116 insertions(+) diff --git a/actions/setup/js/route_slash_command.cjs b/actions/setup/js/route_slash_command.cjs index 3113b670e47..1b2c6a122a0 100644 --- a/actions/setup/js/route_slash_command.cjs +++ b/actions/setup/js/route_slash_command.cjs @@ -320,10 +320,81 @@ async function dispatchWorkflow(workflowId, ref, inputs) { core.info(`Skipping workflow '${workflowId}' because it is disabled.`); return false; } + if (isMissingWorkflowDispatchRefError(error)) { + const fallbackRef = await resolveDefaultBranchRef(ref); + if (!fallbackRef) { + throw new Error(`Failed to dispatch workflow '${workflowId}': ref '${ref}' was not found and no usable fallback ref (missing or identical) was available: ${String(error)}`); + } + core.warning(`Dispatch ref '${ref}' was not found for '${workflowId}'; retrying with fallback ref '${fallbackRef}'.`); + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: workflowId, + ref: fallbackRef, + inputs, + headers: { + "X-GitHub-Api-Version": GITHUB_API_VERSION, + }, + }); + return true; + } throw new Error(`Failed to dispatch workflow '${workflowId}' on ref '${ref}': ${String(error)}`); } } +/** + * Resolves a fallback dispatch ref when the original ref is unavailable. + * Prefers the event payload default branch, then repository metadata, then main. + * Returns an empty string when no usable fallback differs from the original ref. + * @param {string} ref + * @returns {Promise} + */ +async function resolveDefaultBranchRef(ref) { + const payloadDefaultBranch = context.payload?.repository?.default_branch; + if (typeof payloadDefaultBranch === "string" && payloadDefaultBranch.trim()) { + return resolveFallbackRefFromBranchName(ref, payloadDefaultBranch); + } + + try { + const response = await github.rest.repos.get({ + owner: context.repo.owner, + repo: context.repo.repo, + headers: { + "X-GitHub-Api-Version": GITHUB_API_VERSION, + }, + }); + const apiDefaultBranch = response?.data?.default_branch; + if (typeof apiDefaultBranch === "string") { + return resolveFallbackRefFromBranchName(ref, apiDefaultBranch); + } + } catch (repoError) { + core.warning(`Failed to resolve default branch for dispatch fallback: ${String(repoError)}`); + } + + return resolveFallbackRefFromBranchName(ref, "main"); +} + +/** + * Normalizes a branch name into a dispatch ref and returns it only when usable as fallback. + * @param {string} ref + * @param {string} branchName + * @returns {string} + */ +function resolveFallbackRefFromBranchName(ref, branchName) { + const defaultBranchRef = normalizeDispatchRef(branchName); + return isUsableFallbackRef(ref, defaultBranchRef) ? defaultBranchRef : ""; +} + +/** + * Returns true when a candidate ref can be used as a fallback for dispatch. + * @param {string} ref + * @param {string} candidateRef + * @returns {boolean} + */ +function isUsableFallbackRef(ref, candidateRef) { + return Boolean(candidateRef) && candidateRef !== ref; +} + function isBuiltinHelpEnabled() { const raw = (process.env.GH_AW_HELP_COMMAND_ENABLED || "").trim().toLowerCase(); if (!raw || raw === "true") { @@ -517,6 +588,25 @@ function isDisabledWorkflowDispatchError(error) { return message.includes("workflow is disabled") || message.includes("workflow was disabled") || message.includes("disabled workflow"); } +/** + * Returns true when the dispatch failure indicates the requested ref does not exist. + * @param {any} error + * @returns {boolean} + */ +function isMissingWorkflowDispatchRefError(error) { + const status = error?.status ?? error?.response?.status; + const message = [error?.message, error?.response?.data?.message] + .filter(value => typeof value === "string" && value.trim()) + .join(" ") + .toLowerCase(); + + if (status !== 422 || !message) { + return false; + } + + return message.includes("no ref found for"); +} + /** * @param {Record>} slashRouteMap * @param {string} actualCommand diff --git a/actions/setup/js/route_slash_command.test.cjs b/actions/setup/js/route_slash_command.test.cjs index b0c13ac54ed..9fae49678aa 100644 --- a/actions/setup/js/route_slash_command.test.cjs +++ b/actions/setup/js/route_slash_command.test.cjs @@ -474,6 +474,32 @@ describe("route_slash_command", () => { expect(dispatchCalls[0].ref).toBe("refs/heads/feature/pr-branch"); }); + it("retries dispatch on default branch when PR head ref is not available in target repo", async () => { + globals.context.payload.repository = { default_branch: "main" }; + globals.context.payload.issue.pull_request = { url: "https://example.test/pr/1" }; + globals.context.payload.issue.number = 1; + globals.context.payload.comment.body = "/archie please"; + globals.github.rest.actions.createWorkflowDispatch = vi.fn(async params => { + if (params.ref === "refs/heads/feature/pr-branch") { + throw Object.assign(new Error("No ref found for: refs/heads/feature/pr-branch"), { + status: 422, + response: { + status: 422, + data: { message: "No ref found for: refs/heads/feature/pr-branch" }, + }, + }); + } + dispatchCalls.push(params); + }); + + await main(); + + expect(globals.github.rest.actions.createWorkflowDispatch).toHaveBeenCalledTimes(2); + expect(dispatchCalls).toHaveLength(1); + expect(dispatchCalls[0].ref).toBe("refs/heads/main"); + expect(globals.core.warning).toHaveBeenCalledWith(expect.stringContaining("retrying with fallback ref 'refs/heads/main'")); + }); + it("does not add immediate reaction when no valid route reaction is configured", async () => { process.env.GH_AW_SLASH_ROUTING = JSON.stringify({ archie: [{ workflow: "archie", events: ["issue_comment"], ai_reaction: "none" }], From 76077c3560b95e111aa630679a0e1b40fb02746e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Jun 2026 03:51:01 +0000 Subject: [PATCH 2/2] fix: capture PR number from issue pull URL in dispatcher Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/route_slash_command.cjs | 31 ++++++++++++++++++- actions/setup/js/route_slash_command.test.cjs | 18 +++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/route_slash_command.cjs b/actions/setup/js/route_slash_command.cjs index 1b2c6a122a0..219b51416b2 100644 --- a/actions/setup/js/route_slash_command.cjs +++ b/actions/setup/js/route_slash_command.cjs @@ -80,9 +80,38 @@ function normalizeDispatchRef(ref) { return ref.startsWith("refs/") ? ref : `refs/heads/${ref}`; } +/** + * Resolves pull request number for issue-backed PR events. + * Falls back to parsing the pull request API URL when issue.number is unavailable. + * @returns {number} Pull request number, or 0 when missing/invalid. + */ +function resolveIssueBackedPullNumber() { + const issueNumber = context.payload?.issue?.number; + if (typeof issueNumber === "number" && Number.isInteger(issueNumber) && issueNumber > 0) { + return issueNumber; + } + if (typeof issueNumber === "string" && /^\d+$/.test(issueNumber)) { + const parsed = Number(issueNumber); + if (Number.isInteger(parsed) && parsed > 0) { + return parsed; + } + } + + const pullRequestURL = context.payload?.issue?.pull_request?.url; + if (typeof pullRequestURL !== "string") { + return 0; + } + const match = pullRequestURL.match(/\/pulls\/(\d+)(?:$|[/?#])/); + if (!match) { + return 0; + } + const parsed = Number(match[1]); + return Number.isInteger(parsed) && parsed > 0 ? parsed : 0; +} + async function resolveIssueBackedPRHeadRef() { const isIssueBackedPullRequest = context.payload?.issue?.pull_request; - const pullNumber = context.payload?.issue?.number; + const pullNumber = resolveIssueBackedPullNumber(); if (!isIssueBackedPullRequest || !pullNumber) { return ""; } diff --git a/actions/setup/js/route_slash_command.test.cjs b/actions/setup/js/route_slash_command.test.cjs index 9fae49678aa..8e75b5621b9 100644 --- a/actions/setup/js/route_slash_command.test.cjs +++ b/actions/setup/js/route_slash_command.test.cjs @@ -474,6 +474,24 @@ describe("route_slash_command", () => { expect(dispatchCalls[0].ref).toBe("refs/heads/feature/pr-branch"); }); + it("dispatches slash commands from issue comments on PRs when PR number is only in pull_request.url", async () => { + globals.context.payload.issue.pull_request = { url: "https://api.github.com/repos/github/gh-aw/pulls/42" }; + globals.context.payload.comment.body = "/archie please"; + + await main(); + + expect(globals.github.rest.pulls.get).toHaveBeenCalledWith({ + owner: "github", + repo: "gh-aw", + pull_number: 42, + headers: { + "X-GitHub-Api-Version": GITHUB_API_VERSION, + }, + }); + expect(dispatchCalls).toHaveLength(1); + expect(dispatchCalls[0].ref).toBe("refs/heads/feature/pr-branch"); + }); + it("retries dispatch on default branch when PR head ref is not available in target repo", async () => { globals.context.payload.repository = { default_branch: "main" }; globals.context.payload.issue.pull_request = { url: "https://example.test/pr/1" };