* typescript

* fix reactions input

* test comma separated

* bump version

* append-separator

* refactor

* refactor reactions

* get reactions

* handle default token

* return reaction id

* remove reactions

* reactions-edit-mode

* readme

* test-command

* fix step order

* deprecate body-file

* update ci to body-path
This commit is contained in:
Peter Evans 2023-04-05 16:14:13 +09:00 committed by GitHub
parent 9c6357680f
commit 3383acd359
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 3648 additions and 5525 deletions

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
dist/
lib/
node_modules/

View File

@ -1,17 +1,18 @@
{ {
"env": { "env": { "node": true, "jest": true },
"commonjs": true, "parser": "@typescript-eslint/parser",
"es6": true, "parserOptions": { "ecmaVersion": 9, "sourceType": "module" },
"node": true "extends": [
}, "eslint:recommended",
"extends": "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended",
"globals": { "plugin:@typescript-eslint/recommended",
"Atomics": "readonly", "plugin:import/errors",
"SharedArrayBuffer": "readonly" "plugin:import/warnings",
}, "plugin:import/typescript",
"parserOptions": { "plugin:prettier/recommended"
"ecmaVersion": 2018 ],
}, "plugins": ["@typescript-eslint"],
"rules": { "rules": {
} "@typescript-eslint/camelcase": "off"
} }
}

View File

@ -1 +1 @@
**Edit:** Some additional info This is still the second line.

View File

@ -1,5 +1,2 @@
This is a multi-line test comment read from a file. This is a multi-line test comment read from a file.
- With GitHub **Markdown** :sparkles: This is the second line.
- Created by [create-or-update-comment][1]
[1]: https://github.com/peter-evans/create-or-update-comment

View File

@ -28,8 +28,10 @@ jobs:
node-version: 16.x node-version: 16.x
cache: npm cache: npm
- run: npm ci - run: npm ci
- run: npm run build
- run: npm run format-check
- run: npm run lint
- run: npm run test - run: npm run test
- run: npm run package
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: dist name: dist
@ -86,27 +88,34 @@ jobs:
body: | body: |
**Edit:** Some additional info **Edit:** Some additional info
reactions: eyes reactions: eyes
reactions-edit-mode: replace
- name: Test add reactions - name: Test add reactions
uses: ./ uses: ./
with: with:
comment-id: ${{ steps.couc.outputs.comment-id }} comment-id: ${{ steps.couc.outputs.comment-id }}
reactions: heart, hooray, laugh reactions: |
heart
hooray
laugh
- name: Test create comment from file - name: Test create comment from file
uses: ./ uses: ./
id: couc2 id: couc2
with: with:
issue-number: ${{ needs.build.outputs.issue-number }} issue-number: ${{ needs.build.outputs.issue-number }}
body-file: .github/comment-body.md body-path: .github/comment-body.md
reactions: '+1' reactions: |
+1
- name: Test update comment from file - name: Test update comment from file
uses: ./ uses: ./
with: with:
comment-id: ${{ steps.couc2.outputs.comment-id }} comment-id: ${{ steps.couc2.outputs.comment-id }}
body-file: .github/comment-body-addition.md body-path: .github/comment-body-addition.md
reactions: eyes append-separator: space
reactions: eyes, rocket
reactions-edit-mode: replace
package: package:
if: github.event_name == 'push' && github.ref == 'refs/heads/main' if: github.event_name == 'push' && github.ref == 'refs/heads/main'

View File

@ -45,6 +45,7 @@ jobs:
body: | body: |
**Edit:** Some additional info **Edit:** Some additional info
reactions: eyes reactions: eyes
reactions-edit-mode: replace
# Test add reactions # Test add reactions
- name: Add reactions - name: Add reactions
@ -53,19 +54,12 @@ jobs:
comment-id: ${{ steps.couc.outputs.comment-id }} comment-id: ${{ steps.couc.outputs.comment-id }}
reactions: heart, hooray, laugh reactions: heart, hooray, laugh
- name: Add reaction
uses: peter-evans/create-or-update-comment@v2
with:
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
reactions: hooray
# Test create with body from file # Test create with body from file
- name: Create comment - name: Create comment
uses: ./ uses: ./
with: with:
issue-number: 1 issue-number: 1
body-file: .github/comment-body.md body-path: .github/comment-body.md
# Test create from template # Test create from template
- name: Render template - name: Render template
@ -82,3 +76,10 @@ jobs:
with: with:
issue-number: 1 issue-number: 1
body: ${{ steps.template.outputs.result }} body: ${{ steps.template.outputs.result }}
- name: Add reaction
uses: peter-evans/create-or-update-comment@v2
with:
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
reactions: hooray

View File

@ -12,6 +12,7 @@ on:
description: The major version tag to update description: The major version tag to update
options: options:
- v2 - v2
- v3
jobs: jobs:
tag: tag:

3
.gitignore vendored
View File

@ -1 +1,2 @@
node_modules lib/
node_modules/

3
.prettierignore Normal file
View File

@ -0,0 +1,3 @@
dist/
lib/
node_modules/

11
.prettierrc.json Normal file
View File

@ -0,0 +1,11 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": false,
"arrowParens": "avoid",
"parser": "typescript"
}

View File

