From 69b1d9e28486bcfe5ae12fabcfbd059ab790641f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Jun 2026 11:33:39 +0000 Subject: [PATCH 1/2] Apply remaining changes Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/add_workflow_run_comment.cjs | 26 ++++- .../js/add_workflow_run_comment.test.cjs | 40 +++++++ actions/setup/js/route_slash_command.cjs | 102 ++++++++++++++++-- actions/setup/js/route_slash_command.test.cjs | 45 ++++++-- 4 files changed, 193 insertions(+), 20 deletions(-) diff --git a/actions/setup/js/add_workflow_run_comment.cjs b/actions/setup/js/add_workflow_run_comment.cjs index eb57234965d..b3a29c9a349 100644 --- a/actions/setup/js/add_workflow_run_comment.cjs +++ b/actions/setup/js/add_workflow_run_comment.cjs @@ -171,6 +171,18 @@ function reportCommentError(rawContext, message) { core.setFailed(message); } +/** + * @param {Record|null} awContext + * @param {string} key + * @returns {string} + */ +function readAwContextString(awContext, key) { + if (!awContext || typeof awContext[key] !== "string") { + return ""; + } + return awContext[key].trim(); +} + /** * @param {ReusableStatusComment} reusableComment * @param {{ @@ -184,8 +196,11 @@ function reportCommentError(rawContext, message) { * @returns {Promise} */ async function updateReusableStatusComment(reusableComment, invocationContext, rawContext) { - const runUrl = buildWorkflowRunUrl(rawContext, invocationContext.workflowRepo); - const commentBody = buildCommentBody(invocationContext.eventName, runUrl); + const awContext = extractAwContextFromPayload(rawContext?.payload); + const dispatchedRunUrl = readAwContextString(awContext, "dispatched_run_url"); + const dispatchedWorkflowName = readAwContextString(awContext, "dispatched_workflow_name"); + const runUrl = dispatchedRunUrl || buildWorkflowRunUrl(rawContext, invocationContext.workflowRepo); + const commentBody = buildCommentBody(invocationContext.eventName, runUrl, dispatchedWorkflowName || undefined); // Discussion comments use GraphQL node IDs and a dedicated update mutation. if (reusableComment.id.startsWith("DC_")) { @@ -337,10 +352,13 @@ async function main() { * Sanitizes the content and appends all required markers. * @param {string} eventName - The event type * @param {string} runUrl - The URL of the workflow run + * @param {string} [workflowNameOverride] - Optional dispatched workflow name override * @returns {string} The assembled comment body */ -function buildCommentBody(eventName, runUrl) { - const workflowName = process.env.GH_AW_WORKFLOW_NAME || process.env.GITHUB_WORKFLOW || "Workflow"; +function buildCommentBody(eventName, runUrl, workflowNameOverride) { + // Whitespace-only overrides are treated as absent and fall back to env defaults. + const normalizedWorkflowNameOverride = workflowNameOverride?.trim(); + const workflowName = normalizedWorkflowNameOverride || process.env.GH_AW_WORKFLOW_NAME || process.env.GITHUB_WORKFLOW || "Workflow"; const eventTypeDescription = EVENT_TYPE_DESCRIPTIONS[eventName] ?? "event"; // Sanitize before adding markers (defense in depth for custom message templates) diff --git a/actions/setup/js/add_workflow_run_comment.test.cjs b/actions/setup/js/add_workflow_run_comment.test.cjs index 046ecca4391..245a584b410 100644 --- a/actions/setup/js/add_workflow_run_comment.test.cjs +++ b/actions/setup/js/add_workflow_run_comment.test.cjs @@ -342,6 +342,38 @@ describe("add_workflow_run_comment", () => { expect(mockCore.setFailed).not.toHaveBeenCalled(); }); + it("uses dispatched workflow metadata from aw_context when provided", async () => { + global.context = { + eventName: "workflow_dispatch", + runId: 12345, + repo: { owner: "workflowowner", repo: "workflowrepo" }, + payload: { + inputs: { + aw_context: JSON.stringify({ + repo: "targetowner/targetrepo", + event_type: "issue_comment", + item_type: "issue", + item_number: 789, + status_comment_id: 67890, + status_comment_url: "https://github.com/targetowner/targetrepo/issues/789#issuecomment-67890", + dispatched_workflow_name: "archie", + dispatched_run_url: "https://github.com/github/gh-aw/actions/runs/444", + }), + }, + }, + }; + + await runScript(); + + expect(mockGithub.request).toHaveBeenCalledWith( + "PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", + expect.objectContaining({ + comment_id: 67890, + body: expect.stringContaining("[archie](https://github.com/github/gh-aw/actions/runs/444)"), + }) + ); + }); + it("updates reusable centralized slash-command discussion comments via GraphQL", async () => { mockGithub.graphql.mockResolvedValue({ updateDiscussionComment: { @@ -909,6 +941,14 @@ describe("add_workflow_run_comment", () => { expect(body).toContain("[Agentic Commands]"); }); + it("should prefer explicit workflow name override over environment defaults", async () => { + process.env.GH_AW_WORKFLOW_NAME = "Agentic Commands"; + const { buildCommentBody } = await importAddWorkflowRunComment(); + const body = buildCommentBody("issue_comment", "https://example.com/run/1", "archie"); + expect(body).toContain("[archie]"); + expect(body).not.toContain("[Agentic Commands]"); + }); + it("should include tracker-id marker when GH_AW_TRACKER_ID is set", async () => { process.env.GH_AW_TRACKER_ID = "my-tracker"; const { buildCommentBody } = await importAddWorkflowRunComment(); diff --git a/actions/setup/js/route_slash_command.cjs b/actions/setup/js/route_slash_command.cjs index 3113b670e47..563c26df5d1 100644 --- a/actions/setup/js/route_slash_command.cjs +++ b/actions/setup/js/route_slash_command.cjs @@ -295,16 +295,29 @@ async function addImmediateStatusComment() { } } +/** + * @param {Array} values + * @returns {string} + */ +function firstNonEmptyString(values) { + for (const value of values) { + if (typeof value === "string" && value.trim()) { + return value; + } + } + return ""; +} + /** * Dispatches a workflow with the API version header required by GitHub REST. * @param {string} workflowId * @param {string} ref * @param {Record} inputs - * @returns {Promise} + * @returns {Promise<{ dispatched: boolean, run_id?: number, run_url?: string }>} */ async function dispatchWorkflow(workflowId, ref, inputs) { try { - await github.rest.actions.createWorkflowDispatch({ + const dispatchArgs = { owner: context.repo.owner, repo: context.repo.repo, workflow_id: workflowId, @@ -313,17 +326,89 @@ async function dispatchWorkflow(workflowId, ref, inputs) { headers: { "X-GitHub-Api-Version": GITHUB_API_VERSION, }, - }); - return true; + }; + + /** @type {{ data?: any }} */ + let response; + try { + response = await github.request("POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches", { + ...dispatchArgs, + return_run_details: true, + }); + } catch (dispatchError) { + /** @type {any} */ + const err = dispatchError; + const status = err && typeof err === "object" ? err.status : undefined; + const message = typeof err?.response?.data?.message === "string" ? err.response.data.message : String(dispatchError); + const isValidationStatus = status === 400 || status === 422; + const mentionsReturnRunDetails = typeof message === "string" && message.toLowerCase().includes("return_run_details"); + if (!(isValidationStatus && mentionsReturnRunDetails)) { + throw err; + } + core.info("Workflow dispatch endpoint rejected 'return_run_details' (common on older GHES versions); retrying without it."); + response = await github.request("POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches", dispatchArgs); + } + + const responseData = response?.data || {}; + // GitHub may return run metadata in either shape: + // - flat: { workflow_run_id, workflow_run_url } + // - nested: { workflow_run: { id, html_url, url } } + const parsedRunId = Number(responseData?.workflow_run_id ?? responseData?.workflow_run?.id); + const runId = Number.isFinite(parsedRunId) && parsedRunId > 0 ? parsedRunId : undefined; + const runUrlFromResponse = firstNonEmptyString([responseData?.workflow_run_url, responseData?.workflow_run?.html_url, responseData?.workflow_run?.url]); + const serverUrl = context.serverUrl || process.env.GITHUB_SERVER_URL || "https://github.com"; + const runUrlFromRunId = runId ? `${serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}` : undefined; + const runUrl = runUrlFromResponse || runUrlFromRunId; + return { + dispatched: true, + ...(runId ? { run_id: runId } : {}), + ...(runUrl ? { run_url: runUrl } : {}), + }; } catch (error) { if (isDisabledWorkflowDispatchError(error)) { core.info(`Skipping workflow '${workflowId}' because it is disabled.`); - return false; + return { dispatched: false }; } throw new Error(`Failed to dispatch workflow '${workflowId}' on ref '${ref}': ${String(error)}`); } } +/** + * Update the shared status comment with dispatched workflow run metadata. + * @param {{ status_comment_id: string, status_comment_url?: string, status_comment_repo?: string }} statusCommentContext + * @param {string} eventName + * @param {string} workflowId + * @param {string|undefined} runUrl + */ +async function updateStatusCommentWithDispatch(statusCommentContext, eventName, workflowId, runUrl) { + if (!statusCommentContext?.status_comment_id) { + return; + } + if (!runUrl) { + return; + } + try { + await createOrReuseStatusComment({ + ...context, + eventName: "workflow_dispatch", + payload: { + inputs: { + event_name: eventName, + event_payload: JSON.stringify(context.payload || {}), + aw_context: JSON.stringify({ + ...statusCommentContext, + dispatched_workflow_name: workflowId.replace(/\.lock\.yml$/, ""), + dispatched_run_url: runUrl, + }), + }, + }, + nonFatalStatusCommentErrors: true, + }); + } catch (error) { + core.warning(`Failed to update immediate status comment with dispatched run details: ${String(error)}`); + } +} + function isBuiltinHelpEnabled() { const raw = (process.env.GH_AW_HELP_COMMAND_ENABLED || "").trim().toLowerCase(); if (!raw || raw === "true") { @@ -602,7 +687,7 @@ async function main() { const dispatched = await dispatchWorkflow(workflowID, ref, { aw_context: JSON.stringify(awContext), }); - if (dispatched) { + if (dispatched.dispatched) { core.info(`Dispatched '${workflowID}' for label '${labelName}'`); } } @@ -669,8 +754,11 @@ async function main() { const dispatched = await dispatchWorkflow(workflowID, ref, { aw_context: JSON.stringify(awContext), }); - if (dispatched) { + if (dispatched.dispatched) { core.info(`Dispatched '${workflowID}' for '/${commandName}'`); + if (maintainsStatusComment(route) && statusCommentContext) { + await updateStatusCommentWithDispatch(statusCommentContext, identifier, workflowID, dispatched.run_url); + } } } core.info(`Completed centralized routing for '/${commandName}'.`); diff --git a/actions/setup/js/route_slash_command.test.cjs b/actions/setup/js/route_slash_command.test.cjs index b0c13ac54ed..8c5bc04103d 100644 --- a/actions/setup/js/route_slash_command.test.cjs +++ b/actions/setup/js/route_slash_command.test.cjs @@ -98,8 +98,17 @@ describe("route_slash_command", () => { summary: summaryMock, }; globals.github = { - request: vi.fn(async (...args) => { - reactionCalls.push(args); + request: vi.fn(async (route, params) => { + if (String(route).includes("/dispatches")) { + dispatchCalls.push(params); + return { + data: { + workflow_run_id: 123456, + workflow_run_url: "https://github.com/github/gh-aw/actions/runs/123456", + }, + }; + } + reactionCalls.push([route, params]); return { data: { id: 1 } }; }), graphql: vi.fn(async () => ({ repository: { discussion: { id: "D_node" } }, addReaction: { reaction: { id: "R_1" } } })), @@ -114,9 +123,7 @@ describe("route_slash_command", () => { ], }, })), - createWorkflowDispatch: vi.fn(async params => { - dispatchCalls.push(params); - }), + createWorkflowDispatch: vi.fn(), }, pulls: { get: vi.fn(async ({ pull_number }) => ({ @@ -216,6 +223,15 @@ describe("route_slash_command", () => { }, }; } + if (String(route).includes("/dispatches")) { + dispatchCalls.push(params); + return { + data: { + workflow_run_id: 444, + workflow_run_url: "https://github.com/github/gh-aw/actions/runs/444", + }, + }; + } reactionCalls.push([route, params]); return { data: { id: 1 } }; }); @@ -230,6 +246,9 @@ describe("route_slash_command", () => { expect(JSON.parse(dispatchCalls[1].inputs.aw_context).status_comment_id).toBe("999"); expect(globals.github.request.mock.calls.filter(([route]) => String(route).includes("/reactions"))).toHaveLength(1); expect(globals.github.request.mock.calls.filter(([route]) => /\/issues\/77\/comments$/.test(String(route)))).toHaveLength(1); + const statusUpdateCalls = globals.github.request.mock.calls.filter(([route]) => String(route).startsWith("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}")); + expect(statusUpdateCalls.length).toBeGreaterThan(0); + expect(statusUpdateCalls[0][1].body).toContain("[archie](https://github.com/github/gh-aw/actions/runs/444)"); expect(globals.github.request).toHaveBeenCalledWith( expect.stringContaining("/issues/77/comments"), expect.objectContaining({ @@ -260,10 +279,14 @@ describe("route_slash_command", () => { }); globals.context.payload.issue.number = 77; globals.context.payload.comment.body = "/archie please"; - globals.github.request = vi.fn(async route => { + globals.github.request = vi.fn(async (route, params) => { if (String(route).includes("/comments")) { throw new Error("comment API down"); } + if (String(route).includes("/dispatches")) { + dispatchCalls.push(params); + return { data: {} }; + } reactionCalls.push([route]); return { data: { id: 1 } }; }); @@ -628,7 +651,7 @@ describe("route_slash_command", () => { }); it("skips slash routes when target workflow is disabled", async () => { - globals.github.rest.actions.createWorkflowDispatch = vi.fn(async () => { + globals.github.request = vi.fn(async () => { throw Object.assign(new Error("Workflow was disabled"), { status: 422, response: { status: 422, data: { message: "Workflow was disabled" } }, @@ -644,7 +667,7 @@ describe("route_slash_command", () => { }); it("skips label routes when target workflow is disabled", async () => { - globals.github.rest.actions.createWorkflowDispatch = vi.fn(async () => { + globals.github.request = vi.fn(async () => { throw Object.assign(new Error("Workflow is disabled"), { status: 422, response: { status: 422, data: { message: "Workflow is disabled" } }, @@ -668,7 +691,10 @@ describe("route_slash_command", () => { }); it("ignores disabled workflow_dispatch failures for disabled label routes", async () => { - globals.github.rest.actions.createWorkflowDispatch = vi.fn(async params => { + globals.github.request = vi.fn(async (route, params) => { + if (!String(route).includes("/dispatches")) { + return { data: { id: 1 } }; + } if (params.workflow_id === "smoke-otel-backends.lock.yml") { throw Object.assign(new Error("Cannot trigger a 'workflow_dispatch' on a disabled workflow"), { status: 422, @@ -676,6 +702,7 @@ describe("route_slash_command", () => { }); } dispatchCalls.push(params); + return { data: {} }; }); globals.context.eventName = "pull_request"; globals.context.payload = { From 2df417ebc597a1f9188bb76a834970aab373c6fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Jun 2026 14:52:36 +0000 Subject: [PATCH 2/2] Fix runUrl to prefer constructed HTML URL and avoid REST API URLs Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/route_slash_command.cjs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/route_slash_command.cjs b/actions/setup/js/route_slash_command.cjs index 563c26df5d1..61712ddba4f 100644 --- a/actions/setup/js/route_slash_command.cjs +++ b/actions/setup/js/route_slash_command.cjs @@ -351,14 +351,16 @@ async function dispatchWorkflow(workflowId, ref, inputs) { const responseData = response?.data || {}; // GitHub may return run metadata in either shape: - // - flat: { workflow_run_id, workflow_run_url } - // - nested: { workflow_run: { id, html_url, url } } + // - flat: { workflow_run_id, workflow_run_url } (workflow_run_url is a REST API URL) + // - nested: { workflow_run: { id, html_url, url } } (url is a REST API URL; html_url is the Actions page URL) const parsedRunId = Number(responseData?.workflow_run_id ?? responseData?.workflow_run?.id); const runId = Number.isFinite(parsedRunId) && parsedRunId > 0 ? parsedRunId : undefined; - const runUrlFromResponse = firstNonEmptyString([responseData?.workflow_run_url, responseData?.workflow_run?.html_url, responseData?.workflow_run?.url]); const serverUrl = context.serverUrl || process.env.GITHUB_SERVER_URL || "https://github.com"; + // Prefer the constructed HTML URL when runId is available — it is always the Actions run page URL. + // Avoid workflow_run_url and workflow_run.url which are REST API URLs, not Actions run page URLs. const runUrlFromRunId = runId ? `${serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}` : undefined; - const runUrl = runUrlFromResponse || runUrlFromRunId; + const runUrlFromResponse = firstNonEmptyString([responseData?.workflow_run?.html_url]); + const runUrl = runUrlFromRunId || runUrlFromResponse; return { dispatched: true, ...(runId ? { run_id: runId } : {}),