Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 22 additions & 48 deletions actions/setup/js/add_comment.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

const { generateFooterWithMessages, generateXMLMarker } = require("./messages_footer.cjs");
const { generateWorkflowCallIdMarker } = require("./generate_footer.cjs");
const { generateWorkflowCallIdMarker, matchesWorkflowId } = require("./generate_footer.cjs");
const { getRepositoryUrl } = require("./get_repository_url.cjs");
const { replaceTemporaryIdReferences, loadTemporaryIdMapFromResolved, resolveRepoIssueTarget } = require("./temporary_id.cjs");
const { getTrackerID } = require("./get_tracker_id.cjs");
Expand All @@ -28,7 +28,6 @@ const { generateHistoryUrl } = require("./generate_history_link.cjs");
/** @type {string} Safe output type handled by this module */
const HANDLER_TYPE = "add_comment";

// Copy helper functions from original file
async function minimizeComment(github, nodeId, reason = "outdated") {
const query = /* GraphQL */ `
mutation ($nodeId: ID!, $classifier: ReportedContentClassifiers!) {
Expand Down Expand Up @@ -76,19 +75,7 @@ async function findCommentsWithTrackerId(github, owner, repo, issueNumber, workf
break;
}

// Filter comments that contain the workflow-id and are NOT reaction comments.
// Supports both the standalone marker format (<!-- gh-aw-workflow-id: value -->)
// and the combined XML marker format (<!-- gh-aw-agentic-workflow: ..., workflow_id: value, ... -->).
const filteredComments = data
.filter(comment => {
if (!comment.body || comment.body.includes(`<!-- gh-aw-comment-type: reaction -->`)) return false;
// Standalone marker: <!-- gh-aw-workflow-id: value -->
if (comment.body.includes(`<!-- gh-aw-workflow-id: ${workflowId} -->`)) return true;
// Combined XML marker: <!-- gh-aw-agentic-workflow: ..., workflow_id: value, ... -->
if (comment.body.includes(`<!-- gh-aw-agentic-workflow:`) && (comment.body.includes(`workflow_id: ${workflowId},`) || comment.body.includes(`workflow_id: ${workflowId} -->`))) return true;
return false;
})
.map(({ id, node_id, body }) => ({ id, node_id, body }));
const filteredComments = data.filter(comment => matchesWorkflowId(comment.body, workflowId)).map(({ id, node_id, body }) => ({ id, node_id, body }));

comments.push(...filteredComments);

Expand Down Expand Up @@ -141,16 +128,7 @@ async function findDiscussionCommentsWithTrackerId(github, owner, repo, discussi
break;
}

const filteredComments = result.repository.discussion.comments.nodes
.filter(comment => {
if (!comment.body || comment.body.includes(`<!-- gh-aw-comment-type: reaction -->`)) return false;
// Standalone marker: <!-- gh-aw-workflow-id: value -->
if (comment.body.includes(`<!-- gh-aw-workflow-id: ${workflowId} -->`)) return true;
// Combined XML marker: <!-- gh-aw-agentic-workflow: ..., workflow_id: value, ... -->
if (comment.body.includes(`<!-- gh-aw-agentic-workflow:`) && (comment.body.includes(`workflow_id: ${workflowId},`) || comment.body.includes(`workflow_id: ${workflowId} -->`))) return true;
return false;
})
.map(({ id, body }) => ({ id, body }));
const filteredComments = result.repository.discussion.comments.nodes.filter(comment => matchesWorkflowId(comment.body, workflowId)).map(({ id, body }) => ({ id, body }));

comments.push(...filteredComments);

Expand Down Expand Up @@ -260,28 +238,28 @@ async function commentOnDiscussion(github, owner, repo, discussionNumber, messag

// 2. Add comment (with optional replyToId for threading)
const mutation = replyToId
? `mutation($dId: ID!, $body: String!, $replyToId: ID!) {
addDiscussionComment(input: { discussionId: $dId, body: $body, replyToId: $replyToId }) {
comment {
id
body
createdAt
url
? /* GraphQL */ `
mutation ($dId: ID!, $body: String!, $replyToId: ID!) {
addDiscussionComment(input: { discussionId: $dId, body: $body, replyToId: $replyToId }) {
comment {
id
url
}
}
}
}`
: `mutation($dId: ID!, $body: String!) {
addDiscussionComment(input: { discussionId: $dId, body: $body }) {
comment {
id
body
createdAt
url
`
: /* GraphQL */ `
mutation ($dId: ID!, $body: String!) {
addDiscussionComment(input: { discussionId: $dId, body: $body }) {
comment {
id
url
}
}
}
}`;
`;

const variables = replyToId ? { dId: discussionId, body: message, replyToId } : { dId: discussionId, body: message };
const variables = { dId: discussionId, body: message, ...(replyToId ? { replyToId } : {}) };

const result = await github.graphql(mutation, variables);

Expand Down Expand Up @@ -358,12 +336,8 @@ async function main(config = {}) {
processedCount++;

// Merge resolved temp IDs
if (resolvedTemporaryIds) {
for (const [tempId, resolved] of Object.entries(resolvedTemporaryIds)) {
if (!temporaryIdMap.has(tempId)) {
temporaryIdMap.set(tempId, resolved);
}
}
for (const [tempId, resolved] of Object.entries(resolvedTemporaryIds ?? {})) {
if (!temporaryIdMap.has(tempId)) temporaryIdMap.set(tempId, resolved);
}

// Resolve and validate target repository
Expand Down
177 changes: 177 additions & 0 deletions actions/setup/js/add_comment.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1854,6 +1854,183 @@ describe("add_comment", () => {
});
});

describe("staged mode", () => {
it("should return staged preview without creating comment", async () => {
const addCommentScript = fs.readFileSync(path.join(__dirname, "add_comment.cjs"), "utf8");

const originalStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED;
process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true";

try {
let createCommentCalled = false;
mockGithub.rest.issues.createComment = async () => {
createCommentCalled = true;
return { data: { id: 1, html_url: "https://github.com/owner/repo/issues/8535#issuecomment-1" } };
};

const handler = await eval(`(async () => { ${addCommentScript}; return await main({}); })()`);

const message = { type: "add_comment", body: "Staged comment preview" };
const result = await handler(message, {});

expect(result.success).toBe(true);
expect(result.staged).toBe(true);
expect(result.previewInfo).toBeDefined();
expect(result.previewInfo.itemNumber).toBe(8535);
expect(createCommentCalled).toBe(false);
} finally {
if (originalStaged === undefined) {
delete process.env.GH_AW_SAFE_OUTPUTS_STAGED;
} else {
process.env.GH_AW_SAFE_OUTPUTS_STAGED = originalStaged;
}
}
});

it("should return staged preview for discussion when staged mode is set via config", async () => {
const addCommentScript = fs.readFileSync(path.join(__dirname, "add_comment.cjs"), "utf8");

mockContext.eventName = "discussion";
mockContext.payload = { discussion: { number: 55 } };

let createCommentCalled = false;
mockGithub.graphql = async () => {
createCommentCalled = true;
return {};
};

const handler = await eval(`(async () => { ${addCommentScript}; return await main({ staged: true }); })()`);

const message = { type: "add_comment", body: "Staged discussion preview" };
const result = await handler(message, {});

expect(result.success).toBe(true);
expect(result.staged).toBe(true);
expect(result.previewInfo.isDiscussion).toBe(true);
expect(result.previewInfo.itemNumber).toBe(55);
expect(createCommentCalled).toBe(false);
});
});

describe("max count enforcement", () => {
it("should skip comments beyond max count", async () => {
const addCommentScript = fs.readFileSync(path.join(__dirname, "add_comment.cjs"), "utf8");

const handler = await eval(`(async () => { ${addCommentScript}; return await main({ max: 2 }); })()`);

const message = { type: "add_comment", body: "Comment" };

// First two should succeed
const result1 = await handler(message, {});
const result2 = await handler(message, {});
// Third should be skipped
const result3 = await handler(message, {});

expect(result1.success).toBe(true);
expect(result2.success).toBe(true);
expect(result3.success).toBe(false);
expect(result3.error).toMatch(/max count/i);
});
});

describe("footer configuration", () => {
it("should include only XML marker (no attribution text) when footer is disabled", async () => {
const addCommentScript = fs.readFileSync(path.join(__dirname, "add_comment.cjs"), "utf8");

const originalWorkflowName = process.env.GH_AW_WORKFLOW_NAME;
process.env.GH_AW_WORKFLOW_NAME = "No-Footer Workflow";

try {
let capturedBody = null;
mockGithub.rest.issues.createComment = async params => {
capturedBody = params.body;
return { data: { id: 1, html_url: "https://github.com/owner/repo/issues/8535#issuecomment-1" } };
};

const handler = await eval(`(async () => { ${addCommentScript}; return await main({ footer: false }); })()`);

const message = { type: "add_comment", body: "No footer comment" };
const result = await handler(message, {});

expect(result.success).toBe(true);
expect(capturedBody).not.toContain("Generated by");
expect(capturedBody).toContain("<!-- gh-aw-agentic-workflow:");
} finally {
if (originalWorkflowName === undefined) {
delete process.env.GH_AW_WORKFLOW_NAME;
} else {
process.env.GH_AW_WORKFLOW_NAME = originalWorkflowName;
}
}
});
});

describe("hide-older-comments behavior", () => {
it("should skip hiding when no workflow ID is set", async () => {
const addCommentScript = fs.readFileSync(path.join(__dirname, "add_comment.cjs"), "utf8");

const originalWorkflowId = process.env.GH_AW_WORKFLOW_ID;
delete process.env.GH_AW_WORKFLOW_ID;

try {
let listCommentsCalled = false;
mockGithub.rest.issues.listComments = async () => {
listCommentsCalled = true;
return { data: [] };
};
mockGithub.rest.issues.createComment = async () => ({
data: { id: 1, html_url: "https://github.com/owner/repo/issues/8535#issuecomment-1" },
});

const handler = await eval(`(async () => { ${addCommentScript}; return await main({ hide_older_comments: true }); })()`);

const message = { type: "add_comment", body: "Comment without workflow ID" };
const result = await handler(message, {});

expect(result.success).toBe(true);
expect(listCommentsCalled).toBe(false);
} finally {
if (originalWorkflowId === undefined) {
delete process.env.GH_AW_WORKFLOW_ID;
} else {
process.env.GH_AW_WORKFLOW_ID = originalWorkflowId;
}
}
});

it("should skip hiding when hide_older_comments is false (default)", async () => {
const addCommentScript = fs.readFileSync(path.join(__dirname, "add_comment.cjs"), "utf8");

const originalWorkflowId = process.env.GH_AW_WORKFLOW_ID;
process.env.GH_AW_WORKFLOW_ID = "test-workflow";

try {
let listCommentsCalled = false;
mockGithub.rest.issues.listComments = async () => {
listCommentsCalled = true;
return { data: [] };
};
mockGithub.rest.issues.createComment = async () => ({
data: { id: 1, html_url: "https://github.com/owner/repo/issues/8535#issuecomment-1" },
});

const handler = await eval(`(async () => { ${addCommentScript}; return await main({}); })()`);

const message = { type: "add_comment", body: "Comment with hide disabled" };
const result = await handler(message, {});

expect(result.success).toBe(true);
expect(listCommentsCalled).toBe(false);
} finally {
if (originalWorkflowId === undefined) {
delete process.env.GH_AW_WORKFLOW_ID;
} else {
process.env.GH_AW_WORKFLOW_ID = originalWorkflowId;
}
}
});
});

let enforceCommentLimits;
let MAX_COMMENT_LENGTH;
let MAX_MENTIONS;
Expand Down
14 changes: 14 additions & 0 deletions actions/setup/js/generate_footer.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ function getWorkflowIdMarkerContent(workflowId) {
return `gh-aw-workflow-id: ${workflowId}`;
}

/**
* Check if a comment body matches a workflow ID marker.
* Supports standalone (<!-- gh-aw-workflow-id: value -->) and combined XML marker formats.
* @param {string|null|undefined} body - Comment body
* @param {string} workflowId - Workflow ID to match
* @returns {boolean}
*/
function matchesWorkflowId(body, workflowId) {
if (!body || body.includes(`<!-- gh-aw-comment-type: reaction -->`)) return false;
if (body.includes(`<!-- gh-aw-workflow-id: ${workflowId} -->`)) return true;
return body.includes(`<!-- gh-aw-agentic-workflow:`) && (body.includes(`workflow_id: ${workflowId},`) || body.includes(`workflow_id: ${workflowId} -->`));
}

/**
* Generates an XML comment marker with agentic workflow metadata for traceability.
* This marker enables searching and tracing back items generated by an agentic workflow.
Expand Down Expand Up @@ -170,6 +183,7 @@ module.exports = {
generateWorkflowIdMarker,
generateWorkflowCallIdMarker,
getWorkflowIdMarkerContent,
matchesWorkflowId,
generateExpiredEntityFooter,
normalizeCloseOlderKey,
generateCloseKeyMarker,
Expand Down
Loading