@ -4,15 +4,13 @@
A GitHub action to create or update an issue or pull request comment. A GitHub action to create or update an issue or pull request comment.
This action was created to help facilitate a GitHub Actions "ChatOps" solution in conjunction with [slash-command-dispatch](https://github.com/peter-evans/slash-command-dispatch) action.
## Usage ## Usage
### Add a comment to an issue or pull request ### Add a comment to an issue or pull request
```yml ```yml
- name: Create comment - name: Create comment
uses: peter-evans/create-or-update-comment@v2 uses: peter-evans/create-or-update-comment@v3
with: with:
issue-number: 1 issue-number: 1
body: | body: |
@ -28,7 +26,7 @@ This action was created to help facilitate a GitHub Actions "ChatOps" solution i
```yml ```yml
- name: Update comment - name: Update comment
uses: peter-evans/create-or-update-comment@v2 uses: peter-evans/create-or-update-comment@v3
with: with:
comment-id: 557858210 comment-id: 557858210
body: | body: |
@ -40,10 +38,13 @@ This action was created to help facilitate a GitHub Actions "ChatOps" solution i
```yml ```yml
- name: Add reactions - name: Add reactions
uses: peter-evans/create-or-update-comment@v2 uses: peter-evans/create-or-update-comment@v3
with: with:
comment-id: 557858210 comment-id: 557858210
reactions: heart, hooray, laugh reactions: |
heart
hooray
laugh
``` ```
### Action inputs ### Action inputs
@ -54,10 +55,12 @@ This action was created to help facilitate a GitHub Actions "ChatOps" solution i
| `repository` | The full name of the repository in which to create or update a comment. | Current repository | | `repository` | The full name of the repository in which to create or update a comment. | Current repository |
| `issue-number` | The number of the issue or pull request in which to create a comment. | | | `issue-number` | The number of the issue or pull request in which to create a comment. | |
| `comment-id` | The id of the comment to update. | | | `comment-id` | The id of the comment to update. | |
| `body` | The comment body. Cannot be used in conjunction with `body-file`. | | | `body` | The comment body. Cannot be used in conjunction with `body-path`. | |
| `body-file` | The path to a file containing the comment body. Cannot be used in conjunction with `body`. | | | `body-path` | The path to a file containing the comment body. Cannot be used in conjunction with `body`. | |
| `edit-mode` | The mode when updating a comment, `replace` or `append`. | `append` | | `edit-mode` | The mode when updating a comment, `replace` or `append`. | `append` |
| `reactions` | A comma separated list of reactions to add to the comment. (`+1`, `-1`, `laugh`, `confused`, `heart`, `hooray`, `rocket`, `eyes`) | | | `append-separator` | The separator to use when appending to an existing comment. (`newline`, `space`, `none`) | `newline` |
| `reactions` | A comma or newline separated list of reactions to add to the comment. (`+1`, `-1`, `laugh`, `confused`, `heart`, `hooray`, `rocket`, `eyes`) | |
| `reactions-edit-mode` | The mode when updating comment reactions, `replace` or `append`. | `append` |
Note: In *public* repositories this action does not work in `pull_request` workflows when triggered by forks. Note: In *public* repositories this action does not work in `pull_request` workflows when triggered by forks.
Any attempt will be met with the error, `Resource not accessible by integration`. Any attempt will be met with the error, `Resource not accessible by integration`.
@ -70,7 +73,7 @@ Note that in order to read the step output the action step must have an id.
```yml ```yml
- name: Create comment - name: Create comment
uses: peter-evans/create-or-update-comment@v2 uses: peter-evans/create-or-update-comment@v3
id: couc id: couc
with: with:
issue-number: 1 issue-number: 1
@ -95,7 +98,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Add reaction - name: Add reaction
uses: peter-evans/create-or-update-comment@v2 uses: peter-evans/create-or-update-comment@v3
with: with:
comment-id: ${{ github.event.comment.id }} comment-id: ${{ github.event.comment.id }}
reactions: eyes reactions: eyes
@ -110,7 +113,7 @@ If the find-comment action output `comment-id` returns an empty string, a new co
If it returns a value, the comment already exists and the content is replaced. If it returns a value, the comment already exists and the content is replaced.
```yml ```yml
- name: Find Comment - name: Find Comment
uses: peter-evans/find-comment@v2 uses: peter-evans/find-comment@v3
id: fc id: fc
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
@ -118,7 +121,7 @@ If it returns a value, the comment already exists and the content is replaced.
body-includes: Build output body-includes: Build output
- name: Create or update comment - name: Create or update comment
uses: peter-evans/create-or-update-comment@v2 uses: peter-evans/create-or-update-comment@v3
with: with:
comment-id: ${{ steps.fc.outputs.comment-id }} comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
@ -131,7 +134,7 @@ If it returns a value, the comment already exists and the content is replaced.
If required, the create and update steps can be separated for greater control. If required, the create and update steps can be separated for greater control.
```yml ```yml
- name: Find Comment - name: Find Comment
uses: peter-evans/find-comment@v2 uses: peter-evans/find-comment@v3
id: fc id: fc
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
@ -140,7 +143,7 @@ If required, the create and update steps can be separated for greater control.
- name: Create comment - name: Create comment
if: steps.fc.outputs.comment-id == '' if: steps.fc.outputs.comment-id == ''
uses: peter-evans/create-or-update-comment@v2 uses: peter-evans/create-or-update-comment@v3
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
body: | body: |
@ -149,7 +152,7 @@ If required, the create and update steps can be separated for greater control.
- name: Update comment - name: Update comment
if: steps.fc.outputs.comment-id != '' if: steps.fc.outputs.comment-id != ''
uses: peter-evans/create-or-update-comment@v2 uses: peter-evans/create-or-update-comment@v3
with: with:
comment-id: ${{ steps.fc.outputs.comment-id }} comment-id: ${{ steps.fc.outputs.comment-id }}
body: | body: |
@ -161,10 +164,10 @@ If required, the create and update steps can be separated for greater control.
```yml ```yml
- name: Create comment - name: Create comment
uses: peter-evans/create-or-update-comment@v2 uses: peter-evans/create-or-update-comment@v3
with: with:
issue-number: 1 issue-number: 1
body-file: 'comment-body.md' body-path: 'comment-body.md'
``` ```
### Using a markdown template ### Using a markdown template
@ -187,7 +190,7 @@ The template is rendered using the [render-template](https://github.com/chuhlomi
bar: that bar: that
- name: Create comment - name: Create comment
uses: peter-evans/create-or-update-comment@v2 uses: peter-evans/create-or-update-comment@v3
with: with:
issue-number: 1 issue-number: 1
body: ${{ steps.template.outputs.result }} body: ${{ steps.template.outputs.result }}

View File

@ -6,20 +6,28 @@ inputs:
default: ${{ github.token }} default: ${{ github.token }}
repository: repository:
description: 'The full name of the repository in which to create or update a comment.' description: 'The full name of the repository in which to create or update a comment.'
default: ${{ github.repository }}
issue-number: issue-number:
description: 'The number of the issue or pull request in which to create a comment.' description: 'The number of the issue or pull request in which to create a comment.'
comment-id: comment-id:
description: 'The id of the comment to update.' description: 'The id of the comment to update.'
body: body:
description: 'The comment body. Cannot be used in conjunction with `body-file`.' description: 'The comment body. Cannot be used in conjunction with `body-path`.'
body-file: body-path:
description: 'The path to a file containing the comment body. Cannot be used in conjunction with `body`.' description: 'The path to a file containing the comment body. Cannot be used in conjunction with `body`.'
body-file:
description: 'Deprecated in favour of `body-path`.'
edit-mode: edit-mode:
description: 'The mode when updating a comment, "replace" or "append".' description: 'The mode when updating a comment, "replace" or "append".'
reaction-type: default: 'append'
description: 'Deprecated in favour of `reactions`' append-separator:
description: 'The separator to use when appending to an existing comment. (`newline`, `space`, `none`)'
default: 'newline'
reactions: reactions:
description: 'A comma separated list of reactions to add to the comment.' description: 'A comma or newline separated list of reactions to add to the comment.'
reactions-edit-mode:
description: 'The mode when updating comment reactions, "replace" or "append".'
default: 'append'
outputs: outputs:
comment-id: comment-id:
description: 'The id of the created comment' description: 'The id of the created comment'

736
dist/index.js vendored
View File

@ -1,6 +1,425 @@
/******/ (() => { // webpackBootstrap /******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({ /******/ var __webpack_modules__ = ({
/***/ 8007:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __asyncValues = (this && this.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.createOrUpdateComment = void 0;
const core = __importStar(__nccwpck_require__(2186));
const github = __importStar(__nccwpck_require__(5438));
const utils = __importStar(__nccwpck_require__(918));
const util_1 = __nccwpck_require__(3837);
const REACTION_TYPES = [
'+1',
'-1',
'laugh',
'confused',
'heart',
'hooray',
'rocket',
'eyes'
];
function getReactionsSet(reactions) {
const reactionsSet = [
...new Set(reactions.filter(item => {
if (!REACTION_TYPES.includes(item)) {
core.warning(`Skipping invalid reaction '${item}'.`);
return false;
}
return true;
}))
];
if (!reactionsSet) {
throw new Error(`No valid reactions are contained in '${reactions}'.`);
}
return reactionsSet;
}
function addReactions(octokit, owner, repo, commentId, reactions) {
return __awaiter(this, void 0, void 0, function* () {
const results = yield Promise.allSettled(reactions.map((reaction) => __awaiter(this, void 0, void 0, function* () {
yield octokit.rest.reactions.createForIssueComment({
owner: owner,
repo: repo,
comment_id: commentId,
content: reaction
});
core.info(`Setting '${reaction}' reaction on comment.`);
})));
for (let i = 0, l = results.length; i < l; i++) {
if (results[i].status === 'fulfilled') {
core.info(`Added reaction '${reactions[i]}' to comment id '${commentId}'.`);
}
else if (results[i].status === 'rejected') {
core.warning(`Adding reaction '${reactions[i]}' to comment id '${commentId}' failed.`);
}
}
});
}
function removeReactions(octokit, owner, repo, commentId, reactions) {
return __awaiter(this, void 0, void 0, function* () {
const results = yield Promise.allSettled(reactions.map((reaction) => __awaiter(this, void 0, void 0, function* () {
yield octokit.rest.reactions.deleteForIssueComment({
owner: owner,
repo: repo,
comment_id: commentId,
reaction_id: reaction.id
});
core.info(`Removing '${reaction.content}' reaction from comment.`);
})));
for (let i = 0, l = results.length; i < l; i++) {
if (results[i].status === 'fulfilled') {
core.info(`Removed reaction '${reactions[i].content}' from comment id '${commentId}'.`);
}
else if (results[i].status === 'rejected') {
core.warning(`Removing reaction '${reactions[i].content}' from comment id '${commentId}' failed.`);
}
}
});
}
function appendSeparatorTo(body, separator) {
switch (separator) {
case 'newline':
return body + '\n';
case 'space':
return body + ' ';
default: // none
return body;
}
}
function createComment(octokit, owner, repo, issueNumber, body) {
return __awaiter(this, void 0, void 0, function* () {
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}'.`);
return comment.id;
});
}
function updateComment(octokit, owner, repo, commentId, body, editMode, appendSeparator) {
return __awaiter(this, void 0, void 0, function* () {
if (body) {
let commentBody = '';
if (editMode == 'append') {
// Get the comment body
const { data: comment } = yield octokit.rest.issues.getComment({
owner: owner,
repo: repo,
comment_id: commentId
});
commentBody = appendSeparatorTo(comment.body ? comment.body : '', appendSeparator);
}
commentBody = commentBody + body;
core.debug(`Comment body: ${commentBody}`);
yield octokit.rest.issues.updateComment({
owner: owner,
repo: repo,
comment_id: commentId,
body: commentBody
});
core.info(`Updated comment id '${commentId}'.`);
}
return commentId;
});
}
function getAuthenticatedUser(octokit) {
return __awaiter(this, void 0, void 0, function* () {
try {
const { data: user } = yield octokit.rest.users.getAuthenticated();
return user.login;
}
catch (error) {
if (utils
.getErrorMessage(error)
.includes('Resource not accessible by integration')) {
// In this case we can assume the token is the default GITHUB_TOKEN and
// therefore the user is 'github-actions[bot]'.
return 'github-actions[bot]';
}
else {
throw error;
}
}
});
}
function getCommentReactionsForUser(octokit, owner, repo, commentId, user) {
var _a, e_1, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
const userReactions = [];
try {
for (var _d = true, _e = __asyncValues(octokit.paginate.iterator(octokit.rest.reactions.listForIssueComment, {
owner,
repo,
comment_id: commentId,
per_page: 100
})), _f; _f = yield _e.next(), _a = _f.done, !_a;) {
_c = _f.value;
_d = false;
try {
const { data: reactions } = _c;
const filteredReactions = reactions
.filter(reaction => reaction.user.login === user)
.map(reaction => {
return { id: reaction.id, content: reaction.content };
});
userReactions.push(...filteredReactions);
}
finally {
_d = true;
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
}
finally { if (e_1) throw e_1.error; }
}
return userReactions;
});
}
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);
core.setOutput('comment-id', commentId);
if (inputs.reactions) {
const reactionsSet = getReactionsSet(inputs.reactions);
// Remove reactions if reactionsEditMode is 'replace'
if (inputs.commentId && inputs.reactionsEditMode === 'replace') {
const authenticatedUser = yield getAuthenticatedUser(octokit);
const userReactions = yield getCommentReactionsForUser(octokit, owner, repo, commentId, authenticatedUser);
core.debug((0, util_1.inspect)(userReactions));
const reactionsToRemove = userReactions.filter(reaction => !reactionsSet.includes(reaction.content));
yield removeReactions(octokit, owner, repo, commentId, reactionsToRemove);
}
yield addReactions(octokit, owner, repo, commentId, reactionsSet);
}
});
}
exports.createOrUpdateComment = createOrUpdateComment;
/***/ }),
/***/ 3109:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const core = __importStar(__nccwpck_require__(2186));
const create_or_update_comment_1 = __nccwpck_require__(8007);
const fs_1 = __nccwpck_require__(7147);
const util_1 = __nccwpck_require__(3837);
const utils = __importStar(__nccwpck_require__(918));
function getBody(inputs) {
if (inputs.body) {
return inputs.body;
}
else if (inputs.bodyPath) {
return (0, fs_1.readFileSync)(inputs.bodyPath, 'utf-8');
}
else {
return '';
}
}
function run() {
return __awaiter(this, void 0, void 0, function* () {
try {
const inputs = {
token: core.getInput('token'),
repository: core.getInput('repository'),
issueNumber: Number(core.getInput('issue-number')),
commentId: Number(core.getInput('comment-id')),
body: core.getInput('body'),
bodyPath: core.getInput('body-path') || core.getInput('body-file'),
editMode: core.getInput('edit-mode'),
appendSeparator: core.getInput('append-separator'),
reactions: utils.getInputAsArray('reactions'),
reactionsEditMode: core.getInput('reactions-edit-mode')
};
core.debug(`Inputs: ${(0, util_1.inspect)(inputs)}`);
if (!['append', 'replace'].includes(inputs.editMode)) {
throw new Error(`Invalid edit-mode '${inputs.editMode}'.`);
}
if (!['append', 'replace'].includes(inputs.reactionsEditMode)) {
throw new Error(`Invalid reactions edit-mode '${inputs.reactionsEditMode}'.`);
}
if (!['newline', 'space', 'none'].includes(inputs.appendSeparator)) {
throw new Error(`Invalid append-separator '${inputs.appendSeparator}'.`);
}
if (inputs.bodyPath && inputs.body) {
throw new Error("Only one of 'body' or 'body-path' can be set.");
}
if (inputs.bodyPath) {
if (!(0, fs_1.existsSync)(inputs.bodyPath)) {
throw new Error(`File '${inputs.bodyPath}' does not exist.`);
}
}
const body = getBody(inputs);
if (inputs.commentId) {
if (!body && !inputs.reactions) {
throw new Error("Missing comment 'body', 'body-path', or 'reactions'.");
}
}
else if (inputs.issueNumber) {
if (!body) {
throw new Error("Missing comment 'body' or 'body-path'.");
}
}
else {
throw new Error("Missing either 'issue-number' or 'comment-id'.");
}
(0, create_or_update_comment_1.createOrUpdateComment)(inputs, body);
}
catch (error) {
core.debug((0, util_1.inspect)(error));
const errMsg = utils.getErrorMessage(error);
core.setFailed(errMsg);
if (errMsg == 'Resource not accessible by integration') {
core.error(`See this action's readme for details about this error`);
}
}
});
}
run();
/***/ }),
/***/ 918:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getErrorMessage = exports.getStringAsArray = exports.getInputAsArray = void 0;
const core = __importStar(__nccwpck_require__(2186));
function getInputAsArray(name, options) {
return getStringAsArray(core.getInput(name, options));
}
exports.getInputAsArray = getInputAsArray;
function getStringAsArray(str) {
return str
.split(/[\n,]+/)
.map(s => s.trim())
.filter(x => x !== '');
}
exports.getStringAsArray = getStringAsArray;
function getErrorMessage(error) {
if (error instanceof Error)
return error.message;
return String(error);
}
exports.getErrorMessage = getErrorMessage;
/***/ }),
/***/ 7351: /***/ 7351:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
@ -1942,6 +2361,10 @@ function checkBypass(reqUrl) {
if (!reqUrl.hostname) { if (!reqUrl.hostname) {
return false; return false;
} }
const reqHost = reqUrl.hostname;
if (isLoopbackAddress(reqHost)) {
return true;
}
const noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || ''; const noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || '';
if (!noProxy) { if (!noProxy) {
return false; return false;
@ -1967,13 +2390,24 @@ function checkBypass(reqUrl) {
.split(',') .split(',')
.map(x => x.trim().toUpperCase()) .map(x => x.trim().toUpperCase())
.filter(x => x)) { .filter(x => x)) {
if (upperReqHosts.some(x => x === upperNoProxyItem)) { if (upperNoProxyItem === '*' ||
upperReqHosts.some(x => x === upperNoProxyItem ||
x.endsWith(`.${upperNoProxyItem}`) ||
(upperNoProxyItem.startsWith('.') &&
x.endsWith(`${upperNoProxyItem}`)))) {
return true; return true;
} }
} }
return false; return false;
} }
exports.checkBypass = checkBypass; exports.checkBypass = checkBypass;
function isLoopbackAddress(host) {
const hostLower = host.toLowerCase();
return (hostLower === 'localhost' ||
hostLower.startsWith('127.') ||
hostLower.startsWith('[::1]') ||
hostLower.startsWith('[0:0:0:0:0:0:0:1]'));
}
//# sourceMappingURL=proxy.js.map //# sourceMappingURL=proxy.js.map
/***/ }), /***/ }),
@ -6022,6 +6456,20 @@ const isDomainOrSubdomain = function isDomainOrSubdomain(destination, original)
return orig === dest || orig[orig.length - dest.length - 1] === '.' && orig.endsWith(dest); return orig === dest || orig[orig.length - dest.length - 1] === '.' && orig.endsWith(dest);
}; };
/**
* isSameProtocol reports whether the two provided URLs use the same protocol.
*
* Both domains must already be in canonical form.
* @param {string|URL} original
* @param {string|URL} destination
*/
const isSameProtocol = function isSameProtocol(destination, original) {
const orig = new URL$1(original).protocol;
const dest = new URL$1(destination).protocol;
return orig === dest;
};
/** /**
* Fetch function * Fetch function
* *
@ -6053,7 +6501,7 @@ function fetch(url, opts) {
let error = new AbortError('The user aborted a request.'); let error = new AbortError('The user aborted a request.');
reject(error); reject(error);
if (request.body && request.body instanceof Stream.Readable) { if (request.body && request.body instanceof Stream.Readable) {
request.body.destroy(error); destroyStream(request.body, error);
} }
if (!response || !response.body) return; if (!response || !response.body) return;
response.body.emit('error', error); response.body.emit('error', error);
@ -6094,9 +6542,43 @@ function fetch(url, opts) {
req.on('error', function (err) { req.on('error', function (err) {
reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err));
if (response && response.body) {
destroyStream(response.body, err);
}
finalize(); finalize();
}); });
fixResponseChunkedTransferBadEnding(req, function (err) {
if (signal && signal.aborted) {
return;
}
if (response && response.body) {
destroyStream(response.body, err);
}
});
/* c8 ignore next 18 */
if (parseInt(process.version.substring(1)) < 14) {
// Before Node.js 14, pipeline() does not fully support async iterators and does not always
// properly handle when the socket close/end events are out of order.
req.on('socket', function (s) {
s.addListener('close', function (hadError) {
// if a data listener is still present we didn't end cleanly
const hasDataListener = s.listenerCount('data') > 0;
// if end happened before close but the socket didn't emit an error, do it now
if (response && hasDataListener && !hadError && !(signal && signal.aborted)) {
const err = new Error('Premature close');
err.code = 'ERR_STREAM_PREMATURE_CLOSE';
response.body.emit('error', err);
}
});
});
}
req.on('response', function (res) { req.on('response', function (res) {
clearTimeout(reqTimeout); clearTimeout(reqTimeout);
@ -6168,7 +6650,7 @@ function fetch(url, opts) {
size: request.size size: request.size
}; };
if (!isDomainOrSubdomain(request.url, locationURL)) { if (!isDomainOrSubdomain(request.url, locationURL) || !isSameProtocol(request.url, locationURL)) {
for (const name of ['authorization', 'www-authenticate', 'cookie', 'cookie2']) { for (const name of ['authorization', 'www-authenticate', 'cookie', 'cookie2']) {
requestOpts.headers.delete(name); requestOpts.headers.delete(name);
} }
@ -6261,6 +6743,13 @@ function fetch(url, opts) {
response = new Response(body, response_options); response = new Response(body, response_options);
resolve(response); resolve(response);
}); });
raw.on('end', function () {
// some old IIS servers return zero-length OK deflate responses, so 'data' is never emitted.
if (!response) {
response = new Response(body, response_options);
resolve(response);
}
});
return; return;
} }
@ -6280,6 +6769,41 @@ function fetch(url, opts) {
writeToStream(req, request); writeToStream(req, request);
}); });
} }
function fixResponseChunkedTransferBadEnding(request, errorCallback) {
let socket;
request.on('socket', function (s) {
socket = s;
});
request.on('response', function (response) {
const headers = response.headers;
if (headers['transfer-encoding'] === 'chunked' && !headers['content-length']) {
response.once('close', function (hadError) {
// if a data listener is still present we didn't end cleanly
const hasDataListener = socket.listenerCount('data') > 0;
if (hasDataListener && !hadError) {
const err = new Error('Premature close');
err.code = 'ERR_STREAM_PREMATURE_CLOSE';
errorCallback(err);
}
});
}
});
}
function destroyStream(stream, err) {
if (stream.destroy) {
stream.destroy(err);
} else {
// node < 8
stream.emit('error', err);
stream.end();
}
}
/** /**
* Redirect code matching * Redirect code matching
* *
@ -9681,204 +10205,12 @@ module.exports = JSON.parse('[[[0,44],"disallowed_STD3_valid"],[[45,46],"valid"]
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; /******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";
/******/ /******/
/************************************************************************/ /************************************************************************/
var __webpack_exports__ = {}; /******/
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. /******/ // startup
(() => { /******/ // Load entry module and return exports
const { inspect } = __nccwpck_require__(3837); /******/ // This entry module is referenced by other modules so it can't be inlined
const { readFileSync, existsSync } = __nccwpck_require__(7147); /******/ var __webpack_exports__ = __nccwpck_require__(3109);
const core = __nccwpck_require__(2186); /******/ module.exports = __webpack_exports__;
const github = __nccwpck_require__(5438); /******/
const REACTION_TYPES = [
"+1",
"-1",
"laugh",
"confused",
"heart",
"hooray",
"rocket",
"eyes",
];
async function addReactions(octokit, repo, comment_id, reactions) {
let ReactionsSet = [
...new Set(
reactions
.replace(/\s/g, "")
.split(",")
.filter((item) => {
if (!REACTION_TYPES.includes(item)) {
core.info(`Skipping invalid reaction '${item}'.`);
return false;
}
return true;
})
),
];
if (!ReactionsSet) {
core.setFailed(
`No valid reactions are contained in '${reactions}'.`
);
return false;
}
let results = await Promise.allSettled(
ReactionsSet.map(async (item) => {
await octokit.rest.reactions.createForIssueComment({
owner: repo[0],
repo: repo[1],
comment_id: comment_id,
content: item,
});
core.info(`Setting '${item}' reaction on comment.`);
})
);
for (let i = 0, l = results.length; i < l; i++) {
if (results[i].status === "fulfilled") {
core.info(
`Added reaction '${ReactionsSet[i]}' to comment id '${comment_id}'.`
);
} else if (results[i].status === "rejected") {
core.info(
`Adding reaction '${ReactionsSet[i]}' to comment id '${comment_id}' failed with ${results[i].reason}.`
);
}
}
ReactionsSet = undefined;
results = undefined;
}
function getBody(inputs) {
if (inputs.body) {
return inputs.body;
} else if (inputs.bodyFile) {
return readFileSync(inputs.bodyFile, 'utf-8');
} else {
return '';
}
}
async function run() {
try {
const inputs = {
token: core.getInput("token"),
repository: core.getInput("repository"),
issueNumber: core.getInput("issue-number"),
commentId: core.getInput("comment-id"),
body: core.getInput("body"),
bodyFile: core.getInput("body-file"),
editMode: core.getInput("edit-mode"),
reactions: core.getInput("reactions")
? core.getInput("reactions")
: core.getInput("reaction-type"),
};
core.debug(`Inputs: ${inspect(inputs)}`);
const repository = inputs.repository
? inputs.repository
: process.env.GITHUB_REPOSITORY;
const repo = repository.split("/");
core.debug(`repository: ${repository}`);
const editMode = inputs.editMode ? inputs.editMode : "append";
core.debug(`editMode: ${editMode}`);
if (!["append", "replace"].includes(editMode)) {
core.setFailed(`Invalid edit-mode '${editMode}'.`);
return;
}
if (inputs.bodyFile && inputs.body) {
core.setFailed("Only one of 'body' or 'body-file' can be set.");
return;
}
if (inputs.bodyFile) {
if (!existsSync(inputs.bodyFile)) {
core.setFailed(`File '${inputs.bodyFile}' does not exist.`);
return;
}
}
const body = getBody(inputs);
const octokit = github.getOctokit(inputs.token);
if (inputs.commentId) {
// Edit a comment
if (!body && !inputs.reactions) {
core.setFailed("Missing comment 'body', 'body-file', or 'reactions'.");
return;
}
if (body) {
var commentBody = "";
if (editMode == "append") {
// Get the comment body
const { data: comment } = await octokit.rest.issues.getComment({
owner: repo[0],
repo: repo[1],
comment_id: inputs.commentId,
});
commentBody = comment.body + "\n";
}
commentBody = commentBody + body;
core.debug(`Comment body: ${commentBody}`);
await octokit.rest.issues.updateComment({
owner: repo[0],
repo: repo[1],
comment_id: inputs.commentId,
body: commentBody,
});
core.info(`Updated comment id '${inputs.commentId}'.`);
core.setOutput("comment-id", inputs.commentId);
}
// Set comment reactions
if (inputs.reactions) {
await addReactions(octokit, repo, inputs.commentId, inputs.reactions);
}
} else if (inputs.issueNumber) {
// Create a comment
if (!body) {
core.setFailed("Missing comment 'body' or 'body-file'.");
return;
}
const { data: comment } = await octokit.rest.issues.createComment({
owner: repo[0],
repo: repo[1],
issue_number: inputs.issueNumber,
body,
});
core.info(
`Created comment id '${comment.id}' on issue '${inputs.issueNumber}'.`
);
core.setOutput("comment-id", comment.id);
// Set comment reactions
if (inputs.reactions) {
await addReactions(octokit, repo, comment.id, inputs.reactions);
}
} else {
core.setFailed("Missing either 'issue-number' or 'comment-id'.");
return;
}
} catch (error) {
core.debug(inspect(error));
core.setFailed(error.message);
if (error.message == 'Resource not accessible by integration') {
core.error(`See this action's readme for details about this error`);
}
}
}
run();
})();
module.exports = __webpack_exports__;
/******/ })() /******/ })()
; ;

