├── .nvmrc ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── build.yaml │ └── ci.yaml ├── tsconfig.json ├── .prettierrc.json ├── MIGRATION_GUIDE.md ├── LICENSE ├── action.yml ├── package.json ├── src ├── cleanup.ts └── main.ts └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.8.1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node20/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "rootDir": "./src" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 120, 7 | "endOfLine": "lf", 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /MIGRATION_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Migration guide 2 | 3 | ## From v2 to v3 4 | 5 | ### Parameters 6 | 7 | - From `filePath` to `file-path` 8 | - From `GITHUB_TOKEN` to `github-token` 9 | - From `pr_number` to `pr-number` 10 | - From `comment_tag` to `comment-tag` 11 | - From `create_if_not_exists` to `create-if-not-exists` 12 | 13 | ### Mode 14 | 15 | `delete` now deletes a comment immediately. To delete the comment at the end of the job, use `delete-on-completion` mode. -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | compile: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout sources 9 | uses: actions/checkout@v3 10 | 11 | - name: Setup Node 12 | uses: actions/setup-node@v3 13 | 14 | - name: Install dependencies 15 | run: npm install 16 | 17 | - name: Build 18 | run: npm run build 19 | 20 | - name: Lint 21 | run: npm run lint 22 | 23 | - name: Setup Git 24 | if: github.event_name == 'push' && github.ref_name == 'main' 25 | uses: fregante/setup-git-user@v1 26 | 27 | - name: Commit/push if there is a diff 28 | if: github.event_name == 'push' && github.ref_name == 'main' 29 | run: | 30 | if ! git diff --exit-code; then 31 | git add . 32 | git commit -m "chore: compile and lint $GITHUB_SHA [ci skip]" 33 | git push origin main 34 | fi 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Térence Hollander 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 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Comment Pull Request' 2 | branding: 3 | icon: 'message-circle' 4 | color: 'blue' 5 | description: 'Comments a pull request with the provided message' 6 | inputs: 7 | message: 8 | description: 'Message that should be printed in the pull request' 9 | file-path: 10 | description: 'Path of the file that should be commented' 11 | github-token: 12 | description: 'Github token of the repository (automatically created by Github)' 13 | default: ${{ github.token }} 14 | required: false 15 | reactions: 16 | description: 'You can set some reactions on your comments through the `reactions` input.' 17 | pr-number: 18 | description: 'Manual pull request number' 19 | comment-tag: 20 | description: 'A tag on your comment that will be used to identify a comment in case of replacement.' 21 | mode: 22 | description: 'Mode that will be used (upsert/recreate/delete/delete-on-completion)' 23 | default: 'upsert' 24 | create-if-not-exists: 25 | description: 'Whether a comment should be created even if comment-tag is not found.' 26 | default: 'true' 27 | runs: 28 | using: 'node20' 29 | main: 'lib/index.js' 30 | post: 'lib/cleanup/index.js' 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "actions-comment-pull-request", 3 | "version": "3.0.1", 4 | "description": "GitHub action for commenting pull-request", 5 | "main": "lib/main.js", 6 | "scripts": { 7 | "build": "npm run build:main && npm run build:cleanup", 8 | "build:main": "ncc build src/main.ts -o lib", 9 | "build:cleanup": "ncc build src/cleanup.ts -o lib/cleanup", 10 | "lint": "prettier --write **/*.ts" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/thollander/actions-comment-pull-request" 15 | }, 16 | "keywords": [ 17 | "actions", 18 | "container", 19 | "pull-request", 20 | "comment", 21 | "message" 22 | ], 23 | "author": "Térence Hollander", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/thollander/actions-comment-pull-request/issues" 27 | }, 28 | "homepage": "https://github.com/thollander/actions-comment-pull-request#readme", 29 | "dependencies": { 30 | "@actions/core": "^1.11.1", 31 | "@actions/exec": "^1.0.0", 32 | "@actions/github": "^6.0.0", 33 | "@actions/io": "^1.1.3", 34 | "@actions/tool-cache": "^2.0.1" 35 | }, 36 | "devDependencies": { 37 | "@tsconfig/node20": "^20.1.4", 38 | "@types/node": "^22.8.6", 39 | "@vercel/ncc": "^0.38.2", 40 | "prettier": "^3.3.3", 41 | "typescript": "^5.6.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/cleanup.ts: -------------------------------------------------------------------------------- 1 | import * as github from '@actions/github'; 2 | import * as core from '@actions/core'; 3 | 4 | async function run() { 5 | try { 6 | const githubToken: string = core.getInput('github-token'); 7 | const prNumber: string = core.getInput('pr-number'); 8 | const commentTag: string = core.getInput('comment-tag'); 9 | const mode: string = core.getInput('mode'); 10 | 11 | if (mode !== 'delete-on-completion') { 12 | core.debug('This comment was not to be deleted on completion. Skipping'); 13 | return; 14 | } 15 | 16 | if (!commentTag) { 17 | core.debug("No 'comment-tag' parameter passed in. Cannot search for something to delete."); 18 | return; 19 | } 20 | 21 | const context = github.context; 22 | const issueNumber = parseInt(prNumber) || context.payload.pull_request?.number || context.payload.issue?.number; 23 | 24 | const octokit = github.getOctokit(githubToken); 25 | 26 | if (!issueNumber) { 27 | core.setFailed('No issue/pull request in input neither in current context.'); 28 | return; 29 | } 30 | 31 | const commentTagPattern = ``; 32 | 33 | if (commentTagPattern) { 34 | for await (const { data: comments } of octokit.paginate.iterator(octokit.rest.issues.listComments, { 35 | ...context.repo, 36 | issue_number: issueNumber, 37 | })) { 38 | const commentsToDelete = comments.filter((comment) => comment?.body?.includes(commentTagPattern)); 39 | for (const commentToDelete of commentsToDelete) { 40 | core.info(`Deleting comment ${commentToDelete.id}.`); 41 | await octokit.rest.issues.deleteComment({ 42 | ...context.repo, 43 | comment_id: commentToDelete.id, 44 | }); 45 | } 46 | } 47 | } 48 | return; 49 | } catch (error) { 50 | if (error instanceof Error) { 51 | core.setFailed(error.message); 52 | } 53 | } 54 | } 55 | 56 | run(); 57 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | 3 | jobs: 4 | test_comment_pr: 5 | name: (NRT) Comment PR 6 | runs-on: ubuntu-latest 7 | permissions: 8 | pull-requests: write 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | 13 | - name: (BEFORE) Setup test cases 14 | run: echo '**Content of file referenced with absolute path**' > /tmp/foobar.txt 15 | 16 | - name: Comment PR with message 17 | uses: ./ 18 | id: nrt-message 19 | with: 20 | message: | 21 | Current branch is `${{ github.head_ref }}`. 22 | _(execution **${{ github.run_id }}** / attempt **${{ github.run_attempt }}**)_ 23 | comment-tag: nrt-message 24 | reactions: eyes, rocket 25 | mode: recreate 26 | 27 | - name: Create a comment to delete it after 28 | uses: ./ 29 | with: 30 | message: | 31 | This message will be deleted 32 | comment-tag: nrt-message-delete 33 | reactions: eyes 34 | 35 | - name: Delete comment 36 | uses: ./ 37 | with: 38 | comment-tag: nrt-message-delete 39 | mode: delete 40 | 41 | - name: Comment PR with message that will be deleted 42 | uses: ./ 43 | with: 44 | message: | 45 | This PR is being built... 46 | comment-tag: nrt-message-delete-on-completion 47 | reactions: eyes 48 | mode: delete-on-completion 49 | 50 | - name: Comment PR with file 51 | uses: ./ 52 | with: 53 | file-path: README.md 54 | comment-tag: nrt-file 55 | reactions: eyes, rocket 56 | mode: recreate 57 | 58 | - name: Comment PR with file (absolute path) 59 | uses: ./ 60 | with: 61 | file-path: /tmp/foobar.txt 62 | comment-tag: nrt-file-absolute 63 | reactions: eyes, rocket 64 | mode: recreate 65 | 66 | - name: Do not comment PR if not exists 67 | uses: ./ 68 | with: 69 | message: Should not be printed 70 | comment-tag: nrt-create-if-not-exists 71 | create-if-not-exists: false 72 | 73 | - name: Check outputs 74 | run: | 75 | echo "id : ${{ steps.nrt-message.outputs.id }}" 76 | echo "body : ${{ steps.nrt-message.outputs.body }}" 77 | echo "html-url : ${{ steps.nrt-message.outputs.html-url }}" 78 | 79 | - name: (AFTER) Setup test cases 80 | run: rm /tmp/foobar.txt -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import * as github from '@actions/github'; 3 | import * as core from '@actions/core'; 4 | import { GetResponseDataTypeFromEndpointMethod } from '@octokit/types'; 5 | 6 | // See https://docs.github.com/en/rest/reactions#reaction-types 7 | const REACTIONS = ['+1', '-1', 'laugh', 'confused', 'heart', 'hooray', 'rocket', 'eyes'] as const; 8 | type Reaction = (typeof REACTIONS)[number]; 9 | 10 | async function run() { 11 | try { 12 | const message: string = core.getInput('message'); 13 | const filePath: string = core.getInput('file-path'); 14 | const githubToken: string = core.getInput('github-token'); 15 | const prNumber: string = core.getInput('pr-number'); 16 | const commentTag: string = core.getInput('comment-tag'); 17 | const reactions: string = core.getInput('reactions'); 18 | const mode: string = core.getInput('mode'); 19 | const createIfNotExists: boolean = core.getInput('create-if-not-exists') === 'true'; 20 | 21 | if (!message && !filePath && mode !== 'delete') { 22 | core.setFailed('Either "file-path" or "message" should be provided as input unless running as "delete".'); 23 | return; 24 | } 25 | 26 | let content: string = message; 27 | if (!message && filePath) { 28 | content = fs.readFileSync(filePath, 'utf8'); 29 | } 30 | 31 | const context = github.context; 32 | const issueNumber = parseInt(prNumber) || context.payload.pull_request?.number || context.payload.issue?.number; 33 | 34 | const octokit = github.getOctokit(githubToken); 35 | 36 | if (!issueNumber) { 37 | core.setFailed('No issue/pull request in input neither in current context.'); 38 | return; 39 | } 40 | 41 | async function addReactions(commentId: number, reactions: string) { 42 | const validReactions = reactions 43 | .replace(/\s/g, '') 44 | .split(',') 45 | .filter((reaction) => REACTIONS.includes(reaction)); 46 | 47 | await Promise.allSettled( 48 | validReactions.map(async (content) => { 49 | await octokit.rest.reactions.createForIssueComment({ 50 | ...context.repo, 51 | comment_id: commentId, 52 | content, 53 | }); 54 | }), 55 | ); 56 | } 57 | 58 | async function createComment({ 59 | owner, 60 | repo, 61 | issueNumber, 62 | body, 63 | }: { 64 | owner: string; 65 | repo: string; 66 | issueNumber: number; 67 | body: string; 68 | }) { 69 | const { data: comment } = await octokit.rest.issues.createComment({ 70 | owner, 71 | repo, 72 | issue_number: issueNumber, 73 | body, 74 | }); 75 | 76 | core.setOutput('id', comment.id); 77 | core.setOutput('body', comment.body); 78 | core.setOutput('html-url', comment.html_url); 79 | 80 | await addReactions(comment.id, reactions); 81 | 82 | return comment; 83 | } 84 | 85 | async function updateComment({ 86 | owner, 87 | repo, 88 | commentId, 89 | body, 90 | }: { 91 | owner: string; 92 | repo: string; 93 | commentId: number; 94 | body: string; 95 | }) { 96 | const { data: comment } = await octokit.rest.issues.updateComment({ 97 | owner, 98 | repo, 99 | comment_id: commentId, 100 | body, 101 | }); 102 | 103 | core.setOutput('id', comment.id); 104 | core.setOutput('body', comment.body); 105 | core.setOutput('html-url', comment.html_url); 106 | 107 | await addReactions(comment.id, reactions); 108 | 109 | return comment; 110 | } 111 | 112 | async function deleteComment({ owner, repo, commentId }: { owner: string; repo: string; commentId: number }) { 113 | const { data: comment } = await octokit.rest.issues.deleteComment({ 114 | owner, 115 | repo, 116 | comment_id: commentId, 117 | }); 118 | 119 | return comment; 120 | } 121 | 122 | const commentTagPattern = commentTag ? `` : null; 123 | const body = commentTagPattern ? `${content}\n${commentTagPattern}` : content; 124 | 125 | if (commentTagPattern) { 126 | type ListCommentsResponseDataType = GetResponseDataTypeFromEndpointMethod< 127 | typeof octokit.rest.issues.listComments 128 | >; 129 | let comment: ListCommentsResponseDataType[0] | undefined; 130 | for await (const { data: comments } of octokit.paginate.iterator(octokit.rest.issues.listComments, { 131 | ...context.repo, 132 | issue_number: issueNumber, 133 | })) { 134 | comment = comments.find((comment) => comment?.body?.includes(commentTagPattern)); 135 | if (comment) break; 136 | } 137 | 138 | if (comment) { 139 | if (mode === 'upsert') { 140 | await updateComment({ 141 | ...context.repo, 142 | commentId: comment.id, 143 | body, 144 | }); 145 | return; 146 | } else if (mode === 'recreate') { 147 | await deleteComment({ 148 | ...context.repo, 149 | commentId: comment.id, 150 | }); 151 | 152 | await createComment({ 153 | ...context.repo, 154 | issueNumber, 155 | body, 156 | }); 157 | return; 158 | } else if (mode === 'delete') { 159 | await deleteComment({ 160 | ...context.repo, 161 | commentId: comment.id, 162 | }); 163 | return; 164 | } else if (mode === 'delete-on-completion') { 165 | core.debug('Registering this comment to be deleted.'); 166 | } else { 167 | core.setFailed( 168 | `Mode ${mode} is unknown. Please use 'upsert', 'recreate', 'delete' or 'delete-on-completion'.`, 169 | ); 170 | return; 171 | } 172 | } else if (mode === 'delete') { 173 | core.info('No comment has been found with asked pattern. Nothing to delete.'); 174 | return; 175 | } else if (createIfNotExists) { 176 | core.info('No comment has been found with asked pattern. Creating a new comment.'); 177 | } else { 178 | core.info( 179 | 'Not creating comment as the pattern has not been found. Use `create-if-not-exists: true` to create a new comment anyway.', 180 | ); 181 | return; 182 | } 183 | } 184 | 185 | await createComment({ 186 | ...context.repo, 187 | issueNumber, 188 | body, 189 | }); 190 | } catch (error) { 191 | if (error instanceof Error) { 192 | core.setFailed(error.message); 193 | } 194 | } 195 | } 196 | 197 | run(); 198 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Comment Pull Request - GitHub Actions 2 | 3 | ## What is it ? 4 | 5 | A GitHub action that comments with a given message the pull request linked to the pushed branch. 6 | You can even put dynamic data thanks to [Contexts and expression syntax](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/contexts-and-expression-syntax-for-github-actions). 7 | 8 | ## Usage 9 | 10 | ### Classic usage 11 | 12 | ```yml 13 | on: pull_request 14 | 15 | jobs: 16 | example_comment_pr: 17 | runs-on: ubuntu-latest 18 | name: An example job to comment a PR 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | 23 | - name: Comment PR 24 | uses: thollander/actions-comment-pull-request@v3 25 | with: 26 | message: | 27 | Hello world ! :wave: 28 | ``` 29 | 30 | ### Comment a file content 31 | 32 | Thanks to the `file-path` input, a file content can be commented. 33 | You can either pass an absolute file-path or a relative one that will be by default retrieved from `GITHUB_WORKSPACE`. 34 | (Note that if both a `message` and `file-path` are provided, `message` will take precedence.) 35 | 36 | ```yml 37 | - name: PR comment with file 38 | uses: thollander/actions-comment-pull-request@v3 39 | with: 40 | file-path: /path/to/file.txt 41 | ``` 42 | 43 | 44 | ### Setting reactions 45 | 46 | You can also set some reactions on your comments through the `reactions` input. 47 | It takes only valid reactions and adds it to the comment you've just created. (See https://docs.github.com/en/rest/reactions#reaction-types) 48 | 49 | ```yml 50 | - name: PR comment with reactions 51 | uses: thollander/actions-comment-pull-request@v3 52 | with: 53 | message: | 54 | Hello world ! :wave: 55 | reactions: eyes, rocket 56 | ``` 57 | 58 | ### Specifying which pull request to comment on 59 | 60 | You can explicitly input which pull request should be commented on by passing the `pr-number` input. 61 | That is particularly useful for manual workflow for instance (`workflow_run`). 62 | 63 | ```yml 64 | ... 65 | - name: Comment PR 66 | uses: thollander/actions-comment-pull-request@v3 67 | with: 68 | message: | 69 | Hello world ! :wave: 70 | pr-number: 123 # This will comment on pull request #123 71 | ``` 72 | 73 | 74 | ### Update a comment 75 | 76 | Editing an existing comment is also possible thanks to the `comment-tag` input. 77 | 78 | Thanks to this parameter, it will be possible to identify your comment and then to upsert on it. 79 | If the comment is not found at first, it will create a new comment. 80 | 81 | _That is particularly interesting while committing multiple times in a PR and that you just want to have the last execution report printed. It avoids flooding the PR._ 82 | 83 | ```yml 84 | ... 85 | - name: Comment PR with execution number 86 | uses: thollander/actions-comment-pull-request@v3 87 | with: 88 | message: | 89 | _(execution **${{ github.run_id }}** / attempt **${{ github.run_attempt }}**)_ 90 | comment-tag: execution 91 | ``` 92 | 93 | Note: the input `mode` can be used to either `upsert` (by default) or `recreate` the comment (= delete and create) 94 | 95 | ### Delete a comment 96 | 97 | 98 | Deleting a comment with a specific `comment-tag` is possible with the `mode: delete`. If a comment with the `comment-tag` exists, it will be deleted when ran. 99 | 100 | ```yml 101 | ... 102 | - name: Delete a comment 103 | uses: thollander/actions-comment-pull-request@v3 104 | with: 105 | comment-tag: to_delete 106 | mode: delete 107 | ``` 108 | 109 | ### Delete a comment on job completion 110 | 111 | Deleting an existing comment on job completion is also possible thanks to the `comment-tag` input combined with `mode: delete-on-completion`. 112 | 113 | This will delete the comment at the end of the job. 114 | 115 | ```yml 116 | ... 117 | - name: Write a comment that will be deleted at the end of the job 118 | uses: thollander/actions-comment-pull-request@v3 119 | with: 120 | message: | 121 | The PR is being built... 122 | comment-tag: to_delete_on_completion 123 | mode: delete-on-completion 124 | ``` 125 | 126 | ## Inputs 127 | 128 | ### Action inputs 129 | 130 | | Name | Description | Required | Default | 131 | | --- | --- | --- | --- | 132 | | `github-token` | Token that is used to create comments. Defaults to ${{ github.token }} | ✅ | | 133 | | `message` | Comment body | | | 134 | | `file-path` | Path of the file that should be commented | | | 135 | | `reactions` | List of reactions for the comment (comma separated). See https://docs.github.com/en/rest/reactions#reaction-types | | | 136 | | `pr-number` | The number of the pull request where to create the comment | | current pull-request/issue number (deduced from context) | 137 | | `comment-tag` | A tag on your comment that will be used to identify a comment in case of replacement | | | 138 | | `mode` | Mode that will be used to update comment (upsert/recreate/delete/delete-on-completion) | | upsert | 139 | | `create-if-not-exists` | Whether a comment should be created even if `comment-tag` is not found | | true | 140 | 141 | 142 | ## Outputs 143 | 144 | ### Action outputs 145 | 146 | You can get some outputs from this actions : 147 | 148 | | Name | Description | 149 | | --- | --- | 150 | | `id` | Comment id that was created or updated | 151 | | `body` | Comment body | 152 | | `html-url` | URL of the comment created or updated | 153 | 154 | ### Example output 155 | 156 | ```yaml 157 | - name: Comment PR 158 | uses: thollander/actions-comment-pull-request@v3 159 | id: hello 160 | with: 161 | message: | 162 | Hello world ! :wave: 163 | - name: Check outputs 164 | run: | 165 | echo "id : ${{ steps.hello.outputs.id }}" 166 | echo "body : ${{ steps.hello.outputs.body }}" 167 | echo "html-url : ${{ steps.hello.outputs.html-url }}" 168 | ``` 169 | 170 | ## Permissions 171 | 172 | Depending on the permissions granted to your token, you may lack some rights. 173 | To run successfully, this actions needs at least : 174 | 175 | ```yaml 176 | permissions: 177 |   pull-requests: write 178 | ``` 179 | 180 | Add this in case you get `Resource not accessible by integration` error. 181 | See [jobs..permissions](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idpermissions) for more information. 182 | 183 | 184 | > Note that, if the PR comes from a fork, it will have only read permission despite the permissions given in the action for the `pull_request` event. 185 | > In this case, you may use the `pull_request_target` event. With this event, permissions can be given without issue (the difference is that it will execute the action from the target branch and not from the origin PR). 186 | 187 | ## Contributing 188 | 189 | ### Build 190 | 191 | The build steps transpiles the `src/main.ts` to `lib/index.js` which is used in a NodeJS environment. 192 | It is handled by `vercel/ncc` compiler. 193 | 194 | ```sh 195 | $ npm run build 196 | ``` 197 | --------------------------------------------------------------------------------