diff --git a/.changeset/patch-add-noop-safe-output.md b/.changeset/patch-add-noop-safe-output.md new file mode 100644 index 00000000000..54c4bfaa170 --- /dev/null +++ b/.changeset/patch-add-noop-safe-output.md @@ -0,0 +1,7 @@ +--- +"gh-aw": patch +--- + +Add noop safe output for transparent workflow completion + +Agents need to emit human-visible artifacts even when no actions are required (e.g., "No issues found"). The noop safe output provides a fallback mechanism ensuring workflows never complete silently. diff --git a/.github/workflows/ai-triage-campaign.lock.yml b/.github/workflows/ai-triage-campaign.lock.yml index ba1809363d3..fb2007ed150 100644 --- a/.github/workflows/ai-triage-campaign.lock.yml +++ b/.github/workflows/ai-triage-campaign.lock.yml @@ -10,10 +10,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # detection["detection"] # missing_tool["missing_tool"] # update_project["update_project"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# missing_tool --> conclusion +# update_project --> conclusion # agent --> detection # agent --> missing_tool # detection --> missing_tool @@ -254,7 +259,7 @@ jobs: {"missing_tool":{},"update_project":{"max":20}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1692,6 +1697,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2240,6 +2247,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3541,6 +3555,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - missing_tool + - update_project + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "AI Triage Campaign" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + detection: needs: agent if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' diff --git a/.github/workflows/archie.lock.yml b/.github/workflows/archie.lock.yml index bdf7e821337..358f1589394 100644 --- a/.github/workflows/archie.lock.yml +++ b/.github/workflows/archie.lock.yml @@ -1289,7 +1289,7 @@ jobs: {"add_comment":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2767,6 +2767,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -3315,6 +3317,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4622,9 +4631,7 @@ jobs: - activation - add_comment - missing_tool - if: > - (((always()) && (needs.agent.result != 'skipped')) && (needs.activation.outputs.comment_id)) && - (!(needs.add_comment.outputs.comment_id)) + if: ((always()) && (needs.agent.result != 'skipped')) && (!(needs.add_comment.outputs.comment_id)) runs-on: ubuntu-slim permissions: contents: read @@ -4667,6 +4674,40 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } async function main() { const commentId = process.env.GH_AW_COMMENT_ID; const commentRepo = process.env.GH_AW_COMMENT_REPO; @@ -4678,8 +4719,30 @@ jobs: core.info(`Run URL: ${runUrl}`); core.info(`Workflow Name: ${workflowName}`); core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } if (!commentId) { - core.info("No comment ID found, skipping comment update"); + core.info("No comment ID found and no noop messages to process, skipping comment update"); return; } if (!runUrl) { @@ -4710,6 +4773,14 @@ jobs: } else { message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } const isDiscussionComment = commentId.startsWith("DC_"); try { if (isDiscussionComment) { diff --git a/.github/workflows/artifacts-summary.lock.yml b/.github/workflows/artifacts-summary.lock.yml index 3f836e09b4c..93a17235243 100644 --- a/.github/workflows/artifacts-summary.lock.yml +++ b/.github/workflows/artifacts-summary.lock.yml @@ -14,10 +14,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -259,7 +264,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1638,6 +1643,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2186,6 +2193,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3854,6 +3868,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Artifacts Summary" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/audit-workflows.lock.yml b/.github/workflows/audit-workflows.lock.yml index 8863ac2de4d..1a00d1386c2 100644 --- a/.github/workflows/audit-workflows.lock.yml +++ b/.github/workflows/audit-workflows.lock.yml @@ -18,11 +18,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # upload_assets["upload_assets"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion +# upload_assets --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -440,7 +446,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{},"upload_asset":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2680,6 +2686,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -3228,6 +3236,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4103,6 +4118,205 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + - upload_assets + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Agentic Workflow Audit Agent" + GH_AW_CAMPAIGN: "audit-workflows-daily" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/blog-auditor.lock.yml b/.github/workflows/blog-auditor.lock.yml index 2c6ec0da8eb..57017c61f19 100644 --- a/.github/workflows/blog-auditor.lock.yml +++ b/.github/workflows/blog-auditor.lock.yml @@ -14,10 +14,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -362,7 +367,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2026,6 +2031,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2574,6 +2581,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3442,6 +3456,204 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Blog Auditor" + GH_AW_CAMPAIGN: "blog-auditor-weekly" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/brave.lock.yml b/.github/workflows/brave.lock.yml index b1be5defd50..c6313b643e8 100644 --- a/.github/workflows/brave.lock.yml +++ b/.github/workflows/brave.lock.yml @@ -1252,7 +1252,7 @@ jobs: {"add_comment":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2614,6 +2614,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -3162,6 +3164,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4469,9 +4478,7 @@ jobs: - activation - add_comment - missing_tool - if: > - (((always()) && (needs.agent.result != 'skipped')) && (needs.activation.outputs.comment_id)) && - (!(needs.add_comment.outputs.comment_id)) + if: ((always()) && (needs.agent.result != 'skipped')) && (!(needs.add_comment.outputs.comment_id)) runs-on: ubuntu-slim permissions: contents: read @@ -4514,6 +4521,40 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } async function main() { const commentId = process.env.GH_AW_COMMENT_ID; const commentRepo = process.env.GH_AW_COMMENT_REPO; @@ -4525,8 +4566,30 @@ jobs: core.info(`Run URL: ${runUrl}`); core.info(`Workflow Name: ${workflowName}`); core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } if (!commentId) { - core.info("No comment ID found, skipping comment update"); + core.info("No comment ID found and no noop messages to process, skipping comment update"); return; } if (!runUrl) { @@ -4557,6 +4620,14 @@ jobs: } else { message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } const isDiscussionComment = commentId.startsWith("DC_"); try { if (isDiscussionComment) { diff --git a/.github/workflows/changeset.lock.yml b/.github/workflows/changeset.lock.yml index 024bff837f0..cfa4ae2ba83 100644 --- a/.github/workflows/changeset.lock.yml +++ b/.github/workflows/changeset.lock.yml @@ -15,12 +15,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # detection["detection"] # missing_tool["missing_tool"] # pre_activation["pre_activation"] # push_to_pull_request_branch["push_to_pull_request_branch"] # pre_activation --> activation # activation --> agent +# agent --> conclusion +# activation --> conclusion +# push_to_pull_request_branch --> conclusion +# missing_tool --> conclusion # agent --> detection # agent --> missing_tool # detection --> missing_tool @@ -860,7 +865,7 @@ jobs: {"missing_tool":{},"push_to_pull_request_branch":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Push changes to a pull request branch","inputSchema":{"additionalProperties":false,"properties":{"branch":{"description":"Optional branch name. Do not provide this parameter if you want to push changes from the current branch. If not provided, the current branch will be used.","type":"string"},"message":{"description":"Commit message","type":"string"},"pull_request_number":{"description":"Optional pull request number for target '*'","type":["number","string"]}},"required":["message"],"type":"object"},"name":"push_to_pull_request_branch"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Push changes to a pull request branch","inputSchema":{"additionalProperties":false,"properties":{"branch":{"description":"Optional branch name. Do not provide this parameter if you want to push changes from the current branch. If not provided, the current branch will be used.","type":"string"},"message":{"description":"Commit message","type":"string"},"pull_request_number":{"description":"Optional pull request number for target '*'","type":["number","string"]}},"required":["message"],"type":"object"},"name":"push_to_pull_request_branch"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2306,6 +2311,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2854,6 +2861,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4653,6 +4667,203 @@ jobs: path: /tmp/gh-aw/aw.patch if-no-files-found: ignore + conclusion: + needs: + - agent + - activation + - push_to_pull_request_branch + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Changeset Generator" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + detection: needs: agent if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml index e72328edd78..8f9f1baf344 100644 --- a/.github/workflows/ci-doctor.lock.yml +++ b/.github/workflows/ci-doctor.lock.yml @@ -15,6 +15,7 @@ # activation["activation"] # add_comment["add_comment"] # agent["agent"] +# conclusion["conclusion"] # create_issue["create_issue"] # detection["detection"] # missing_tool["missing_tool"] @@ -24,6 +25,11 @@ # create_issue --> add_comment # detection --> add_comment # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_issue --> conclusion +# add_comment --> conclusion +# missing_tool --> conclusion # agent --> create_issue # detection --> create_issue # agent --> detection @@ -710,7 +716,7 @@ jobs: {"add_comment":{"max":1},"create_issue":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2122,6 +2128,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2670,6 +2678,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3971,6 +3986,204 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_issue + - add_comment + - missing_tool + if: ((always()) && (needs.agent.result != 'skipped')) && (!(needs.add_comment.outputs.comment_id)) + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "CI Failure Doctor" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_issue: needs: - agent diff --git a/.github/workflows/cli-consistency-checker.lock.yml b/.github/workflows/cli-consistency-checker.lock.yml index 9e9c18b9beb..29c745e66cf 100644 --- a/.github/workflows/cli-consistency-checker.lock.yml +++ b/.github/workflows/cli-consistency-checker.lock.yml @@ -10,10 +10,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_issue["create_issue"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_issue --> conclusion +# missing_tool --> conclusion # agent --> create_issue # detection --> create_issue # agent --> detection @@ -260,7 +265,7 @@ jobs: {"create_issue":{"max":5},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1676,6 +1681,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2224,6 +2231,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3892,6 +3906,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_issue + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "CLI Consistency Checker" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_issue: needs: - agent diff --git a/.github/workflows/cli-version-checker.lock.yml b/.github/workflows/cli-version-checker.lock.yml index fc17cd3ba4e..69bdfb766bb 100644 --- a/.github/workflows/cli-version-checker.lock.yml +++ b/.github/workflows/cli-version-checker.lock.yml @@ -14,10 +14,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_issue["create_issue"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_issue --> conclusion +# missing_tool --> conclusion # agent --> create_issue # detection --> create_issue # agent --> detection @@ -287,7 +292,7 @@ jobs: {"create_issue":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1852,6 +1857,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2400,6 +2407,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4068,6 +4082,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_issue + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "CLI Version Checker" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_issue: needs: - agent diff --git a/.github/workflows/cloclo.lock.yml b/.github/workflows/cloclo.lock.yml index e3229960d35..c1c4fbc58e0 100644 --- a/.github/workflows/cloclo.lock.yml +++ b/.github/workflows/cloclo.lock.yml @@ -1464,7 +1464,7 @@ jobs: {"add_comment":{"max":1},"create_pull_request":{},"missing_tool":{},"push_to_pull_request_branch":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Push changes to a pull request branch","inputSchema":{"additionalProperties":false,"properties":{"branch":{"description":"Optional branch name. Do not provide this parameter if you want to push changes from the current branch. If not provided, the current branch will be used.","type":"string"},"message":{"description":"Commit message","type":"string"},"pull_request_number":{"description":"Optional pull request number for target '*'","type":["number","string"]}},"required":["message"],"type":"object"},"name":"push_to_pull_request_branch"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Push changes to a pull request branch","inputSchema":{"additionalProperties":false,"properties":{"branch":{"description":"Optional branch name. Do not provide this parameter if you want to push changes from the current branch. If not provided, the current branch will be used.","type":"string"},"message":{"description":"Commit message","type":"string"},"pull_request_number":{"description":"Optional pull request number for target '*'","type":["number","string"]}},"required":["message"],"type":"object"},"name":"push_to_pull_request_branch"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -3161,6 +3161,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -3709,6 +3711,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4716,9 +4725,7 @@ jobs: - add_comment - push_to_pull_request_branch - missing_tool - if: > - (((always()) && (needs.agent.result != 'skipped')) && (needs.activation.outputs.comment_id)) && - (!(needs.add_comment.outputs.comment_id)) + if: ((always()) && (needs.agent.result != 'skipped')) && (!(needs.add_comment.outputs.comment_id)) runs-on: ubuntu-slim permissions: contents: read @@ -4761,6 +4768,40 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } async function main() { const commentId = process.env.GH_AW_COMMENT_ID; const commentRepo = process.env.GH_AW_COMMENT_REPO; @@ -4772,8 +4813,30 @@ jobs: core.info(`Run URL: ${runUrl}`); core.info(`Workflow Name: ${workflowName}`); core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } if (!commentId) { - core.info("No comment ID found, skipping comment update"); + core.info("No comment ID found and no noop messages to process, skipping comment update"); return; } if (!runUrl) { @@ -4804,6 +4867,14 @@ jobs: } else { message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } const isDiscussionComment = commentId.startsWith("DC_"); try { if (isDiscussionComment) { diff --git a/.github/workflows/commit-changes-analyzer.lock.yml b/.github/workflows/commit-changes-analyzer.lock.yml index 015e0c8e7f3..77ea44f4a7f 100644 --- a/.github/workflows/commit-changes-analyzer.lock.yml +++ b/.github/workflows/commit-changes-analyzer.lock.yml @@ -14,10 +14,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -365,7 +370,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1957,6 +1962,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2505,6 +2512,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3373,6 +3387,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Commit Changes Analyzer" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/copilot-agent-analysis.lock.yml b/.github/workflows/copilot-agent-analysis.lock.yml index 848e041a534..4363400d312 100644 --- a/.github/workflows/copilot-agent-analysis.lock.yml +++ b/.github/workflows/copilot-agent-analysis.lock.yml @@ -16,10 +16,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -397,7 +402,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2320,6 +2325,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2868,6 +2875,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3736,6 +3750,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Copilot Agent PR Analysis" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/copilot-pr-nlp-analysis.lock.yml b/.github/workflows/copilot-pr-nlp-analysis.lock.yml index fb1b5a47b36..67654f2fccb 100644 --- a/.github/workflows/copilot-pr-nlp-analysis.lock.yml +++ b/.github/workflows/copilot-pr-nlp-analysis.lock.yml @@ -17,10 +17,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -326,7 +331,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2413,6 +2418,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2961,6 +2968,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4629,6 +4643,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Copilot PR Conversation NLP Analysis" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/copilot-pr-prompt-analysis.lock.yml b/.github/workflows/copilot-pr-prompt-analysis.lock.yml index 8c51ddce952..642fbdff941 100644 --- a/.github/workflows/copilot-pr-prompt-analysis.lock.yml +++ b/.github/workflows/copilot-pr-prompt-analysis.lock.yml @@ -16,10 +16,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -296,7 +301,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1979,6 +1984,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2527,6 +2534,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4195,6 +4209,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Copilot PR Prompt Pattern Analysis" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/copilot-session-insights.lock.yml b/.github/workflows/copilot-session-insights.lock.yml index 5643dc8005b..f5cf5781e60 100644 --- a/.github/workflows/copilot-session-insights.lock.yml +++ b/.github/workflows/copilot-session-insights.lock.yml @@ -17,11 +17,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # upload_assets["upload_assets"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion +# upload_assets --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -436,7 +442,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{},"upload_asset":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -3229,6 +3235,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -3777,6 +3785,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4652,6 +4667,204 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + - upload_assets + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Copilot Session Insights" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/craft.lock.yml b/.github/workflows/craft.lock.yml index 3a643301b56..ae09dc5179c 100644 --- a/.github/workflows/craft.lock.yml +++ b/.github/workflows/craft.lock.yml @@ -1257,7 +1257,7 @@ jobs: {"add_comment":{"max":1},"missing_tool":{},"push_to_pull_request_branch":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Push changes to a pull request branch","inputSchema":{"additionalProperties":false,"properties":{"branch":{"description":"Optional branch name. Do not provide this parameter if you want to push changes from the current branch. If not provided, the current branch will be used.","type":"string"},"message":{"description":"Commit message","type":"string"},"pull_request_number":{"description":"Optional pull request number for target '*'","type":["number","string"]}},"required":["message"],"type":"object"},"name":"push_to_pull_request_branch"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Push changes to a pull request branch","inputSchema":{"additionalProperties":false,"properties":{"branch":{"description":"Optional branch name. Do not provide this parameter if you want to push changes from the current branch. If not provided, the current branch will be used.","type":"string"},"message":{"description":"Commit message","type":"string"},"pull_request_number":{"description":"Optional pull request number for target '*'","type":["number","string"]}},"required":["message"],"type":"object"},"name":"push_to_pull_request_branch"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2768,6 +2768,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -3316,6 +3318,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4755,9 +4764,7 @@ jobs: - add_comment - push_to_pull_request_branch - missing_tool - if: > - (((always()) && (needs.agent.result != 'skipped')) && (needs.activation.outputs.comment_id)) && - (!(needs.add_comment.outputs.comment_id)) + if: ((always()) && (needs.agent.result != 'skipped')) && (!(needs.add_comment.outputs.comment_id)) runs-on: ubuntu-slim permissions: contents: read @@ -4800,6 +4807,40 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } async function main() { const commentId = process.env.GH_AW_COMMENT_ID; const commentRepo = process.env.GH_AW_COMMENT_REPO; @@ -4811,8 +4852,30 @@ jobs: core.info(`Run URL: ${runUrl}`); core.info(`Workflow Name: ${workflowName}`); core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } if (!commentId) { - core.info("No comment ID found, skipping comment update"); + core.info("No comment ID found and no noop messages to process, skipping comment update"); return; } if (!runUrl) { @@ -4843,6 +4906,14 @@ jobs: } else { message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } const isDiscussionComment = commentId.startsWith("DC_"); try { if (isDiscussionComment) { diff --git a/.github/workflows/daily-code-metrics.lock.yml b/.github/workflows/daily-code-metrics.lock.yml index ebd5115ed62..297ac8da665 100644 --- a/.github/workflows/daily-code-metrics.lock.yml +++ b/.github/workflows/daily-code-metrics.lock.yml @@ -14,10 +14,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -382,7 +387,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2300,6 +2305,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2848,6 +2855,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3716,6 +3730,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Daily Code Metrics and Trend Tracking Agent" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/daily-doc-updater.lock.yml b/.github/workflows/daily-doc-updater.lock.yml index 97e40c7ab26..77ff3eee302 100644 --- a/.github/workflows/daily-doc-updater.lock.yml +++ b/.github/workflows/daily-doc-updater.lock.yml @@ -10,10 +10,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_pull_request["create_pull_request"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_pull_request --> conclusion +# missing_tool --> conclusion # agent --> create_pull_request # activation --> create_pull_request # detection --> create_pull_request @@ -381,7 +386,7 @@ jobs: {"create_pull_request":{},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1886,6 +1891,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2434,6 +2441,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3433,6 +3447,203 @@ jobs: path: /tmp/gh-aw/aw.patch if-no-files-found: ignore + conclusion: + needs: + - agent + - activation + - create_pull_request + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Daily Documentation Updater" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_pull_request: needs: - agent diff --git a/.github/workflows/daily-file-diet.lock.yml b/.github/workflows/daily-file-diet.lock.yml index 17f898be63e..fc5287fc509 100644 --- a/.github/workflows/daily-file-diet.lock.yml +++ b/.github/workflows/daily-file-diet.lock.yml @@ -15,10 +15,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_issue["create_issue"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_issue --> conclusion +# missing_tool --> conclusion # agent --> create_issue # detection --> create_issue # agent --> detection @@ -278,7 +283,7 @@ jobs: {"create_issue":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1787,6 +1792,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2335,6 +2342,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3041,6 +3055,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_issue + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Daily File Diet" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_issue: needs: - agent diff --git a/.github/workflows/daily-firewall-report.lock.yml b/.github/workflows/daily-firewall-report.lock.yml index e1af7529479..0b4f1772409 100644 --- a/.github/workflows/daily-firewall-report.lock.yml +++ b/.github/workflows/daily-firewall-report.lock.yml @@ -16,11 +16,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # upload_assets["upload_assets"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion +# upload_assets --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -323,7 +329,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{},"upload_asset":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2396,6 +2402,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2944,6 +2952,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4619,6 +4634,204 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + - upload_assets + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Daily Firewall Logs Collector and Reporter" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/daily-multi-device-docs-tester.lock.yml b/.github/workflows/daily-multi-device-docs-tester.lock.yml index 525eef7b914..8acc45314c9 100644 --- a/.github/workflows/daily-multi-device-docs-tester.lock.yml +++ b/.github/workflows/daily-multi-device-docs-tester.lock.yml @@ -10,11 +10,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_issue["create_issue"] # detection["detection"] # missing_tool["missing_tool"] # upload_assets["upload_assets"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_issue --> conclusion +# missing_tool --> conclusion +# upload_assets --> conclusion # agent --> create_issue # detection --> create_issue # agent --> detection @@ -369,7 +375,7 @@ jobs: {"create_issue":{"max":1},"missing_tool":{},"upload_asset":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1810,6 +1816,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2358,6 +2366,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3233,6 +3248,204 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_issue + - missing_tool + - upload_assets + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Multi-Device Docs Tester" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_issue: needs: - agent diff --git a/.github/workflows/daily-news.lock.yml b/.github/workflows/daily-news.lock.yml index b28b4030dcd..59c90eb602e 100644 --- a/.github/workflows/daily-news.lock.yml +++ b/.github/workflows/daily-news.lock.yml @@ -18,11 +18,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # upload_assets["upload_assets"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion +# upload_assets --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -329,7 +335,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{},"upload_asset":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2405,6 +2411,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2953,6 +2961,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4628,6 +4643,205 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + - upload_assets + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Daily News" + GH_AW_CAMPAIGN: "daily-news-weekday" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/daily-repo-chronicle.lock.yml b/.github/workflows/daily-repo-chronicle.lock.yml index 8ae783b991e..9045443e9d0 100644 --- a/.github/workflows/daily-repo-chronicle.lock.yml +++ b/.github/workflows/daily-repo-chronicle.lock.yml @@ -16,11 +16,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # upload_assets["upload_assets"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion +# upload_assets --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -316,7 +322,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{},"upload_asset":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2249,6 +2255,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2797,6 +2805,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4472,6 +4487,205 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + - upload_assets + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "The Daily Repository Chronicle" + GH_AW_CAMPAIGN: "daily-repo-chronicle" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/daily-team-status.lock.yml b/.github/workflows/daily-team-status.lock.yml index ac25dde138c..fdc23127820 100644 --- a/.github/workflows/daily-team-status.lock.yml +++ b/.github/workflows/daily-team-status.lock.yml @@ -22,12 +22,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # pre_activation["pre_activation"] # pre_activation --> activation # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -265,7 +270,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1569,6 +1574,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2117,6 +2124,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3418,6 +3432,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Daily Team Status" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/dependabot-go-checker.lock.yml b/.github/workflows/dependabot-go-checker.lock.yml index eed1aea9b54..433bb047879 100644 --- a/.github/workflows/dependabot-go-checker.lock.yml +++ b/.github/workflows/dependabot-go-checker.lock.yml @@ -10,10 +10,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_issue["create_issue"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_issue --> conclusion +# missing_tool --> conclusion # agent --> create_issue # detection --> create_issue # agent --> detection @@ -254,7 +259,7 @@ jobs: {"create_issue":{"max":5},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1710,6 +1715,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2258,6 +2265,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3559,6 +3573,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_issue + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Dependabot Dependency Checker" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_issue: needs: - agent diff --git a/.github/workflows/dev-hawk.lock.yml b/.github/workflows/dev-hawk.lock.yml index acb2afa3148..adc9bc0cca9 100644 --- a/.github/workflows/dev-hawk.lock.yml +++ b/.github/workflows/dev-hawk.lock.yml @@ -11,6 +11,7 @@ # activation["activation"] # add_comment["add_comment"] # agent["agent"] +# conclusion["conclusion"] # detection["detection"] # missing_tool["missing_tool"] # pre_activation["pre_activation"] @@ -18,6 +19,10 @@ # agent --> add_comment # detection --> add_comment # activation --> agent +# agent --> conclusion +# activation --> conclusion +# add_comment --> conclusion +# missing_tool --> conclusion # agent --> detection # agent --> missing_tool # detection --> missing_tool @@ -678,7 +683,7 @@ jobs: {"add_comment":{"max":1,"target":"*"},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2013,6 +2018,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2561,6 +2568,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3862,6 +3876,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - add_comment + - missing_tool + if: ((always()) && (needs.agent.result != 'skipped')) && (!(needs.add_comment.outputs.comment_id)) + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Dev Hawk" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + detection: needs: agent if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index bad5f3c1184..f74f3868160 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -10,10 +10,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # detection["detection"] # missing_tool["missing_tool"] # push_to_pull_request_branch["push_to_pull_request_branch"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# push_to_pull_request_branch --> conclusion +# missing_tool --> conclusion # agent --> detection # agent --> missing_tool # detection --> missing_tool @@ -249,7 +254,7 @@ jobs: {"missing_tool":{},"push_to_pull_request_branch":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Push changes to a pull request branch","inputSchema":{"additionalProperties":false,"properties":{"branch":{"description":"Optional branch name. Do not provide this parameter if you want to push changes from the current branch. If not provided, the current branch will be used.","type":"string"},"message":{"description":"Commit message","type":"string"},"pull_request_number":{"description":"Optional pull request number for target '*'","type":["number","string"]}},"required":["message"],"type":"object"},"name":"push_to_pull_request_branch"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Push changes to a pull request branch","inputSchema":{"additionalProperties":false,"properties":{"branch":{"description":"Optional branch name. Do not provide this parameter if you want to push changes from the current branch. If not provided, the current branch will be used.","type":"string"},"message":{"description":"Commit message","type":"string"},"pull_request_number":{"description":"Optional pull request number for target '*'","type":["number","string"]}},"required":["message"],"type":"object"},"name":"push_to_pull_request_branch"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1535,6 +1540,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2083,6 +2090,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3515,6 +3529,203 @@ jobs: path: /tmp/gh-aw/aw.patch if-no-files-found: ignore + conclusion: + needs: + - agent + - activation + - push_to_pull_request_branch + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Dev" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + detection: needs: agent if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' diff --git a/.github/workflows/developer-docs-consolidator.lock.yml b/.github/workflows/developer-docs-consolidator.lock.yml index 0a76d475f1c..fffbdb67c45 100644 --- a/.github/workflows/developer-docs-consolidator.lock.yml +++ b/.github/workflows/developer-docs-consolidator.lock.yml @@ -15,11 +15,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # create_pull_request["create_pull_request"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# create_pull_request --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> create_pull_request @@ -416,7 +422,7 @@ jobs: {"create_discussion":{"max":1},"create_pull_request":{},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2437,6 +2443,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2985,6 +2993,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3984,6 +3999,204 @@ jobs: path: /tmp/gh-aw/aw.patch if-no-files-found: ignore + conclusion: + needs: + - agent + - activation + - create_discussion + - create_pull_request + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Developer Documentation Consolidator" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/dictation-prompt.lock.yml b/.github/workflows/dictation-prompt.lock.yml index ea82821a415..fe37b29c951 100644 --- a/.github/workflows/dictation-prompt.lock.yml +++ b/.github/workflows/dictation-prompt.lock.yml @@ -14,10 +14,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_pull_request["create_pull_request"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_pull_request --> conclusion +# missing_tool --> conclusion # agent --> create_pull_request # activation --> create_pull_request # detection --> create_pull_request @@ -254,7 +259,7 @@ jobs: {"create_pull_request":{},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1633,6 +1638,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2181,6 +2188,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3613,6 +3627,203 @@ jobs: path: /tmp/gh-aw/aw.patch if-no-files-found: ignore + conclusion: + needs: + - agent + - activation + - create_pull_request + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Dictation Prompt Generator" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_pull_request: needs: - agent diff --git a/.github/workflows/docs-noob-tester.lock.yml b/.github/workflows/docs-noob-tester.lock.yml index 0d1aab8478c..cdcc8af9252 100644 --- a/.github/workflows/docs-noob-tester.lock.yml +++ b/.github/workflows/docs-noob-tester.lock.yml @@ -10,11 +10,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # upload_assets["upload_assets"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion +# upload_assets --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -263,7 +269,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{},"upload_asset":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1689,6 +1695,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2237,6 +2245,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3912,6 +3927,204 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + - upload_assets + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Documentation Noob Tester" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/duplicate-code-detector.lock.yml b/.github/workflows/duplicate-code-detector.lock.yml index 472891a9bd8..e5d9383194b 100644 --- a/.github/workflows/duplicate-code-detector.lock.yml +++ b/.github/workflows/duplicate-code-detector.lock.yml @@ -14,10 +14,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_issue["create_issue"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_issue --> conclusion +# missing_tool --> conclusion # agent --> create_issue # detection --> create_issue # agent --> detection @@ -277,7 +282,7 @@ jobs: {"create_issue":{"max":3},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1707,6 +1712,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2255,6 +2262,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -2961,6 +2975,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_issue + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Duplicate Code Detector" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_issue: needs: - agent diff --git a/.github/workflows/example-workflow-analyzer.lock.yml b/.github/workflows/example-workflow-analyzer.lock.yml index a7cd65313c4..30b880953df 100644 --- a/.github/workflows/example-workflow-analyzer.lock.yml +++ b/.github/workflows/example-workflow-analyzer.lock.yml @@ -14,10 +14,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -371,7 +376,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1739,6 +1744,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2287,6 +2294,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3155,6 +3169,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Weekly Workflow Analysis" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/github-mcp-tools-report.lock.yml b/.github/workflows/github-mcp-tools-report.lock.yml index 85aef0b4ad7..53a6fb51c4e 100644 --- a/.github/workflows/github-mcp-tools-report.lock.yml +++ b/.github/workflows/github-mcp-tools-report.lock.yml @@ -14,11 +14,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # create_pull_request["create_pull_request"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# create_pull_request --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> create_pull_request @@ -392,7 +398,7 @@ jobs: {"create_discussion":{"max":1},"create_pull_request":{},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2259,6 +2265,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2807,6 +2815,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3806,6 +3821,204 @@ jobs: path: /tmp/gh-aw/aw.patch if-no-files-found: ignore + conclusion: + needs: + - agent + - activation + - create_discussion + - create_pull_request + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "GitHub MCP Remote Server Tools Report Generator" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/go-logger.lock.yml b/.github/workflows/go-logger.lock.yml index cf86adedf1f..455898e5f88 100644 --- a/.github/workflows/go-logger.lock.yml +++ b/.github/workflows/go-logger.lock.yml @@ -10,10 +10,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_pull_request["create_pull_request"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_pull_request --> conclusion +# missing_tool --> conclusion # agent --> create_pull_request # activation --> create_pull_request # detection --> create_pull_request @@ -398,7 +403,7 @@ jobs: {"create_pull_request":{},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2005,6 +2010,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2553,6 +2560,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3552,6 +3566,203 @@ jobs: path: /tmp/gh-aw/aw.patch if-no-files-found: ignore + conclusion: + needs: + - agent + - activation + - create_pull_request + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Go Logger Enhancement" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_pull_request: needs: - agent diff --git a/.github/workflows/go-pattern-detector.lock.yml b/.github/workflows/go-pattern-detector.lock.yml index 2a4aa7f79c7..34540140343 100644 --- a/.github/workflows/go-pattern-detector.lock.yml +++ b/.github/workflows/go-pattern-detector.lock.yml @@ -14,12 +14,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_issue["create_issue"] # detection["detection"] # missing_tool["missing_tool"] # pre_activation["pre_activation"] # pre_activation --> activation # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_issue --> conclusion +# missing_tool --> conclusion # agent --> create_issue # detection --> create_issue # agent --> detection @@ -368,7 +373,7 @@ jobs: {"create_issue":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1779,6 +1784,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2327,6 +2334,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3195,6 +3209,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_issue + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Go Pattern Detector" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_issue: needs: - agent diff --git a/.github/workflows/grumpy-reviewer.lock.yml b/.github/workflows/grumpy-reviewer.lock.yml index d3fcf66dded..b020d7eecbb 100644 --- a/.github/workflows/grumpy-reviewer.lock.yml +++ b/.github/workflows/grumpy-reviewer.lock.yml @@ -1276,7 +1276,7 @@ jobs: {"add_comment":{"max":1},"create_pull_request_review_comment":{"max":5},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Create a review comment on a GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body content","type":"string"},"line":{"description":"Line number for the comment","type":["number","string"]},"path":{"description":"File path for the review comment","type":"string"},"side":{"description":"Optional side of the diff: LEFT or RIGHT","enum":["LEFT","RIGHT"],"type":"string"},"start_line":{"description":"Optional start line for multi-line comments","type":["number","string"]}},"required":["path","line","body"],"type":"object"},"name":"create_pull_request_review_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Create a review comment on a GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body content","type":"string"},"line":{"description":"Line number for the comment","type":["number","string"]},"path":{"description":"File path for the review comment","type":"string"},"side":{"description":"Optional side of the diff: LEFT or RIGHT","enum":["LEFT","RIGHT"],"type":"string"},"start_line":{"description":"Optional start line for multi-line comments","type":["number","string"]}},"required":["path","line","body"],"type":"object"},"name":"create_pull_request_review_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2669,6 +2669,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -3217,6 +3219,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4525,9 +4534,7 @@ jobs: - add_comment - create_pr_review_comment - missing_tool - if: > - (((always()) && (needs.agent.result != 'skipped')) && (needs.activation.outputs.comment_id)) && - (!(needs.add_comment.outputs.comment_id)) + if: ((always()) && (needs.agent.result != 'skipped')) && (!(needs.add_comment.outputs.comment_id)) runs-on: ubuntu-slim permissions: contents: read @@ -4570,6 +4577,40 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } async function main() { const commentId = process.env.GH_AW_COMMENT_ID; const commentRepo = process.env.GH_AW_COMMENT_REPO; @@ -4581,8 +4622,30 @@ jobs: core.info(`Run URL: ${runUrl}`); core.info(`Workflow Name: ${workflowName}`); core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } if (!commentId) { - core.info("No comment ID found, skipping comment update"); + core.info("No comment ID found and no noop messages to process, skipping comment update"); return; } if (!runUrl) { @@ -4613,6 +4676,14 @@ jobs: } else { message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } const isDiscussionComment = commentId.startsWith("DC_"); try { if (isDiscussionComment) { diff --git a/.github/workflows/instructions-janitor.lock.yml b/.github/workflows/instructions-janitor.lock.yml index 569713115ed..c26bdd9c5fa 100644 --- a/.github/workflows/instructions-janitor.lock.yml +++ b/.github/workflows/instructions-janitor.lock.yml @@ -10,10 +10,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_pull_request["create_pull_request"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_pull_request --> conclusion +# missing_tool --> conclusion # agent --> create_pull_request # activation --> create_pull_request # detection --> create_pull_request @@ -381,7 +386,7 @@ jobs: {"create_pull_request":{},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1884,6 +1889,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2432,6 +2439,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3431,6 +3445,203 @@ jobs: path: /tmp/gh-aw/aw.patch if-no-files-found: ignore + conclusion: + needs: + - agent + - activation + - create_pull_request + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Instructions Janitor" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_pull_request: needs: - agent diff --git a/.github/workflows/issue-classifier.lock.yml b/.github/workflows/issue-classifier.lock.yml index e7c8e829e24..b426cffe135 100644 --- a/.github/workflows/issue-classifier.lock.yml +++ b/.github/workflows/issue-classifier.lock.yml @@ -15,6 +15,7 @@ # activation["activation"] # add_labels["add_labels"] # agent["agent"] +# conclusion["conclusion"] # detection["detection"] # missing_tool["missing_tool"] # pre_activation["pre_activation"] @@ -22,6 +23,10 @@ # agent --> add_labels # detection --> add_labels # activation --> agent +# agent --> conclusion +# activation --> conclusion +# add_labels --> conclusion +# missing_tool --> conclusion # agent --> detection # agent --> missing_tool # detection --> missing_tool @@ -1085,7 +1090,7 @@ jobs: {"add_labels":{"allowed":["bug","feature","enhancement","documentation"],"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Add labels to a GitHub issue or pull request","inputSchema":{"additionalProperties":false,"properties":{"item_number":{"description":"Issue or PR number (optional for current context)","type":"number"},"labels":{"description":"Labels to add","items":{"type":"string"},"type":"array"}},"required":["labels"],"type":"object"},"name":"add_labels"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Add labels to a GitHub issue or pull request","inputSchema":{"additionalProperties":false,"properties":{"item_number":{"description":"Issue or PR number (optional for current context)","type":"number"},"labels":{"description":"Labels to add","items":{"type":"string"},"type":"array"}},"required":["labels"],"type":"object"},"name":"add_labels"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2311,6 +2316,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2859,6 +2866,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3001,6 +3015,203 @@ jobs: path: /tmp/gh-aw/agent-stdio.log if-no-files-found: warn + conclusion: + needs: + - agent + - activation + - add_labels + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Issue Classifier" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + detection: needs: agent if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' diff --git a/.github/workflows/lockfile-stats.lock.yml b/.github/workflows/lockfile-stats.lock.yml index 97d2e0b5998..f6dd4b33a80 100644 --- a/.github/workflows/lockfile-stats.lock.yml +++ b/.github/workflows/lockfile-stats.lock.yml @@ -14,10 +14,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -384,7 +389,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2094,6 +2099,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2642,6 +2649,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3510,6 +3524,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Lockfile Statistics Analysis Agent" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index 5d5d7e30e93..606fcc7c28f 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -30,12 +30,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # notion_add_comment["notion_add_comment"] # post_to_slack_channel["post_to_slack_channel"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -351,7 +356,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{},"notion-add-comment":{"description":"Add a comment to a Notion page","inputs":{"comment":{"description":"The comment text to add","required":true,"type":"string"}},"output":"Comment added to Notion successfully!"},"post-to-slack-channel":{"description":"Post a message to a Slack channel. Message must be 200 characters or less. Supports basic Slack markdown: *bold*, _italic_, ~strike~, `code`, ```code block```, \u003equote, and links \u003curl|text\u003e. Requires GH_AW_SLACK_CHANNEL_ID environment variable to be set.","inputs":{"message":{"description":"The message to post (max 200 characters, supports Slack markdown)","required":true,"type":"string"}},"output":"Message posted to Slack successfully!"}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2211,6 +2216,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2759,6 +2766,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4427,6 +4441,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "MCP Inspector Agent" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/mergefest.lock.yml b/.github/workflows/mergefest.lock.yml index d6b8d0d1220..cb1b8d92e33 100644 --- a/.github/workflows/mergefest.lock.yml +++ b/.github/workflows/mergefest.lock.yml @@ -602,7 +602,7 @@ jobs: {"missing_tool":{},"push_to_pull_request_branch":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Push changes to a pull request branch","inputSchema":{"additionalProperties":false,"properties":{"branch":{"description":"Optional branch name. Do not provide this parameter if you want to push changes from the current branch. If not provided, the current branch will be used.","type":"string"},"message":{"description":"Commit message","type":"string"},"pull_request_number":{"description":"Optional pull request number for target '*'","type":["number","string"]}},"required":["message"],"type":"object"},"name":"push_to_pull_request_branch"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Push changes to a pull request branch","inputSchema":{"additionalProperties":false,"properties":{"branch":{"description":"Optional branch name. Do not provide this parameter if you want to push changes from the current branch. If not provided, the current branch will be used.","type":"string"},"message":{"description":"Commit message","type":"string"},"pull_request_number":{"description":"Optional pull request number for target '*'","type":["number","string"]}},"required":["message"],"type":"object"},"name":"push_to_pull_request_branch"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2192,6 +2192,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2740,6 +2742,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4178,7 +4187,7 @@ jobs: - activation - push_to_pull_request_branch - missing_tool - if: ((always()) && (needs.agent.result != 'skipped')) && (needs.activation.outputs.comment_id) + if: (always()) && (needs.agent.result != 'skipped') runs-on: ubuntu-slim permissions: contents: read @@ -4221,6 +4230,40 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } async function main() { const commentId = process.env.GH_AW_COMMENT_ID; const commentRepo = process.env.GH_AW_COMMENT_REPO; @@ -4232,8 +4275,30 @@ jobs: core.info(`Run URL: ${runUrl}`); core.info(`Workflow Name: ${workflowName}`); core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } if (!commentId) { - core.info("No comment ID found, skipping comment update"); + core.info("No comment ID found and no noop messages to process, skipping comment update"); return; } if (!runUrl) { @@ -4264,6 +4329,14 @@ jobs: } else { message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } const isDiscussionComment = commentId.startsWith("DC_"); try { if (isDiscussionComment) { diff --git a/.github/workflows/notion-issue-summary.lock.yml b/.github/workflows/notion-issue-summary.lock.yml index aec3ec5d290..72cef6c7e94 100644 --- a/.github/workflows/notion-issue-summary.lock.yml +++ b/.github/workflows/notion-issue-summary.lock.yml @@ -14,8 +14,11 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # notion_add_comment["notion_add_comment"] # activation --> agent +# agent --> conclusion +# activation --> conclusion # agent --> notion_add_comment # ``` # @@ -1492,6 +1495,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2040,6 +2045,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3341,6 +3353,201 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Issue Summary to Notion" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + notion_add_comment: needs: agent if: > diff --git a/.github/workflows/pdf-summary.lock.yml b/.github/workflows/pdf-summary.lock.yml index 1fc24f4dd47..8e48325e51e 100644 --- a/.github/workflows/pdf-summary.lock.yml +++ b/.github/workflows/pdf-summary.lock.yml @@ -1300,7 +1300,7 @@ jobs: {"add_comment":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2720,6 +2720,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -3268,6 +3270,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4575,9 +4584,7 @@ jobs: - activation - add_comment - missing_tool - if: > - (((always()) && (needs.agent.result != 'skipped')) && (needs.activation.outputs.comment_id)) && - (!(needs.add_comment.outputs.comment_id)) + if: ((always()) && (needs.agent.result != 'skipped')) && (!(needs.add_comment.outputs.comment_id)) runs-on: ubuntu-slim permissions: contents: read @@ -4620,6 +4627,40 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } async function main() { const commentId = process.env.GH_AW_COMMENT_ID; const commentRepo = process.env.GH_AW_COMMENT_REPO; @@ -4631,8 +4672,30 @@ jobs: core.info(`Run URL: ${runUrl}`); core.info(`Workflow Name: ${workflowName}`); core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } if (!commentId) { - core.info("No comment ID found, skipping comment update"); + core.info("No comment ID found and no noop messages to process, skipping comment update"); return; } if (!runUrl) { @@ -4663,6 +4726,14 @@ jobs: } else { message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } const isDiscussionComment = commentId.startsWith("DC_"); try { if (isDiscussionComment) { diff --git a/.github/workflows/plan.lock.yml b/.github/workflows/plan.lock.yml index f9107840025..c250a48803a 100644 --- a/.github/workflows/plan.lock.yml +++ b/.github/workflows/plan.lock.yml @@ -844,7 +844,7 @@ jobs: {"create_issue":{"max":5},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2195,6 +2195,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2743,6 +2745,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4050,7 +4059,7 @@ jobs: - activation - create_issue - missing_tool - if: ((always()) && (needs.agent.result != 'skipped')) && (needs.activation.outputs.comment_id) + if: (always()) && (needs.agent.result != 'skipped') runs-on: ubuntu-slim permissions: contents: read @@ -4093,6 +4102,40 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } async function main() { const commentId = process.env.GH_AW_COMMENT_ID; const commentRepo = process.env.GH_AW_COMMENT_REPO; @@ -4104,8 +4147,30 @@ jobs: core.info(`Run URL: ${runUrl}`); core.info(`Workflow Name: ${workflowName}`); core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } if (!commentId) { - core.info("No comment ID found, skipping comment update"); + core.info("No comment ID found and no noop messages to process, skipping comment update"); return; } if (!runUrl) { @@ -4136,6 +4201,14 @@ jobs: } else { message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } const isDiscussionComment = commentId.startsWith("DC_"); try { if (isDiscussionComment) { diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index da48f96a893..c3cd2a79d85 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -1596,7 +1596,7 @@ jobs: {"add_comment":{"max":3,"target":"*"},"add_labels":{"allowed":["poetry","creative","automation","ai-generated","epic","haiku","sonnet","limerick"],"max":5},"create_issue":{"max":2},"create_pull_request":{},"create_pull_request_review_comment":{"max":2},"missing_tool":{},"push_to_pull_request_branch":{},"update_issue":{"max":2},"upload_asset":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Create a review comment on a GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body content","type":"string"},"line":{"description":"Line number for the comment","type":["number","string"]},"path":{"description":"File path for the review comment","type":"string"},"side":{"description":"Optional side of the diff: LEFT or RIGHT","enum":["LEFT","RIGHT"],"type":"string"},"start_line":{"description":"Optional start line for multi-line comments","type":["number","string"]}},"required":["path","line","body"],"type":"object"},"name":"create_pull_request_review_comment"},{"description":"Add labels to a GitHub issue or pull request","inputSchema":{"additionalProperties":false,"properties":{"item_number":{"description":"Issue or PR number (optional for current context)","type":"number"},"labels":{"description":"Labels to add","items":{"type":"string"},"type":"array"}},"required":["labels"],"type":"object"},"name":"add_labels"},{"description":"Update a GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Optional new issue body","type":"string"},"issue_number":{"description":"Optional issue number for target '*'","type":["number","string"]},"status":{"description":"Optional new issue status","enum":["open","closed"],"type":"string"},"title":{"description":"Optional new issue title","type":"string"}},"type":"object"},"name":"update_issue"},{"description":"Push changes to a pull request branch","inputSchema":{"additionalProperties":false,"properties":{"branch":{"description":"Optional branch name. Do not provide this parameter if you want to push changes from the current branch. If not provided, the current branch will be used.","type":"string"},"message":{"description":"Commit message","type":"string"},"pull_request_number":{"description":"Optional pull request number for target '*'","type":["number","string"]}},"required":["message"],"type":"object"},"name":"push_to_pull_request_branch"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Create a review comment on a GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body content","type":"string"},"line":{"description":"Line number for the comment","type":["number","string"]},"path":{"description":"File path for the review comment","type":"string"},"side":{"description":"Optional side of the diff: LEFT or RIGHT","enum":["LEFT","RIGHT"],"type":"string"},"start_line":{"description":"Optional start line for multi-line comments","type":["number","string"]}},"required":["path","line","body"],"type":"object"},"name":"create_pull_request_review_comment"},{"description":"Add labels to a GitHub issue or pull request","inputSchema":{"additionalProperties":false,"properties":{"item_number":{"description":"Issue or PR number (optional for current context)","type":"number"},"labels":{"description":"Labels to add","items":{"type":"string"},"type":"array"}},"required":["labels"],"type":"object"},"name":"add_labels"},{"description":"Update a GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Optional new issue body","type":"string"},"issue_number":{"description":"Optional issue number for target '*'","type":["number","string"]},"status":{"description":"Optional new issue status","enum":["open","closed"],"type":"string"},"title":{"description":"Optional new issue title","type":"string"}},"type":"object"},"name":"update_issue"},{"description":"Push changes to a pull request branch","inputSchema":{"additionalProperties":false,"properties":{"branch":{"description":"Optional branch name. Do not provide this parameter if you want to push changes from the current branch. If not provided, the current branch will be used.","type":"string"},"message":{"description":"Commit message","type":"string"},"pull_request_number":{"description":"Optional pull request number for target '*'","type":["number","string"]}},"required":["message"],"type":"object"},"name":"push_to_pull_request_branch"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2991,6 +2991,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -3539,6 +3541,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4991,9 +5000,7 @@ jobs: - push_to_pull_request_branch - missing_tool - upload_assets - if: > - (((always()) && (needs.agent.result != 'skipped')) && (needs.activation.outputs.comment_id)) && - (!(needs.add_comment.outputs.comment_id)) + if: ((always()) && (needs.agent.result != 'skipped')) && (!(needs.add_comment.outputs.comment_id)) runs-on: ubuntu-slim permissions: contents: read @@ -5036,6 +5043,40 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } async function main() { const commentId = process.env.GH_AW_COMMENT_ID; const commentRepo = process.env.GH_AW_COMMENT_REPO; @@ -5047,8 +5088,30 @@ jobs: core.info(`Run URL: ${runUrl}`); core.info(`Workflow Name: ${workflowName}`); core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } if (!commentId) { - core.info("No comment ID found, skipping comment update"); + core.info("No comment ID found and no noop messages to process, skipping comment update"); return; } if (!runUrl) { @@ -5079,6 +5142,14 @@ jobs: } else { message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } const isDiscussionComment = commentId.startsWith("DC_"); try { if (isDiscussionComment) { diff --git a/.github/workflows/pr-nitpick-reviewer.lock.yml b/.github/workflows/pr-nitpick-reviewer.lock.yml index 7c48c3d59f2..4a68eb9087b 100644 --- a/.github/workflows/pr-nitpick-reviewer.lock.yml +++ b/.github/workflows/pr-nitpick-reviewer.lock.yml @@ -1073,7 +1073,7 @@ jobs: {"add_comment":{"max":3},"create_discussion":{"max":1},"create_pull_request_review_comment":{"max":10},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Create a review comment on a GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body content","type":"string"},"line":{"description":"Line number for the comment","type":["number","string"]},"path":{"description":"File path for the review comment","type":"string"},"side":{"description":"Optional side of the diff: LEFT or RIGHT","enum":["LEFT","RIGHT"],"type":"string"},"start_line":{"description":"Optional start line for multi-line comments","type":["number","string"]}},"required":["path","line","body"],"type":"object"},"name":"create_pull_request_review_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Create a review comment on a GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body content","type":"string"},"line":{"description":"Line number for the comment","type":["number","string"]},"path":{"description":"File path for the review comment","type":"string"},"side":{"description":"Optional side of the diff: LEFT or RIGHT","enum":["LEFT","RIGHT"],"type":"string"},"start_line":{"description":"Optional start line for multi-line comments","type":["number","string"]}},"required":["path","line","body"],"type":"object"},"name":"create_pull_request_review_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2772,6 +2772,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -3320,6 +3322,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4629,9 +4638,7 @@ jobs: - add_comment - create_pr_review_comment - missing_tool - if: > - (((always()) && (needs.agent.result != 'skipped')) && (needs.activation.outputs.comment_id)) && - (!(needs.add_comment.outputs.comment_id)) + if: ((always()) && (needs.agent.result != 'skipped')) && (!(needs.add_comment.outputs.comment_id)) runs-on: ubuntu-slim permissions: contents: read @@ -4674,6 +4681,40 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } async function main() { const commentId = process.env.GH_AW_COMMENT_ID; const commentRepo = process.env.GH_AW_COMMENT_REPO; @@ -4685,8 +4726,30 @@ jobs: core.info(`Run URL: ${runUrl}`); core.info(`Workflow Name: ${workflowName}`); core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } if (!commentId) { - core.info("No comment ID found, skipping comment update"); + core.info("No comment ID found and no noop messages to process, skipping comment update"); return; } if (!runUrl) { @@ -4717,6 +4780,14 @@ jobs: } else { message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } const isDiscussionComment = commentId.startsWith("DC_"); try { if (isDiscussionComment) { diff --git a/.github/workflows/prompt-clustering-analysis.lock.yml b/.github/workflows/prompt-clustering-analysis.lock.yml index 74da3f37ad6..b6a8ebd029d 100644 --- a/.github/workflows/prompt-clustering-analysis.lock.yml +++ b/.github/workflows/prompt-clustering-analysis.lock.yml @@ -17,10 +17,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -435,7 +440,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2434,6 +2439,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2982,6 +2989,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3850,6 +3864,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Copilot Agent Prompt Clustering Analysis" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/python-data-charts.lock.yml b/.github/workflows/python-data-charts.lock.yml index adabbfe4691..4e02dbc8304 100644 --- a/.github/workflows/python-data-charts.lock.yml +++ b/.github/workflows/python-data-charts.lock.yml @@ -16,11 +16,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # upload_assets["upload_assets"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion +# upload_assets --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -321,7 +327,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{},"upload_asset":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2569,6 +2575,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -3117,6 +3125,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4792,6 +4807,204 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + - upload_assets + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Python Data Visualization Generator" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/q.lock.yml b/.github/workflows/q.lock.yml index 14ce079a586..9cde7c57f94 100644 --- a/.github/workflows/q.lock.yml +++ b/.github/workflows/q.lock.yml @@ -1346,7 +1346,7 @@ jobs: {"add_comment":{"max":1},"create_pull_request":{},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -3061,6 +3061,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -3609,6 +3611,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -5048,9 +5057,7 @@ jobs: - create_pull_request - add_comment - missing_tool - if: > - (((always()) && (needs.agent.result != 'skipped')) && (needs.activation.outputs.comment_id)) && - (!(needs.add_comment.outputs.comment_id)) + if: ((always()) && (needs.agent.result != 'skipped')) && (!(needs.add_comment.outputs.comment_id)) runs-on: ubuntu-slim permissions: contents: read @@ -5093,6 +5100,40 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } async function main() { const commentId = process.env.GH_AW_COMMENT_ID; const commentRepo = process.env.GH_AW_COMMENT_REPO; @@ -5104,8 +5145,30 @@ jobs: core.info(`Run URL: ${runUrl}`); core.info(`Workflow Name: ${workflowName}`); core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } if (!commentId) { - core.info("No comment ID found, skipping comment update"); + core.info("No comment ID found and no noop messages to process, skipping comment update"); return; } if (!runUrl) { @@ -5136,6 +5199,14 @@ jobs: } else { message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } const isDiscussionComment = commentId.startsWith("DC_"); try { if (isDiscussionComment) { diff --git a/.github/workflows/repo-tree-map.lock.yml b/.github/workflows/repo-tree-map.lock.yml index 6bb78d6a392..455760874e8 100644 --- a/.github/workflows/repo-tree-map.lock.yml +++ b/.github/workflows/repo-tree-map.lock.yml @@ -14,10 +14,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -253,7 +258,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1668,6 +1673,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2216,6 +2223,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3517,6 +3531,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Repository Tree Map Generator" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/repository-quality-improver.lock.yml b/.github/workflows/repository-quality-improver.lock.yml index cec6b694129..3d7d2584f36 100644 --- a/.github/workflows/repository-quality-improver.lock.yml +++ b/.github/workflows/repository-quality-improver.lock.yml @@ -15,10 +15,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -300,7 +305,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2186,6 +2191,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2734,6 +2741,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4035,6 +4049,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Repository Quality Improvement Agent" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/research.lock.yml b/.github/workflows/research.lock.yml index 4bd8e511c9f..3d28210c3ae 100644 --- a/.github/workflows/research.lock.yml +++ b/.github/workflows/research.lock.yml @@ -15,10 +15,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -265,7 +270,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1602,6 +1607,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2150,6 +2157,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3818,6 +3832,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Basic Research Agent" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/safe-output-health.lock.yml b/.github/workflows/safe-output-health.lock.yml index 5e8ec8eea59..67bb6cd291e 100644 --- a/.github/workflows/safe-output-health.lock.yml +++ b/.github/workflows/safe-output-health.lock.yml @@ -16,10 +16,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -410,7 +415,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2227,6 +2232,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2775,6 +2782,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3643,6 +3657,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Safe Output Health Monitor" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/schema-consistency-checker.lock.yml b/.github/workflows/schema-consistency-checker.lock.yml index f3c8a01ec21..4a90ea40b34 100644 --- a/.github/workflows/schema-consistency-checker.lock.yml +++ b/.github/workflows/schema-consistency-checker.lock.yml @@ -14,10 +14,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -384,7 +389,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2100,6 +2105,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2648,6 +2655,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3516,6 +3530,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Schema Consistency Checker" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/scout.lock.yml b/.github/workflows/scout.lock.yml index 598ca276fc3..fb775276c02 100644 --- a/.github/workflows/scout.lock.yml +++ b/.github/workflows/scout.lock.yml @@ -1439,7 +1439,7 @@ jobs: {"add_comment":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -3175,6 +3175,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -3723,6 +3725,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4597,9 +4606,7 @@ jobs: - activation - add_comment - missing_tool - if: > - (((always()) && (needs.agent.result != 'skipped')) && (needs.activation.outputs.comment_id)) && - (!(needs.add_comment.outputs.comment_id)) + if: ((always()) && (needs.agent.result != 'skipped')) && (!(needs.add_comment.outputs.comment_id)) runs-on: ubuntu-slim permissions: contents: read @@ -4642,6 +4649,40 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } async function main() { const commentId = process.env.GH_AW_COMMENT_ID; const commentRepo = process.env.GH_AW_COMMENT_REPO; @@ -4653,8 +4694,30 @@ jobs: core.info(`Run URL: ${runUrl}`); core.info(`Workflow Name: ${workflowName}`); core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } if (!commentId) { - core.info("No comment ID found, skipping comment update"); + core.info("No comment ID found and no noop messages to process, skipping comment update"); return; } if (!runUrl) { @@ -4685,6 +4748,14 @@ jobs: } else { message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } const isDiscussionComment = commentId.startsWith("DC_"); try { if (isDiscussionComment) { diff --git a/.github/workflows/security-fix-pr.lock.yml b/.github/workflows/security-fix-pr.lock.yml index 9dd2e5f420e..f9e4da0a605 100644 --- a/.github/workflows/security-fix-pr.lock.yml +++ b/.github/workflows/security-fix-pr.lock.yml @@ -10,10 +10,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_pull_request["create_pull_request"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_pull_request --> conclusion +# missing_tool --> conclusion # agent --> create_pull_request # activation --> create_pull_request # detection --> create_pull_request @@ -379,7 +384,7 @@ jobs: {"create_pull_request":{},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1832,6 +1837,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2380,6 +2387,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3379,6 +3393,203 @@ jobs: path: /tmp/gh-aw/aw.patch if-no-files-found: ignore + conclusion: + needs: + - agent + - activation + - create_pull_request + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Security Fix PR" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_pull_request: needs: - agent diff --git a/.github/workflows/semantic-function-refactor.lock.yml b/.github/workflows/semantic-function-refactor.lock.yml index 5a271114edd..2a99da7f1b1 100644 --- a/.github/workflows/semantic-function-refactor.lock.yml +++ b/.github/workflows/semantic-function-refactor.lock.yml @@ -15,10 +15,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_issue["create_issue"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_issue --> conclusion +# missing_tool --> conclusion # agent --> create_issue # detection --> create_issue # agent --> detection @@ -387,7 +392,7 @@ jobs: {"create_issue":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2188,6 +2193,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2736,6 +2743,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3604,6 +3618,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_issue + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Semantic Function Refactoring" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_issue: needs: - agent diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 802c4d0a450..58b8814bd58 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -14,12 +14,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_issue["create_issue"] # detection["detection"] # missing_tool["missing_tool"] # pre_activation["pre_activation"] # pre_activation --> activation # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_issue --> conclusion +# missing_tool --> conclusion # agent --> create_issue # detection --> create_issue # agent --> detection @@ -372,7 +377,7 @@ jobs: {"create_issue":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1747,6 +1752,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2295,6 +2302,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3163,6 +3177,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_issue + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Smoke Claude" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_issue: needs: - agent diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index 57c82be58e5..a7bef82a61f 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -10,12 +10,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_issue["create_issue"] # detection["detection"] # missing_tool["missing_tool"] # pre_activation["pre_activation"] # pre_activation --> activation # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_issue --> conclusion +# missing_tool --> conclusion # agent --> create_issue # detection --> create_issue # agent --> detection @@ -259,7 +264,7 @@ jobs: {"create_issue":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1449,6 +1454,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -1997,6 +2004,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -2703,6 +2717,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_issue + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Smoke Codex" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_issue: needs: - agent diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 4cdc7a4a00d..5dc37815432 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -10,12 +10,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_issue["create_issue"] # detection["detection"] # missing_tool["missing_tool"] # pre_activation["pre_activation"] # pre_activation --> activation # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_issue --> conclusion +# missing_tool --> conclusion # agent --> create_issue # detection --> create_issue # agent --> detection @@ -267,7 +272,7 @@ jobs: {"create_issue":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1519,6 +1524,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2067,6 +2074,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3735,6 +3749,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_issue + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Smoke Copilot" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_issue: needs: - agent diff --git a/.github/workflows/smoke-detector.lock.yml b/.github/workflows/smoke-detector.lock.yml index 1620a04ec07..9a25f06557b 100644 --- a/.github/workflows/smoke-detector.lock.yml +++ b/.github/workflows/smoke-detector.lock.yml @@ -1190,7 +1190,7 @@ jobs: {"add_comment":{"max":1,"target":"*"},"create_issue":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2818,6 +2818,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -3366,6 +3368,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4241,9 +4250,7 @@ jobs: - create_issue - add_comment - missing_tool - if: > - (((always()) && (needs.agent.result != 'skipped')) && (needs.activation.outputs.comment_id)) && - (!(needs.add_comment.outputs.comment_id)) + if: ((always()) && (needs.agent.result != 'skipped')) && (!(needs.add_comment.outputs.comment_id)) runs-on: ubuntu-slim permissions: contents: read @@ -4286,6 +4293,40 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } async function main() { const commentId = process.env.GH_AW_COMMENT_ID; const commentRepo = process.env.GH_AW_COMMENT_REPO; @@ -4297,8 +4338,30 @@ jobs: core.info(`Run URL: ${runUrl}`); core.info(`Workflow Name: ${workflowName}`); core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } if (!commentId) { - core.info("No comment ID found, skipping comment update"); + core.info("No comment ID found and no noop messages to process, skipping comment update"); return; } if (!runUrl) { @@ -4329,6 +4392,14 @@ jobs: } else { message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } const isDiscussionComment = commentId.startsWith("DC_"); try { if (isDiscussionComment) { diff --git a/.github/workflows/static-analysis-report.lock.yml b/.github/workflows/static-analysis-report.lock.yml index e8cbcc36a72..19cd22bce27 100644 --- a/.github/workflows/static-analysis-report.lock.yml +++ b/.github/workflows/static-analysis-report.lock.yml @@ -15,10 +15,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -403,7 +408,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2115,6 +2120,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2663,6 +2670,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3531,6 +3545,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Static Analysis Report" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/super-linter.lock.yml b/.github/workflows/super-linter.lock.yml index 668c2e67efa..c1ea9b710cf 100644 --- a/.github/workflows/super-linter.lock.yml +++ b/.github/workflows/super-linter.lock.yml @@ -14,12 +14,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_issue["create_issue"] # detection["detection"] # missing_tool["missing_tool"] # super_linter["super_linter"] # activation --> agent # super_linter --> agent +# agent --> conclusion +# activation --> conclusion +# create_issue --> conclusion +# missing_tool --> conclusion # agent --> create_issue # detection --> create_issue # agent --> detection @@ -290,7 +295,7 @@ jobs: {"create_issue":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1741,6 +1746,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2289,6 +2296,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3590,6 +3604,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_issue + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Super Linter Report" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_issue: needs: - agent diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index f1f63c65fa8..e083ff4dd5c 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -16,6 +16,7 @@ # activation["activation"] # add_comment["add_comment"] # agent["agent"] +# conclusion["conclusion"] # create_pull_request["create_pull_request"] # detection["detection"] # missing_tool["missing_tool"] @@ -24,6 +25,12 @@ # create_pull_request --> add_comment # detection --> add_comment # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_pull_request --> conclusion +# add_comment --> conclusion +# missing_tool --> conclusion +# upload_assets --> conclusion # agent --> create_pull_request # activation --> create_pull_request # detection --> create_pull_request @@ -727,7 +734,7 @@ jobs: {"add_comment":{"max":1},"create_pull_request":{},"missing_tool":{},"upload_asset":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2412,6 +2419,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2960,6 +2969,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4766,6 +4782,205 @@ jobs: path: /tmp/gh-aw/aw.patch if-no-files-found: ignore + conclusion: + needs: + - agent + - activation + - create_pull_request + - add_comment + - missing_tool + - upload_assets + if: ((always()) && (needs.agent.result != 'skipped')) && (!(needs.add_comment.outputs.comment_id)) + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Technical Doc Writer" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_pull_request: needs: - agent diff --git a/.github/workflows/test-ollama-threat-detection.lock.yml b/.github/workflows/test-ollama-threat-detection.lock.yml index b27102294b5..db91915b59e 100644 --- a/.github/workflows/test-ollama-threat-detection.lock.yml +++ b/.github/workflows/test-ollama-threat-detection.lock.yml @@ -10,10 +10,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_issue["create_issue"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_issue --> conclusion +# missing_tool --> conclusion # agent --> create_issue # detection --> create_issue # agent --> detection @@ -249,7 +254,7 @@ jobs: {"create_issue":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1464,6 +1469,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2012,6 +2019,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3313,6 +3327,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_issue + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Test Ollama Threat Scanning" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_issue: needs: - agent diff --git a/.github/workflows/tidy.lock.yml b/.github/workflows/tidy.lock.yml index b2e44f99e4b..d3ecb986f89 100644 --- a/.github/workflows/tidy.lock.yml +++ b/.github/workflows/tidy.lock.yml @@ -630,7 +630,7 @@ jobs: {"create_pull_request":{},"missing_tool":{},"push_to_pull_request_branch":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Push changes to a pull request branch","inputSchema":{"additionalProperties":false,"properties":{"branch":{"description":"Optional branch name. Do not provide this parameter if you want to push changes from the current branch. If not provided, the current branch will be used.","type":"string"},"message":{"description":"Commit message","type":"string"},"pull_request_number":{"description":"Optional pull request number for target '*'","type":["number","string"]}},"required":["message"],"type":"object"},"name":"push_to_pull_request_branch"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Push changes to a pull request branch","inputSchema":{"additionalProperties":false,"properties":{"branch":{"description":"Optional branch name. Do not provide this parameter if you want to push changes from the current branch. If not provided, the current branch will be used.","type":"string"},"message":{"description":"Commit message","type":"string"},"pull_request_number":{"description":"Optional pull request number for target '*'","type":["number","string"]}},"required":["message"],"type":"object"},"name":"push_to_pull_request_branch"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1995,6 +1995,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2543,6 +2545,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3982,7 +3991,7 @@ jobs: - create_pull_request - push_to_pull_request_branch - missing_tool - if: ((always()) && (needs.agent.result != 'skipped')) && (needs.activation.outputs.comment_id) + if: (always()) && (needs.agent.result != 'skipped') runs-on: ubuntu-slim permissions: contents: read @@ -4025,6 +4034,40 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } async function main() { const commentId = process.env.GH_AW_COMMENT_ID; const commentRepo = process.env.GH_AW_COMMENT_REPO; @@ -4036,8 +4079,30 @@ jobs: core.info(`Run URL: ${runUrl}`); core.info(`Workflow Name: ${workflowName}`); core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } if (!commentId) { - core.info("No comment ID found, skipping comment update"); + core.info("No comment ID found and no noop messages to process, skipping comment update"); return; } if (!runUrl) { @@ -4068,6 +4133,14 @@ jobs: } else { message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } const isDiscussionComment = commentId.startsWith("DC_"); try { if (isDiscussionComment) { diff --git a/.github/workflows/typist.lock.yml b/.github/workflows/typist.lock.yml index 91a94fd4fc0..2ebb388954d 100644 --- a/.github/workflows/typist.lock.yml +++ b/.github/workflows/typist.lock.yml @@ -15,10 +15,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -387,7 +392,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2258,6 +2263,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2806,6 +2813,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3674,6 +3688,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Typist - Go Type Analysis" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index a12590f97fd..d61176fbc7a 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -1177,7 +1177,7 @@ jobs: {"add_comment":{"max":1},"create_pull_request":{},"missing_tool":{},"upload_asset":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2914,6 +2914,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -3462,6 +3464,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4476,9 +4485,7 @@ jobs: - add_comment - missing_tool - upload_assets - if: > - (((always()) && (needs.agent.result != 'skipped')) && (needs.activation.outputs.comment_id)) && - (!(needs.add_comment.outputs.comment_id)) + if: ((always()) && (needs.agent.result != 'skipped')) && (!(needs.add_comment.outputs.comment_id)) runs-on: ubuntu-slim permissions: contents: read @@ -4521,6 +4528,40 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } async function main() { const commentId = process.env.GH_AW_COMMENT_ID; const commentRepo = process.env.GH_AW_COMMENT_REPO; @@ -4532,8 +4573,30 @@ jobs: core.info(`Run URL: ${runUrl}`); core.info(`Workflow Name: ${workflowName}`); core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } if (!commentId) { - core.info("No comment ID found, skipping comment update"); + core.info("No comment ID found and no noop messages to process, skipping comment update"); return; } if (!runUrl) { @@ -4564,6 +4627,14 @@ jobs: } else { message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } const isDiscussionComment = commentId.startsWith("DC_"); try { if (isDiscussionComment) { diff --git a/.github/workflows/video-analyzer.lock.yml b/.github/workflows/video-analyzer.lock.yml index 8232d38142f..7b2f26b7f7e 100644 --- a/.github/workflows/video-analyzer.lock.yml +++ b/.github/workflows/video-analyzer.lock.yml @@ -14,10 +14,15 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_issue["create_issue"] # detection["detection"] # missing_tool["missing_tool"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_issue --> conclusion +# missing_tool --> conclusion # agent --> create_issue # detection --> create_issue # agent --> detection @@ -264,7 +269,7 @@ jobs: {"create_issue":{"max":1},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1755,6 +1760,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2303,6 +2310,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -3604,6 +3618,203 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_issue + - missing_tool + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Video Analysis Agent" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_issue: needs: - agent diff --git a/.github/workflows/weekly-issue-summary.lock.yml b/.github/workflows/weekly-issue-summary.lock.yml index ef5fbf7436c..4f2eedd9ea9 100644 --- a/.github/workflows/weekly-issue-summary.lock.yml +++ b/.github/workflows/weekly-issue-summary.lock.yml @@ -16,11 +16,17 @@ # graph LR # activation["activation"] # agent["agent"] +# conclusion["conclusion"] # create_discussion["create_discussion"] # detection["detection"] # missing_tool["missing_tool"] # upload_assets["upload_assets"] # activation --> agent +# agent --> conclusion +# activation --> conclusion +# create_discussion --> conclusion +# missing_tool --> conclusion +# upload_assets --> conclusion # agent --> create_discussion # detection --> create_discussion # agent --> detection @@ -269,7 +275,7 @@ jobs: {"create_discussion":{"max":1},"missing_tool":{},"upload_asset":{}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"}] + [{"description":"Create a new GitHub discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Discussion body/content","type":"string"},"category":{"description":"Discussion category","type":"string"},"title":{"description":"Discussion title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_discussion"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -2157,6 +2163,8 @@ jobs: return 40; case "upload_asset": return 10; + case "noop": + return 1; default: return 1; } @@ -2705,6 +2713,13 @@ jobs: continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); @@ -4380,6 +4395,205 @@ jobs: main(); } + conclusion: + needs: + - agent + - activation + - create_discussion + - missing_tool + - upload_assets + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + steps: + - name: Debug job inputs + env: + COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + AGENT_CONCLUSION: ${{ needs.agent.result }} + run: | + echo "Comment ID: $COMMENT_ID" + echo "Comment Repo: $COMMENT_REPO" + echo "Agent Output Types: $AGENT_OUTPUT_TYPES" + echo "Agent Conclusion: $AGENT_CONCLUSION" + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Weekly Issue Summary" + GH_AW_CAMPAIGN: "weekly-issue-summary" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function main() { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + const runUrl = process.env.GH_AW_RUN_URL; + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; + core.info(`Comment ID: ${commentId}`); + core.info(`Comment Repo: ${commentRepo}`); + core.info(`Run URL: ${runUrl}`); + core.info(`Workflow Name: ${workflowName}`); + core.info(`Agent Conclusion: ${agentConclusion}`); + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { + core.info("No comment ID found and no noop messages to process, skipping comment update"); + return; + } + if (!runUrl) { + core.setFailed("Run URL is required"); + return; + } + const repoOwner = commentRepo ? commentRepo.split("/")[0] : context.repo.owner; + const repoName = commentRepo ? commentRepo.split("/")[1] : context.repo.repo; + core.info(`Updating comment in ${repoOwner}/${repoName}`); + let statusEmoji = "❌"; + let statusText = "failed"; + let message; + if (agentConclusion === "success") { + statusEmoji = "✅"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) completed successfully.`; + } else if (agentConclusion === "cancelled") { + statusEmoji = "🚫"; + statusText = "was cancelled"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "skipped") { + statusEmoji = "⏭️"; + statusText = "was skipped"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else if (agentConclusion === "timed_out") { + statusEmoji = "⏱️"; + statusText = "timed out"; + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } else { + message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; + } + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + const isDiscussionComment = commentId.startsWith("DC_"); + try { + if (isDiscussionComment) { + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: message } + ); + const comment = result.updateDiscussionComment.comment; + core.info(`Successfully updated discussion comment`); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: message, + headers: { + Accept: "application/vnd.github+json", + }, + }); + core.info(`Successfully updated comment`); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } + } catch (error) { + core.warning(`Failed to update comment: ${error instanceof Error ? error.message : String(error)}`); + } + } + main().catch(error => { + core.setFailed(error instanceof Error ? error.message : String(error)); + }); + create_discussion: needs: - agent diff --git a/pkg/cli/workflows/test-claude-noop.md b/pkg/cli/workflows/test-claude-noop.md new file mode 100644 index 00000000000..01a2341a105 --- /dev/null +++ b/pkg/cli/workflows/test-claude-noop.md @@ -0,0 +1,27 @@ +--- +on: + command: + name: test-noop + reaction: eyes +permissions: + contents: read + actions: read + issues: write + pull-requests: write +engine: claude +safe-outputs: + noop: + max: 5 +timeout-minutes: 5 +--- + +# Test No-Op Safe Output + +Test the noop safe output functionality. + +Create noop outputs with transparency messages: +- "Analysis complete - no issues found" +- "Code review passed - all checks successful" +- "No changes needed - everything looks good" + +Output as JSONL format. diff --git a/pkg/cli/workflows/test-codex-noop.md b/pkg/cli/workflows/test-codex-noop.md new file mode 100644 index 00000000000..453985b7b66 --- /dev/null +++ b/pkg/cli/workflows/test-codex-noop.md @@ -0,0 +1,27 @@ +--- +on: + command: + name: test-noop + reaction: eyes +permissions: + contents: read + actions: read + issues: write + pull-requests: write +engine: codex +safe-outputs: + noop: + max: 5 +timeout-minutes: 5 +--- + +# Test No-Op Safe Output + +Test the noop safe output functionality. + +Create noop outputs with transparency messages: +- "Analysis complete - no issues found" +- "Code review passed - all checks successful" +- "No changes needed - everything looks good" + +Output as JSONL format. diff --git a/pkg/cli/workflows/test-copilot-noop.md b/pkg/cli/workflows/test-copilot-noop.md new file mode 100644 index 00000000000..4b6d319a55e --- /dev/null +++ b/pkg/cli/workflows/test-copilot-noop.md @@ -0,0 +1,27 @@ +--- +on: + command: + name: test-noop + reaction: eyes +permissions: + contents: read + actions: read + issues: write + pull-requests: write +engine: copilot +safe-outputs: + noop: + max: 5 +timeout-minutes: 5 +--- + +# Test No-Op Safe Output + +Test the noop safe output functionality. + +Create noop outputs with transparency messages: +- "Analysis complete - no issues found" +- "Code review passed - all checks successful" +- "No changes needed - everything looks good" + +Output as JSONL format. diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 0c3f4f21af7..cb8b371355c 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -2827,6 +2827,36 @@ } ] }, + "noop": { + "oneOf": [ + { + "type": "object", + "description": "Configuration for no-op safe output (logging only, no GitHub API calls). Always available as a fallback to ensure human-visible artifacts.", + "properties": { + "max": { + "type": "integer", + "description": "Maximum number of noop messages (default: 1)", + "minimum": 1, + "default": 1 + }, + "github-token": { + "$ref": "#/$defs/github_token", + "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." + } + }, + "additionalProperties": false + }, + { + "type": "null", + "description": "Enable noop output with default configuration (max: 1)" + }, + { + "type": "boolean", + "const": false, + "description": "Explicitly disable noop output (false). Noop is enabled by default when safe-outputs is configured." + } + ] + }, "upload-assets": { "oneOf": [ { diff --git a/pkg/workflow/comment_env_vars_conditional_test.go b/pkg/workflow/comment_env_vars_conditional_test.go index b714a966797..71979a93ddc 100644 --- a/pkg/workflow/comment_env_vars_conditional_test.go +++ b/pkg/workflow/comment_env_vars_conditional_test.go @@ -37,7 +37,7 @@ Should have comment env vars. safeOutputType: "create_pull_request", }, { - name: "create-pull-request without reaction should not have comment env vars", + name: "create-pull-request without reaction should have conclusion job with comment env vars", markdown: `--- on: pull_request: @@ -48,9 +48,9 @@ safe-outputs: # Test PR without reaction -Should NOT have comment env vars. +Conclusion job will have comment env vars (may be empty). `, - expectCommentEnvs: false, + expectCommentEnvs: true, // Changed: conclusion job always has these safeOutputType: "create_pull_request", }, { @@ -72,7 +72,7 @@ Should have comment env vars. safeOutputType: "push_to_pull_request_branch", }, { - name: "push-to-pull-request-branch without reaction should not have comment env vars", + name: "push-to-pull-request-branch without reaction should have conclusion job with comment env vars", markdown: `--- on: pull_request: @@ -83,13 +83,13 @@ safe-outputs: # Test push without reaction -Should NOT have comment env vars. +Conclusion job will have comment env vars (may be empty). `, - expectCommentEnvs: false, + expectCommentEnvs: true, // Changed: conclusion job always has these safeOutputType: "push_to_pull_request_branch", }, { - name: "create-pull-request with reaction:none should not have comment env vars", + name: "create-pull-request with reaction:none should have conclusion job with comment env vars", markdown: `--- on: pull_request: @@ -101,9 +101,9 @@ safe-outputs: # Test PR with reaction:none -Should NOT have comment env vars. +Conclusion job will have comment env vars (may be empty). `, - expectCommentEnvs: false, + expectCommentEnvs: true, // Changed: conclusion job always has these safeOutputType: "create_pull_request", }, } diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 87c0a5c5323..000456f4a2f 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -264,6 +264,7 @@ type SafeOutputsConfig struct { CreateAgentTasks *CreateAgentTaskConfig `yaml:"create-agent-task,omitempty"` // Create GitHub Copilot agent tasks UpdateProjects *UpdateProjectConfig `yaml:"update-project,omitempty"` // Smart project board management (create/add/update) MissingTool *MissingToolConfig `yaml:"missing-tool,omitempty"` // Optional for reporting missing functionality + NoOp *NoOpConfig `yaml:"noop,omitempty"` // No-op output for logging only (always available as fallback) ThreatDetection *ThreatDetectionConfig `yaml:"threat-detection,omitempty"` // Threat detection configuration Jobs map[string]*SafeJobConfig `yaml:"jobs,omitempty"` // Safe-jobs configuration (moved from top-level) AllowedDomains []string `yaml:"allowed-domains,omitempty"` diff --git a/pkg/workflow/js.go b/pkg/workflow/js.go index 3810fdeb9d0..d886dccc9d8 100644 --- a/pkg/workflow/js.go +++ b/pkg/workflow/js.go @@ -41,6 +41,9 @@ var validateErrorsScript string //go:embed js/missing_tool.cjs var missingToolScript string +//go:embed js/noop.cjs +var noopScript string + //go:embed js/safe_outputs_mcp_server.cjs var safeOutputsMCPServerScript string @@ -56,9 +59,6 @@ var checkoutPRBranchScript string //go:embed js/redact_secrets.cjs var redactSecretsScript string -//go:embed js/notify_comment_error.cjs -var notifyCommentErrorScript string - //go:embed js/sanitize_content.cjs var sanitizeContentScript string diff --git a/pkg/workflow/js/collect_ndjson_output.cjs b/pkg/workflow/js/collect_ndjson_output.cjs index d6b5e222d72..f06c10fc20c 100644 --- a/pkg/workflow/js/collect_ndjson_output.cjs +++ b/pkg/workflow/js/collect_ndjson_output.cjs @@ -35,6 +35,8 @@ async function main() { return 40; case "upload_asset": return 10; + case "noop": + return 1; // Default max for noop messages default: return 1; } @@ -591,6 +593,13 @@ async function main() { continue; } break; + case "noop": + if (!item.message || typeof item.message !== "string") { + errors.push(`Line ${i + 1}: noop requires a 'message' string field`); + continue; + } + item.message = sanitizeContent(item.message, maxBodyLength); + break; case "create_code_scanning_alert": if (!item.file || typeof item.file !== "string") { errors.push(`Line ${i + 1}: create_code_scanning_alert requires a 'file' field (string)`); diff --git a/pkg/workflow/js/collect_ndjson_output.test.cjs b/pkg/workflow/js/collect_ndjson_output.test.cjs index 28d64dac770..14a49dcb729 100644 --- a/pkg/workflow/js/collect_ndjson_output.test.cjs +++ b/pkg/workflow/js/collect_ndjson_output.test.cjs @@ -2271,4 +2271,106 @@ Line 3"} expect(parsedOutput.errors[0]).toContain("Too few items of type 'add_comment'. Minimum required: 2, found: 1."); }); }); + + describe("noop output validation", () => { + it("should validate noop with message", async () => { + const testFile = "/tmp/gh-aw/test-ndjson-output.txt"; + const ndjsonContent = `{"type": "noop", "message": "No issues found in this review"}`; + + fs.writeFileSync(testFile, ndjsonContent); + process.env.GH_AW_SAFE_OUTPUTS = testFile; + + await eval(`(async () => { ${collectScript} })()`); + + const setOutputCalls = mockCore.setOutput.mock.calls; + const outputCall = setOutputCalls.find(call => call[0] === "output"); + expect(outputCall).toBeDefined(); + + const parsedOutput = JSON.parse(outputCall[1]); + expect(parsedOutput.items).toHaveLength(1); + expect(parsedOutput.items[0].type).toBe("noop"); + expect(parsedOutput.items[0].message).toBe("No issues found in this review"); + expect(parsedOutput.errors).toHaveLength(0); + }); + + it("should reject noop without message", async () => { + const testFile = "/tmp/gh-aw/test-ndjson-output.txt"; + const ndjsonContent = `{"type": "noop"}`; + + fs.writeFileSync(testFile, ndjsonContent); + process.env.GH_AW_SAFE_OUTPUTS = testFile; + + await eval(`(async () => { ${collectScript} })()`); + + const setOutputCalls = mockCore.setOutput.mock.calls; + const outputCall = setOutputCalls.find(call => call[0] === "output"); + expect(outputCall).toBeDefined(); + + const parsedOutput = JSON.parse(outputCall[1]); + expect(parsedOutput.items).toHaveLength(0); + expect(parsedOutput.errors).toHaveLength(1); + expect(parsedOutput.errors[0]).toContain("noop requires a 'message' string field"); + }); + + it("should reject noop with non-string message", async () => { + const testFile = "/tmp/gh-aw/test-ndjson-output.txt"; + const ndjsonContent = `{"type": "noop", "message": 123}`; + + fs.writeFileSync(testFile, ndjsonContent); + process.env.GH_AW_SAFE_OUTPUTS = testFile; + + await eval(`(async () => { ${collectScript} })()`); + + const setOutputCalls = mockCore.setOutput.mock.calls; + const outputCall = setOutputCalls.find(call => call[0] === "output"); + expect(outputCall).toBeDefined(); + + const parsedOutput = JSON.parse(outputCall[1]); + expect(parsedOutput.items).toHaveLength(0); + expect(parsedOutput.errors).toHaveLength(1); + expect(parsedOutput.errors[0]).toContain("noop requires a 'message' string field"); + }); + + it("should sanitize noop message content", async () => { + const testFile = "/tmp/gh-aw/test-ndjson-output.txt"; + const ndjsonContent = `{"type": "noop", "message": "Test @mention and fixes #123"}`; + + fs.writeFileSync(testFile, ndjsonContent); + process.env.GH_AW_SAFE_OUTPUTS = testFile; + + await eval(`(async () => { ${collectScript} })()`); + + const setOutputCalls = mockCore.setOutput.mock.calls; + const outputCall = setOutputCalls.find(call => call[0] === "output"); + expect(outputCall).toBeDefined(); + + const parsedOutput = JSON.parse(outputCall[1]); + expect(parsedOutput.items).toHaveLength(1); + expect(parsedOutput.items[0].message).toContain("`@mention`"); + expect(parsedOutput.items[0].message).toContain("`fixes #123`"); + }); + + it("should handle multiple noop messages", async () => { + const testFile = "/tmp/gh-aw/test-ndjson-output.txt"; + const ndjsonContent = `{"type": "noop", "message": "First message"} +{"type": "noop", "message": "Second message"} +{"type": "noop", "message": "Third message"}`; + + fs.writeFileSync(testFile, ndjsonContent); + process.env.GH_AW_SAFE_OUTPUTS = testFile; + + await eval(`(async () => { ${collectScript} })()`); + + const setOutputCalls = mockCore.setOutput.mock.calls; + const outputCall = setOutputCalls.find(call => call[0] === "output"); + expect(outputCall).toBeDefined(); + + const parsedOutput = JSON.parse(outputCall[1]); + expect(parsedOutput.items).toHaveLength(3); + expect(parsedOutput.items[0].message).toBe("First message"); + expect(parsedOutput.items[1].message).toBe("Second message"); + expect(parsedOutput.items[2].message).toBe("Third message"); + expect(parsedOutput.errors).toHaveLength(0); + }); + }); }); diff --git a/pkg/workflow/js/noop.cjs b/pkg/workflow/js/noop.cjs new file mode 100644 index 00000000000..bed5ad21b2d --- /dev/null +++ b/pkg/workflow/js/noop.cjs @@ -0,0 +1,68 @@ +// @ts-check +/// + +const { loadAgentOutput } = require("./load_agent_output.cjs"); + +/** + * Main function to handle noop safe output + * No-op is a fallback output type that logs messages for transparency + * without taking any GitHub API actions + */ +async function main() { + // Check if we're in staged mode + const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + + const result = loadAgentOutput(); + if (!result.success) { + return; + } + + // Find all noop items + const noopItems = result.items.filter(/** @param {any} item */ item => item.type === "noop"); + if (noopItems.length === 0) { + core.info("No noop items found in agent output"); + return; + } + + core.info(`Found ${noopItems.length} noop item(s)`); + + // If in staged mode, emit step summary instead of logging + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: No-Op Messages Preview\n\n"; + summaryContent += "The following messages would be logged if staged mode was disabled:\n\n"; + + for (let i = 0; i < noopItems.length; i++) { + const item = noopItems[i]; + summaryContent += `### Message ${i + 1}\n`; + summaryContent += `${item.message}\n\n`; + summaryContent += "---\n\n"; + } + + await core.summary.addRaw(summaryContent).write(); + core.info("📝 No-op message preview written to step summary"); + return; + } + + // Process each noop item - just log the messages for transparency + let summaryContent = "\n\n## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + + for (let i = 0; i < noopItems.length; i++) { + const item = noopItems[i]; + core.info(`No-op message ${i + 1}: ${item.message}`); + summaryContent += `- ${item.message}\n`; + } + + // Write summary for all noop messages + await core.summary.addRaw(summaryContent).write(); + + // Export the first noop message for use in add-comment default reporting + if (noopItems.length > 0) { + core.setOutput("noop_message", noopItems[0].message); + core.exportVariable("GH_AW_NOOP_MESSAGE", noopItems[0].message); + } + + core.info(`Successfully processed ${noopItems.length} noop message(s)`); +} + +await main(); diff --git a/pkg/workflow/js/noop.test.cjs b/pkg/workflow/js/noop.test.cjs new file mode 100644 index 00000000000..6a54f0df21f --- /dev/null +++ b/pkg/workflow/js/noop.test.cjs @@ -0,0 +1,197 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import fs from "fs"; + +// Mock the global objects that GitHub Actions provides +const mockCore = { + debug: vi.fn(), + info: vi.fn(), + warning: vi.fn(), + error: vi.fn(), + setFailed: vi.fn(), + setOutput: vi.fn(), + exportVariable: vi.fn(), + summary: { + addRaw: vi.fn().mockReturnThis(), + write: vi.fn().mockResolvedValue(), + }, +}; + +// Set up global mocks before importing the module +global.core = mockCore; +global.fs = fs; + +describe("noop", () => { + beforeEach(() => { + // Reset mocks before each test + vi.clearAllMocks(); + // Reset environment variables + delete process.env.GH_AW_SAFE_OUTPUTS_STAGED; + delete process.env.GH_AW_AGENT_OUTPUT; + }); + + it("should handle empty agent output", async () => { + const agentOutputPath = "/tmp/gh-aw/agent_output.json"; + fs.mkdirSync("/tmp/gh-aw", { recursive: true }); + fs.writeFileSync(agentOutputPath, JSON.stringify({ + items: [], + errors: [], + })); + process.env.GH_AW_AGENT_OUTPUT = agentOutputPath; + + await import("./noop.cjs"); + + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No noop items found")); + }); + + it("should process single noop message", async () => { + const agentOutputPath = "/tmp/gh-aw/agent_output.json"; + fs.mkdirSync("/tmp/gh-aw", { recursive: true }); + fs.writeFileSync(agentOutputPath, JSON.stringify({ + items: [ + { + type: "noop", + message: "No issues found in this review", + }, + ], + errors: [], + })); + process.env.GH_AW_AGENT_OUTPUT = agentOutputPath; + + // Need to re-import to execute + delete require.cache[require.resolve("./noop.cjs")]; + await import("./noop.cjs"); + + expect(mockCore.info).toHaveBeenCalledWith("Found 1 noop item(s)"); + expect(mockCore.info).toHaveBeenCalledWith("No-op message 1: No issues found in this review"); + expect(mockCore.setOutput).toHaveBeenCalledWith("noop_message", "No issues found in this review"); + expect(mockCore.exportVariable).toHaveBeenCalledWith("GH_AW_NOOP_MESSAGE", "No issues found in this review"); + expect(mockCore.summary.addRaw).toHaveBeenCalled(); + expect(mockCore.summary.write).toHaveBeenCalled(); + }); + + it("should process multiple noop messages", async () => { + const agentOutputPath = "/tmp/gh-aw/agent_output.json"; + fs.mkdirSync("/tmp/gh-aw", { recursive: true }); + fs.writeFileSync(agentOutputPath, JSON.stringify({ + items: [ + { + type: "noop", + message: "First message", + }, + { + type: "noop", + message: "Second message", + }, + { + type: "noop", + message: "Third message", + }, + ], + errors: [], + })); + process.env.GH_AW_AGENT_OUTPUT = agentOutputPath; + + delete require.cache[require.resolve("./noop.cjs")]; + await import("./noop.cjs"); + + expect(mockCore.info).toHaveBeenCalledWith("Found 3 noop item(s)"); + expect(mockCore.info).toHaveBeenCalledWith("No-op message 1: First message"); + expect(mockCore.info).toHaveBeenCalledWith("No-op message 2: Second message"); + expect(mockCore.info).toHaveBeenCalledWith("No-op message 3: Third message"); + expect(mockCore.setOutput).toHaveBeenCalledWith("noop_message", "First message"); + expect(mockCore.exportVariable).toHaveBeenCalledWith("GH_AW_NOOP_MESSAGE", "First message"); + }); + + it("should show preview in staged mode", async () => { + process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true"; + const agentOutputPath = "/tmp/gh-aw/agent_output.json"; + fs.mkdirSync("/tmp/gh-aw", { recursive: true }); + fs.writeFileSync(agentOutputPath, JSON.stringify({ + items: [ + { + type: "noop", + message: "Test message in staged mode", + }, + ], + errors: [], + })); + process.env.GH_AW_AGENT_OUTPUT = agentOutputPath; + + delete require.cache[require.resolve("./noop.cjs")]; + await import("./noop.cjs"); + + expect(mockCore.info).toHaveBeenCalledWith("Found 1 noop item(s)"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("📝 No-op message preview written to step summary")); + expect(mockCore.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("🎭 Staged Mode")); + expect(mockCore.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("Test message in staged mode")); + expect(mockCore.setOutput).not.toHaveBeenCalled(); + }); + + it("should ignore non-noop items", async () => { + const agentOutputPath = "/tmp/gh-aw/agent_output.json"; + fs.mkdirSync("/tmp/gh-aw", { recursive: true }); + fs.writeFileSync(agentOutputPath, JSON.stringify({ + items: [ + { + type: "create_issue", + title: "Test Issue", + body: "Test body", + }, + { + type: "noop", + message: "This is the only noop", + }, + { + type: "add_comment", + body: "Test comment", + }, + ], + errors: [], + })); + process.env.GH_AW_AGENT_OUTPUT = agentOutputPath; + + delete require.cache[require.resolve("./noop.cjs")]; + await import("./noop.cjs"); + + expect(mockCore.info).toHaveBeenCalledWith("Found 1 noop item(s)"); + expect(mockCore.info).toHaveBeenCalledWith("No-op message 1: This is the only noop"); + }); + + it("should handle missing agent output file", async () => { + process.env.GH_AW_AGENT_OUTPUT = "/tmp/gh-aw/nonexistent.json"; + + delete require.cache[require.resolve("./noop.cjs")]; + await import("./noop.cjs"); + + // loadAgentOutput should handle this and return success: false + // The noop function should return early + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No GH_AW_AGENT_OUTPUT")); + }); + + it("should generate proper step summary format", async () => { + const agentOutputPath = "/tmp/gh-aw/agent_output.json"; + fs.mkdirSync("/tmp/gh-aw", { recursive: true }); + fs.writeFileSync(agentOutputPath, JSON.stringify({ + items: [ + { + type: "noop", + message: "Analysis complete", + }, + { + type: "noop", + message: "No action required", + }, + ], + errors: [], + })); + process.env.GH_AW_AGENT_OUTPUT = agentOutputPath; + + delete require.cache[require.resolve("./noop.cjs")]; + await import("./noop.cjs"); + + const summaryCall = mockCore.summary.addRaw.mock.calls[0][0]; + expect(summaryCall).toContain("## No-Op Messages"); + expect(summaryCall).toContain("- Analysis complete"); + expect(summaryCall).toContain("- No action required"); + }); +}); diff --git a/pkg/workflow/js/notify_comment_error.cjs b/pkg/workflow/js/notify_comment_error.cjs index 67419119c70..0734ca2c64b 100644 --- a/pkg/workflow/js/notify_comment_error.cjs +++ b/pkg/workflow/js/notify_comment_error.cjs @@ -3,6 +3,9 @@ // This script updates an existing comment created by the activation job // to notify about the workflow completion status (success or failure). +// It also processes noop messages and adds them to the activation comment. + +const { loadAgentOutput } = require("./load_agent_output.cjs"); async function main() { const commentId = process.env.GH_AW_COMMENT_ID; @@ -17,11 +20,41 @@ async function main() { core.info(`Workflow Name: ${workflowName}`); core.info(`Agent Conclusion: ${agentConclusion}`); + // Load agent output to check for noop messages + let noopMessages = []; + const agentOutputResult = loadAgentOutput(); + if (agentOutputResult.success && agentOutputResult.data) { + const noopItems = agentOutputResult.data.items.filter(item => item.type === "noop"); + if (noopItems.length > 0) { + core.info(`Found ${noopItems.length} noop message(s)`); + noopMessages = noopItems.map(item => item.message); + } + } + + // If there's no comment to update but we have noop messages, write to step summary + if (!commentId && noopMessages.length > 0) { + core.info("No comment ID found, writing noop messages to step summary"); + + let summaryContent = "## No-Op Messages\n\n"; + summaryContent += "The following messages were logged for transparency:\n\n"; + + if (noopMessages.length === 1) { + summaryContent += noopMessages[0]; + } else { + summaryContent += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + + await core.summary.addRaw(summaryContent).write(); + core.info(`Successfully wrote ${noopMessages.length} noop message(s) to step summary`); + return; + } + if (!commentId) { - core.info("No comment ID found, skipping comment update"); + core.info("No comment ID found and no noop messages to process, skipping comment update"); return; } + // At this point, we have a comment to update if (!runUrl) { core.setFailed("Run URL is required"); return; @@ -58,6 +91,16 @@ async function main() { message = `${statusEmoji} Agentic [${workflowName}](${runUrl}) ${statusText} and wasn't able to produce a result.`; } + // Add noop messages to the comment if any + if (noopMessages.length > 0) { + message += "\n\n"; + if (noopMessages.length === 1) { + message += noopMessages[0]; + } else { + message += noopMessages.map((msg, idx) => `${idx + 1}. ${msg}`).join("\n"); + } + } + // Check if this is a discussion comment (GraphQL node ID format) const isDiscussionComment = commentId.startsWith("DC_"); diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json index 53a35a6d2cc..25d6740b63c 100644 --- a/pkg/workflow/js/safe_outputs_tools.json +++ b/pkg/workflow/js/safe_outputs_tools.json @@ -245,5 +245,20 @@ }, "additionalProperties": false } + }, + { + "name": "noop", + "description": "Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').", + "inputSchema": { + "type": "object", + "required": ["message"], + "properties": { + "message": { + "type": "string", + "description": "Message to log for transparency" + } + }, + "additionalProperties": false + } } ] diff --git a/pkg/workflow/js/types/safe-outputs-config.d.ts b/pkg/workflow/js/types/safe-outputs-config.d.ts index a891cd7a8dc..837a1c4ff69 100644 --- a/pkg/workflow/js/types/safe-outputs-config.d.ts +++ b/pkg/workflow/js/types/safe-outputs-config.d.ts @@ -92,6 +92,11 @@ interface UploadAssetConfig extends SafeOutputConfig { "allowed-exts"?: string[]; } +/** + * Configuration for no-op output + */ +interface NoOpConfig extends SafeOutputConfig {} + /** * Configuration for reporting missing tools */ @@ -146,6 +151,7 @@ type SpecificSafeOutputConfig = | UpdateIssueConfig | PushToPullRequestBranchConfig | UploadAssetConfig + | NoOpConfig | MissingToolConfig | ThreatDetectionConfig; @@ -165,6 +171,7 @@ export { UpdateIssueConfig, PushToPullRequestBranchConfig, UploadAssetConfig, + NoOpConfig, MissingToolConfig, ThreatDetectionConfig, SpecificSafeOutputConfig, diff --git a/pkg/workflow/js/types/safe-outputs.d.ts b/pkg/workflow/js/types/safe-outputs.d.ts index db8b3e680ee..3e11a9314b3 100644 --- a/pkg/workflow/js/types/safe-outputs.d.ts +++ b/pkg/workflow/js/types/safe-outputs.d.ts @@ -158,6 +158,15 @@ interface UploadAssetItem extends BaseSafeOutputItem { file_path: string; } +/** + * JSONL item for no-op (logging only) + */ +interface NoOpItem extends BaseSafeOutputItem { + type: "noop"; + /** Message to log for transparency */ + message: string; +} + /** * Union type of all possible safe output items */ @@ -172,7 +181,8 @@ type SafeOutputItem = | UpdateIssueItem | PushToPrBranchItem | MissingToolItem - | UploadAssetItem; + | UploadAssetItem + | NoOpItem; /** * Sanitized safe output items @@ -196,6 +206,7 @@ export { PushToPrBranchItem, MissingToolItem, UploadAssetItem, + NoOpItem, SafeOutputItem, SafeOutputItems, }; diff --git a/pkg/workflow/noop.go b/pkg/workflow/noop.go new file mode 100644 index 00000000000..04c56473b6a --- /dev/null +++ b/pkg/workflow/noop.go @@ -0,0 +1,91 @@ +package workflow + +import ( + "fmt" +) + +// NoOpConfig holds configuration for no-op safe output (logging only) +type NoOpConfig struct { + BaseSafeOutputConfig `yaml:",inline"` +} + +// buildCreateOutputNoOpJob creates the noop job +func (c *Compiler) buildCreateOutputNoOpJob(data *WorkflowData, mainJobName string) (*Job, error) { + if data.SafeOutputs == nil || data.SafeOutputs.NoOp == nil { + return nil, fmt.Errorf("safe-outputs.noop configuration is required") + } + + // Build custom environment variables specific to noop + var customEnvVars []string + if data.SafeOutputs.NoOp.Max > 0 { + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_NOOP_MAX: %d\n", data.SafeOutputs.NoOp.Max)) + } + + // Add workflow metadata for consistency + customEnvVars = append(customEnvVars, buildWorkflowMetadataEnvVarsWithCampaign(data.Name, data.Source, data.Campaign)...) + + // Build the GitHub Script step using the common helper + steps := c.buildGitHubScriptStep(data, GitHubScriptStepConfig{ + StepName: "Process No-Op Messages", + StepID: "noop", + MainJobName: mainJobName, + CustomEnvVars: customEnvVars, + Script: getNoOpScript(), + Token: data.SafeOutputs.NoOp.GitHubToken, + }) + + // Create outputs for the job + outputs := map[string]string{ + "noop_message": "${{ steps.noop.outputs.noop_message }}", + } + + // Build the job condition using BuildSafeOutputType + jobCondition := BuildSafeOutputType("noop").Render() + + // Create the job + job := &Job{ + Name: "noop", + RunsOn: c.formatSafeOutputsRunsOn(data.SafeOutputs), + If: jobCondition, + Permissions: NewPermissionsContentsRead().RenderToYAML(), + TimeoutMinutes: 5, // Short timeout since it's just logging + Steps: steps, + Outputs: outputs, + Needs: []string{mainJobName}, // Depend on the main workflow job + } + + return job, nil +} + +// parseNoOpConfig handles noop configuration +func (c *Compiler) parseNoOpConfig(outputMap map[string]any) *NoOpConfig { + if configData, exists := outputMap["noop"]; exists { + // Handle the case where configData is false (explicitly disabled) + if configBool, ok := configData.(bool); ok && !configBool { + return nil + } + + noopConfig := &NoOpConfig{} + + // Handle the case where configData is nil (noop: with no value) + if configData == nil { + // Set default max for noop messages + noopConfig.Max = 1 + return noopConfig + } + + if configMap, ok := configData.(map[string]any); ok { + // Parse common base fields with default max of 1 + c.parseBaseSafeOutputConfig(configMap, &noopConfig.BaseSafeOutputConfig, 1) + } + + return noopConfig + } + + return nil +} + +// getNoOpScript returns the JavaScript implementation +func getNoOpScript() string { + return noopScript +} diff --git a/pkg/workflow/notify_comment.go b/pkg/workflow/notify_comment.go index cbab61080ba..53bd1ad4005 100644 --- a/pkg/workflow/notify_comment.go +++ b/pkg/workflow/notify_comment.go @@ -21,25 +21,24 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa notifyCommentLog.Printf("Building conclusion job: main_job=%s, safe_output_jobs_count=%d", mainJobName, len(safeOutputJobNames)) // Create this job when: - // 1. add-comment is configured with a reaction, OR - // 2. command is configured with a reaction (which auto-creates a comment in activation) + // 1. Safe outputs are configured (because noop is always enabled as a fallback) + // The job will: + // - Update activation comment with noop messages (if comment exists) + // - Write noop messages to step summary (if no comment) hasAddComment := data.SafeOutputs != nil && data.SafeOutputs.AddComments != nil hasCommand := data.Command != "" + hasNoOp := data.SafeOutputs != nil && data.SafeOutputs.NoOp != nil hasReaction := data.AIReaction != "" && data.AIReaction != "none" + hasSafeOutputs := data.SafeOutputs != nil - notifyCommentLog.Printf("Configuration checks: has_add_comment=%t, has_command=%t, has_reaction=%t", hasAddComment, hasCommand, hasReaction) + notifyCommentLog.Printf("Configuration checks: has_add_comment=%t, has_command=%t, has_noop=%t, has_reaction=%t, has_safe_outputs=%t", hasAddComment, hasCommand, hasNoOp, hasReaction, hasSafeOutputs) - // Only create this job when reactions are being used AND either add-comment or command is configured - // This job updates the activation comment, which is only created when AIReaction is configured - if !hasReaction { - notifyCommentLog.Printf("Skipping job: no reaction configured") - return nil, nil // No reaction configured or explicitly disabled, no comment to update - } - - if !hasAddComment && !hasCommand { - notifyCommentLog.Printf("Skipping job: neither add-comment nor command configured") - return nil, nil // Neither add-comment nor command is configured, no need for conclusion job + // Always create this job when safe-outputs exist (because noop is always enabled) + // This ensures noop messages can be handled even without reactions + if !hasSafeOutputs { + notifyCommentLog.Printf("Skipping job: no safe-outputs configured") + return nil, nil // No safe-outputs configured, no need for conclusion job } // Build the job steps @@ -82,7 +81,7 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa StepID: "conclusion", MainJobName: mainJobName, CustomEnvVars: customEnvVars, - Script: notifyCommentErrorScript, + Script: getNotifyCommentErrorScript(), Token: token, }) steps = append(steps, scriptSteps...) @@ -90,11 +89,10 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa // Build the condition for this job: // 1. always() - run even if agent fails // 2. agent was activated (not skipped) - // 3. comment_id exists (comment was created in activation) - // 4. add_comment job either doesn't exist OR hasn't created a comment yet + // 3. IF comment_id exists: add_comment job either doesn't exist OR hasn't created a comment yet // - // Note: The job should run even when create_pull_request or push_to_pull_request_branch - // output types are present, as those don't update the activation comment. + // Note: The job should always run to handle noop messages (either update comment or write to summary) + // The script (notify_comment_error.cjs) handles the case where there's no comment gracefully alwaysFunc := BuildFunctionCall("always") @@ -104,9 +102,6 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa BuildStringLiteral("skipped"), ) - // Check that a comment was created in activation - commentIdExists := BuildPropertyAccess(fmt.Sprintf("needs.%s.outputs.comment_id", constants.ActivationJobName)) - // Check if add_comment job exists in the safe output jobs hasAddCommentJob := false for _, jobName := range safeOutputJobNames { @@ -119,24 +114,18 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa // Build the condition based on whether add_comment job exists var condition ConditionNode if hasAddCommentJob { - // If add_comment job exists, check that it hasn't already created a comment - // (i.e., check that needs.add_comment.outputs.comment_id is empty/false) + // If add_comment job exists, also check that it hasn't already created a comment + // This prevents duplicate updates when add_comment has already updated the activation comment noAddCommentOutput := &NotNode{ Child: BuildPropertyAccess("needs.add_comment.outputs.comment_id"), } condition = buildAnd( - buildAnd( - buildAnd(alwaysFunc, agentNotSkipped), - commentIdExists, - ), + buildAnd(alwaysFunc, agentNotSkipped), noAddCommentOutput, ) } else { // If add_comment job doesn't exist, just check the basic conditions - condition = buildAnd( - buildAnd(alwaysFunc, agentNotSkipped), - commentIdExists, - ) + condition = buildAnd(alwaysFunc, agentNotSkipped) } // Build dependencies - this job depends on all safe output jobs to ensure it runs last diff --git a/pkg/workflow/notify_comment_test.go b/pkg/workflow/notify_comment_test.go index 36ec841b1ac..f9d7c1927a6 100644 --- a/pkg/workflow/notify_comment_test.go +++ b/pkg/workflow/notify_comment_test.go @@ -28,7 +28,6 @@ func TestConclusionJob(t *testing.T) { expectConditions: []string{ "always()", "needs.agent.result != 'skipped'", - "needs.activation.outputs.comment_id", "!(needs.add_comment.outputs.comment_id)", }, expectNeeds: []string{constants.AgentJobName, constants.ActivationJobName, "add_comment", "create_issue", "missing_tool"}, @@ -43,7 +42,6 @@ func TestConclusionJob(t *testing.T) { expectConditions: []string{ "always()", "needs.agent.result != 'skipped'", - "needs.activation.outputs.comment_id", "!(needs.add_comment.outputs.comment_id)", }, expectNeeds: []string{constants.AgentJobName, constants.ActivationJobName, "add_comment", "create_issue", "missing_tool"}, @@ -57,20 +55,32 @@ func TestConclusionJob(t *testing.T) { expectJob: false, }, { - name: "conclusion job not created when add-comment is configured but ai-reaction is not", + name: "conclusion job created when add-comment is configured but ai-reaction is not", addCommentConfig: true, aiReaction: "", command: "", safeOutputJobNames: []string{"add_comment", "missing_tool"}, - expectJob: false, + expectJob: true, + expectConditions: []string{ + "always()", + "needs.agent.result != 'skipped'", + "!(needs.add_comment.outputs.comment_id)", + }, + expectNeeds: []string{constants.AgentJobName, constants.ActivationJobName, "add_comment", "missing_tool"}, }, { - name: "conclusion job not created when reaction is explicitly set to none", + name: "conclusion job created when reaction is explicitly set to none", addCommentConfig: true, aiReaction: "none", command: "", safeOutputJobNames: []string{"add_comment", "missing_tool"}, - expectJob: false, + expectJob: true, + expectConditions: []string{ + "always()", + "needs.agent.result != 'skipped'", + "!(needs.add_comment.outputs.comment_id)", + }, + expectNeeds: []string{constants.AgentJobName, constants.ActivationJobName, "add_comment", "missing_tool"}, }, { name: "conclusion job created when command and reaction are configured (no add-comment)", @@ -82,7 +92,6 @@ func TestConclusionJob(t *testing.T) { expectConditions: []string{ "always()", "needs.agent.result != 'skipped'", - "needs.activation.outputs.comment_id", }, expectNeeds: []string{constants.AgentJobName, constants.ActivationJobName, "missing_tool"}, }, @@ -96,21 +105,25 @@ func TestConclusionJob(t *testing.T) { expectConditions: []string{ "always()", "needs.agent.result != 'skipped'", - "needs.activation.outputs.comment_id", }, expectNeeds: []string{constants.AgentJobName, constants.ActivationJobName, "push_to_pull_request_branch", "missing_tool"}, }, { - name: "conclusion job not created when command is configured but reaction is none", + name: "conclusion job created when command is configured but reaction is none", addCommentConfig: false, aiReaction: "none", command: "test-command", safeOutputJobNames: []string{"missing_tool"}, - expectJob: false, + expectJob: true, + expectConditions: []string{ + "always()", + "needs.agent.result != 'skipped'", + }, + expectNeeds: []string{constants.AgentJobName, constants.ActivationJobName, "missing_tool"}, }, } - for _, tt := range tests { + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create a test workflow compiler := NewCompiler(false, "", "") @@ -128,6 +141,12 @@ func TestConclusionJob(t *testing.T) { }, }, } + } else if len(tt.safeOutputJobNames) > 0 { + // If there are safe output jobs but no add-comment, create a minimal SafeOutputs config + // This represents a scenario where other safe outputs exist (like missing_tool) + workflowData.SafeOutputs = &SafeOutputsConfig{ + MissingTool: &MissingToolConfig{}, + } } // Build the conclusion job @@ -230,11 +249,6 @@ func TestConclusionJobIntegration(t *testing.T) { // Convert job to YAML string for checking jobYAML := strings.Join(job.Steps, "") - // Check that the job references activation outputs - if !strings.Contains(job.If, "needs.activation.outputs.comment_id") { - t.Error("Expected conclusion to reference activation.outputs.comment_id") - } - // Check that environment variables reference activation outputs if !strings.Contains(jobYAML, "needs.activation.outputs.comment_id") { t.Error("Expected GH_AW_COMMENT_ID to reference activation.outputs.comment_id") @@ -248,16 +262,13 @@ func TestConclusionJobIntegration(t *testing.T) { t.Error("Expected GH_AW_AGENT_CONCLUSION to reference needs.agent.result") } - // Check all six conditions are present + // Check expected conditions are present if !strings.Contains(job.If, "always()") { t.Error("Expected always() in conclusion condition") } if !strings.Contains(job.If, "needs.agent.result != 'skipped'") { t.Error("Expected agent not skipped check in conclusion condition") } - if !strings.Contains(job.If, "needs.activation.outputs.comment_id") { - t.Error("Expected comment_id check in conclusion condition") - } if !strings.Contains(job.If, "!(needs.add_comment.outputs.comment_id)") { t.Error("Expected NOT add_comment.outputs.comment_id check in conclusion condition") } diff --git a/pkg/workflow/reaction_none_test.go b/pkg/workflow/reaction_none_test.go index 1fdc90abc37..7bda52b2adc 100644 --- a/pkg/workflow/reaction_none_test.go +++ b/pkg/workflow/reaction_none_test.go @@ -92,9 +92,9 @@ Test command workflow with reaction explicitly disabled. t.Error("Activation job should have 'contents: read' permission for checkout step") } - // Verify that conclusion job is NOT created - if strings.Contains(compiled, "conclusion:") { - t.Error("conclusion job should not be created when reaction is 'none'") + // Verify that conclusion job IS created (to handle noop messages) + if !strings.Contains(compiled, "conclusion:") { + t.Error("conclusion job should be created when safe-outputs exist (to handle noop)") } } diff --git a/pkg/workflow/safe_outputs.go b/pkg/workflow/safe_outputs.go index 56f55595434..a3b6987369e 100644 --- a/pkg/workflow/safe_outputs.go +++ b/pkg/workflow/safe_outputs.go @@ -41,6 +41,7 @@ func HasSafeOutputsEnabled(safeOutputs *SafeOutputsConfig) bool { safeOutputs.PushToPullRequestBranch != nil || safeOutputs.UploadAssets != nil || safeOutputs.MissingTool != nil || + safeOutputs.NoOp != nil || len(safeOutputs.Jobs) > 0 if safeOutputsLog.Enabled() { @@ -413,6 +414,19 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut } } + // Handle noop (parse configuration if present, or enable by default as fallback) + noopConfig := c.parseNoOpConfig(outputMap) + if noopConfig != nil { + config.NoOp = noopConfig + } else { + // Enable noop by default if safe-outputs exists and it wasn't explicitly disabled + // This ensures there's always a fallback for transparency + if _, exists := outputMap["noop"]; !exists { + config.NoOp = &NoOpConfig{} + config.NoOp.Max = 1 // Default max + } + } + // Handle staged flag if staged, exists := outputMap["staged"]; exists { if stagedBool, ok := staged.(bool); ok { @@ -923,6 +937,9 @@ func generateFilteredToolsJSON(data *WorkflowData) (string, error) { if data.SafeOutputs.MissingTool != nil { enabledTools["missing_tool"] = true } + if data.SafeOutputs.NoOp != nil { + enabledTools["noop"] = true + } // Filter tools to only include enabled ones var filteredTools []map[string]any diff --git a/pkg/workflow/safe_outputs_tools_test.go b/pkg/workflow/safe_outputs_tools_test.go index 54d3ccce28a..2bb66531bc6 100644 --- a/pkg/workflow/safe_outputs_tools_test.go +++ b/pkg/workflow/safe_outputs_tools_test.go @@ -278,6 +278,7 @@ func TestGetSafeOutputsToolsJSON(t *testing.T) { "push_to_pull_request_branch", "upload_asset", "missing_tool", + "noop", } var actualTools []string diff --git a/pkg/workflow/scripts.go b/pkg/workflow/scripts.go index 35bdb601454..587c7965280 100644 --- a/pkg/workflow/scripts.go +++ b/pkg/workflow/scripts.go @@ -53,6 +53,9 @@ var pushToPullRequestBranchScriptSource string //go:embed js/create_pull_request.cjs var createPullRequestScriptSource string +//go:embed js/notify_comment_error.cjs +var notifyCommentErrorScriptSource string + // Log parser source scripts // //go:embed js/parse_claude_log.cjs @@ -108,6 +111,9 @@ var ( createPullRequestScript string createPullRequestScriptOnce sync.Once + notifyCommentErrorScript string + notifyCommentErrorScriptOnce sync.Once + interpolatePromptBundled string interpolatePromptBundledOnce sync.Once @@ -361,6 +367,26 @@ func getCreatePullRequestScript() string { return createPullRequestScript } +// getNotifyCommentErrorScript returns the bundled notify_comment_error script +// Bundling is performed on first access and cached for subsequent calls +// This bundles load_agent_output.cjs inline to avoid require() issues in GitHub Actions +func getNotifyCommentErrorScript() string { + notifyCommentErrorScriptOnce.Do(func() { + scriptsLog.Print("Bundling notify_comment_error script") + sources := GetJavaScriptSources() + bundled, err := BundleJavaScriptFromSources(notifyCommentErrorScriptSource, sources, "") + if err != nil { + scriptsLog.Printf("Bundling failed for notify_comment_error, using source as-is: %v", err) + // If bundling fails, use the source as-is + notifyCommentErrorScript = notifyCommentErrorScriptSource + } else { + scriptsLog.Printf("Successfully bundled notify_comment_error script: %d bytes", len(bundled)) + notifyCommentErrorScript = bundled + } + }) + return notifyCommentErrorScript +} + // getInterpolatePromptScript returns the bundled interpolate_prompt script // Bundling is performed on first access and cached for subsequent calls // This bundles is_truthy.cjs inline to avoid require() issues in GitHub Actions diff --git a/schemas/agent-output.json b/schemas/agent-output.json index 2f4cdc82daa..f8de40955b2 100644 --- a/schemas/agent-output.json +++ b/schemas/agent-output.json @@ -37,7 +37,8 @@ {"$ref": "#/$defs/CreateDiscussionOutput"}, {"$ref": "#/$defs/MissingToolOutput"}, {"$ref": "#/$defs/CreateCodeScanningAlertOutput"}, - {"$ref": "#/$defs/UpdateProjectOutput"} + {"$ref": "#/$defs/UpdateProjectOutput"}, + {"$ref": "#/$defs/NoOpOutput"} ] }, "CreateIssueOutput": { @@ -357,6 +358,23 @@ }, "required": ["type", "project"], "additionalProperties": false + }, + "NoOpOutput": { + "title": "No-Op Output", + "description": "Output for logging a message without taking any GitHub actions. Always available as a fallback to ensure human-visible artifacts.", + "type": "object", + "properties": { + "type": { + "const": "noop" + }, + "message": { + "type": "string", + "description": "Message to log for transparency", + "minLength": 1 + } + }, + "required": ["type", "message"], + "additionalProperties": false } } } \ No newline at end of file