192
index.js
View File

@ -1,192 +0,0 @@
const { inspect } = require("util");
const { readFileSync, existsSync } = require("fs");
const core = require("@actions/core");
const github = require("@actions/github");
const REACTION_TYPES = [
"+1",
"-1",
"laugh",
"confused",
"heart",
"hooray",
"rocket",
"eyes",
];
async function addReactions(octokit, repo, comment_id, reactions) {
let ReactionsSet = [
...new Set(
reactions
.replace(/\s/g, "")
.split(",")
.filter((item) => {
if (!REACTION_TYPES.includes(item)) {
core.info(`Skipping invalid reaction '${item}'.`);
return false;
}
return true;
})
),
];
if (!ReactionsSet) {
core.setFailed(
`No valid reactions are contained in '${reactions}'.`
);
return false;
}
let results = await Promise.allSettled(
ReactionsSet.map(async (item) => {
await octokit.rest.reactions.createForIssueComment({
owner: repo[0],
repo: repo[1],
comment_id: comment_id,
content: item,
});
core.info(`Setting '${item}' reaction on comment.`);
})
);
for (let i = 0, l = results.length; i < l; i++) {
if (results[i].status === "fulfilled") {
core.info(
`Added reaction '${ReactionsSet[i]}' to comment id '${comment_id}'.`
);
} else if (results[i].status === "rejected") {
core.info(
`Adding reaction '${ReactionsSet[i]}' to comment id '${comment_id}' failed with ${results[i].reason}.`
);
}
}
ReactionsSet = undefined;
results = undefined;
}
function getBody(inputs) {
if (inputs.body) {
return inputs.body;
} else if (inputs.bodyFile) {
return readFileSync(inputs.bodyFile, 'utf-8');
} else {
return '';
}
}
async function run() {
try {
const inputs = {
token: core.getInput("token"),
repository: core.getInput("repository"),
issueNumber: core.getInput("issue-number"),
commentId: core.getInput("comment-id"),
body: core.getInput("body"),
bodyFile: core.getInput("body-file"),
editMode: core.getInput("edit-mode"),
reactions: core.getInput("reactions")
? core.getInput("reactions")
: core.getInput("reaction-type"),
};
core.debug(`Inputs: ${inspect(inputs)}`);
const repository = inputs.repository
? inputs.repository
: process.env.GITHUB_REPOSITORY;
const repo = repository.split("/");
core.debug(`repository: ${repository}`);
const editMode = inputs.editMode ? inputs.editMode : "append";
core.debug(`editMode: ${editMode}`);
if (!["append", "replace"].includes(editMode)) {
core.setFailed(`Invalid edit-mode '${editMode}'.`);
return;
}
if (inputs.bodyFile && inputs.body) {
core.setFailed("Only one of 'body' or 'body-file' can be set.");
return;
}
if (inputs.bodyFile) {
if (!existsSync(inputs.bodyFile)) {
core.setFailed(`File '${inputs.bodyFile}' does not exist.`);
return;
}
}
const body = getBody(inputs);
const octokit = github.getOctokit(inputs.token);
if (inputs.commentId) {
// Edit a comment
if (!body && !inputs.reactions) {
core.setFailed("Missing comment 'body', 'body-file', or 'reactions'.");
return;
}
if (body) {
var commentBody = "";
if (editMode == "append") {
// Get the comment body
const { data: comment } = await octokit.rest.issues.getComment({
owner: repo[0],
repo: repo[1],
comment_id: inputs.commentId,
});
commentBody = comment.body + "\n";
}
commentBody = commentBody + body;
core.debug(`Comment body: ${commentBody}`);
await octokit.rest.issues.updateComment({
owner: repo[0],
repo: repo[1],
comment_id: inputs.commentId,
body: commentBody,
});
core.info(`Updated comment id '${inputs.commentId}'.`);
core.setOutput("comment-id", inputs.commentId);
}
// Set comment reactions
if (inputs.reactions) {
await addReactions(octokit, repo, inputs.commentId, inputs.reactions);
}
} else if (inputs.issueNumber) {
// Create a comment
if (!body) {
core.setFailed("Missing comment 'body' or 'body-file'.");
return;
}
const { data: comment } = await octokit.rest.issues.createComment({
owner: repo[0],
repo: repo[1],
issue_number: inputs.issueNumber,
body,
});
core.info(
`Created comment id '${comment.id}' on issue '${inputs.issueNumber}'.`
);
core.setOutput("comment-id", comment.id);
// Set comment reactions
if (inputs.reactions) {
await addReactions(octokit, repo, comment.id, inputs.reactions);
}
} else {
core.setFailed("Missing either 'issue-number' or 'comment-id'.");
return;
}
} catch (error) {
core.debug(inspect(error));
core.setFailed(error.message);
if (error.message == 'Resource not accessible by integration') {
core.error(`See this action's readme for details about this error`);
}
}
}
run();

