diff --git a/.github/workflows/aw-failure-investigator.lock.yml b/.github/workflows/aw-failure-investigator.lock.yml index c5036d1a0a7..fc56fb023dc 100644 --- a/.github/workflows/aw-failure-investigator.lock.yml +++ b/.github/workflows/aw-failure-investigator.lock.yml @@ -1131,6 +1131,7 @@ jobs: DISABLE_BUG_COMMAND: 1 DISABLE_ERROR_REPORTING: 1 DISABLE_TELEMETRY: 1 + GH_AW_LLM_PROVIDER: anthropic GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_MCP_CONFIG: ${{ runner.temp }}/gh-aw/mcp-config/mcp-servers.json GH_AW_MODEL_AGENT_CLAUDE: ${{ vars.GH_AW_MODEL_AGENT_CLAUDE || vars.GH_AW_DEFAULT_MODEL_CLAUDE || '' }} @@ -1743,7 +1744,7 @@ jobs: fi # shellcheck disable=SC1003,SC2086 sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env ANTHROPIC_API_KEY --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; threat-detect --engine claude --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && threat-detect --engine claude --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} BASH_DEFAULT_TIMEOUT_MS: 60000 @@ -1752,6 +1753,7 @@ jobs: DISABLE_BUG_COMMAND: 1 DISABLE_ERROR_REPORTING: 1 DISABLE_TELEMETRY: 1 + GH_AW_LLM_PROVIDER: anthropic GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_MODEL_DETECTION_CLAUDE: ${{ vars.GH_AW_MODEL_DETECTION_CLAUDE || vars.GH_AW_DEFAULT_MODEL_CLAUDE || '' }} GH_AW_PHASE: detection diff --git a/actions/setup/js/add_workflow_run_comment.cjs b/actions/setup/js/add_workflow_run_comment.cjs index 1c23decc644..eb57234965d 100644 --- a/actions/setup/js/add_workflow_run_comment.cjs +++ b/actions/setup/js/add_workflow_run_comment.cjs @@ -171,6 +171,54 @@ function reportCommentError(rawContext, message) { core.setFailed(message); } +/** + * @param {ReusableStatusComment} reusableComment + * @param {{ + * source: "native" | "workflow_dispatch" | "repository_dispatch"; + * eventName: string; + * eventPayload: any; + * workflowRepo: { owner: string, repo: string }; + * eventRepo: { owner: string, repo: string }; + * }} invocationContext + * @param {any} rawContext + * @returns {Promise} + */ +async function updateReusableStatusComment(reusableComment, invocationContext, rawContext) { + const runUrl = buildWorkflowRunUrl(rawContext, invocationContext.workflowRepo); + const commentBody = buildCommentBody(invocationContext.eventName, runUrl); + + // Discussion comments use GraphQL node IDs and a dedicated update mutation. + if (reusableComment.id.startsWith("DC_")) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { id url } + } + }`, + { commentId: reusableComment.id, body: commentBody } + ); + const updatedUrl = result?.updateDiscussionComment?.comment?.url; + return typeof updatedUrl === "string" && updatedUrl.trim() ? updatedUrl : reusableComment.url; + } + + const commentRepo = reusableComment.repo || invocationContext.eventRepo; + const numericCommentId = Number(reusableComment.id); + if (!Number.isInteger(numericCommentId) || numericCommentId <= 0) { + throw new Error(`${ERR_VALIDATION}: Reusable status comment ID must be a positive integer (received "${reusableComment.id}")`); + } + + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: commentRepo.owner, + repo: commentRepo.repo, + comment_id: numericCommentId, + body: commentBody, + headers: { Accept: "application/vnd.github+json" }, + }); + const updatedUrl = response?.data?.html_url; + return typeof updatedUrl === "string" && updatedUrl.trim() ? updatedUrl : reusableComment.url; +} + /** * Add a comment with a workflow run link to the triggering item. * This script ONLY creates comments - it does NOT add reactions. @@ -190,7 +238,17 @@ async function createOrReuseStatusComment(rawContext = context) { if (!reusableComment.repo) { core.warning("Reusable status comment repo missing; falling back to the invocation event repo."); } - const outputs = setCommentOutputs(reusableComment.id, reusableComment.url, reusableComment.repo || invocationContext.eventRepo, { logReuse: true }); + let reusableCommentUrl = reusableComment.url; + try { + reusableCommentUrl = await updateReusableStatusComment(reusableComment, invocationContext, rawContext); + core.info("Updated reusable status comment with current workflow run metadata"); + } catch (error) { + core.warning(`Failed to update reusable status comment body: ${getErrorMessage(error)}`); + if (!reusableCommentUrl) { + core.warning("No fallback reusable status comment URL available; comment-url output will be empty."); + } + } + const outputs = setCommentOutputs(reusableComment.id, reusableCommentUrl, reusableComment.repo || invocationContext.eventRepo, { logReuse: true }); return { ...outputs, reused: true, diff --git a/actions/setup/js/add_workflow_run_comment.test.cjs b/actions/setup/js/add_workflow_run_comment.test.cjs index 8c6ce2db7b7..50b52854f83 100644 --- a/actions/setup/js/add_workflow_run_comment.test.cjs +++ b/actions/setup/js/add_workflow_run_comment.test.cjs @@ -211,6 +211,12 @@ describe("add_workflow_run_comment", () => { }); it("reuses an existing status comment from client_payload aw_context", async () => { + mockGithub.request.mockResolvedValueOnce({ + data: { + id: 67890, + html_url: "https://github.com/statusowner/statusrepo/issues/789#issuecomment-67890", + }, + }); global.context = { eventName: "repository_dispatch", runId: 12345, @@ -233,9 +239,17 @@ describe("add_workflow_run_comment", () => { await runScript(); - expect(mockGithub.request).not.toHaveBeenCalled(); + expect(mockGithub.request).toHaveBeenCalledWith( + "PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", + expect.objectContaining({ + owner: "statusowner", + repo: "statusrepo", + comment_id: 67890, + body: expect.stringContaining("https://github.com/workflowowner/workflowrepo/actions/runs/12345"), + }) + ); expect(mockCore.setOutput).toHaveBeenCalledWith("comment-id", "67890"); - expect(mockCore.setOutput).toHaveBeenCalledWith("comment-url", "https://github.com/targetowner/targetrepo/issues/789#issuecomment-67890"); + expect(mockCore.setOutput).toHaveBeenCalledWith("comment-url", "https://github.com/statusowner/statusrepo/issues/789#issuecomment-67890"); expect(mockCore.setOutput).toHaveBeenCalledWith("comment-repo", "statusowner/statusrepo"); expect(mockCore.info).toHaveBeenCalledWith("Reusing existing status comment outputs"); }); @@ -243,6 +257,12 @@ describe("add_workflow_run_comment", () => { describe("main() - workflow_dispatch aw_context reuse", () => { it("reuses an existing status comment from aw_context", async () => { + mockGithub.request.mockResolvedValueOnce({ + data: { + id: 67890, + html_url: "https://github.com/targetowner/targetrepo/issues/789#issuecomment-67890", + }, + }); global.context = { eventName: "workflow_dispatch", runId: 12345, @@ -263,7 +283,15 @@ describe("add_workflow_run_comment", () => { await runScript(); - expect(mockGithub.request).not.toHaveBeenCalled(); + expect(mockGithub.request).toHaveBeenCalledWith( + "PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", + expect.objectContaining({ + owner: "targetowner", + repo: "targetrepo", + comment_id: 67890, + body: expect.stringContaining("https://github.com/workflowowner/workflowrepo/actions/runs/12345"), + }) + ); expect(mockCore.setOutput).toHaveBeenCalledWith("comment-id", "67890"); expect(mockCore.setOutput).toHaveBeenCalledWith("comment-url", "https://github.com/targetowner/targetrepo/issues/789#issuecomment-67890"); expect(mockCore.setOutput).toHaveBeenCalledWith("comment-repo", "targetowner/targetrepo"); @@ -271,6 +299,12 @@ describe("add_workflow_run_comment", () => { }); it("reuses an existing status comment from camelCase awContext", async () => { + mockGithub.request.mockResolvedValueOnce({ + data: { + id: 67890, + html_url: "https://github.com/statusowner/statusrepo/issues/789#issuecomment-67890", + }, + }); global.context = { eventName: "workflow_dispatch", runId: 12345, @@ -292,8 +326,113 @@ describe("add_workflow_run_comment", () => { await runScript(); - expect(mockGithub.request).not.toHaveBeenCalled(); + expect(mockGithub.request).toHaveBeenCalledWith( + "PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", + expect.objectContaining({ + owner: "statusowner", + repo: "statusrepo", + comment_id: 67890, + body: expect.stringContaining("https://github.com/workflowowner/workflowrepo/actions/runs/12345"), + }) + ); + expect(mockCore.setOutput).toHaveBeenCalledWith("comment-id", "67890"); + expect(mockCore.setOutput).toHaveBeenCalledWith("comment-url", "https://github.com/statusowner/statusrepo/issues/789#issuecomment-67890"); expect(mockCore.setOutput).toHaveBeenCalledWith("comment-repo", "statusowner/statusrepo"); + expect(mockCore.info).toHaveBeenCalledWith("Reusing existing status comment outputs"); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + }); + + it("updates reusable discussion comments via GraphQL", async () => { + mockGithub.graphql.mockResolvedValue({ + updateDiscussionComment: { + comment: { + id: "DC_kwDOReusable123", + url: "https://github.com/targetowner/targetrepo/discussions/789#discussioncomment-67890", + }, + }, + }); + global.context = { + eventName: "workflow_dispatch", + runId: 12345, + repo: { owner: "workflowowner", repo: "workflowrepo" }, + payload: { + inputs: { + aw_context: JSON.stringify({ + repo: "targetowner/targetrepo", + event_type: "discussion_comment", + item_type: "discussion", + item_number: 789, + status_comment_id: "DC_kwDOReusable123", + }), + }, + }, + }; + + await runScript(); + + expect(mockGithub.graphql).toHaveBeenCalledWith( + expect.stringContaining("updateDiscussionComment"), + expect.objectContaining({ + commentId: "DC_kwDOReusable123", + body: expect.stringContaining("https://github.com/workflowowner/workflowrepo/actions/runs/12345"), + }) + ); + expect(mockGithub.request).not.toHaveBeenCalled(); + expect(mockCore.setOutput).toHaveBeenCalledWith("comment-id", "DC_kwDOReusable123"); + expect(mockCore.setOutput).toHaveBeenCalledWith("comment-url", "https://github.com/targetowner/targetrepo/discussions/789#discussioncomment-67890"); + }); + + it("warns and falls back to stale URL when reusable issue-comment update fails", async () => { + mockGithub.request.mockRejectedValueOnce(new Error("network timeout")); + 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", + }), + }, + }, + }; + + await runScript(); + + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Failed to update reusable status comment body")); + expect(mockCore.setOutput).toHaveBeenCalledWith("comment-url", "https://github.com/targetowner/targetrepo/issues/789#issuecomment-67890"); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + }); + + it("warns when reusable comment ID is not a positive integer", 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: "67890abc", + status_comment_url: "https://github.com/targetowner/targetrepo/issues/789#issuecomment-67890", + }), + }, + }, + }; + + await runScript(); + + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining(`${ERR_VALIDATION}: Reusable status comment ID must be a positive integer (received "67890abc")`)); + expect(mockCore.setOutput).toHaveBeenCalledWith("comment-url", "https://github.com/targetowner/targetrepo/issues/789#issuecomment-67890"); + expect(mockCore.setFailed).not.toHaveBeenCalled(); }); });