├── .github ├── FUNDING.yml ├── comment-body-addition.md ├── comment-body.md ├── comment-template.md ├── workflows │ ├── automerge-dependabot.yml │ ├── slash-command-dispatch.yml │ ├── update-major-version.yml │ ├── test-v3.yml │ ├── test-command.yml │ └── ci.yml └── dependabot.yml ├── .gitignore ├── .eslintignore ├── .prettierignore ├── .prettierrc.json ├── jest.config.js ├── tsconfig.json ├── src ├── utils.ts ├── main.ts └── create-or-update-comment.ts ├── .eslintrc.json ├── LICENSE ├── package.json ├── action.yml └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: peter-evans -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.github/comment-body-addition.md: -------------------------------------------------------------------------------- 1 | This is still the second line. -------------------------------------------------------------------------------- /.github/comment-body.md: -------------------------------------------------------------------------------- 1 | This is a multi-line test comment read from a file. 2 | This is the second line. -------------------------------------------------------------------------------- /.github/comment-template.md: -------------------------------------------------------------------------------- 1 | This is a test comment template 2 | Render template variables such as {{ .foo }} and {{ .bar }}. 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid", 10 | "parser": "typescript" 11 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es6" 7 | ], 8 | "outDir": "./lib", 9 | "rootDir": "./src", 10 | "declaration": true, 11 | "strict": true, 12 | "noImplicitAny": false, 13 | "esModuleInterop": true 14 | }, 15 | "exclude": ["__test__", "lib", "node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/automerge-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Auto-merge Dependabot 2 | on: pull_request 3 | 4 | jobs: 5 | automerge: 6 | runs-on: ubuntu-latest 7 | if: github.actor == 'dependabot[bot]' 8 | steps: 9 | - uses: peter-evans/enable-pull-request-automerge@v3 10 | with: 11 | token: ${{ secrets.ACTIONS_BOT_TOKEN }} 12 | pull-request-number: ${{ github.event.pull_request.number }} 13 | merge-method: squash 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "wednesday" 8 | labels: 9 | - "dependencies" 10 | 11 | - package-ecosystem: "npm" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | day: "wednesday" 16 | ignore: 17 | - dependency-name: "*" 18 | update-types: ["version-update:semver-major"] 19 | labels: 20 | - "dependencies" 21 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | 3 | export function getInputAsArray( 4 | name: string, 5 | options?: core.InputOptions 6 | ): string[] { 7 | return getStringAsArray(core.getInput(name, options)) 8 | } 9 | 10 | export function getStringAsArray(str: string): string[] { 11 | return str 12 | .split(/[\n,]+/) 13 | .map(s => s.trim()) 14 | .filter(x => x !== '') 15 | } 16 | 17 | export function getErrorMessage(error: unknown) { 18 | if (error instanceof Error) return error.message 19 | return String(error) 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { "node": true, "jest": true }, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { "ecmaVersion": 9, "sourceType": "module" }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:import/errors", 10 | "plugin:import/warnings", 11 | "plugin:import/typescript", 12 | "plugin:prettier/recommended" 13 | ], 14 | "plugins": ["@typescript-eslint"], 15 | "rules": { 16 | "@typescript-eslint/camelcase": "off" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/slash-command-dispatch.yml: -------------------------------------------------------------------------------- 1 | name: Slash Command Dispatch 2 | on: 3 | issue_comment: 4 | types: [created] 5 | jobs: 6 | slashCommandDispatch: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Slash Command Dispatch 10 | uses: peter-evans/slash-command-dispatch@v4 11 | with: 12 | token: ${{ secrets.ACTIONS_BOT_TOKEN }} 13 | config: > 14 | [ 15 | { 16 | "command": "rebase", 17 | "permission": "admin", 18 | "repository": "peter-evans/slash-command-dispatch-processor", 19 | "issue_type": "pull-request" 20 | }, 21 | { 22 | "command": "test", 23 | "permission": "admin", 24 | "named_args": true 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /.github/workflows/update-major-version.yml: -------------------------------------------------------------------------------- 1 | name: Update Major Version 2 | run-name: Update ${{ github.event.inputs.main_version }} to ${{ github.event.inputs.target }} 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | target: 8 | description: The target tag or reference 9 | required: true 10 | main_version: 11 | type: choice 12 | description: The major version tag to update 13 | options: 14 | - v3 15 | - v4 16 | 17 | jobs: 18 | tag: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | token: ${{ secrets.ACTIONS_BOT_TOKEN }} 24 | fetch-depth: 0 25 | - name: Git config 26 | run: | 27 | git config user.name actions-bot 28 | git config user.email actions-bot@users.noreply.github.com 29 | - name: Tag new target 30 | run: git tag -f ${{ github.event.inputs.main_version }} ${{ github.event.inputs.target }} 31 | - name: Push new tag 32 | run: git push origin ${{ github.event.inputs.main_version }} --force 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Peter Evans 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-or-update-comment", 3 | "version": "4.0.0", 4 | "description": "Create or update an issue or pull request comment", 5 | "main": "lib/main.js", 6 | "scripts": { 7 | "build": "tsc && ncc build", 8 | "format": "prettier --write '**/*.ts'", 9 | "format-check": "prettier --check '**/*.ts'", 10 | "lint": "eslint src/**/*.ts", 11 | "test": "jest --passWithNoTests" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/peter-evans/create-or-update-comment.git" 16 | }, 17 | "keywords": [ 18 | "actions", 19 | "create", 20 | "update", 21 | "comment" 22 | ], 23 | "author": "Peter Evans", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/peter-evans/create-or-update-comment/issues" 27 | }, 28 | "homepage": "https://github.com/peter-evans/create-or-update-comment#readme", 29 | "dependencies": { 30 | "@actions/core": "^1.11.1", 31 | "@actions/github": "^6.0.0" 32 | }, 33 | "devDependencies": { 34 | "@types/jest": "^27.0.3", 35 | "@types/node": "^18.19.80", 36 | "@typescript-eslint/eslint-plugin": "^5.62.0", 37 | "@typescript-eslint/parser": "^5.62.0", 38 | "@vercel/ncc": "^0.38.3", 39 | "eslint": "^8.57.1", 40 | "eslint-plugin-github": "^4.10.2", 41 | "eslint-plugin-jest": "^27.9.0", 42 | "eslint-plugin-prettier": "^5.2.3", 43 | "jest": "^27.5.1", 44 | "jest-circus": "^27.5.1", 45 | "js-yaml": "^4.1.0", 46 | "prettier": "^3.5.3", 47 | "ts-jest": "^27.1.5", 48 | "typescript": "^4.9.5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Create or Update Comment' 2 | description: 'Create or update an issue or pull request comment' 3 | inputs: 4 | token: 5 | description: 'GITHUB_TOKEN or a repo scoped PAT.' 6 | default: ${{ github.token }} 7 | repository: 8 | description: 'The full name of the repository in which to create or update a comment.' 9 | default: ${{ github.repository }} 10 | issue-number: 11 | description: 'The number of the issue or pull request in which to create a comment.' 12 | comment-id: 13 | description: 'The id of the comment to update.' 14 | body: 15 | description: 'The comment body. Cannot be used in conjunction with `body-path`.' 16 | body-path: 17 | description: 'The path to a file containing the comment body. Cannot be used in conjunction with `body`.' 18 | body-file: 19 | description: 'Deprecated in favour of `body-path`.' 20 | edit-mode: 21 | description: 'The mode when updating a comment, "replace" or "append".' 22 | default: 'append' 23 | append-separator: 24 | description: 'The separator to use when appending to an existing comment. (`newline`, `space`, `none`)' 25 | default: 'newline' 26 | reactions: 27 | description: 'A comma or newline separated list of reactions to add to the comment.' 28 | reactions-edit-mode: 29 | description: 'The mode when updating comment reactions, "replace" or "append".' 30 | default: 'append' 31 | outputs: 32 | comment-id: 33 | description: 'The id of the created comment' 34 | runs: 35 | using: 'node20' 36 | main: 'dist/index.js' 37 | branding: 38 | icon: 'message-square' 39 | color: 'gray-dark' 40 | -------------------------------------------------------------------------------- /.github/workflows/test-v3.yml: -------------------------------------------------------------------------------- 1 | name: Test v3 2 | on: workflow_dispatch 3 | jobs: 4 | testCreateOrUpdateComment: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 8 | 9 | # Test create 10 | - name: Create comment 11 | uses: peter-evans/create-or-update-comment@v4 12 | id: couc 13 | with: 14 | token: ${{ secrets.ACTIONS_BOT_TOKEN }} 15 | issue-number: 1 16 | body: | 17 | This is a multi-line test comment 18 | - With GitHub **Markdown** :sparkles: 19 | - Created by [create-or-update-comment][1] 20 | 21 | [1]: https://github.com/peter-evans/create-or-update-comment 22 | reactions: '+1' 23 | 24 | # Test update 25 | - name: Update comment 26 | uses: peter-evans/create-or-update-comment@v4 27 | with: 28 | token: ${{ secrets.ACTIONS_BOT_TOKEN }} 29 | comment-id: ${{ steps.couc.outputs.comment-id }} 30 | body: | 31 | **Edit:** Some additional info 32 | reactions: eyes 33 | reactions-edit-mode: replace 34 | 35 | # Test add reactions 36 | - name: Add reactions 37 | uses: peter-evans/create-or-update-comment@v4 38 | with: 39 | token: ${{ secrets.ACTIONS_BOT_TOKEN }} 40 | comment-id: ${{ steps.couc.outputs.comment-id }} 41 | reactions: heart, hooray, laugh 42 | 43 | # Test create with body from file 44 | - name: Create comment 45 | uses: peter-evans/create-or-update-comment@v4 46 | with: 47 | token: ${{ secrets.ACTIONS_BOT_TOKEN }} 48 | issue-number: 1 49 | body-file: .github/comment-body.md 50 | 51 | # Test create from template 52 | - name: Render template 53 | id: template 54 | uses: chuhlomin/render-template@807354a04d9300c9c2ac177c0aa41556c92b3f75 # v1.10 55 | with: 56 | template: .github/comment-template.md 57 | vars: | 58 | foo: this 59 | bar: that 60 | 61 | - name: Create comment 62 | uses: peter-evans/create-or-update-comment@v4 63 | with: 64 | token: ${{ secrets.ACTIONS_BOT_TOKEN }} 65 | issue-number: 1 66 | body: ${{ steps.template.outputs.result }} 67 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import {Inputs, createOrUpdateComment} from './create-or-update-comment' 3 | import {existsSync, readFileSync} from 'fs' 4 | import {inspect} from 'util' 5 | import * as utils from './utils' 6 | 7 | function getBody(inputs: Inputs) { 8 | if (inputs.body) { 9 | return inputs.body 10 | } else if (inputs.bodyPath) { 11 | return readFileSync(inputs.bodyPath, 'utf-8') 12 | } else { 13 | return '' 14 | } 15 | } 16 | 17 | async function run(): Promise { 18 | try { 19 | const inputs: Inputs = { 20 | token: core.getInput('token'), 21 | repository: core.getInput('repository'), 22 | issueNumber: Number(core.getInput('issue-number')), 23 | commentId: Number(core.getInput('comment-id')), 24 | body: core.getInput('body'), 25 | bodyPath: core.getInput('body-path') || core.getInput('body-file'), 26 | editMode: core.getInput('edit-mode'), 27 | appendSeparator: core.getInput('append-separator'), 28 | reactions: utils.getInputAsArray('reactions'), 29 | reactionsEditMode: core.getInput('reactions-edit-mode') 30 | } 31 | core.debug(`Inputs: ${inspect(inputs)}`) 32 | 33 | if (!['append', 'replace'].includes(inputs.editMode)) { 34 | throw new Error(`Invalid edit-mode '${inputs.editMode}'.`) 35 | } 36 | 37 | if (!['append', 'replace'].includes(inputs.reactionsEditMode)) { 38 | throw new Error( 39 | `Invalid reactions edit-mode '${inputs.reactionsEditMode}'.` 40 | ) 41 | } 42 | 43 | if (!['newline', 'space', 'none'].includes(inputs.appendSeparator)) { 44 | throw new Error(`Invalid append-separator '${inputs.appendSeparator}'.`) 45 | } 46 | 47 | if (inputs.bodyPath && inputs.body) { 48 | throw new Error("Only one of 'body' or 'body-path' can be set.") 49 | } 50 | 51 | if (inputs.bodyPath) { 52 | if (!existsSync(inputs.bodyPath)) { 53 | throw new Error(`File '${inputs.bodyPath}' does not exist.`) 54 | } 55 | } 56 | 57 | const body = getBody(inputs) 58 | 59 | if (inputs.commentId) { 60 | if (!body && !inputs.reactions) { 61 | throw new Error("Missing comment 'body', 'body-path', or 'reactions'.") 62 | } 63 | } else if (inputs.issueNumber) { 64 | if (!body) { 65 | throw new Error("Missing comment 'body' or 'body-path'.") 66 | } 67 | } else { 68 | throw new Error("Missing either 'issue-number' or 'comment-id'.") 69 | } 70 | 71 | createOrUpdateComment(inputs, body) 72 | } catch (error) { 73 | core.debug(inspect(error)) 74 | const errMsg = utils.getErrorMessage(error) 75 | core.setFailed(errMsg) 76 | if (errMsg == 'Resource not accessible by integration') { 77 | core.error(`See this action's readme for details about this error`) 78 | } 79 | } 80 | } 81 | 82 | run() 83 | -------------------------------------------------------------------------------- /.github/workflows/test-command.yml: -------------------------------------------------------------------------------- 1 | name: Test Command 2 | on: 3 | repository_dispatch: 4 | types: [test-command] 5 | jobs: 6 | testCreateOrUpdateComment: 7 | runs-on: ubuntu-latest 8 | steps: 9 | # Get the target repository and branch 10 | - name: Get the target repository and branch 11 | id: vars 12 | run: | 13 | repository=${{ github.event.client_payload.slash_command.repository }} 14 | if [[ -z "$repository" ]]; then repository=${{ github.repository }}; fi 15 | echo "repository=$repository" >> $GITHUB_OUTPUT 16 | branch=${{ github.event.client_payload.slash_command.branch }} 17 | if [[ -z "$branch" ]]; then branch="main"; fi 18 | echo "branch=$branch" >> $GITHUB_OUTPUT 19 | 20 | # Checkout the branch to test 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | repository: ${{ steps.vars.outputs.repository }} 24 | ref: ${{ steps.vars.outputs.branch }} 25 | 26 | # Test create 27 | - name: Create comment 28 | uses: ./ 29 | id: couc 30 | with: 31 | issue-number: 1 32 | body: | 33 | This is a multi-line test comment 34 | - With GitHub **Markdown** :sparkles: 35 | - Created by [create-or-update-comment][1] 36 | 37 | [1]: https://github.com/peter-evans/create-or-update-comment 38 | reactions: '+1' 39 | 40 | # Test update 41 | - name: Update comment 42 | uses: ./ 43 | with: 44 | comment-id: ${{ steps.couc.outputs.comment-id }} 45 | body: | 46 | **Edit:** Some additional info 47 | reactions: eyes 48 | reactions-edit-mode: replace 49 | 50 | # Test add reactions 51 | - name: Add reactions 52 | uses: ./ 53 | with: 54 | comment-id: ${{ steps.couc.outputs.comment-id }} 55 | reactions: heart, hooray, laugh 56 | 57 | # Test create with body from file 58 | - name: Create comment 59 | uses: ./ 60 | with: 61 | issue-number: 1 62 | body-path: .github/comment-body.md 63 | 64 | # Test create from template 65 | - name: Render template 66 | id: template 67 | uses: chuhlomin/render-template@807354a04d9300c9c2ac177c0aa41556c92b3f75 # v1.10 68 | with: 69 | template: .github/comment-template.md 70 | vars: | 71 | foo: this 72 | bar: that 73 | 74 | - name: Create comment 75 | uses: ./ 76 | with: 77 | issue-number: 1 78 | body: ${{ steps.template.outputs.result }} 79 | 80 | - name: Add reaction 81 | uses: peter-evans/create-or-update-comment@v4 82 | with: 83 | repository: ${{ github.event.client_payload.github.payload.repository.full_name }} 84 | comment-id: ${{ github.event.client_payload.github.payload.comment.id }} 85 | reactions: hooray 86 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [main] 5 | paths-ignore: 6 | - 'README.md' 7 | - 'docs/**' 8 | pull_request: 9 | branches: [main] 10 | paths-ignore: 11 | - 'README.md' 12 | - 'docs/**' 13 | 14 | permissions: 15 | issues: write 16 | pull-requests: write 17 | contents: write 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | outputs: 23 | issue-number: ${{ steps.vars.outputs.issue-number }} 24 | steps: 25 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 27 | with: 28 | node-version: 20.x 29 | cache: npm 30 | - run: npm ci 31 | - run: npm run build 32 | - run: npm run format-check 33 | - run: npm run lint 34 | - run: npm run test 35 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 36 | with: 37 | name: dist 38 | path: dist 39 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 40 | with: 41 | name: action.yml 42 | path: action.yml 43 | - id: vars 44 | run: | 45 | if [[ "${{ github.event_name }}" == "pull_request" ]]; then \ 46 | echo "issue-number=${{ github.event.number }}" >> $GITHUB_OUTPUT; \ 47 | else \ 48 | echo "issue-number=1" >> $GITHUB_OUTPUT; \ 49 | fi 50 | 51 | test: 52 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository 53 | needs: [build] 54 | runs-on: ubuntu-latest 55 | strategy: 56 | matrix: 57 | target: [built, committed] 58 | steps: 59 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 60 | - if: matrix.target == 'built' || github.event_name == 'pull_request' 61 | uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 62 | with: 63 | name: dist 64 | path: dist 65 | - if: matrix.target == 'built' || github.event_name == 'pull_request' 66 | uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 67 | with: 68 | name: action.yml 69 | path: . 70 | 71 | - name: Test create comment 72 | uses: ./ 73 | id: couc 74 | with: 75 | issue-number: ${{ needs.build.outputs.issue-number }} 76 | body: | 77 | This is a multi-line test comment 78 | - With GitHub **Markdown** :sparkles: 79 | - Created by [create-or-update-comment][1] 80 | 81 | [1]: https://github.com/peter-evans/create-or-update-comment 82 | reactions: '+1' 83 | 84 | - name: Test update comment 85 | uses: ./ 86 | with: 87 | comment-id: ${{ steps.couc.outputs.comment-id }} 88 | body: | 89 | **Edit:** Some additional info 90 | reactions: eyes 91 | reactions-edit-mode: replace 92 | 93 | - name: Test add reactions 94 | uses: ./ 95 | with: 96 | comment-id: ${{ steps.couc.outputs.comment-id }} 97 | reactions: | 98 | heart 99 | hooray 100 | laugh 101 | 102 | - name: Test create comment from file 103 | uses: ./ 104 | id: couc2 105 | with: 106 | issue-number: ${{ needs.build.outputs.issue-number }} 107 | body-path: .github/comment-body.md 108 | reactions: | 109 | +1 110 | 111 | - name: Test update comment from file 112 | uses: ./ 113 | with: 114 | comment-id: ${{ steps.couc2.outputs.comment-id }} 115 | body-path: .github/comment-body-addition.md 116 | append-separator: space 117 | reactions: eyes, rocket 118 | reactions-edit-mode: replace 119 | 120 | package: 121 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 122 | needs: [test] 123 | runs-on: ubuntu-latest 124 | steps: 125 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 126 | - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 127 | with: 128 | name: dist 129 | path: dist 130 | - name: Create Pull Request 131 | uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 132 | with: 133 | token: ${{ secrets.ACTIONS_BOT_TOKEN }} 134 | commit-message: Update distribution 135 | title: Update distribution 136 | body: | 137 | - Updates the distribution for changes on `main` 138 | 139 | Auto-generated by [create-pull-request][1] 140 | 141 | [1]: https://github.com/peter-evans/create-pull-request 142 | branch: update-distribution 143 | -------------------------------------------------------------------------------- /src/create-or-update-comment.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as github from '@actions/github' 3 | import * as utils from './utils' 4 | import {inspect} from 'util' 5 | 6 | export interface Inputs { 7 | token: string 8 | repository: string 9 | issueNumber: number 10 | commentId: number 11 | body: string 12 | bodyPath: string 13 | editMode: string 14 | appendSeparator: string 15 | reactions: string[] 16 | reactionsEditMode: string 17 | } 18 | 19 | const REACTION_TYPES = [ 20 | '+1', 21 | '-1', 22 | 'laugh', 23 | 'confused', 24 | 'heart', 25 | 'hooray', 26 | 'rocket', 27 | 'eyes' 28 | ] 29 | 30 | function getReactionsSet(reactions: string[]): string[] { 31 | const reactionsSet = [ 32 | ...new Set( 33 | reactions.filter(item => { 34 | if (!REACTION_TYPES.includes(item)) { 35 | core.warning(`Skipping invalid reaction '${item}'.`) 36 | return false 37 | } 38 | return true 39 | }) 40 | ) 41 | ] 42 | if (!reactionsSet) { 43 | throw new Error(`No valid reactions are contained in '${reactions}'.`) 44 | } 45 | return reactionsSet 46 | } 47 | 48 | async function addReactions( 49 | octokit, 50 | owner: string, 51 | repo: string, 52 | commentId: number, 53 | reactions: string[] 54 | ) { 55 | const results = await Promise.allSettled( 56 | reactions.map(async reaction => { 57 | await octokit.rest.reactions.createForIssueComment({ 58 | owner: owner, 59 | repo: repo, 60 | comment_id: commentId, 61 | content: reaction 62 | }) 63 | core.info(`Setting '${reaction}' reaction on comment.`) 64 | }) 65 | ) 66 | for (let i = 0, l = results.length; i < l; i++) { 67 | if (results[i].status === 'fulfilled') { 68 | core.info( 69 | `Added reaction '${reactions[i]}' to comment id '${commentId}'.` 70 | ) 71 | } else if (results[i].status === 'rejected') { 72 | core.warning( 73 | `Adding reaction '${reactions[i]}' to comment id '${commentId}' failed.` 74 | ) 75 | } 76 | } 77 | } 78 | 79 | async function removeReactions( 80 | octokit, 81 | owner: string, 82 | repo: string, 83 | commentId: number, 84 | reactions: Reaction[] 85 | ) { 86 | const results = await Promise.allSettled( 87 | reactions.map(async reaction => { 88 | await octokit.rest.reactions.deleteForIssueComment({ 89 | owner: owner, 90 | repo: repo, 91 | comment_id: commentId, 92 | reaction_id: reaction.id 93 | }) 94 | core.info(`Removing '${reaction.content}' reaction from comment.`) 95 | }) 96 | ) 97 | for (let i = 0, l = results.length; i < l; i++) { 98 | if (results[i].status === 'fulfilled') { 99 | core.info( 100 | `Removed reaction '${reactions[i].content}' from comment id '${commentId}'.` 101 | ) 102 | } else if (results[i].status === 'rejected') { 103 | core.warning( 104 | `Removing reaction '${reactions[i].content}' from comment id '${commentId}' failed.` 105 | ) 106 | } 107 | } 108 | } 109 | 110 | function appendSeparatorTo(body: string, separator: string): string { 111 | switch (separator) { 112 | case 'newline': 113 | return body + '\n' 114 | case 'space': 115 | return body + ' ' 116 | default: // none 117 | return body 118 | } 119 | } 120 | 121 | function truncateBody(body: string) { 122 | // 65536 characters is the maximum allowed for issue comments. 123 | const truncateWarning = '...*[Comment body truncated]*' 124 | if (body.length > 65536) { 125 | core.warning(`Comment body is too long. Truncating to 65536 characters.`) 126 | return body.substring(0, 65536 - truncateWarning.length) + truncateWarning 127 | } 128 | return body 129 | } 130 | 131 | async function createComment( 132 | octokit, 133 | owner: string, 134 | repo: string, 135 | issueNumber: number, 136 | body: string 137 | ): Promise { 138 | body = truncateBody(body) 139 | 140 | const {data: comment} = await octokit.rest.issues.createComment({ 141 | owner: owner, 142 | repo: repo, 143 | issue_number: issueNumber, 144 | body 145 | }) 146 | core.info(`Created comment id '${comment.id}' on issue '${issueNumber}'.`) 147 | return comment.id 148 | } 149 | 150 | async function updateComment( 151 | octokit, 152 | owner: string, 153 | repo: string, 154 | commentId: number, 155 | body: string, 156 | editMode: string, 157 | appendSeparator: string 158 | ): Promise { 159 | if (body) { 160 | let commentBody = '' 161 | if (editMode == 'append') { 162 | // Get the comment body 163 | const {data: comment} = await octokit.rest.issues.getComment({ 164 | owner: owner, 165 | repo: repo, 166 | comment_id: commentId 167 | }) 168 | commentBody = appendSeparatorTo( 169 | comment.body ? comment.body : '', 170 | appendSeparator 171 | ) 172 | } 173 | commentBody = truncateBody(commentBody + body) 174 | core.debug(`Comment body: ${commentBody}`) 175 | await octokit.rest.issues.updateComment({ 176 | owner: owner, 177 | repo: repo, 178 | comment_id: commentId, 179 | body: commentBody 180 | }) 181 | core.info(`Updated comment id '${commentId}'.`) 182 | } 183 | return commentId 184 | } 185 | 186 | async function getAuthenticatedUser(octokit): Promise { 187 | try { 188 | const {data: user} = await octokit.rest.users.getAuthenticated() 189 | return user.login 190 | } catch (error) { 191 | if ( 192 | utils 193 | .getErrorMessage(error) 194 | .includes('Resource not accessible by integration') 195 | ) { 196 | // In this case we can assume the token is the default GITHUB_TOKEN and 197 | // therefore the user is 'github-actions[bot]'. 198 | return 'github-actions[bot]' 199 | } else { 200 | throw error 201 | } 202 | } 203 | } 204 | 205 | type Reaction = { 206 | id: number 207 | content: string 208 | } 209 | 210 | async function getCommentReactionsForUser( 211 | octokit, 212 | owner: string, 213 | repo: string, 214 | commentId: number, 215 | user: string 216 | ): Promise { 217 | const userReactions: Reaction[] = [] 218 | for await (const {data: reactions} of octokit.paginate.iterator( 219 | octokit.rest.reactions.listForIssueComment, 220 | { 221 | owner, 222 | repo, 223 | comment_id: commentId, 224 | per_page: 100 225 | } 226 | )) { 227 | const filteredReactions: Reaction[] = reactions 228 | .filter(reaction => reaction.user.login === user) 229 | .map(reaction => { 230 | return {id: reaction.id, content: reaction.content} 231 | }) 232 | userReactions.push(...filteredReactions) 233 | } 234 | return userReactions 235 | } 236 | 237 | export async function createOrUpdateComment( 238 | inputs: Inputs, 239 | body: string 240 | ): Promise { 241 | const [owner, repo] = inputs.repository.split('/') 242 | 243 | const octokit = github.getOctokit(inputs.token) 244 | 245 | const commentId = inputs.commentId 246 | ? await updateComment( 247 | octokit, 248 | owner, 249 | repo, 250 | inputs.commentId, 251 | body, 252 | inputs.editMode, 253 | inputs.appendSeparator 254 | ) 255 | : await createComment(octokit, owner, repo, inputs.issueNumber, body) 256 | 257 | core.setOutput('comment-id', commentId) 258 | 259 | if (inputs.reactions) { 260 | const reactionsSet = getReactionsSet(inputs.reactions) 261 | 262 | // Remove reactions if reactionsEditMode is 'replace' 263 | if (inputs.commentId && inputs.reactionsEditMode === 'replace') { 264 | const authenticatedUser = await getAuthenticatedUser(octokit) 265 | const userReactions = await getCommentReactionsForUser( 266 | octokit, 267 | owner, 268 | repo, 269 | commentId, 270 | authenticatedUser 271 | ) 272 | core.debug(inspect(userReactions)) 273 | 274 | const reactionsToRemove = userReactions.filter( 275 | reaction => !reactionsSet.includes(reaction.content) 276 | ) 277 | await removeReactions(octokit, owner, repo, commentId, reactionsToRemove) 278 | } 279 | 280 | await addReactions(octokit, owner, repo, commentId, reactionsSet) 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create or Update Comment 2 | [![CI](https://github.com/peter-evans/create-or-update-comment/workflows/CI/badge.svg)](https://github.com/peter-evans/create-or-update-comment/actions?query=workflow%3ACI) 3 | [![GitHub Marketplace](https://img.shields.io/badge/Marketplace-Create%20or%20Update%20Comment-blue.svg?colorA=24292e&colorB=0366d6&style=flat&longCache=true&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAM6wAADOsB5dZE0gAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAERSURBVCiRhZG/SsMxFEZPfsVJ61jbxaF0cRQRcRJ9hlYn30IHN/+9iquDCOIsblIrOjqKgy5aKoJQj4O3EEtbPwhJbr6Te28CmdSKeqzeqr0YbfVIrTBKakvtOl5dtTkK+v4HfA9PEyBFCY9AGVgCBLaBp1jPAyfAJ/AAdIEG0dNAiyP7+K1qIfMdonZic6+WJoBJvQlvuwDqcXadUuqPA1NKAlexbRTAIMvMOCjTbMwl1LtI/6KWJ5Q6rT6Ht1MA58AX8Apcqqt5r2qhrgAXQC3CZ6i1+KMd9TRu3MvA3aH/fFPnBodb6oe6HM8+lYHrGdRXW8M9bMZtPXUji69lmf5Cmamq7quNLFZXD9Rq7v0Bpc1o/tp0fisAAAAASUVORK5CYII=)](https://github.com/marketplace/actions/create-or-update-comment) 4 | 5 | A GitHub action to create or update an issue or pull request comment. 6 | 7 | ## Usage 8 | 9 | ### Add a comment to an issue or pull request 10 | 11 | ```yml 12 | - name: Create comment 13 | uses: peter-evans/create-or-update-comment@v4 14 | with: 15 | issue-number: 1 16 | body: | 17 | This is a multi-line test comment 18 | - With GitHub **Markdown** :sparkles: 19 | - Created by [create-or-update-comment][1] 20 | 21 | [1]: https://github.com/peter-evans/create-or-update-comment 22 | reactions: '+1' 23 | ``` 24 | 25 | ### Update a comment 26 | 27 | ```yml 28 | - name: Update comment 29 | uses: peter-evans/create-or-update-comment@v4 30 | with: 31 | comment-id: 557858210 32 | body: | 33 | **Edit:** Some additional info 34 | reactions: eyes 35 | ``` 36 | 37 | ### Add comment reactions 38 | 39 | ```yml 40 | - name: Add reactions 41 | uses: peter-evans/create-or-update-comment@v4 42 | with: 43 | comment-id: 557858210 44 | reactions: | 45 | heart 46 | hooray 47 | laugh 48 | ``` 49 | 50 | ### Action inputs 51 | 52 | | Name | Description | Default | 53 | | --- | --- | --- | 54 | | `token` | `GITHUB_TOKEN` (`issues: write`, `pull-requests: write`) or a `repo` scoped [PAT](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). | `GITHUB_TOKEN` | 55 | | `repository` | The full name of the repository in which to create or update a comment. | Current repository | 56 | | `issue-number` | The number of the issue or pull request in which to create a comment. | | 57 | | `comment-id` | The id of the comment to update. | | 58 | | `body` | The comment body. Cannot be used in conjunction with `body-path`. | | 59 | | `body-path` | The path to a file containing the comment body. Cannot be used in conjunction with `body`. | | 60 | | `edit-mode` | The mode when updating a comment, `replace` or `append`. | `append` | 61 | | `append-separator` | The separator to use when appending to an existing comment. (`newline`, `space`, `none`) | `newline` | 62 | | `reactions` | A comma or newline separated list of reactions to add to the comment. (`+1`, `-1`, `laugh`, `confused`, `heart`, `hooray`, `rocket`, `eyes`) | | 63 | | `reactions-edit-mode` | The mode when updating comment reactions, `replace` or `append`. | `append` | 64 | 65 | Note: In *public* repositories this action does not work in `pull_request` workflows when triggered by forks. 66 | Any attempt will be met with the error, `Resource not accessible by integration`. 67 | This is due to token restrictions put in place by GitHub Actions. Private repositories can be configured to [enable workflows](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#enabling-workflows-for-forks-of-private-repositories) from forks to run without restriction. See [here](https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#restrictions-on-repository-forks) for further explanation. Alternatively, use the [`pull_request_target`](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target) event to comment on pull requests. 68 | 69 | #### Outputs 70 | 71 | The ID of the created comment will be output for use in later steps. 72 | Note that in order to read the step output the action step must have an id. 73 | 74 | ```yml 75 | - name: Create comment 76 | uses: peter-evans/create-or-update-comment@v4 77 | id: couc 78 | with: 79 | issue-number: 1 80 | body: | 81 | My comment 82 | - name: Check outputs 83 | run: | 84 | echo "Comment ID - ${{ steps.couc.outputs.comment-id }}" 85 | ``` 86 | 87 | ### Where to find the id of a comment 88 | 89 | How to find the id of a comment will depend a lot on the use case. 90 | Here is one example where the id can be found in the `github` context during an `issue_comment` event. 91 | 92 | ```yml 93 | on: 94 | issue_comment: 95 | types: [created] 96 | jobs: 97 | commentCreated: 98 | runs-on: ubuntu-latest 99 | steps: 100 | - name: Add reaction 101 | uses: peter-evans/create-or-update-comment@v4 102 | with: 103 | comment-id: ${{ github.event.comment.id }} 104 | reactions: eyes 105 | ``` 106 | 107 | Some use cases might find the [find-comment](https://github.com/peter-evans/find-comment) action useful. 108 | This will search an issue or pull request for the first comment containing a specified string, and/or by a specified author. 109 | See the repository for detailed usage. 110 | 111 | In the following example, find-comment is used to determine if a comment has already been created on a pull request. 112 | If the find-comment action output `comment-id` returns an empty string, a new comment will be created. 113 | If it returns a value, the comment already exists and the content is replaced. 114 | ```yml 115 | - name: Find Comment 116 | uses: peter-evans/find-comment@v3 117 | id: fc 118 | with: 119 | issue-number: ${{ github.event.pull_request.number }} 120 | comment-author: 'github-actions[bot]' 121 | body-includes: Build output 122 | 123 | - name: Create or update comment 124 | uses: peter-evans/create-or-update-comment@v4 125 | with: 126 | comment-id: ${{ steps.fc.outputs.comment-id }} 127 | issue-number: ${{ github.event.pull_request.number }} 128 | body: | 129 | Build output 130 | ${{ steps.build.outputs.build-log }} 131 | edit-mode: replace 132 | ``` 133 | 134 | If required, the create and update steps can be separated for greater control. 135 | ```yml 136 | - name: Find Comment 137 | uses: peter-evans/find-comment@v3 138 | id: fc 139 | with: 140 | issue-number: ${{ github.event.pull_request.number }} 141 | comment-author: 'github-actions[bot]' 142 | body-includes: This comment was written by a bot! 143 | 144 | - name: Create comment 145 | if: steps.fc.outputs.comment-id == '' 146 | uses: peter-evans/create-or-update-comment@v4 147 | with: 148 | issue-number: ${{ github.event.pull_request.number }} 149 | body: | 150 | This comment was written by a bot! 151 | reactions: rocket 152 | 153 | - name: Update comment 154 | if: steps.fc.outputs.comment-id != '' 155 | uses: peter-evans/create-or-update-comment@v4 156 | with: 157 | comment-id: ${{ steps.fc.outputs.comment-id }} 158 | body: | 159 | This comment has been updated! 160 | reactions: hooray 161 | ``` 162 | 163 | ### Setting the comment body from a file 164 | 165 | ```yml 166 | - name: Create comment 167 | uses: peter-evans/create-or-update-comment@v4 168 | with: 169 | issue-number: 1 170 | body-path: 'comment-body.md' 171 | ``` 172 | 173 | ### Using a markdown template 174 | 175 | In this example, a markdown template file is added to the repository at `.github/comment-template.md` with the following content. 176 | ``` 177 | This is a test comment template 178 | Render template variables such as {{ .foo }} and {{ .bar }}. 179 | ``` 180 | 181 | The template is rendered using the [render-template](https://github.com/chuhlomin/render-template) action and the result is used to create the comment. 182 | ```yml 183 | - name: Render template 184 | id: template 185 | uses: chuhlomin/render-template@v1.4 186 | with: 187 | template: .github/comment-template.md 188 | vars: | 189 | foo: this 190 | bar: that 191 | 192 | - name: Create comment 193 | uses: peter-evans/create-or-update-comment@v4 194 | with: 195 | issue-number: 1 196 | body: ${{ steps.template.outputs.result }} 197 | ``` 198 | 199 | ### Accessing issues and comments in other repositories 200 | 201 | You can create and update comments in another repository by using a [PAT](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) instead of `GITHUB_TOKEN`. 202 | The user associated with the PAT must have write access to the repository. 203 | 204 | ## License 205 | 206 | [MIT](LICENSE) 207 | --------------------------------------------------------------------------------