11
jest.config.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}

7654
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,25 @@
{ {
"name": "create-or-update-comment", "name": "create-or-update-comment",
"version": "2.0.0", "version": "3.0.0",
"description": "Create or update an issue or pull request comment", "description": "Create or update an issue or pull request comment",
"main": "index.js", "main": "lib/main.js",
"scripts": { "scripts": {
"lint": "eslint index.js", "build": "tsc && ncc build",
"package": "ncc build index.js -o dist", "format": "prettier --write '**/*.ts'",
"test": "eslint index.js && jest --passWithNoTests" "format-check": "prettier --check '**/*.ts'",
"lint": "eslint src/**/*.ts",
"test": "jest --passWithNoTests"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/peter-evans/create-or-update-comment.git" "url": "git+https://github.com/peter-evans/create-or-update-comment.git"
}, },
"keywords": [], "keywords": [
"actions",
"create",
"update",
"comment"
],
"author": "Peter Evans", "author": "Peter Evans",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
@ -24,8 +31,19 @@
"@actions/github": "^5.1.1" "@actions/github": "^5.1.1"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^27.0.3",
"@types/node": "^18.15.10",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"@vercel/ncc": "^0.36.1", "@vercel/ncc": "^0.36.1",
"eslint": "^8.37.0", "eslint": "^8.36.0",
"jest": "^29.5.0" "eslint-plugin-github": "^4.7.0",
"eslint-plugin-jest": "^27.2.1",
"jest": "^27.5.1",
"jest-circus": "^27.4.2",
"js-yaml": "^4.1.0",
"prettier": "^2.8.7",
"ts-jest": "^27.1.5",
"typescript": "^4.9.5"
} }
} }

