From 30134a61e056d0d7f51f0c7eba0a3f9250a2e7f6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 8 Feb 2026 23:10:23 +0000
Subject: [PATCH 1/4] Initial plan
From 6e8b9aecbe2aed1872215bef73a663b1f9ac1ba7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 8 Feb 2026 23:18:09 +0000
Subject: [PATCH 2/4] Add handle_noop_message script and update
handle_agent_failure to skip noop-only scenarios
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/handle_agent_failure.cjs | 15 ++
actions/setup/js/handle_noop_message.cjs | 175 +++++++++++++
actions/setup/js/handle_noop_message.test.cjs | 239 ++++++++++++++++++
pkg/workflow/notify_comment.go | 23 ++
4 files changed, 452 insertions(+)
create mode 100644 actions/setup/js/handle_noop_message.cjs
create mode 100644 actions/setup/js/handle_noop_message.test.cjs
diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs
index 1db43e27807..4b1bce8251b 100644
--- a/actions/setup/js/handle_agent_failure.cjs
+++ b/actions/setup/js/handle_agent_failure.cjs
@@ -357,6 +357,7 @@ async function main() {
// Check if agent succeeded but produced no safe outputs
let hasMissingSafeOutputs = false;
+ let hasOnlyNoopOutputs = false;
if (agentConclusion === "success") {
const { loadAgentOutput } = require("./load_agent_output.cjs");
const agentOutputResult = loadAgentOutput();
@@ -364,15 +365,29 @@ async function main() {
if (!agentOutputResult.success || !agentOutputResult.items || agentOutputResult.items.length === 0) {
hasMissingSafeOutputs = true;
core.info("Agent succeeded but produced no safe outputs");
+ } else {
+ // Check if all outputs are noop types
+ const nonNoopItems = agentOutputResult.items.filter(item => item.type !== "noop");
+ if (nonNoopItems.length === 0) {
+ hasOnlyNoopOutputs = true;
+ core.info("Agent succeeded with only noop outputs - this is not a failure");
+ }
}
}
// Only proceed if the agent job actually failed OR there are assignment errors OR create_discussion errors OR missing safe outputs
+ // BUT skip if we only have noop outputs (that's a successful no-action scenario)
if (agentConclusion !== "failure" && !hasAssignmentErrors && !hasCreateDiscussionErrors && !hasMissingSafeOutputs) {
core.info(`Agent job did not fail and no assignment/discussion errors and has safe outputs (conclusion: ${agentConclusion}), skipping failure handling`);
return;
}
+ // If we only have noop outputs, skip failure handling - this is a successful no-action scenario
+ if (hasOnlyNoopOutputs) {
+ core.info("Agent completed with only noop outputs - skipping failure handling");
+ return;
+ }
+
// Check if the failure was due to PR checkout (e.g., PR was merged and branch deleted)
// If checkout_pr_success is "false", skip creating an issue as this is expected behavior
if (agentConclusion === "failure" && checkoutPRSuccess === "false") {
diff --git a/actions/setup/js/handle_noop_message.cjs b/actions/setup/js/handle_noop_message.cjs
new file mode 100644
index 00000000000..8b3009a2f24
--- /dev/null
+++ b/actions/setup/js/handle_noop_message.cjs
@@ -0,0 +1,175 @@
+// @ts-check
+///
+
+const { getErrorMessage } = require("./error_helpers.cjs");
+const { sanitizeContent } = require("./sanitize_content.cjs");
+const { generateFooterWithExpiration } = require("./ephemerals.cjs");
+const { renderTemplate } = require("./messages_core.cjs");
+
+/**
+ * Search for or create the parent issue for all agentic workflow no-op runs
+ * @returns {Promise<{number: number, node_id: string}>} Parent issue number and node ID
+ */
+async function ensureAgentRunsIssue() {
+ const { owner, repo } = context.repo;
+ const parentTitle = "[agentic-workflows] Agent runs";
+ const parentLabel = "agentic-workflows";
+
+ core.info(`Searching for agent runs issue: "${parentTitle}"`);
+
+ // Search for existing agent runs issue
+ const searchQuery = `repo:${owner}/${repo} is:issue is:open label:${parentLabel} in:title "${parentTitle}"`;
+
+ try {
+ const searchResult = await github.rest.search.issuesAndPullRequests({
+ q: searchQuery,
+ per_page: 1,
+ });
+
+ if (searchResult.data.total_count > 0) {
+ const existingIssue = searchResult.data.items[0];
+ core.info(`Found existing agent runs issue #${existingIssue.number}: ${existingIssue.html_url}`);
+
+ return {
+ number: existingIssue.number,
+ node_id: existingIssue.node_id,
+ };
+ }
+ } catch (error) {
+ core.warning(`Error searching for agent runs issue: ${getErrorMessage(error)}`);
+ }
+
+ // Create agent runs issue if it doesn't exist
+ core.info(`No agent runs issue found, creating one`);
+
+ let parentBodyContent = `This issue tracks all no-op runs from agentic workflows in this repository. Each workflow run that completes with a no-op message (indicating no action was needed) posts a comment here.
+
+### Purpose
+
+This issue helps you:
+- Track workflows that ran but determined no action was needed
+- Distinguish between failures and intentional no-ops
+- Monitor workflow health by seeing when workflows decide not to act
+
+### What is a No-Op?
+
+A no-op (no operation) occurs when an agentic workflow runs successfully but determines that no action is required. For example:
+- A security scanner that finds no issues
+- An update checker that finds nothing to update
+- A monitoring workflow that finds everything is healthy
+
+These are successful outcomes, not failures, and help provide transparency into workflow behavior.
+
+### Resources
+
+- [GitHub Agentic Workflows Documentation](https://github.com/github/gh-aw)
+- [Safe Outputs Reference](https://github.com/github/gh-aw/blob/main/docs/reference/safe-outputs.md)
+
+---
+
+> This issue is automatically managed by GitHub Agentic Workflows. Do not close this issue manually.`;
+
+ // Add expiration marker (30 days from now) inside the quoted section using helper
+ const footer = generateFooterWithExpiration({
+ footerText: parentBodyContent,
+ expiresHours: 24 * 30, // 30 days
+ });
+ const parentBody = footer;
+
+ try {
+ const newIssue = await github.rest.issues.create({
+ owner,
+ repo,
+ title: parentTitle,
+ body: parentBody,
+ labels: [parentLabel],
+ });
+
+ core.info(`✓ Created agent runs issue #${newIssue.data.number}: ${newIssue.data.html_url}`);
+ return {
+ number: newIssue.data.number,
+ node_id: newIssue.data.node_id,
+ };
+ } catch (error) {
+ core.error(`Failed to create agent runs issue: ${getErrorMessage(error)}`);
+ throw error;
+ }
+}
+
+/**
+ * Handle posting a no-op message to the agent runs issue
+ * This script is called from the conclusion job when the agent produced only a noop safe-output
+ */
+async function main() {
+ try {
+ // Get workflow context
+ const workflowName = process.env.GH_AW_WORKFLOW_NAME || "unknown";
+ const runUrl = process.env.GH_AW_RUN_URL || "";
+ const noopMessage = process.env.GH_AW_NOOP_MESSAGE || "";
+
+ core.info(`Workflow name: ${workflowName}`);
+ core.info(`Run URL: ${runUrl}`);
+ core.info(`No-op message: ${noopMessage}`);
+
+ if (!noopMessage) {
+ core.info("No no-op message found, skipping");
+ return;
+ }
+
+ const { owner, repo } = context.repo;
+
+ // Ensure agent runs issue exists
+ let agentRunsIssue;
+ try {
+ agentRunsIssue = await ensureAgentRunsIssue();
+ } catch (error) {
+ core.warning(`Could not create agent runs issue: ${getErrorMessage(error)}`);
+ // Don't fail the workflow if we can't create the issue
+ return;
+ }
+
+ // Extract run ID from URL (e.g., https://github.com/owner/repo/actions/runs/123 -> "123")
+ let runId = "";
+ const runIdMatch = runUrl.match(/\/actions\/runs\/(\d+)/);
+ if (runIdMatch) {
+ runId = runIdMatch[1];
+ }
+
+ // Build the comment body
+ const timestamp = new Date().toISOString();
+ let commentBody = `### No-Op Run: ${sanitizeContent(workflowName)}
+
+**Run ID:** [${runId}](${runUrl})
+**Timestamp:** ${timestamp}
+
+**Message:**
+
+${sanitizeContent(noopMessage)}
+
+---
+
+*This workflow completed successfully with no action required.*`;
+
+ // Sanitize the full comment body
+ const fullCommentBody = sanitizeContent(commentBody, { maxLength: 65000 });
+
+ try {
+ await github.rest.issues.createComment({
+ owner,
+ repo,
+ issue_number: agentRunsIssue.number,
+ body: fullCommentBody,
+ });
+
+ core.info(`✓ Posted no-op message to agent runs issue #${agentRunsIssue.number}`);
+ } catch (error) {
+ core.warning(`Failed to post comment to agent runs issue: ${getErrorMessage(error)}`);
+ // Don't fail the workflow
+ }
+ } catch (error) {
+ core.warning(`Error in handle_noop_message: ${getErrorMessage(error)}`);
+ // Don't fail the workflow
+ }
+}
+
+module.exports = { main, ensureAgentRunsIssue };
diff --git a/actions/setup/js/handle_noop_message.test.cjs b/actions/setup/js/handle_noop_message.test.cjs
new file mode 100644
index 00000000000..c76b5d39361
--- /dev/null
+++ b/actions/setup/js/handle_noop_message.test.cjs
@@ -0,0 +1,239 @@
+// @ts-check
+
+const { describe, it, expect, beforeEach, vi } = require("vitest");
+
+describe("handle_noop_message", () => {
+ let mockCore;
+ let mockGithub;
+ let mockContext;
+ let originalEnv;
+
+ beforeEach(() => {
+ // Save original environment
+ originalEnv = { ...process.env };
+
+ // Mock core
+ mockCore = {
+ info: vi.fn(),
+ warning: vi.fn(),
+ error: vi.fn(),
+ };
+
+ // Mock GitHub API
+ mockGithub = {
+ rest: {
+ search: {
+ issuesAndPullRequests: vi.fn(),
+ },
+ issues: {
+ create: vi.fn(),
+ createComment: vi.fn(),
+ },
+ },
+ };
+
+ // Mock context
+ mockContext = {
+ repo: {
+ owner: "test-owner",
+ repo: "test-repo",
+ },
+ };
+
+ // Setup globals
+ global.core = mockCore;
+ global.github = mockGithub;
+ global.context = mockContext;
+ });
+
+ afterEach(() => {
+ // Restore environment
+ process.env = originalEnv;
+ vi.clearAllMocks();
+ });
+
+ it("should skip if no noop message is present", async () => {
+ process.env.GH_AW_WORKFLOW_NAME = "Test Workflow";
+ process.env.GH_AW_RUN_URL = "https://github.com/test-owner/test-repo/actions/runs/123";
+ process.env.GH_AW_NOOP_MESSAGE = "";
+
+ const { main } = require("./handle_noop_message.cjs");
+ await main();
+
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No no-op message found, skipping"));
+ expect(mockGithub.rest.search.issuesAndPullRequests).not.toHaveBeenCalled();
+ });
+
+ it("should create agent runs issue if it doesn't exist", async () => {
+ process.env.GH_AW_WORKFLOW_NAME = "Test Workflow";
+ process.env.GH_AW_RUN_URL = "https://github.com/test-owner/test-repo/actions/runs/123456";
+ process.env.GH_AW_NOOP_MESSAGE = "No updates needed";
+
+ // Mock search to return no results
+ mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+ data: {
+ total_count: 0,
+ items: [],
+ },
+ });
+
+ // Mock issue creation
+ mockGithub.rest.issues.create.mockResolvedValue({
+ data: {
+ number: 42,
+ node_id: "MDU6SXNzdWU0Mg==",
+ html_url: "https://github.com/test-owner/test-repo/issues/42",
+ },
+ });
+
+ // Mock comment creation
+ mockGithub.rest.issues.createComment.mockResolvedValue({
+ data: {
+ id: 1,
+ html_url: "https://github.com/test-owner/test-repo/issues/42#issuecomment-1",
+ },
+ });
+
+ const { main } = require("./handle_noop_message.cjs");
+ await main();
+
+ // Verify search was performed
+ expect(mockGithub.rest.search.issuesAndPullRequests).toHaveBeenCalledWith({
+ q: expect.stringContaining("[agentic-workflows] Agent runs"),
+ per_page: 1,
+ });
+
+ // Verify issue was created with correct title
+ const createCall = mockGithub.rest.issues.create.mock.calls[0][0];
+ expect(createCall.title).toBe("[agentic-workflows] Agent runs");
+ expect(createCall.labels).toContain("agentic-workflows");
+ expect(createCall.body).toContain("tracks all no-op runs");
+
+ // Verify comment was posted
+ const commentCall = mockGithub.rest.issues.createComment.mock.calls[0][0];
+ expect(commentCall.issue_number).toBe(42);
+ expect(commentCall.body).toContain("Test Workflow");
+ expect(commentCall.body).toContain("No updates needed");
+ expect(commentCall.body).toContain("123456");
+ });
+
+ it("should use existing agent runs issue if it exists", async () => {
+ process.env.GH_AW_WORKFLOW_NAME = "Another Workflow";
+ process.env.GH_AW_RUN_URL = "https://github.com/test-owner/test-repo/actions/runs/789";
+ process.env.GH_AW_NOOP_MESSAGE = "Everything is up to date";
+
+ // Mock search to return existing issue
+ mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+ data: {
+ total_count: 1,
+ items: [
+ {
+ number: 99,
+ node_id: "MDU6SXNzdWU5OQ==",
+ html_url: "https://github.com/test-owner/test-repo/issues/99",
+ },
+ ],
+ },
+ });
+
+ // Mock comment creation
+ mockGithub.rest.issues.createComment.mockResolvedValue({
+ data: {
+ id: 2,
+ html_url: "https://github.com/test-owner/test-repo/issues/99#issuecomment-2",
+ },
+ });
+
+ const { main } = require("./handle_noop_message.cjs");
+ await main();
+
+ // Verify issue was not created
+ expect(mockGithub.rest.issues.create).not.toHaveBeenCalled();
+
+ // Verify comment was posted to existing issue
+ const commentCall = mockGithub.rest.issues.createComment.mock.calls[0][0];
+ expect(commentCall.issue_number).toBe(99);
+ expect(commentCall.body).toContain("Another Workflow");
+ expect(commentCall.body).toContain("Everything is up to date");
+ });
+
+ it("should handle comment creation failure gracefully", async () => {
+ process.env.GH_AW_WORKFLOW_NAME = "Test Workflow";
+ process.env.GH_AW_RUN_URL = "https://github.com/test-owner/test-repo/actions/runs/456";
+ process.env.GH_AW_NOOP_MESSAGE = "No action required";
+
+ // Mock existing issue
+ mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+ data: {
+ total_count: 1,
+ items: [{ number: 10, node_id: "MDU6SXNzdWUxMA==", html_url: "https://github.com/test-owner/test-repo/issues/10" }],
+ },
+ });
+
+ // Mock comment creation failure
+ mockGithub.rest.issues.createComment.mockRejectedValue(new Error("API rate limit exceeded"));
+
+ const { main } = require("./handle_noop_message.cjs");
+ await main();
+
+ // Verify warning was logged but workflow didn't fail
+ expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Failed to post comment"));
+ });
+
+ it("should handle issue creation failure gracefully", async () => {
+ process.env.GH_AW_WORKFLOW_NAME = "Test Workflow";
+ process.env.GH_AW_RUN_URL = "https://github.com/test-owner/test-repo/actions/runs/789";
+ process.env.GH_AW_NOOP_MESSAGE = "All checks passed";
+
+ // Mock no existing issue
+ mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+ data: { total_count: 0, items: [] },
+ });
+
+ // Mock issue creation failure
+ mockGithub.rest.issues.create.mockRejectedValue(new Error("Insufficient permissions"));
+
+ const { main } = require("./handle_noop_message.cjs");
+ await main();
+
+ // Verify warning was logged but workflow didn't fail
+ expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Could not create agent runs issue"));
+ });
+
+ it("should extract run ID from URL correctly", async () => {
+ process.env.GH_AW_WORKFLOW_NAME = "Test";
+ process.env.GH_AW_RUN_URL = "https://github.com/owner/repo/actions/runs/987654321";
+ process.env.GH_AW_NOOP_MESSAGE = "Done";
+
+ mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+ data: { total_count: 1, items: [{ number: 1, node_id: "ID", html_url: "url" }] },
+ });
+
+ mockGithub.rest.issues.createComment.mockResolvedValue({ data: {} });
+
+ const { main } = require("./handle_noop_message.cjs");
+ await main();
+
+ const commentCall = mockGithub.rest.issues.createComment.mock.calls[0][0];
+ expect(commentCall.body).toContain("987654321");
+ });
+
+ it("should sanitize workflow name in comment", async () => {
+ process.env.GH_AW_WORKFLOW_NAME = "Test Workflow";
+ process.env.GH_AW_RUN_URL = "https://github.com/test/test/actions/runs/123";
+ process.env.GH_AW_NOOP_MESSAGE = "Clean";
+
+ mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+ data: { total_count: 1, items: [{ number: 1, node_id: "ID", html_url: "url" }] },
+ });
+
+ mockGithub.rest.issues.createComment.mockResolvedValue({ data: {} });
+
+ const { main } = require("./handle_noop_message.cjs");
+ await main();
+
+ const commentCall = mockGithub.rest.issues.createComment.mock.calls[0][0];
+ // Verify XSS attempt was sanitized (specific behavior depends on sanitizeContent implementation)
+ expect(commentCall.body).not.toContain(" Workflow";
process.env.GH_AW_RUN_URL = "https://github.com/test/test/actions/runs/123";
process.env.GH_AW_NOOP_MESSAGE = "Clean";
+ process.env.GH_AW_AGENT_CONCLUSION = "success";
+
+ // Mock loadAgentOutput to return only noop outputs
+ mockLoadAgentOutput.mockReturnValue({
+ success: true,
+ items: [{ type: "noop", message: "Clean" }],
+ });
mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
data: { total_count: 1, items: [{ number: 1, node_id: "ID", html_url: "url" }] },
@@ -229,7 +317,7 @@ describe("handle_noop_message", () => {
mockGithub.rest.issues.createComment.mockResolvedValue({ data: {} });
- const { main } = await import("./handle_noop_message.cjs");
+ const { main } = await import("./handle_noop_message.cjs?t=" + Date.now());
await main();
const commentCall = mockGithub.rest.issues.createComment.mock.calls[0][0];
diff --git a/actions/setup/js/handle_noop_message.test.cjs.bak b/actions/setup/js/handle_noop_message.test.cjs.bak
new file mode 100644
index 00000000000..1879aea3b90
--- /dev/null
+++ b/actions/setup/js/handle_noop_message.test.cjs.bak
@@ -0,0 +1,397 @@
+// @ts-check
+
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
+
+// Mock load_agent_output at the module level
+vi.mock("./load_agent_output.cjs", () => ({
+ loadAgentOutput: vi.fn(),
+}));
+
+describe("handle_noop_message", () => {
+ let mockCore;
+ let mockGithub;
+ let mockContext;
+ let originalEnv;
+ let mockLoadAgentOutput;
+
+ beforeEach(async () => {
+ // Save original environment
+ originalEnv = { ...process.env };
+
+ // Get the mocked loadAgentOutput
+ const loadModule = await import("./load_agent_output.cjs");
+ mockLoadAgentOutput = loadModule.loadAgentOutput;
+
+ // Mock core
+ mockCore = {
+ info: vi.fn(),
+ warning: vi.fn(),
+ error: vi.fn(),
+ };
+
+ // Mock GitHub API
+ mockGithub = {
+ rest: {
+ search: {
+ issuesAndPullRequests: vi.fn(),
+ },
+ issues: {
+ create: vi.fn(),
+ createComment: vi.fn(),
+ },
+ },
+ };
+
+ // Mock context
+ mockContext = {
+ repo: {
+ owner: "test-owner",
+ repo: "test-repo",
+ },
+ };
+
+ // Setup globals
+ global.core = mockCore;
+ global.github = mockGithub;
+ global.context = mockContext;
+ });
+
+ afterEach(() => {
+ // Restore environment
+ process.env = originalEnv;
+ vi.clearAllMocks();
+ });
+
+ it("should skip if no noop message is present", async () => {
+ process.env.GH_AW_WORKFLOW_NAME = "Test Workflow";
+ process.env.GH_AW_RUN_URL = "https://github.com/test-owner/test-repo/actions/runs/123";
+ process.env.GH_AW_NOOP_MESSAGE = "";
+ process.env.GH_AW_AGENT_CONCLUSION = "success";
+
+ const { main } = await import("./handle_noop_message.cjs");
+ await main();
+
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No no-op message found, skipping"));
+ expect(mockGithub.rest.search.issuesAndPullRequests).not.toHaveBeenCalled();
+ });
+
+ it("should skip if agent did not succeed", async () => {
+ process.env.GH_AW_WORKFLOW_NAME = "Test Workflow";
+ process.env.GH_AW_RUN_URL = "https://github.com/test-owner/test-repo/actions/runs/123";
+ process.env.GH_AW_NOOP_MESSAGE = "Some message";
+ process.env.GH_AW_AGENT_CONCLUSION = "failure";
+
+ const { main } = await import("./handle_noop_message.cjs");
+ await main();
+
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Agent did not succeed"));
+ expect(mockGithub.rest.search.issuesAndPullRequests).not.toHaveBeenCalled();
+ });
+
+ it("should skip if there are non-noop outputs", async () => {
+ process.env.GH_AW_WORKFLOW_NAME = "Test Workflow";
+ process.env.GH_AW_RUN_URL = "https://github.com/test-owner/test-repo/actions/runs/123";
+ process.env.GH_AW_NOOP_MESSAGE = "Some message";
+ process.env.GH_AW_AGENT_CONCLUSION = "success";
+ process.env.GH_AW_AGENT_OUTPUT = "/tmp/agent_output.json";
+
+ // Mock loadAgentOutput to return noop + other outputs
+ const fs = require("fs");
+ const originalReadFileSync = fs.readFileSync;
+ fs.readFileSync = vi.fn(filePath => {
+ if (filePath === "/tmp/agent_output.json") {
+ return JSON.stringify({
+ items: [
+ { type: "noop", message: "No action needed" },
+ { type: "create_issue", title: "Some issue" },
+ ],
+ });
+ }
+ return originalReadFileSync(filePath);
+ });
+
+ const { main } = await import("./handle_noop_message.cjs");
+ await main();
+
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Found 1 non-noop output(s)"));
+ expect(mockGithub.rest.search.issuesAndPullRequests).not.toHaveBeenCalled();
+
+ // Restore
+ fs.readFileSync = originalReadFileSync;
+ });
+
+ it("should create agent runs issue if it doesn't exist", async () => {
+ process.env.GH_AW_WORKFLOW_NAME = "Test Workflow";
+ process.env.GH_AW_RUN_URL = "https://github.com/test-owner/test-repo/actions/runs/123456";
+ process.env.GH_AW_NOOP_MESSAGE = "No updates needed";
+ process.env.GH_AW_AGENT_CONCLUSION = "success";
+ process.env.GH_AW_AGENT_OUTPUT = "/tmp/agent_output.json";
+
+ // Mock loadAgentOutput to return only noop outputs
+ const fs = require("fs");
+ const originalReadFileSync = fs.readFileSync;
+ fs.readFileSync = vi.fn(filePath => {
+ if (filePath === "/tmp/agent_output.json") {
+ return JSON.stringify({
+ items: [{ type: "noop", message: "No updates needed" }],
+ });
+ }
+ return originalReadFileSync(filePath);
+ });
+
+ // Mock search to return no results
+ mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+ data: {
+ total_count: 0,
+ items: [],
+ },
+ });
+
+ // Mock issue creation
+ mockGithub.rest.issues.create.mockResolvedValue({
+ data: {
+ number: 42,
+ node_id: "MDU6SXNzdWU0Mg==",
+ html_url: "https://github.com/test-owner/test-repo/issues/42",
+ },
+ });
+
+ // Mock comment creation
+ mockGithub.rest.issues.createComment.mockResolvedValue({
+ data: {
+ id: 1,
+ html_url: "https://github.com/test-owner/test-repo/issues/42#issuecomment-1",
+ },
+ });
+
+ const { main } = await import("./handle_noop_message.cjs");
+ await main();
+
+ // Verify search was performed
+ expect(mockGithub.rest.search.issuesAndPullRequests).toHaveBeenCalledWith({
+ q: expect.stringContaining("[agentic-workflows] Agent runs"),
+ per_page: 1,
+ });
+
+ // Verify issue was created with correct title
+ const createCall = mockGithub.rest.issues.create.mock.calls[0][0];
+ expect(createCall.title).toBe("[agentic-workflows] Agent runs");
+ expect(createCall.labels).toContain("agentic-workflows");
+ expect(createCall.body).toContain("tracks all no-op runs");
+
+ // Verify comment was posted
+ const commentCall = mockGithub.rest.issues.createComment.mock.calls[0][0];
+ expect(commentCall.issue_number).toBe(42);
+ expect(commentCall.body).toContain("Test Workflow");
+ expect(commentCall.body).toContain("No updates needed");
+ expect(commentCall.body).toContain("123456");
+
+ // Restore
+ fs.readFileSync = originalReadFileSync;
+ });
+
+ it("should use existing agent runs issue if it exists", async () => {
+ process.env.GH_AW_WORKFLOW_NAME = "Another Workflow";
+ process.env.GH_AW_RUN_URL = "https://github.com/test-owner/test-repo/actions/runs/789";
+ process.env.GH_AW_NOOP_MESSAGE = "Everything is up to date";
+ process.env.GH_AW_AGENT_CONCLUSION = "success";
+ process.env.GH_AW_AGENT_OUTPUT = "/tmp/agent_output.json";
+
+ // Mock loadAgentOutput to return only noop outputs
+ const fs = require("fs");
+ const originalReadFileSync = fs.readFileSync;
+ fs.readFileSync = vi.fn(filePath => {
+ if (filePath === "/tmp/agent_output.json") {
+ return JSON.stringify({
+ items: [{ type: "noop", message: "Everything is up to date" }],
+ });
+ }
+ return originalReadFileSync(filePath);
+ });
+
+ // Mock search to return existing issue
+ mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+ data: {
+ total_count: 1,
+ items: [
+ {
+ number: 99,
+ node_id: "MDU6SXNzdWU5OQ==",
+ html_url: "https://github.com/test-owner/test-repo/issues/99",
+ },
+ ],
+ },
+ });
+
+ // Mock comment creation
+ mockGithub.rest.issues.createComment.mockResolvedValue({
+ data: {
+ id: 2,
+ html_url: "https://github.com/test-owner/test-repo/issues/99#issuecomment-2",
+ },
+ });
+
+ const { main } = await import("./handle_noop_message.cjs");
+ await main();
+
+ // Verify issue was not created
+ expect(mockGithub.rest.issues.create).not.toHaveBeenCalled();
+
+ // Verify comment was posted to existing issue
+ const commentCall = mockGithub.rest.issues.createComment.mock.calls[0][0];
+ expect(commentCall.issue_number).toBe(99);
+ expect(commentCall.body).toContain("Another Workflow");
+ expect(commentCall.body).toContain("Everything is up to date");
+
+ // Restore
+ fs.readFileSync = originalReadFileSync;
+ });
+
+ it("should handle comment creation failure gracefully", async () => {
+ process.env.GH_AW_WORKFLOW_NAME = "Test Workflow";
+ process.env.GH_AW_RUN_URL = "https://github.com/test-owner/test-repo/actions/runs/456";
+ process.env.GH_AW_NOOP_MESSAGE = "No action required";
+ process.env.GH_AW_AGENT_CONCLUSION = "success";
+ process.env.GH_AW_AGENT_OUTPUT = "/tmp/agent_output.json";
+
+ // Mock loadAgentOutput to return only noop outputs
+ const fs = require("fs");
+ const originalReadFileSync = fs.readFileSync;
+ fs.readFileSync = vi.fn(filePath => {
+ if (filePath === "/tmp/agent_output.json") {
+ return JSON.stringify({
+ items: [{ type: "noop", message: "No action required" }],
+ });
+ }
+ return originalReadFileSync(filePath);
+ });
+
+ // Mock existing issue
+ mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+ data: {
+ total_count: 1,
+ items: [{ number: 10, node_id: "MDU6SXNzdWUxMA==", html_url: "https://github.com/test-owner/test-repo/issues/10" }],
+ },
+ });
+
+ // Mock comment creation failure
+ mockGithub.rest.issues.createComment.mockRejectedValue(new Error("API rate limit exceeded"));
+
+ const { main } = await import("./handle_noop_message.cjs");
+ await main();
+
+ // Verify warning was logged but workflow didn't fail
+ expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Failed to post comment"));
+
+ // Restore
+ fs.readFileSync = originalReadFileSync;
+ });
+
+ it("should handle issue creation failure gracefully", async () => {
+ process.env.GH_AW_WORKFLOW_NAME = "Test Workflow";
+ process.env.GH_AW_RUN_URL = "https://github.com/test-owner/test-repo/actions/runs/789";
+ process.env.GH_AW_NOOP_MESSAGE = "All checks passed";
+ process.env.GH_AW_AGENT_CONCLUSION = "success";
+ process.env.GH_AW_AGENT_OUTPUT = "/tmp/agent_output.json";
+
+ // Mock loadAgentOutput to return only noop outputs
+ const fs = require("fs");
+ const originalReadFileSync = fs.readFileSync;
+ fs.readFileSync = vi.fn(filePath => {
+ if (filePath === "/tmp/agent_output.json") {
+ return JSON.stringify({
+ items: [{ type: "noop", message: "All checks passed" }],
+ });
+ }
+ return originalReadFileSync(filePath);
+ });
+
+ // Mock no existing issue
+ mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+ data: { total_count: 0, items: [] },
+ });
+
+ // Mock issue creation failure
+ mockGithub.rest.issues.create.mockRejectedValue(new Error("Insufficient permissions"));
+
+ const { main } = await import("./handle_noop_message.cjs");
+ await main();
+
+ // Verify warning was logged but workflow didn't fail
+ expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Could not create agent runs issue"));
+
+ // Restore
+ fs.readFileSync = originalReadFileSync;
+ });
+
+ it("should extract run ID from URL correctly", async () => {
+ process.env.GH_AW_WORKFLOW_NAME = "Test";
+ process.env.GH_AW_RUN_URL = "https://github.com/owner/repo/actions/runs/987654321";
+ process.env.GH_AW_NOOP_MESSAGE = "Done";
+ process.env.GH_AW_AGENT_CONCLUSION = "success";
+ process.env.GH_AW_AGENT_OUTPUT = "/tmp/agent_output.json";
+
+ // Mock loadAgentOutput to return only noop outputs
+ const fs = require("fs");
+ const originalReadFileSync = fs.readFileSync;
+ fs.readFileSync = vi.fn(filePath => {
+ if (filePath === "/tmp/agent_output.json") {
+ return JSON.stringify({
+ items: [{ type: "noop", message: "Done" }],
+ });
+ }
+ return originalReadFileSync(filePath);
+ });
+
+ mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+ data: { total_count: 1, items: [{ number: 1, node_id: "ID", html_url: "url" }] },
+ });
+
+ mockGithub.rest.issues.createComment.mockResolvedValue({ data: {} });
+
+ const { main } = await import("./handle_noop_message.cjs");
+ await main();
+
+ const commentCall = mockGithub.rest.issues.createComment.mock.calls[0][0];
+ expect(commentCall.body).toContain("987654321");
+
+ // Restore
+ fs.readFileSync = originalReadFileSync;
+ });
+
+ it("should sanitize workflow name in comment", async () => {
+ process.env.GH_AW_WORKFLOW_NAME = "Test Workflow";
+ process.env.GH_AW_RUN_URL = "https://github.com/test/test/actions/runs/123";
+ process.env.GH_AW_NOOP_MESSAGE = "Clean";
+ process.env.GH_AW_AGENT_CONCLUSION = "success";
+ process.env.GH_AW_AGENT_OUTPUT = "/tmp/agent_output.json";
+
+ // Mock loadAgentOutput to return only noop outputs
+ const fs = require("fs");
+ const originalReadFileSync = fs.readFileSync;
+ fs.readFileSync = vi.fn(filePath => {
+ if (filePath === "/tmp/agent_output.json") {
+ return JSON.stringify({
+ items: [{ type: "noop", message: "Clean" }],
+ });
+ }
+ return originalReadFileSync(filePath);
+ });
+
+ mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+ data: { total_count: 1, items: [{ number: 1, node_id: "ID", html_url: "url" }] },
+ });
+
+ mockGithub.rest.issues.createComment.mockResolvedValue({ data: {} });
+
+ const { main } = await import("./handle_noop_message.cjs");
+ await main();
+
+ const commentCall = mockGithub.rest.issues.createComment.mock.calls[0][0];
+ // Verify XSS attempt was sanitized (specific behavior depends on sanitizeContent implementation)
+ expect(commentCall.body).not.toContain("