Skip to content
Open
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
2 changes: 2 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
93 changes: 87 additions & 6 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,41 @@ const REACTION_TYPES = [
'rocket',
'eyes'
];
function createCommentTag(tag) {
return `<!-- comment-tag: ${tag} -->`;
}
function extractCommentTag(body) {
const match = body.match(/<!-- comment-tag: (.+?) -->/);
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 => {
Expand Down Expand Up @@ -137,20 +172,26 @@ 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,
repo: repo,
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 = '';
Expand All @@ -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({
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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'.");
Expand Down
150 changes: 131 additions & 19 deletions src/create-or-update-comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface Inputs {
repository: string
issueNumber: number
commentId: number
commentTag: string
body: string
bodyPath: string
editMode: string
Expand All @@ -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 `<!-- comment-tag: ${sanitizedTag} -->`
}

function extractCommentTag(body: string): string | null {
const match = body.match(/<!-- comment-tag: (.+?) -->/)
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<number | null> {
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(
Expand Down Expand Up @@ -133,8 +180,13 @@ async function createComment(
owner: string,
repo: string,
issueNumber: number,
body: string
body: string,
commentTag?: string
): Promise<number> {
if (commentTag) {
body = addCommentTagToBody(body, commentTag)
}

body = truncateBody(body)

const {data: comment} = await octokit.rest.issues.createComment({
Expand All @@ -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
}

Expand All @@ -154,23 +211,39 @@ async function updateComment(
commentId: number,
body: string,
editMode: string,
appendSeparator: string
appendSeparator: string,
commentTag?: string
): Promise<number> {
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,
Expand Down Expand Up @@ -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)

Expand Down
8 changes: 8 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ async function run(): Promise<void> {
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'),
Expand Down Expand Up @@ -60,6 +61,13 @@ async function run(): Promise<void> {
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'.")
Expand Down