View File

@ -0,0 +1,270 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import * as utils from './utils'
import {inspect} from 'util'
export interface Inputs {
token: string
repository: string
issueNumber: number
commentId: number
body: string
bodyPath: string
editMode: string
appendSeparator: string
reactions: string[]
reactionsEditMode: string
}
const REACTION_TYPES = [
'+1',
'-1',
'laugh',
'confused',
'heart',
'hooray',
'rocket',
'eyes'
]
function getReactionsSet(reactions: string[]): string[] {
const reactionsSet = [
...new Set(
reactions.filter(item => {
if (!REACTION_TYPES.includes(item)) {
core.warning(`Skipping invalid reaction '${item}'.`)
return false
}
return true
})
)
]
if (!reactionsSet) {
throw new Error(`No valid reactions are contained in '${reactions}'.`)
}
return reactionsSet
}
async function addReactions(
octokit,
owner: string,
repo: string,
commentId: number,
reactions: string[]
) {
const results = await Promise.allSettled(
reactions.map(async reaction => {
await octokit.rest.reactions.createForIssueComment({
owner: owner,
repo: repo,
comment_id: commentId,
content: reaction
})
core.info(`Setting '${reaction}' reaction on comment.`)
})
)
for (let i = 0, l = results.length; i < l; i++) {
if (results[i].status === 'fulfilled') {
core.info(
`Added reaction '${reactions[i]}' to comment id '${commentId}'.`
)
} else if (results[i].status === 'rejected') {
core.warning(
`Adding reaction '${reactions[i]}' to comment id '${commentId}' failed.`
)
}
}
}
async function removeReactions(
octokit,
owner: string,
repo: string,
commentId: number,
reactions: Reaction[]
) {
const results = await Promise.allSettled(
reactions.map(async reaction => {
await octokit.rest.reactions.deleteForIssueComment({
owner: owner,
repo: repo,
comment_id: commentId,
reaction_id: reaction.id
})
core.info(`Removing '${reaction.content}' reaction from comment.`)
})
)
for (let i = 0, l = results.length; i < l; i++) {
if (results[i].status === 'fulfilled') {
core.info(
`Removed reaction '${reactions[i].content}' from comment id '${commentId}'.`
)
} else if (results[i].status === 'rejected') {
core.warning(
`Removing reaction '${reactions[i].content}' from comment id '${commentId}' failed.`
)
}
}
}
function appendSeparatorTo(body: string, separator: string): string {
switch (separator) {
case 'newline':
return body + '\n'
case 'space':
return body + ' '
default: // none
return body
}
}
async function createComment(
octokit,
owner: string,
repo: string,
issueNumber: number,
body: string
): Promise<number> {
const {data: comment} = await octokit.rest.issues.createComment({
owner: owner,
repo: repo,
issue_number: issueNumber,
body
})
core.info(`Created comment id '${comment.id}' on issue '${issueNumber}'.`)
return comment.id
}
async function updateComment(
octokit,
owner: string,
repo: string,
commentId: number,
body: string,
editMode: string,
appendSeparator: string
): Promise<number> {
if (body) {
let commentBody = ''
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
)
}
commentBody = commentBody + body
core.debug(`Comment body: ${commentBody}`)
await octokit.rest.issues.updateComment({
owner: owner,
repo: repo,
comment_id: commentId,
body: commentBody
})
core.info(`Updated comment id '${commentId}'.`)
}
return commentId
}
async function getAuthenticatedUser(octokit): Promise<string> {
try {
const {data: user} = await octokit.rest.users.getAuthenticated()
return user.login
} catch (error) {
if (
utils
.getErrorMessage(error)
.includes('Resource not accessible by integration')
) {
// In this case we can assume the token is the default GITHUB_TOKEN and
// therefore the user is 'github-actions[bot]'.
return 'github-actions[bot]'
} else {
throw error
}
}
}
type Reaction = {
id: number
content: string
}
async function getCommentReactionsForUser(
octokit,
owner: string,
repo: string,
commentId: number,
user: string
): Promise<Reaction[]> {
const userReactions: Reaction[] = []
for await (const {data: reactions} of octokit.paginate.iterator(
octokit.rest.reactions.listForIssueComment,
{
owner,
repo,
comment_id: commentId,
per_page: 100
}
)) {
const filteredReactions: Reaction[] = reactions
.filter(reaction => reaction.user.login === user)
.map(reaction => {
return {id: reaction.id, content: reaction.content}
})
userReactions.push(...filteredReactions)
}
return userReactions
}
export async function createOrUpdateComment(
inputs: Inputs,
body: string
): Promise<void> {
const [owner, repo] = inputs.repository.split('/')
const octokit = github.getOctokit(inputs.token)
const commentId = inputs.commentId
? await updateComment(
octokit,
owner,
repo,
inputs.commentId,
body,
inputs.editMode,
inputs.appendSeparator
)
: await createComment(octokit, owner, repo, inputs.issueNumber, body)
core.setOutput('comment-id', commentId)
if (inputs.reactions) {
const reactionsSet = getReactionsSet(inputs.reactions)
// Remove reactions if reactionsEditMode is 'replace'
if (inputs.commentId && inputs.reactionsEditMode === 'replace') {
const authenticatedUser = await getAuthenticatedUser(octokit)
const userReactions = await getCommentReactionsForUser(
octokit,
owner,
repo,
commentId,
authenticatedUser
)
core.debug(inspect(userReactions))
const reactionsToRemove = userReactions.filter(
reaction => !reactionsSet.includes(reaction.content)
)
await removeReactions(octokit, owner, repo, commentId, reactionsToRemove)
}
await addReactions(octokit, owner, repo, commentId, reactionsSet)
}
}

