From f4cc87372f6a4959d0b4060459f1ba177a09a13c Mon Sep 17 00:00:00 2001 From: Shlomi Date: Mon, 18 Aug 2025 01:51:03 +0300 Subject: [PATCH] feat: add comment-tag support for create-or-update functionality --- action.yml | 2 + dist/index.js | 93 ++++++++++++++++++-- src/create-or-update-comment.ts | 150 ++++++++++++++++++++++++++++---- src/main.ts | 8 ++ 4 files changed, 228 insertions(+), 25 deletions(-) diff --git a/action.yml b/action.yml index 29d32e1f..234def8b 100644 --- a/action.yml +++ b/action.yml @@ -11,6 +11,8 @@ inputs: description: 'The number of the issue or pull request in which to create a comment.' comment-id: description: 'The id of the comment to update.' + comment-tag: + description: 'A unique identifier to find and update existing comments. If no matching comment is found, creates a new one.' body: description: 'The comment body. Cannot be used in conjunction with `body-path`.' body-path: diff --git a/dist/index.js b/dist/index.js index 0711893a..2ca54fc9 100644 --- a/dist/index.js +++ b/dist/index.js @@ -61,6 +61,41 @@ const REACTION_TYPES = [ 'rocket', 'eyes' ]; +function createCommentTag(tag) { + return ``; +} +function extractCommentTag(body) { + const match = body.match(//); + return match ? match[1] : null; +} +function addCommentTagToBody(body, tag) { + const commentTag = createCommentTag(tag); + return `${commentTag}\n${body}`; +} +function findCommentByTag(octokit, owner, repo, issueNumber, tag) { + return __awaiter(this, void 0, void 0, function* () { + try { + const comments = yield octokit.paginate(octokit.rest.issues.listComments, { + owner: owner, + repo: repo, + issue_number: issueNumber + }); + const targetTag = createCommentTag(tag); + for (const comment of comments) { + if (comment.body && comment.body.includes(targetTag)) { + core.info(`Found existing comment with tag '${tag}' - comment id '${comment.id}'.`); + return comment.id; + } + } + core.info(`No existing comment found with tag '${tag}'.`); + return null; + } + catch (error) { + core.warning(`Failed to search for comments with tag '${tag}': ${utils.getErrorMessage(error)}`); + return null; + } + }); +} function getReactionsSet(reactions) { const reactionsSet = [ ...new Set(reactions.filter(item => { @@ -137,8 +172,11 @@ function truncateBody(body) { } return body; } -function createComment(octokit, owner, repo, issueNumber, body) { +function createComment(octokit, owner, repo, issueNumber, body, commentTag) { return __awaiter(this, void 0, void 0, function* () { + if (commentTag) { + body = addCommentTagToBody(body, commentTag); + } body = truncateBody(body); const { data: comment } = yield octokit.rest.issues.createComment({ owner: owner, @@ -146,11 +184,14 @@ function createComment(octokit, owner, repo, issueNumber, body) { issue_number: issueNumber, body }); - core.info(`Created comment id '${comment.id}' on issue '${issueNumber}'.`); + const logMessage = commentTag + ? `Created comment id '${comment.id}' with tag '${commentTag}' on issue '${issueNumber}'.` + : `Created comment id '${comment.id}' on issue '${issueNumber}'.`; + core.info(logMessage); return comment.id; }); } -function updateComment(octokit, owner, repo, commentId, body, editMode, appendSeparator) { +function updateComment(octokit, owner, repo, commentId, body, editMode, appendSeparator, commentTag) { return __awaiter(this, void 0, void 0, function* () { if (body) { let commentBody = ''; @@ -163,6 +204,19 @@ function updateComment(octokit, owner, repo, commentId, body, editMode, appendSe }); commentBody = appendSeparatorTo(comment.body ? comment.body : '', appendSeparator); } + else if (editMode === 'replace' && commentTag) { + // For replace mode with comment-tag, preserve the tag + const { data: comment } = yield octokit.rest.issues.getComment({ + owner: owner, + repo: repo, + comment_id: commentId + }); + const existingTag = extractCommentTag(comment.body || ''); + if (existingTag === commentTag) { + // Preserve the existing tag + body = addCommentTagToBody(body, commentTag); + } + } commentBody = truncateBody(commentBody + body); core.debug(`Comment body: ${commentBody}`); yield octokit.rest.issues.updateComment({ @@ -237,9 +291,27 @@ function createOrUpdateComment(inputs, body) { return __awaiter(this, void 0, void 0, function* () { const [owner, repo] = inputs.repository.split('/'); const octokit = github.getOctokit(inputs.token); - const commentId = inputs.commentId - ? yield updateComment(octokit, owner, repo, inputs.commentId, body, inputs.editMode, inputs.appendSeparator) - : yield createComment(octokit, owner, repo, inputs.issueNumber, body); + let commentId; + if (inputs.commentId) { + // Direct update using comment-id (existing behavior) + commentId = yield updateComment(octokit, owner, repo, inputs.commentId, body, inputs.editMode, inputs.appendSeparator); + } + else if (inputs.commentTag) { + // Find existing comment by tag or create new one + const existingCommentId = yield findCommentByTag(octokit, owner, repo, inputs.issueNumber, inputs.commentTag); + if (existingCommentId) { + // Update existing comment found by tag + commentId = yield updateComment(octokit, owner, repo, existingCommentId, body, inputs.editMode, inputs.appendSeparator, inputs.commentTag); + } + else { + // Create new comment with tag + commentId = yield createComment(octokit, owner, repo, inputs.issueNumber, body, inputs.commentTag); + } + } + else { + // Create new comment without tag (existing behavior) + commentId = yield createComment(octokit, owner, repo, inputs.issueNumber, body); + } core.setOutput('comment-id', commentId); if (inputs.reactions) { const reactionsSet = getReactionsSet(inputs.reactions); @@ -322,6 +394,7 @@ function run() { repository: core.getInput('repository'), issueNumber: Number(core.getInput('issue-number')), commentId: Number(core.getInput('comment-id')), + commentTag: core.getInput('comment-tag'), body: core.getInput('body'), bodyPath: core.getInput('body-path') || core.getInput('body-file'), editMode: core.getInput('edit-mode'), @@ -353,6 +426,14 @@ function run() { throw new Error("Missing comment 'body', 'body-path', or 'reactions'."); } } + else if (inputs.commentTag) { + if (!inputs.issueNumber) { + throw new Error("Missing 'issue-number' when using 'comment-tag'."); + } + if (!body) { + throw new Error("Missing comment 'body' or 'body-path' when using 'comment-tag'."); + } + } else if (inputs.issueNumber) { if (!body) { throw new Error("Missing comment 'body' or 'body-path'."); diff --git a/src/create-or-update-comment.ts b/src/create-or-update-comment.ts index f6be7b93..a803e1d4 100644 --- a/src/create-or-update-comment.ts +++ b/src/create-or-update-comment.ts @@ -8,6 +8,7 @@ export interface Inputs { repository: string issueNumber: number commentId: number + commentTag: string body: string bodyPath: string editMode: string @@ -27,6 +28,52 @@ const REACTION_TYPES = [ 'eyes' ] +function createCommentTag(tag: string): string { + // Sanitize the tag to prevent HTML injection + const sanitizedTag = tag.replace(/[<>]/g, '').replace(/--/g, '-') + return `` +} + +function extractCommentTag(body: string): string | null { + const match = body.match(//) + return match ? match[1] : null +} + +function addCommentTagToBody(body: string, tag: string): string { + const commentTag = createCommentTag(tag) + return `${commentTag}\n${body}` +} + +async function findCommentByTag( + octokit, + owner: string, + repo: string, + issueNumber: number, + tag: string +): Promise { + try { + const comments = await octokit.paginate(octokit.rest.issues.listComments, { + owner: owner, + repo: repo, + issue_number: issueNumber + }) + + const targetTag = createCommentTag(tag) + for (const comment of comments) { + if (comment.body && comment.body.includes(targetTag)) { + core.info(`Found existing comment with tag '${tag}' - comment id '${comment.id}'.`) + return comment.id + } + } + + core.info(`No existing comment found with tag '${tag}'.`) + return null + } catch (error) { + core.warning(`Failed to search for comments with tag '${tag}': ${utils.getErrorMessage(error)}`) + return null + } +} + function getReactionsSet(reactions: string[]): string[] { const reactionsSet = [ ...new Set( @@ -133,8 +180,13 @@ async function createComment( owner: string, repo: string, issueNumber: number, - body: string + body: string, + commentTag?: string ): Promise { + if (commentTag) { + body = addCommentTagToBody(body, commentTag) + } + body = truncateBody(body) const {data: comment} = await octokit.rest.issues.createComment({ @@ -143,7 +195,12 @@ async function createComment( issue_number: issueNumber, body }) - core.info(`Created comment id '${comment.id}' on issue '${issueNumber}'.`) + + const logMessage = commentTag + ? `Created comment id '${comment.id}' with tag '${commentTag}' on issue '${issueNumber}'.` + : `Created comment id '${comment.id}' on issue '${issueNumber}'.` + core.info(logMessage) + return comment.id } @@ -154,23 +211,39 @@ async function updateComment( commentId: number, body: string, editMode: string, - appendSeparator: string + appendSeparator: string, + commentTag?: string ): Promise { if (body) { let commentBody = '' + + // Get the existing comment + const {data: comment} = await octokit.rest.issues.getComment({ + owner: owner, + repo: repo, + comment_id: commentId + }) + + const existingBody = comment.body || '' + const existingTag = extractCommentTag(existingBody) + if (editMode == 'append') { - // Get the comment body - const {data: comment} = await octokit.rest.issues.getComment({ - owner: owner, - repo: repo, - comment_id: commentId - }) - commentBody = appendSeparatorTo( - comment.body ? comment.body : '', - appendSeparator - ) + // For append mode, preserve existing content as-is + // (if we found the comment by tag, the tag is already there) + commentBody = appendSeparatorTo(existingBody, appendSeparator) + body + } else if (editMode === 'replace') { + // For replace mode, replace the content but preserve the tag if it matches + if (commentTag && (existingTag === commentTag || !existingTag)) { + // Preserve or add the tag + body = addCommentTagToBody(body, commentTag) + } else if (existingTag && !commentTag) { + // If there was an existing tag but no new tag specified, preserve the existing tag + body = addCommentTagToBody(body, existingTag) + } + commentBody = body } - commentBody = truncateBody(commentBody + body) + + commentBody = truncateBody(commentBody) core.debug(`Comment body: ${commentBody}`) await octokit.rest.issues.updateComment({ owner: owner, @@ -242,17 +315,56 @@ export async function createOrUpdateComment( const octokit = github.getOctokit(inputs.token) - const commentId = inputs.commentId - ? await updateComment( + let commentId: number + + if (inputs.commentId) { + // Direct update using comment-id (existing behavior) + commentId = await updateComment( + octokit, + owner, + repo, + inputs.commentId, + body, + inputs.editMode, + inputs.appendSeparator + ) + } else if (inputs.commentTag) { + // Find existing comment by tag or create new one + const existingCommentId = await findCommentByTag( + octokit, + owner, + repo, + inputs.issueNumber, + inputs.commentTag + ) + + if (existingCommentId) { + // Update existing comment found by tag + commentId = await updateComment( octokit, owner, repo, - inputs.commentId, + existingCommentId, body, inputs.editMode, - inputs.appendSeparator + inputs.appendSeparator, + inputs.commentTag + ) + } else { + // Create new comment with tag + commentId = await createComment( + octokit, + owner, + repo, + inputs.issueNumber, + body, + inputs.commentTag ) - : await createComment(octokit, owner, repo, inputs.issueNumber, body) + } + } else { + // Create new comment without tag (existing behavior) + commentId = await createComment(octokit, owner, repo, inputs.issueNumber, body) + } core.setOutput('comment-id', commentId) diff --git a/src/main.ts b/src/main.ts index ba8e259f..7412eeeb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,6 +21,7 @@ async function run(): Promise { repository: core.getInput('repository'), issueNumber: Number(core.getInput('issue-number')), commentId: Number(core.getInput('comment-id')), + commentTag: core.getInput('comment-tag'), body: core.getInput('body'), bodyPath: core.getInput('body-path') || core.getInput('body-file'), editMode: core.getInput('edit-mode'), @@ -60,6 +61,13 @@ async function run(): Promise { if (!body && !inputs.reactions) { throw new Error("Missing comment 'body', 'body-path', or 'reactions'.") } + } else if (inputs.commentTag) { + if (!inputs.issueNumber) { + throw new Error("Missing 'issue-number' when using 'comment-tag'.") + } + if (!body) { + throw new Error("Missing comment 'body' or 'body-path' when using 'comment-tag'.") + } } else if (inputs.issueNumber) { if (!body) { throw new Error("Missing comment 'body' or 'body-path'.")