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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ jobs:
| repo-token | with | Valid GitHub token, either the temporary token GitHub provides or a personal access token. | no | {{ github.token }} |
| message-id | with | Message id to use when searching existing comments. If found, updates the existing (sticky comment). | no | |
| delete-on-status | with | If specified and a comment exists and the status is matching the value of this option, the comment will be deleted | no | |
| delete-method | with | What to do when `delete-on-status` matches: `delete` removes the comment, `minimize` collapses it. See [Minimizing comments](#minimizing-comments). Not supported with `proxy-url`. | no | delete |
| create-minimized | with | Minimize (collapse) the comment immediately after it is created. Applies only on creation, not updates. See [Minimizing comments](#minimizing-comments). Not supported with `proxy-url`. | no | false |
| minimize-reason | with | Reason shown when a comment is minimized: `outdated`, `resolved`, `off-topic`, `duplicate`, `spam`, or `abuse`. | no | outdated |
| refresh-message-position | with | Should the sticky message be the last one in the PR's feed. | no | false |
| allow-repeats | with | Boolean flag to allow identical messages to be posted each time this action is run. | no | false |
| proxy-url | with | String for your proxy service URL if you'd like this to work with fork-based PRs. | no | |
Expand All @@ -114,6 +117,7 @@ jobs:
| ----------------- | ----------------------------------------------------------------- |
| `comment-created` | `"true"` if a new comment was created, `"false"` otherwise. |
| `comment-updated` | `"true"` if an existing comment was updated, `"false"` otherwise. |
| `comment-minimized` | `"true"` if a comment was minimized, omitted otherwise. |
| `comment-id` | The numeric ID of the created or updated comment. |
| `artifact-url` | If files were attached, the URL to download the artifact. |
| `truncated` | `"true"` if the message was truncated, `"false"` otherwise. |
Expand Down Expand Up @@ -572,6 +576,25 @@ jobs:
delete-on-status: success
```

### Minimizing comments

Instead of removing a comment, you can collapse (minimize) it. Set `delete-method: minimize`
alongside `delete-on-status` to minimize the comment when the status matches, or set
`create-minimized: true` to post a comment that starts collapsed. Use `minimize-reason` to
control the label GitHub shows (`outdated` by default).

```yaml
- uses: mshick/add-pr-comment@v3
with:
message: "Heads up — this is supplementary."
create-minimized: true
minimize-reason: outdated
```

Minimizing uses GitHub's GraphQL API and requires write permissions, so it is **not**
available through `proxy-url` (the fork-PR path). It works for both PR/issue comments and
commit comments.

### Template Variables

Messages support template variables that are replaced with dynamic values at runtime. Template variables use the `%VARIABLE%` syntax and must be enabled by setting `template-variables: true`.
Expand Down
14 changes: 14 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,25 @@ inputs:
description: "A replacement to use, overrides the message. Multple lines can replace same-indexed find patterns."
delete-on-status:
description: "Delete comment on specified status."
create-minimized:
description: "Minimize (collapse) the comment immediately after it is created. Applies only on creation, not updates. Not supported with proxy-url."
default: "false"
required: false
delete-method:
description: 'What to do when delete-on-status matches. "delete" (default) removes the comment, "minimize" collapses it. Not supported with proxy-url.'
default: "delete"
required: false
minimize-reason:
description: 'Reason shown when a comment is minimized. One of: outdated (default), resolved, off-topic, duplicate, spam, abuse.'
default: "outdated"
required: false
outputs:
comment-created:
description: "Whether a comment was created."
comment-updated:
description: "Whether a comment was updated."
comment-minimized:
description: "Whether a comment was minimized."
comment-id:
description: "If a comment was created or updated, the comment id."
artifact-url:
Expand Down
116 changes: 101 additions & 15 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -119176,8 +119176,8 @@ async function getExistingComment(octokit, owner, repo, issueNumber, messageId)
}
}
if (found) {
const { id, body } = found;
return { id, body };
const { id, body, node_id } = found;
return { id, body, node_id };
}
return;
}
Expand Down Expand Up @@ -119225,8 +119225,8 @@ async function getExistingCommitComment(octokit, owner, repo, commitSha, message
}
}
if (found) {
const { id, body = '' } = found;
return { id, body };
const { id, body = '', node_id } = found;
return { id, body, node_id };
}
return;
}
Expand Down Expand Up @@ -119256,6 +119256,21 @@ async function deleteCommitComment(octokit, owner, repo, commentId) {
}));
}

const MINIMIZE_REASONS = [
'ABUSE',
'DUPLICATE',
'OFF_TOPIC',
'OUTDATED',
'RESOLVED',
'SPAM',
];
function normalizeMinimizeReason(input) {
const normalized = input.trim().toUpperCase().replace(/-/g, '_');
if (!MINIMIZE_REASONS.includes(normalized)) {
throw new Error(`Invalid minimize-reason: "${input}". Must be one of: outdated, resolved, off-topic, duplicate, spam, abuse.`);
}
return normalized;
}
async function getInputs() {
const messageIdInput = getInput('message-id', { required: false });
const messageId = messageIdInput === '' ? 'add-pr-comment' : `add-pr-comment:${messageIdInput}`;
Expand Down Expand Up @@ -119285,6 +119300,14 @@ async function getInputs() {
const truncate = truncateInput;
const truncateSeparator = getInput('truncate-separator', { required: false });
const deleteOnStatus = getInput('delete-on-status', { required: false });
const createMinimized = getInput('create-minimized', { required: false }) === 'true';
const deleteMethodInput = getInput('delete-method', { required: false }) || 'delete';
if (deleteMethodInput !== 'delete' && deleteMethodInput !== 'minimize') {
throw new Error(`Invalid delete-method: "${deleteMethodInput}". Must be "delete" or "minimize".`);
}
const deleteMethod = deleteMethodInput;
const minimizeReasonInput = getInput('minimize-reason', { required: false }) || 'outdated';
const minimizeReason = normalizeMinimizeReason(minimizeReasonInput);
const commentTarget = getInput('comment-target', { required: false }) || 'pr';
if (commentTarget !== 'pr' && commentTarget !== 'commit') {
throw new Error(`Invalid comment-target: "${commentTarget}". Must be "pr" or "commit".`);
Expand Down Expand Up @@ -119325,6 +119348,9 @@ async function getInputs() {
repo: repoName || payload.repo.repo,
updateOnly: updateOnly,
deleteOnStatus,
createMinimized,
deleteMethod,
minimizeReason,
};
}

Expand Down Expand Up @@ -121621,6 +121647,22 @@ function findAndReplaceInMessage(find, replace, original) {
return message;
}

const MINIMIZE_COMMENT_MUTATION = `
mutation MinimizeComment($id: ID!, $classifier: ReportedContentClassifiers!) {
minimizeComment(input: { subjectId: $id, classifier: $classifier }) {
minimizedComment {
isMinimized
}
}
}
`;
async function minimizeComment(octokit, nodeId, classifier) {
await withRetry(() => octokit.graphql(MINIMIZE_COMMENT_MUTATION, {
id: nodeId,
classifier,
}));
}

async function createCommentProxy(params) {
const { repoToken, owner, repo, issueNumber, body, commentId, proxyUrl } = params;
const http = new HttpClient('http-client-add-pr-comment');
Expand Down Expand Up @@ -124426,7 +124468,7 @@ function replaceTemplateVariables(message) {

async function manageComment(adapter, options) {
let { message } = options;
const { allowRepeats, updateOnly, refreshMessagePosition, deleteOnStatus, status, messageId, messageFind, messageReplace, templateVariables, } = options;
const { allowRepeats, updateOnly, refreshMessagePosition, deleteOnStatus, status, messageId, messageFind, messageReplace, templateVariables, createMinimized, deleteMethod, minimizeReason, } = options;
let existingComment;
if (!allowRepeats) {
debug('repeat comments are disallowed, checking for existing');
Expand All @@ -124442,9 +124484,16 @@ async function manageComment(adapter, options) {
}
if (deleteOnStatus && deleteOnStatus === status) {
if (existingComment) {
info('deleting existing comment because delete-on-status matched');
await adapter.delete(existingComment.id);
setOutput('comment-deleted', 'true');
if (deleteMethod === 'minimize') {
info('minimizing existing comment because delete-on-status matched');
await adapter.minimize(existingComment.nodeId, minimizeReason);
setOutput('comment-minimized', 'true');
}
else {
info('deleting existing comment because delete-on-status matched');
await adapter.delete(existingComment.id);
setOutput('comment-deleted', 'true');
}
}
else {
info('skipping creating comment because delete-on-status matched');
Expand All @@ -124463,10 +124512,12 @@ async function manageComment(adapter, options) {
}
const body = addMessageHeader(messageId, message);
let comment;
let created = false;
if (existingComment?.id) {
if (refreshMessagePosition) {
await adapter.delete(existingComment.id);
comment = await adapter.create(body);
created = true;
}
else {
comment = await adapter.update(existingComment.id, body);
Expand All @@ -124475,10 +124526,15 @@ async function manageComment(adapter, options) {
}
else {
comment = await adapter.create(body);
created = true;
setOutput('comment-created', 'true');
}
if (comment) {
setOutput('comment-id', comment.id);
if (created && createMinimized) {
await adapter.minimize(comment.nodeId, minimizeReason);
setOutput('comment-minimized', 'true');
}
}
else {
setOutput('comment-created', 'false');
Expand All @@ -124487,7 +124543,7 @@ async function manageComment(adapter, options) {
}
const run = async () => {
try {
const { allowRepeats, attachName, attachPath, attachText, commentTarget, messagePath, messageInput, messageId, refreshMessagePosition, repoToken, proxyUrl, issue, pullRequestNumber, commitSha, repo, owner, updateOnly, deleteOnStatus, messageCancelled, messageFailure, messageSuccess, messageSkipped, preformatted, templateVariables, status, messageFind, messageReplace, truncate, truncateSeparator, } = await getInputs();
const { allowRepeats, attachName, attachPath, attachText, commentTarget, messagePath, messageInput, messageId, refreshMessagePosition, repoToken, proxyUrl, issue, pullRequestNumber, commitSha, repo, owner, updateOnly, deleteOnStatus, createMinimized, deleteMethod, minimizeReason, messageCancelled, messageFailure, messageSuccess, messageSkipped, preformatted, templateVariables, status, messageFind, messageReplace, truncate, truncateSeparator, } = await getInputs();
const octokit = getOctokit(repoToken);
let message = await getMessage({
messagePath,
Expand Down Expand Up @@ -124536,13 +124592,28 @@ const run = async () => {
messageReplace,
message,
templateVariables,
createMinimized,
deleteMethod,
minimizeReason,
};
if (commentTarget === 'commit') {
await manageComment({
getExisting: () => getExistingCommitComment(octokit, owner, repo, commitSha, messageId),
create: (body) => createCommitComment(octokit, owner, repo, commitSha, body),
update: (id, body) => updateCommitComment(octokit, owner, repo, id, body),
getExisting: async () => {
const existing = await getExistingCommitComment(octokit, owner, repo, commitSha, messageId);
return existing
? { id: existing.id, nodeId: existing.node_id, body: existing.body }
: undefined;
},
create: async (body) => {
const c = await createCommitComment(octokit, owner, repo, commitSha, body);
return { id: c.id, nodeId: c.node_id };
},
update: async (id, body) => {
const c = await updateCommitComment(octokit, owner, repo, id, body);
return { id: c.id, nodeId: c.node_id };
},
delete: (id) => deleteCommitComment(octokit, owner, repo, id),
minimize: (nodeId, reason) => minimizeComment(octokit, nodeId, reason),
}, commentOptions);
return;
}
Expand All @@ -124564,6 +124635,9 @@ const run = async () => {
return;
}
if (proxyUrl) {
if (createMinimized || deleteMethod === 'minimize') {
throw new Error('create-minimized and delete-method: minimize are not supported with proxy-url, which is used for fork PRs that lack the write permissions minimize requires.');
}
// Proxy has its own create/update flow, so it's handled separately
let existingComment;
if (!allowRepeats) {
Expand Down Expand Up @@ -124617,12 +124691,24 @@ const run = async () => {
return;
}
await manageComment({
getExisting: () => getExistingComment(octokit, owner, repo, issueNumber, messageId),
create: (body) => createComment(octokit, owner, repo, issueNumber, body),
update: (id, body) => updateComment(octokit, owner, repo, id, body),
getExisting: async () => {
const existing = await getExistingComment(octokit, owner, repo, issueNumber, messageId);
return existing
? { id: existing.id, nodeId: existing.node_id, body: existing.body ?? undefined }
: undefined;
},
create: async (body) => {
const c = await createComment(octokit, owner, repo, issueNumber, body);
return { id: c.id, nodeId: c.node_id };
},
update: async (id, body) => {
const c = await updateComment(octokit, owner, repo, id, body);
return { id: c.id, nodeId: c.node_id };
},
delete: async (id) => {
await deleteComment(octokit, owner, repo, id);
},
minimize: (nodeId, reason) => minimizeComment(octokit, nodeId, reason),
}, commentOptions);
}
catch (err) {
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

Loading
Loading