82
src/main.ts Normal file
View File

@ -0,0 +1,82 @@
import * as core from '@actions/core'
import {Inputs, createOrUpdateComment} from './create-or-update-comment'
import {existsSync, readFileSync} from 'fs'
import {inspect} from 'util'
import * as utils from './utils'
function getBody(inputs: Inputs) {
if (inputs.body) {
return inputs.body
} else if (inputs.bodyPath) {
return readFileSync(inputs.bodyPath, 'utf-8')
} else {
return ''
}
}
async function run(): Promise<void> {
try {
const inputs: Inputs = {
token: core.getInput('token'),
repository: core.getInput('repository'),
issueNumber: Number(core.getInput('issue-number')),
commentId: Number(core.getInput('comment-id')),
body: core.getInput('body'),
bodyPath: core.getInput('body-path') || core.getInput('body-file'),
editMode: core.getInput('edit-mode'),
appendSeparator: core.getInput('append-separator'),
reactions: utils.getInputAsArray('reactions'),
reactionsEditMode: core.getInput('reactions-edit-mode')
}
core.debug(`Inputs: ${inspect(inputs)}`)
if (!['append', 'replace'].includes(inputs.editMode)) {
throw new Error(`Invalid edit-mode '${inputs.editMode}'.`)
}
if (!['append', 'replace'].includes(inputs.reactionsEditMode)) {
throw new Error(
`Invalid reactions edit-mode '${inputs.reactionsEditMode}'.`
)
}
if (!['newline', 'space', 'none'].includes(inputs.appendSeparator)) {
throw new Error(`Invalid append-separator '${inputs.appendSeparator}'.`)
}
if (inputs.bodyPath && inputs.body) {
throw new Error("Only one of 'body' or 'body-path' can be set.")
}
if (inputs.bodyPath) {
if (!existsSync(inputs.bodyPath)) {
throw new Error(`File '${inputs.bodyPath}' does not exist.`)
}
}
const body = getBody(inputs)
if (inputs.commentId) {
if (!body && !inputs.reactions) {
throw new Error("Missing comment 'body', 'body-path', or 'reactions'.")
}
} else if (inputs.issueNumber) {
if (!body) {
throw new Error("Missing comment 'body' or 'body-path'.")
}
} else {
throw new Error("Missing either 'issue-number' or 'comment-id'.")
}
createOrUpdateComment(inputs, body)
} catch (error) {
core.debug(inspect(error))
const errMsg = utils.getErrorMessage(error)
core.setFailed(errMsg)
if (errMsg == 'Resource not accessible by integration') {
core.error(`See this action's readme for details about this error`)
}
}
}
run()

20
src/utils.ts Normal file
View File

@ -0,0 +1,20 @@
import * as core from '@actions/core'
export function getInputAsArray(
name: string,
options?: core.InputOptions
): string[] {
return getStringAsArray(core.getInput(name, options))
}
export function getStringAsArray(str: string): string[] {
return str
.split(/[\n,]+/)
.map(s => s.trim())
.filter(x => x !== '')
}
export function getErrorMessage(error: unknown) {
if (error instanceof Error) return error.message
return String(error)
}

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"lib": [
"es6"
],
"outDir": "./lib",
"rootDir": "./src",
"declaration": true,
"strict": true,
"noImplicitAny": false,
"esModuleInterop": true
},
"exclude": ["__test__", "lib", "node_modules"]
}