├── .github ├── FUNDING.yml └── workflows │ ├── reaction-comments.yml │ └── release.yml ├── .gitignore ├── .nvmrc ├── .prettierrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── action.yml ├── assets └── screenshot.png ├── dist ├── index.js └── package.json ├── package-lock.json ├── package.json └── src ├── data.js ├── index.js ├── schema.js └── utils.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: dessant 2 | patreon: dessant 3 | custom: 4 | - https://armin.dev/go/paypal 5 | - https://armin.dev/go/bitcoin 6 | -------------------------------------------------------------------------------- /.github/workflows/reaction-comments.yml: -------------------------------------------------------------------------------- 1 | name: 'Reaction Comments' 2 | 3 | on: 4 | issue_comment: 5 | types: [created, edited] 6 | pull_request_review_comment: 7 | types: [created, edited] 8 | schedule: 9 | - cron: '0 0 * * *' 10 | 11 | permissions: 12 | actions: write 13 | issues: write 14 | pull-requests: write 15 | 16 | jobs: 17 | action: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: dessant/reaction-comments@v4 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | 8 | jobs: 9 | release: 10 | name: Release on GitHub 11 | runs-on: ubuntu-22.04 12 | permissions: 13 | contents: write 14 | steps: 15 | - name: Create GitHub release 16 | uses: softprops/action-gh-release@v1 17 | with: 18 | tag_name: ${{ github.ref_name }} 19 | name: ${{ github.ref_name }} 20 | body: > 21 | Learn more about this release from the [changelog](https://github.com/dessant/reaction-comments/blob/main/CHANGELOG.md#changelog). 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .assets 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.9.0 2 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | bracketSpacing: false 3 | arrowParens: 'avoid' 4 | trailingComma: 'none' 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. 4 | 5 | ## [4.0.0](https://github.com/dessant/reaction-comments/compare/v3.0.0...v4.0.0) (2023-11-19) 6 | 7 | 8 | ### ⚠ BREAKING CHANGES 9 | 10 | * the action now requires Node.js 20 11 | 12 | ### Bug Fixes 13 | 14 | * retry and throttle GitHub API requests ([5c35d10](https://github.com/dessant/reaction-comments/commit/5c35d10027d40ecbad90b041ff2294d784fe1a31)) 15 | * update dependencies ([a12ad49](https://github.com/dessant/reaction-comments/commit/a12ad495101d8ddc46e3c35a2a43d16c6da11d4b)) 16 | 17 | ## [3.0.0](https://github.com/dessant/reaction-comments/compare/v2.2.0...v3.0.0) (2022-12-04) 18 | 19 | 20 | ### ⚠ BREAKING CHANGES 21 | 22 | * the action now requires Node.js 16 23 | 24 | ### Bug Fixes 25 | 26 | * update dependencies ([69813be](https://github.com/dessant/reaction-comments/commit/69813be6e69ef88fafb73376b325afc9ababc8fd)) 27 | * update docs ([9c2ad50](https://github.com/dessant/reaction-comments/commit/9c2ad5077134edd5c3d3e25e787974d33f114050)) 28 | 29 | ## [2.2.0](https://github.com/dessant/reaction-comments/compare/v2.1.2...v2.2.0) (2021-10-02) 30 | 31 | 32 | ### Features 33 | 34 | * add option for logging output parameters ([0e746c9](https://github.com/dessant/reaction-comments/commit/0e746c972ed6fa0d3a0ae63a774b60b3ebec6c1f)) 35 | 36 | 37 | ### Bug Fixes 38 | 39 | * remove broken link in default message ([a28549d](https://github.com/dessant/reaction-comments/commit/a28549d35841d6b4f48e3272fc03eb3fa40258a4)) 40 | 41 | ### [2.1.2](https://github.com/dessant/reaction-comments/compare/v2.1.1...v2.1.2) (2021-07-10) 42 | 43 | 44 | ### Bug Fixes 45 | 46 | * add required permission ([c83ec47](https://github.com/dessant/reaction-comments/commit/c83ec4712f6cbc2cc93b756700840d7f43880fc6)) 47 | 48 | ### [2.1.1](https://github.com/dessant/reaction-comments/compare/v2.1.0...v2.1.1) (2021-07-09) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * update GitHub API calls ([30b1031](https://github.com/dessant/reaction-comments/commit/30b10312a5c5735e1060558adcec6a3b78353e70)) 54 | 55 | ## [2.1.0](https://github.com/dessant/reaction-comments/compare/v2.0.0...v2.1.0) (2021-07-09) 56 | 57 | 58 | ### Features 59 | 60 | * make github-token optional and document the use of personal access tokens ([adb3aa7](https://github.com/dessant/reaction-comments/commit/adb3aa7d45ba2a0bfab68933e3ca98cc383c61db)) 61 | 62 | 63 | ### Bug Fixes 64 | 65 | * declare required permissions ([8524539](https://github.com/dessant/reaction-comments/commit/8524539bc2cebd41594dd2a3042fbc86e91b6abd)) 66 | 67 | ## [2.0.0](https://github.com/dessant/reaction-comments/compare/v1.0.2...v2.0.0) (2021-01-07) 68 | 69 | 70 | ### ⚠ BREAKING CHANGES 71 | 72 | * The deployment method and configuration options have changed. 73 | 74 | * feat: move to GitHub Actions ([a74bb54](https://github.com/dessant/reaction-comments/commit/a74bb54bca3d02001dd7ba6ac185b25aec10249b)) 75 | 76 | ### [1.0.2](https://github.com/dessant/reaction-comments/compare/v1.0.1...v1.0.2) (2019-10-25) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * typo ([4d07f88](https://github.com/dessant/reaction-comments/commit/4d07f8852771104cbf7897c3e7b0238e113ae01a)) 82 | 83 | ### [1.0.1](https://github.com/dessant/reaction-comments/compare/v1.0.0...v1.0.1) (2019-10-25) 84 | 85 | 86 | ### Bug Fixes 87 | 88 | * update dependencies ([cbd2cce](https://github.com/dessant/reaction-comments/commit/cbd2cceabbf7c37c558cdfa931f46744a671d0ad)) 89 | 90 | ## [1.0.0](https://github.com/dessant/reaction-comments/compare/v0.3.2...v1.0.0) (2019-06-09) 91 | 92 | 93 | ### Bug Fixes 94 | 95 | * remove indentation from original comment ([8752a00](https://github.com/dessant/reaction-comments/commit/8752a00)) 96 | * update default comment in schema ([5b2be1d](https://github.com/dessant/reaction-comments/commit/5b2be1d)) 97 | 98 | 99 | ### Features 100 | 101 | * support new reactions ([b7ce8e6](https://github.com/dessant/reaction-comments/commit/b7ce8e6)) 102 | * update dependencies ([1b0d617](https://github.com/dessant/reaction-comments/commit/1b0d617)) 103 | * use GitHub tooltip copy as default comment ([4161662](https://github.com/dessant/reaction-comments/commit/4161662)) 104 | 105 | 106 | ### BREAKING CHANGES 107 | 108 | * probot < 9.2.13 no longer supported. 109 | 110 | 111 | 112 | 113 | ## [0.3.2](https://github.com/dessant/reaction-comments/compare/v0.3.1...v0.3.2) (2019-01-20) 114 | 115 | 116 | ### Bug Fixes 117 | 118 | * apply stricter config validation ([8eb0cf9](https://github.com/dessant/reaction-comments/commit/8eb0cf9)) 119 | * respect exemptLabels config option ([c62b168](https://github.com/dessant/reaction-comments/commit/c62b168)) 120 | 121 | 122 | 123 | 124 | ## [0.3.1](https://github.com/dessant/reaction-comments/compare/v0.3.0...v0.3.1) (2018-10-03) 125 | 126 | 127 | ### Bug Fixes 128 | 129 | * allow newer versions of node ([96955a4](https://github.com/dessant/reaction-comments/commit/96955a4)) 130 | 131 | 132 | 133 | 134 | # [0.3.0](https://github.com/dessant/reaction-comments/compare/v0.2.0...v0.3.0) (2018-07-23) 135 | 136 | 137 | ### Features 138 | 139 | * notify maintainers about configuration errors ([f761a6e](https://github.com/dessant/reaction-comments/commit/f761a6e)) 140 | 141 | 142 | 143 | 144 | # [0.2.0](https://github.com/dessant/reaction-comments/compare/v0.1.1...v0.2.0) (2018-06-27) 145 | 146 | 147 | ### Features 148 | 149 | * add {comment-author} as a placeholder ([4bad098](https://github.com/dessant/reaction-comments/commit/4bad098)) 150 | * log additional data and add DRY_RUN env var ([f311d57](https://github.com/dessant/reaction-comments/commit/f311d57)) 151 | 152 | 153 | 154 | 155 | ## [0.1.1](https://github.com/dessant/reaction-comments/compare/v0.1.0...v0.1.1) (2018-06-12) 156 | 157 | 158 | ### Bug Fixes 159 | 160 | * remove database items for deleted installation ([5b4510f](https://github.com/dessant/reaction-comments/commit/5b4510f)) 161 | 162 | 163 | 164 | 165 | # 0.1.0 (2018-06-12) 166 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2023 Armin Sebastian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reaction Comments 2 | 3 | Reaction Comments is a GitHub Action that deletes reaction comments, 4 | such as +1, and encourages the use of GitHub reactions. 5 | 6 | 7 | 8 | ## Supporting the Project 9 | 10 | The continued development of Reaction Comments is made possible 11 | thanks to the support of awesome backers. If you'd like to join them, 12 | please consider contributing with 13 | [Patreon](https://armin.dev/go/patreon?pr=reaction-comments&src=repo), 14 | [PayPal](https://armin.dev/go/paypal?pr=reaction-comments&src=repo) or 15 | [Bitcoin](https://armin.dev/go/bitcoin?pr=reaction-comments&src=repo). 16 | 17 | ## How It Works 18 | 19 | The action detects if new or edited comments consist solely of emojis 20 | and shortcodes used in GitHub reactions. Matching comments are replaced with 21 | the message set in `issue-comment` or `pr-comment`, and deleted after a day. 22 | When the `issue-comment` or `pr-comment` parameter is set to `''`, 23 | matching comments are immediately deleted. 24 | 25 | ## Usage 26 | 27 | Create the `reaction-comments.yml` workflow file in the `.github/workflows` 28 | directory, use one of the [example workflows](#examples) to get started. 29 | 30 | ### Inputs 31 | 32 | The action can be configured using [input parameters](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepswith). 33 | 34 | 35 | - **`github-token`** 36 | - GitHub access token, value must be `${{ github.token }}` or an encrypted 37 | secret that contains a [personal access token](#using-a-personal-access-token) 38 | - Optional, defaults to `${{ github.token }}` 39 | - **`exempt-issue-labels`** 40 | - Do not process comments on issues with any of these labels, 41 | value must be a comma separated list of labels 42 | - Optional, defaults to `''` 43 | - **`issue-comment`** 44 | - Replace reaction comments on issues with this message, 45 | `{comment-author}` is an optional placeholder 46 | - Optional, defaults to `:wave: @{comment-author}, would you like to leave 47 | a reaction instead?` 48 | - **`exempt-pr-labels`** 49 | - Do not process comments on pull requests with any of these labels, 50 | value must be a comma separated list of labels 51 | - Optional, defaults to `''` 52 | - **`pr-comment`** 53 | - Replace reaction comments on pull requests with this message, 54 | `{comment-author}` is an optional placeholder 55 | - Optional, defaults to `:wave: @{comment-author}, would you like to leave 56 | a reaction instead?` 57 | - **`process-only`** 58 | - Process comments only on issues or pull requests, value must be 59 | either `issues` or `prs` 60 | - Optional, defaults to `''` 61 | - **`log-output`** 62 | - Log output parameters, value must be either `true` or `false` 63 | - Optional, defaults to `false` 64 | 65 | ### Outputs 66 | 67 | 68 | - **`comments`** 69 | - Comments that have been either deleted or scheduled for removal, 70 | value is a JSON string in the form of 71 | `[{"owner": "actions", "repo": "toolkit", "issue_number": 1, 72 | "comment_id": 754701878, "is_review_comment": false, "status": "deleted"}]`, 73 | value of `status` is either `scheduled` or `deleted` 74 | - Defaults to `''` 75 | 76 | ## Examples 77 | 78 | The following workflow will replace new or edited reaction comments 79 | with a helpful message, and delete them after a day. 80 | 81 | 82 | ```yaml 83 | name: 'Reaction Comments' 84 | 85 | on: 86 | issue_comment: 87 | types: [created, edited] 88 | pull_request_review_comment: 89 | types: [created, edited] 90 | schedule: 91 | - cron: '0 0 * * *' 92 | 93 | permissions: 94 | actions: write 95 | issues: write 96 | pull-requests: write 97 | 98 | jobs: 99 | action: 100 | runs-on: ubuntu-latest 101 | steps: 102 | - uses: dessant/reaction-comments@v4 103 | ``` 104 | 105 | ### Available input parameters 106 | 107 | This workflow declares all the available input parameters of the action 108 | and their default values. Any of the parameters can be omitted. 109 | 110 | 111 | ```yaml 112 | name: 'Reaction Comments' 113 | 114 | on: 115 | issue_comment: 116 | types: [created, edited] 117 | pull_request_review_comment: 118 | types: [created, edited] 119 | schedule: 120 | - cron: '0 0 * * *' 121 | 122 | permissions: 123 | actions: write 124 | issues: write 125 | pull-requests: write 126 | 127 | jobs: 128 | action: 129 | runs-on: ubuntu-latest 130 | steps: 131 | - uses: dessant/reaction-comments@v4 132 | with: 133 | github-token: ${{ github.token }} 134 | exempt-issue-labels: '' 135 | issue-comment: > 136 | :wave: @{comment-author}, would you like to leave 137 | a reaction instead? 138 | exempt-pr-labels: '' 139 | pr-comment: > 140 | :wave: @{comment-author}, would you like to leave 141 | a reaction instead? 142 | process-only: '' 143 | log-output: false 144 | ``` 145 | 146 | ### Ignoring comments 147 | 148 | This step will process comments only on issues, and ignore threads 149 | with the the `help` or `party-parrot` labels applied. 150 | 151 | 152 | ```yaml 153 | steps: 154 | - uses: dessant/reaction-comments@v4 155 | with: 156 | exempt-issue-labels: 'help, party-parrot' 157 | process-only: 'issues' 158 | ``` 159 | 160 | This step will process comments only on pull requests, and ignore threads 161 | with the `pinned` label applied. 162 | 163 | 164 | ```yaml 165 | steps: 166 | - uses: dessant/reaction-comments@v4 167 | with: 168 | exempt-pr-labels: 'pinned' 169 | process-only: 'prs' 170 | ``` 171 | 172 | ### Deleting comments 173 | 174 | By default, reaction comments are replaced with a message and deleted 175 | after a day. This step will immediately delete new or edited reaction comments 176 | on issues and pull requests. 177 | 178 | 179 | ```yaml 180 | steps: 181 | - uses: dessant/reaction-comments@v4 182 | with: 183 | issue-comment: '' 184 | pr-comment: '' 185 | ``` 186 | 187 | ### Using a personal access token 188 | 189 | The action uses an installation access token by default to interact with GitHub. 190 | You may also authenticate with a personal access token to perform actions 191 | as a GitHub user instead of the `github-actions` app. 192 | 193 | Create a [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) 194 | with the `repo` or `public_repo` scopes enabled, and add the token as an 195 | [encrypted secret](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository) 196 | for the repository or organization, then provide the action with the secret 197 | using the `github-token` input parameter. 198 | 199 | 200 | ```yaml 201 | steps: 202 | - uses: dessant/reaction-comments@v4 203 | with: 204 | github-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 205 | ``` 206 | 207 | ## License 208 | 209 | Copyright (c) 2018-2023 Armin Sebastian 210 | 211 | This software is released under the terms of the MIT License. 212 | See the [LICENSE](LICENSE) file for further information. 213 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Reaction Comments' 2 | description: 'Delete +1 comments and encourage the use of GitHub reactions' 3 | author: 'Armin Sebastian' 4 | inputs: 5 | github-token: 6 | description: 'GitHub access token' 7 | default: '${{ github.token }}' 8 | exempt-issue-labels: 9 | description: 'Do not process comments on issues with these labels, value must be a comma separated list of labels' 10 | default: '' 11 | issue-comment: 12 | description: 'Replace matching comments on issues with this message, `{comment-author}` is an optional placeholder' 13 | default: > 14 | :wave: @{comment-author}, would you like to leave 15 | a reaction instead? 16 | exempt-pr-labels: 17 | description: 'Do not process comments on pull requests with these labels, value must be a comma separated list of labels' 18 | default: '' 19 | pr-comment: 20 | description: 'Replace matching comments on pull requests with this message, `{comment-author}` is an optional placeholder' 21 | default: > 22 | :wave: @{comment-author}, would you like to leave 23 | a reaction instead? 24 | process-only: 25 | description: 'Process comments only on issues or pull requests, value must be either `issues` or `prs`' 26 | default: '' 27 | log-output: 28 | description: 'Log output parameters, value must be either `true` or `false`' 29 | default: false 30 | outputs: 31 | comments: 32 | description: 'Comments that have been either deleted or scheduled for removal, value is a JSON string' 33 | runs: 34 | using: 'node20' 35 | main: 'dist/index.js' 36 | branding: 37 | icon: 'message-square' 38 | color: 'green' 39 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dessant/reaction-comments/e86d247c12bd5c043eec379a1a4453f20cadf913/assets/screenshot.png -------------------------------------------------------------------------------- /dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reaction-comments", 3 | "version": "4.0.0", 4 | "description": "A GitHub Action that deletes +1 comments and encourages the use of GitHub reactions.", 5 | "author": "Armin Sebastian", 6 | "license": "MIT", 7 | "homepage": "https://github.com/dessant/reaction-comments", 8 | "repository": { 9 | "url": "https://github.com/dessant/reaction-comments.git", 10 | "type": "git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/dessant/reaction-comments/issues" 14 | }, 15 | "type": "module", 16 | "main": "src/index.js", 17 | "scripts": { 18 | "build": "ncc build src/index.js -o dist", 19 | "update": "ncu --upgrade", 20 | "release": "commit-and-tag-version", 21 | "push": "git push --tags origin main" 22 | }, 23 | "dependencies": { 24 | "@actions/artifact": "^1.1.2", 25 | "@actions/core": "^1.10.1", 26 | "@actions/github": "^6.0.0", 27 | "@octokit/plugin-throttling": "^8.1.3", 28 | "@octokit/plugin-retry": "^6.0.1", 29 | "adm-zip": "^0.5.10", 30 | "dedent": "^1.5.1", 31 | "fs-extra": "^11.1.1", 32 | "joi": "^17.11.0" 33 | }, 34 | "devDependencies": { 35 | "@vercel/ncc": "^0.38.1", 36 | "commit-and-tag-version": "^12.0.0", 37 | "npm-check-updates": "^16.14.6", 38 | "prettier": "^3.1.0" 39 | }, 40 | "engines": { 41 | "node": ">=20.0.0" 42 | }, 43 | "keywords": [ 44 | "github", 45 | "issues", 46 | "pull requests", 47 | "+1", 48 | "comments", 49 | "reactions", 50 | "github reactions", 51 | "automation", 52 | "github actions", 53 | "project management", 54 | "bot" 55 | ], 56 | "private": true 57 | } 58 | -------------------------------------------------------------------------------- /src/data.js: -------------------------------------------------------------------------------- 1 | const reactionRx = 2 | /^(?:\s*(?:\+1|-1|:(?:\+1|-1|thumbsup|thumbsdown|smile|tada|confused|heart|rocket|eyes):|\u{1f44d}(?:\u{1f3fb}|\u{1f3fc}|\u{1f3fd}|\u{1f3fe}|\u{1f3ff})?|\u{1f44e}(?:\u{1f3fb}|\u{1f3fc}|\u{1f3fd}|\u{1f3fe}|\u{1f3ff})?|\u{1f604}|\u{1f389}|\u{1f615}|\u{2764}\u{fe0f}|\u{1f680}|\u{1f440})\s*)+$/u; 3 | 4 | export {reactionRx}; 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import core from '@actions/core'; 2 | import github from '@actions/github'; 3 | import artifact from '@actions/artifact'; 4 | import {writeJson, remove} from 'fs-extra/esm'; 5 | import dedent from 'dedent'; 6 | import zip from 'adm-zip'; 7 | 8 | import {getConfig, getClient} from './utils.js'; 9 | import {reactionRx} from './data.js'; 10 | 11 | async function run() { 12 | try { 13 | const config = getConfig(); 14 | const client = getClient(config['github-token']); 15 | 16 | const app = new App(config, client); 17 | 18 | let output; 19 | if (github.context.eventName === 'schedule') { 20 | output = await app.processScheduledComments(); 21 | } else { 22 | output = await app.processNewComment(); 23 | } 24 | 25 | core.debug('Setting output (comments)'); 26 | if (output && output.length) { 27 | core.setOutput('comments', JSON.stringify(output)); 28 | 29 | if (config['log-output']) { 30 | core.info('Output (comments):'); 31 | core.info(JSON.stringify(output, null, 2)); 32 | } 33 | } else { 34 | core.setOutput('comments', ''); 35 | } 36 | } catch (err) { 37 | core.setFailed(err); 38 | } 39 | } 40 | 41 | class App { 42 | constructor(config, client) { 43 | this.config = config; 44 | this.client = client; 45 | } 46 | 47 | async processNewComment() { 48 | const payload = github.context.payload; 49 | 50 | if (payload.sender.type === 'Bot') { 51 | return; 52 | } 53 | 54 | const commentBody = payload.comment.body; 55 | if (!reactionRx.test(commentBody)) { 56 | return; 57 | } 58 | 59 | const isReviewComment = payload.comment.hasOwnProperty('commit_id'); 60 | 61 | const threadType = 62 | isReviewComment || payload.issue.pull_request ? 'pr' : 'issue'; 63 | 64 | const processOnly = this.config['process-only']; 65 | if (processOnly && processOnly !== threadType) { 66 | return; 67 | } 68 | 69 | const exemptLabels = this.config[`exempt-${threadType}-labels`]; 70 | if (exemptLabels) { 71 | const labels = threadData.labels.map(label => label.name); 72 | for (const label of exemptLabels) { 73 | if (labels.includes(label)) { 74 | return; 75 | } 76 | } 77 | } 78 | 79 | const {owner, repo} = github.context.repo; 80 | const issue = {owner, repo, issue_number: github.context.issue.number}; 81 | 82 | const commentId = payload.comment.id; 83 | const comment = {owner, repo, comment_id: commentId}; 84 | 85 | const threadData = payload.issue || payload.pull_request; 86 | const lock = { 87 | active: threadData.locked, 88 | reason: threadData.active_lock_reason 89 | }; 90 | 91 | let reactionComment = this.config[`${threadType}-comment`]; 92 | 93 | if (reactionComment) { 94 | reactionComment = reactionComment.replace( 95 | /{comment-author}/, 96 | payload.comment.user.login 97 | ); 98 | 99 | const editedComment = dedent` 100 | ${reactionComment} 101 | 102 |
103 |
104 | 105 | 106 | This comment is scheduled for deletion. Click here to view the original content. 107 | 108 |
109 | 110 | ${dedent(commentBody)} 111 |
112 |
113 | `; 114 | 115 | core.debug(`Editing comment (comment: ${commentId})`); 116 | try { 117 | await this.ensureUnlock(issue, lock, () => 118 | (isReviewComment 119 | ? this.client.rest.pulls.updateReviewComment 120 | : this.client.rest.issues.updateComment)({ 121 | ...comment, 122 | body: editedComment 123 | }) 124 | ); 125 | } catch (err) { 126 | if (err.status === 404) { 127 | return; 128 | } else { 129 | throw err; 130 | } 131 | } 132 | 133 | const storageContent = { 134 | commentId, 135 | isReviewComment, 136 | issueNumber: issue.issue_number 137 | }; 138 | 139 | await this.setWorkflowRunStorage(storageContent); 140 | } else { 141 | core.debug(`Deleting comment (comment: ${commentId})`); 142 | try { 143 | await this.ensureUnlock(issue, lock, () => 144 | (isReviewComment 145 | ? this.client.rest.pulls.deleteReviewComment 146 | : this.client.rest.issues.deleteComment)(comment) 147 | ); 148 | } catch (err) { 149 | if (err.status === 404) { 150 | return; 151 | } else { 152 | throw err; 153 | } 154 | } 155 | } 156 | 157 | return [ 158 | { 159 | ...issue, 160 | comment_id: commentId, 161 | is_review_comment: isReviewComment, 162 | status: reactionComment ? 'scheduled' : 'deleted' 163 | } 164 | ]; 165 | } 166 | 167 | async processScheduledComments() { 168 | const {owner, repo} = github.context.repo; 169 | 170 | const { 171 | data: {workflow_id: workflowId} 172 | } = await this.client.rest.actions.getWorkflowRun({ 173 | owner, 174 | repo, 175 | run_id: github.context.runId 176 | }); 177 | 178 | const { 179 | data: { 180 | workflow_runs: [lastScheduledRun] 181 | } 182 | } = await this.client.rest.actions.listWorkflowRuns({ 183 | owner, 184 | repo, 185 | event: 'schedule', 186 | status: 'completed', 187 | per_page: 1, 188 | workflow_id: workflowId 189 | }); 190 | 191 | let lastProcessedWorkflowRunId; 192 | let lastScheduledRunArtifactId; 193 | 194 | if (lastScheduledRun) { 195 | ({ 196 | storage: {lastProcessedWorkflowRunId} = {}, 197 | artifactId: lastScheduledRunArtifactId 198 | } = await this.getWorkflowRunStorage(lastScheduledRun.id)); 199 | } 200 | 201 | const workflowRuns = await this.client.paginate( 202 | this.client.rest.actions.listWorkflowRuns, 203 | { 204 | owner, 205 | repo, 206 | status: 'completed', 207 | conclusion: 'success', 208 | per_page: 100, 209 | workflow_id: workflowId 210 | }, 211 | function (response, done) { 212 | const data = []; 213 | 214 | for (const workflowRun of response.data) { 215 | if (workflowRun.id === lastProcessedWorkflowRunId) { 216 | done(); 217 | break; 218 | } 219 | 220 | if (workflowRun.event !== 'schedule') { 221 | data.push(workflowRun); 222 | } 223 | } 224 | 225 | return data; 226 | } 227 | ); 228 | 229 | const comments = []; 230 | 231 | for (const workflowRun of workflowRuns.reverse()) { 232 | // only delete comments scheduled more than a day ago 233 | if ( 234 | Date.now() < 235 | new Date(workflowRun.created_at).getTime() + 24 * 60 * 60 * 1000 236 | ) { 237 | break; 238 | } 239 | 240 | const workflowRunId = workflowRun.id; 241 | 242 | const {storage, artifactId} = 243 | await this.getWorkflowRunStorage(workflowRunId); 244 | 245 | if (storage) { 246 | const {commentId, isReviewComment} = storage; 247 | const issue = {owner, repo, issue_number: storage.issueNumber}; 248 | const comment = {owner, repo, comment_id: commentId}; 249 | 250 | let commentBody; 251 | try { 252 | ({ 253 | data: {body: commentBody} 254 | } = await (isReviewComment 255 | ? this.client.rest.pulls.getReviewComment 256 | : this.client.rest.issues.getComment)(comment)); 257 | } catch (err) { 258 | if (err.status === 404) { 259 | await this.client.rest.actions.deleteArtifact({ 260 | owner, 261 | repo, 262 | artifact_id: artifactId 263 | }); 264 | 265 | lastProcessedWorkflowRunId = workflowRunId; 266 | 267 | continue; 268 | } else { 269 | throw err; 270 | } 271 | } 272 | 273 | if (//.test(commentBody) || reactionRx.test(commentBody)) { 274 | const {data: issueData} = await this.client.rest.issues.get({ 275 | ...issue, 276 | headers: { 277 | Accept: 'application/vnd.github.sailor-v-preview+json' 278 | } 279 | }); 280 | 281 | const lock = { 282 | active: issueData.locked, 283 | reason: issueData.active_lock_reason 284 | }; 285 | 286 | core.debug(`Deleting comment (comment: ${commentId})`); 287 | await this.ensureUnlock(issue, lock, () => 288 | (isReviewComment 289 | ? this.client.rest.pulls.deleteReviewComment 290 | : this.client.rest.issues.deleteComment)(comment) 291 | ); 292 | 293 | await this.client.rest.actions.deleteArtifact({ 294 | owner, 295 | repo, 296 | artifact_id: artifactId 297 | }); 298 | 299 | comments.push({ 300 | ...issue, 301 | comment_id: commentId, 302 | is_review_comment: isReviewComment, 303 | status: 'deleted' 304 | }); 305 | } 306 | } 307 | 308 | lastProcessedWorkflowRunId = workflowRunId; 309 | } 310 | 311 | if (lastProcessedWorkflowRunId) { 312 | const storageContent = {lastProcessedWorkflowRunId}; 313 | 314 | await this.setWorkflowRunStorage(storageContent); 315 | } 316 | 317 | if (lastScheduledRunArtifactId) { 318 | await this.client.rest.actions.deleteArtifact({ 319 | owner, 320 | repo, 321 | artifact_id: lastScheduledRunArtifactId 322 | }); 323 | } 324 | 325 | if (comments.length) { 326 | return comments; 327 | } 328 | } 329 | 330 | async getWorkflowRunStorage(runId) { 331 | const {owner, repo} = github.context.repo; 332 | 333 | const { 334 | data: {artifacts} 335 | } = await this.client.rest.actions.listWorkflowRunArtifacts({ 336 | owner, 337 | repo, 338 | run_id: runId 339 | }); 340 | 341 | const artifact = artifacts.find( 342 | item => item.name === 'storage' && !item.expired 343 | ); 344 | 345 | if (artifact) { 346 | const {data: archive} = await this.client.rest.actions.downloadArtifact({ 347 | owner, 348 | repo, 349 | artifact_id: artifact.id, 350 | archive_format: 'zip' 351 | }); 352 | 353 | const storage = JSON.parse( 354 | new zip(Buffer.from(archive)).readAsText('storage.json') 355 | ); 356 | 357 | return {storage, artifactId: artifact.id}; 358 | } 359 | 360 | return {}; 361 | } 362 | 363 | async setWorkflowRunStorage(storageContent) { 364 | const storagePath = 'storage.json'; 365 | 366 | await writeJson(storagePath, storageContent); 367 | 368 | const artifactClient = artifact.create(); 369 | const artifactName = 'storage'; 370 | const artifactFiles = [storagePath]; 371 | const artifactRootDirectory = '.'; 372 | const artifactOptions = { 373 | continueOnError: false, 374 | retentionDays: 60 375 | }; 376 | 377 | const uploadResult = await artifactClient.uploadArtifact( 378 | artifactName, 379 | artifactFiles, 380 | artifactRootDirectory, 381 | artifactOptions 382 | ); 383 | 384 | await remove(storagePath); 385 | 386 | if (uploadResult.failedItems.length) { 387 | throw new Error('Artifact could not be uploaded'); 388 | } 389 | } 390 | 391 | async ensureUnlock(issue, lock, action) { 392 | if (lock.active) { 393 | if (!lock.hasOwnProperty('reason')) { 394 | const {data: issueData} = await this.client.rest.issues.get({ 395 | ...issue, 396 | headers: { 397 | Accept: 'application/vnd.github.sailor-v-preview+json' 398 | } 399 | }); 400 | lock.reason = issueData.active_lock_reason; 401 | } 402 | await this.client.rest.issues.unlock(issue); 403 | 404 | let actionError; 405 | try { 406 | await action(); 407 | } catch (err) { 408 | actionError = err; 409 | } 410 | 411 | if (lock.reason) { 412 | issue = { 413 | ...issue, 414 | lock_reason: lock.reason, 415 | headers: { 416 | Accept: 'application/vnd.github.sailor-v-preview+json' 417 | } 418 | }; 419 | } 420 | await this.client.rest.issues.lock(issue); 421 | 422 | if (actionError) { 423 | throw actionError; 424 | } 425 | } else { 426 | await action(); 427 | } 428 | } 429 | } 430 | 431 | run(); 432 | -------------------------------------------------------------------------------- /src/schema.js: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | const extendedJoi = Joi.extend(joi => { 4 | return { 5 | type: 'stringList', 6 | base: joi.array(), 7 | coerce: { 8 | from: 'string', 9 | method(value) { 10 | value = value.trim(); 11 | if (value) { 12 | value = value 13 | .split(',') 14 | .map(item => item.trim()) 15 | .filter(Boolean); 16 | } 17 | 18 | return {value}; 19 | } 20 | } 21 | }; 22 | }).extend(joi => { 23 | return { 24 | type: 'processOnly', 25 | base: joi.string(), 26 | coerce: { 27 | from: 'string', 28 | method(value) { 29 | value = value.trim(); 30 | if (['issues', 'prs'].includes(value)) { 31 | value = value.slice(0, -1); 32 | } 33 | 34 | return {value}; 35 | } 36 | } 37 | }; 38 | }); 39 | 40 | const schema = Joi.object({ 41 | 'github-token': Joi.string().trim().max(100), 42 | 43 | 'exempt-issue-labels': Joi.alternatives() 44 | .try( 45 | extendedJoi 46 | .stringList() 47 | .items(Joi.string().trim().max(50)) 48 | .min(1) 49 | .max(30) 50 | .unique(), 51 | Joi.string().trim().valid('') 52 | ) 53 | .default(''), 54 | 55 | 'issue-comment': Joi.string() 56 | .trim() 57 | .max(10000) 58 | .allow('') 59 | .default( 60 | ':wave: @{comment-author}, would you like to leave a reaction instead?' 61 | ), 62 | 63 | 'exempt-pr-labels': Joi.alternatives() 64 | .try( 65 | extendedJoi 66 | .stringList() 67 | .items(Joi.string().trim().max(50)) 68 | .min(1) 69 | .max(30) 70 | .unique(), 71 | Joi.string().trim().valid('') 72 | ) 73 | .default(''), 74 | 75 | 'pr-comment': Joi.string() 76 | .trim() 77 | .max(10000) 78 | .allow('') 79 | .default( 80 | ':wave: @{comment-author}, would you like to leave a reaction instead?' 81 | ), 82 | 83 | 'process-only': extendedJoi 84 | .processOnly() 85 | .valid('issue', 'pr', '') 86 | .default(''), 87 | 88 | 'log-output': Joi.boolean().default(false) 89 | }); 90 | 91 | export {schema}; 92 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import core from '@actions/core'; 2 | import github from '@actions/github'; 3 | import {retry} from '@octokit/plugin-retry'; 4 | import {throttling} from '@octokit/plugin-throttling'; 5 | 6 | import {schema} from './schema.js'; 7 | 8 | function getConfig() { 9 | const input = Object.fromEntries( 10 | Object.keys(schema.describe().keys).map(item => [item, core.getInput(item)]) 11 | ); 12 | 13 | const {error, value} = schema.validate(input, {abortEarly: false}); 14 | if (error) { 15 | throw error; 16 | } 17 | 18 | return value; 19 | } 20 | 21 | function getClient(token) { 22 | const requestRetries = 3; 23 | 24 | const rateLimitCallback = function ( 25 | retryAfter, 26 | options, 27 | octokit, 28 | retryCount 29 | ) { 30 | core.info( 31 | `Request quota exhausted for request ${options.method} ${options.url}` 32 | ); 33 | 34 | if (retryCount < requestRetries) { 35 | core.info(`Retrying after ${retryAfter} seconds`); 36 | 37 | return true; 38 | } 39 | }; 40 | 41 | const options = { 42 | request: {retries: requestRetries}, 43 | throttle: { 44 | onSecondaryRateLimit: rateLimitCallback, 45 | onRateLimit: rateLimitCallback 46 | } 47 | }; 48 | 49 | return github.getOctokit(token, options, retry, throttling); 50 | } 51 | 52 | export {getConfig, getClient}; 53 | --------------------------------------------------------------------------------