├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── pull_request.yml │ └── release.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode └── launch.json ├── LICENSE ├── Makefile ├── OWNERS ├── README.md ├── __test__ ├── inputs.test.ts └── runner.test.ts ├── action.yml ├── dist └── index.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── githubHelper.ts ├── inputsHelper.ts ├── main.ts ├── paramsHelper.ts ├── runner.ts └── utils.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "jest", 4 | "@typescript-eslint" 5 | ], 6 | "extends": [ 7 | "plugin:github/recommended" 8 | ], 9 | "parser": "@typescript-eslint/parser", 10 | "parserOptions": { 11 | "ecmaVersion": 9, 12 | "sourceType": "module", 13 | "project": "./tsconfig.json" 14 | }, 15 | "rules": { 16 | "filenames/match-regex": "off", 17 | "i18n-text/no-en": "off", 18 | "no-console": "off", 19 | "object-shorthand": [ 20 | 2, 21 | "consistent" 22 | ], 23 | "eslint-comments/no-use": "off", 24 | "import/no-namespace": "off", 25 | "no-unused-vars": "off", 26 | "@typescript-eslint/no-unused-vars": "error", 27 | "@typescript-eslint/explicit-member-accessibility": [ 28 | "error", 29 | { 30 | "accessibility": "no-public" 31 | } 32 | ], 33 | "@typescript-eslint/no-require-imports": "error", 34 | "@typescript-eslint/array-type": "error", 35 | "@typescript-eslint/await-thenable": "error", 36 | "camelcase": "off", 37 | "@typescript-eslint/explicit-function-return-type": [ 38 | "error", 39 | { 40 | "allowExpressions": true 41 | } 42 | ], 43 | "@typescript-eslint/func-call-spacing": [ 44 | "error", 45 | "never" 46 | ], 47 | "@typescript-eslint/no-array-constructor": "error", 48 | "@typescript-eslint/no-empty-interface": "error", 49 | "@typescript-eslint/no-explicit-any": "error", 50 | "@typescript-eslint/no-extraneous-class": "error", 51 | "@typescript-eslint/no-for-in-array": "error", 52 | "@typescript-eslint/no-inferrable-types": "error", 53 | "@typescript-eslint/no-misused-new": "error", 54 | "@typescript-eslint/no-namespace": "error", 55 | "@typescript-eslint/no-non-null-assertion": "warn", 56 | "@typescript-eslint/no-unnecessary-qualifier": "error", 57 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 58 | "@typescript-eslint/no-useless-constructor": "error", 59 | "@typescript-eslint/no-var-requires": "error", 60 | "@typescript-eslint/prefer-for-of": "warn", 61 | "@typescript-eslint/prefer-function-type": "warn", 62 | "@typescript-eslint/prefer-includes": "error", 63 | "@typescript-eslint/prefer-string-starts-ends-with": "error", 64 | "@typescript-eslint/promise-function-async": "error", 65 | "@typescript-eslint/require-array-sort-compare": "error", 66 | "@typescript-eslint/restrict-plus-operands": "error", 67 | "semi": "off", 68 | "@typescript-eslint/semi": [ 69 | "error", 70 | "never" 71 | ], 72 | "@typescript-eslint/type-annotation-spacing": "error", 73 | "@typescript-eslint/unbound-method": "error" 74 | }, 75 | "env": { 76 | "node": true, 77 | "es6": true, 78 | "jest/globals": true 79 | } 80 | } -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: PR Build and test 2 | 3 | on: [pull_request] 4 | 5 | permissions: 6 | statuses: write 7 | pull-requests: write 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: ./ 15 | with: 16 | addHoldComment: "true" 17 | - run: npm ci 18 | - run: npm run build 19 | - run: npm run format-check 20 | - run: npm run lint 21 | - run: npm run pack 22 | - run: npm test 23 | # If something fails I want to set the status commit to "error" without adding anymore comments 24 | # I'm happy to leave the PR with `/hold` if there is an error 25 | - if: always() 26 | uses: ./ 27 | with: 28 | addHoldComment: "true" 29 | status: "${{ job.status }}" 30 | 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release commit-status-updater 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | env: 12 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | - uses: ./ 18 | with: 19 | name: "release" 20 | - run: npm ci 21 | - run: npm run build 22 | - run: npm run format-check 23 | - run: npm run lint 24 | - run: npm run pack 25 | - run: npm test 26 | - run: npm run semantic-release 27 | - if: always() 28 | uses: ./ 29 | with: 30 | name: "release" 31 | status: "${{ job.status }}" 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | coverage 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug Jest Tests", 11 | "cwd": "${workspaceFolder}", 12 | "args": [ 13 | "--inspect-brk", 14 | "${workspaceRoot}/node_modules/.bin/jest", 15 | "--runInBand", 16 | "--config", 17 | "${workspaceRoot}/jest.config.js" 18 | ], 19 | "windows": { 20 | "args": [ 21 | "--inspect-brk", 22 | "${workspaceRoot}/node_modules/jest/bin/jest.js", 23 | "--runInBand", 24 | "--config", 25 | "${workspaceRoot}/jest.config.js" 26 | ], 27 | }, 28 | "console": "integratedTerminal", 29 | "internalConsoleOptions": "neverOpen" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 The Ouzi Authors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | split-by-dot = $(word $2,$(subst ., ,$1)) 2 | 3 | .PHONY: clean 4 | clean: 5 | npm run clean 6 | 7 | .PHONY: install 8 | install: 9 | npm install 10 | 11 | .PHONY: build 12 | build: 13 | npm run build 14 | 15 | .PHONY: test 16 | test: build 17 | npm test 18 | 19 | .PHONY: coverage 20 | coverage: build 21 | npm run coverage 22 | 23 | .PHONY: all 24 | all: 25 | npm run all 26 | 27 | .PHONY: release 28 | release: all 29 | npm prune --production 30 | 31 | .PHONY: semantic-release 32 | semantic-release: 33 | npm ci 34 | npx semantic-release 35 | 36 | .PHONY: semantic-release-dry-run 37 | semantic-release-dry-run: 38 | npm ci 39 | npx semantic-release -d 40 | 41 | .PHONY: install-npm-check-updates 42 | install-npm-check-updates: 43 | npm install npm-check-updates 44 | 45 | .PHONY: update-dependencies 46 | update-dependencies: install-npm-check-updates 47 | ncu -u 48 | npm install 49 | 50 | .PHONY: tag-major 51 | tag-major: check-version 52 | $(eval TAG := $(call split-by-dot,$(VERSION),1)) 53 | @echo "Tagging major version $(TAG)" 54 | git tag -f $(TAG) 55 | git push -f origin $(TAG) 56 | 57 | .PHONY: check-version 58 | check-version: 59 | ifndef VERSION 60 | $(error VERSION not defined) 61 | endif -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs at https://go.k8s.io/owners 2 | approvers: 3 | - belitre 4 | - givanov 5 | - alexouzounis 6 | reviewers: 7 | - belitre 8 | - givanov 9 | - alexouzounis 10 | aliases: 11 | ouzi-team: 12 | - belitre 13 | - givanov 14 | - alexouzounis -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Github Action commit-status-updater 2 | 3 | ![Build and test](https://github.com/ouzi-dev/commit-status-updater/workflows/Build%20and%20test/badge.svg) 4 | ![PR Build and test](https://github.com/ouzi-dev/commit-status-updater/workflows/PR%20Build%20and%20test/badge.svg) 5 | 6 | ## Overview 7 | 8 | A simple Github Action that allows us to update the status of a commit. 9 | 10 | GitHub does not update the status of a commit when running workflow and therefore tools that rely on the context/status of a given commit are not compatible with it. 11 | 12 | Currently the action supports `pull_request` and `push` events: 13 | * When the event is `pull_request`, the action will set the status to the last commit in the pull request at the moment the workflow was triggered. 14 | * When the event is `push`, the action will set the status to the last commit pushed at the moment the workflow was triggered. 15 | 16 | ## Input Parameters 17 | 18 | | Name | Required | Description | Default | Accepted values | 19 | |---|---|---|---|---| 20 | | `token` | :heavy_check_mark: | Auth token used to add status commits | `${ github.token }` | `^[0-9a-f]{40}$` | 21 | | `name` | :heavy_check_mark: | Name of the status check to add to the commit | `GithubActions - ${GITHUB_WORKFLOW}` | Any string | 22 | | `status` | :heavy_check_mark: | Commit or job status, based on this the action will set the correct status in the commit. See below for details. | `pending` | `error`, `failure`, `pending`, `success` and `cancelled` | 23 | | `url` | | URL for the status check | `""` | Any string | 24 | | `description` | | Description for the status check | `""` | Any string | 25 | 26 | ### `status` 27 | 28 | If set to `pending` it will set status commit `pending`. 29 | 30 | If set to `failure` or `cancelled` it will set status commit `failure`. 31 | 32 | If set to `success` it will set status commit `success`. 33 | 34 | If set to `error` it will set status commit `error`. 35 | 36 | ## Input Parameters specific to `pull_request` event 37 | 38 | _These parameters are all optional and are used only for pull requests_ 39 | 40 | | Name | Description | 41 | |---|---| 42 | | `ignoreForks` | Default is `true`. If the pull request is from a fork the action won't add a status by default. This is because the action won't have a token with permissions to add the status to the commit. You can disable this, but then you'll have to provide a token with enough permissions to add status to the commits in the forks! | 43 | | `addHoldComment` | Default is `false`. If `true` the action will add a comment to the pull request. This is useful if you use prow, since prow won't detect the GitHub actions, so you can use `/hold` and `/hold cancel` to avoid merging the PR before you want. __Important: this will be disabled for forks if `ignoreForks` is set to `true`, this is because the default GitHub token won't have permissions to add comments if your PR comes from a fork.__ | 44 | | `pendingComment` | Default is `/hold`. This is the message to add to the pull request when the status is `pending`. | 45 | | `successComment` | Default is `/hold cancel`. This is the message to add to the pull request when the status is `success`. | 46 | | `failComment` | Default is `/hold`. This is the message to add to the pull request when the status is `failure`, `error` or `cancelled`. | 47 | 48 | ## Workflow permissions 49 | 50 | By default, if we don't add `permissions` to the workflow or the job, the action will be able to set the status commit and add comments. But if we set any permissions for the workflow/job, then we need to be sure we provide the correct ones for the action: 51 | 52 | * If we just want to set the status commit we need to be sure the job (or the whole workflow) has the permission: `statuses: write` 53 | * If we want to add a comment we need to be sure the job (or the whole workflow) has the permissions: `pull-requests: write` 54 | 55 | ## Examples 56 | 57 | ### Action sets push commit to pending status 58 | 59 | ``` 60 | name: Test 61 | 62 | on: [push] 63 | 64 | jobs: 65 | build: 66 | runs-on: ubuntu-latest 67 | steps: 68 | - uses: actions/checkout@v3 69 | - uses: ouzi-dev/commit-status-updater@v2 70 | ``` 71 | 72 | ### Action sets push commit to pending status with specific permissions 73 | 74 | ``` 75 | name: Test 76 | 77 | on: [push] 78 | 79 | permissions: 80 | statuses: write 81 | 82 | jobs: 83 | build: 84 | runs-on: ubuntu-latest 85 | steps: 86 | - uses: actions/checkout@v3 87 | - uses: ouzi-dev/commit-status-updater@v2 88 | ``` 89 | 90 | 91 | ### Action sets push commit to pending status with custom name 92 | 93 | ``` 94 | name: Test 95 | 96 | on: [push] 97 | 98 | jobs: 99 | build: 100 | runs-on: ubuntu-latest 101 | steps: 102 | - uses: actions/checkout@v3 103 | - uses: ouzi-dev/commit-status-updater@v2 104 | with: 105 | name: "name of my status check" 106 | ``` 107 | 108 | ### Action sets push commit to pending status on start, and updates check at the end of the workflow 109 | 110 | ``` 111 | name: Test 112 | 113 | on: [push] 114 | 115 | jobs: 116 | build: 117 | runs-on: ubuntu-latest 118 | steps: 119 | - uses: actions/checkout@v3 120 | - uses: ouzi-dev/commit-status-updater@v2 121 | - if: always() 122 | uses: ouzi-dev/commit-status-updater@v2 123 | with: 124 | status: "${{ job.status }}" 125 | ``` 126 | 127 | ### Action sets pull request commit to pending status without comment 128 | 129 | ``` 130 | name: Test 131 | 132 | on: [pull_request] 133 | 134 | jobs: 135 | build: 136 | runs-on: ubuntu-latest 137 | steps: 138 | - uses: actions/checkout@v3 139 | - uses: ouzi-dev/commit-status-updater@v2 140 | ``` 141 | 142 | ### Action sets pull request commit to error status without comment 143 | 144 | ``` 145 | name: Test 146 | 147 | on: [pull_request] 148 | 149 | jobs: 150 | build: 151 | runs-on: ubuntu-latest 152 | steps: 153 | - uses: actions/checkout@v3 154 | - uses: ouzi-dev/commit-status-updater@v2 155 | with: 156 | status: "error" 157 | ``` 158 | 159 | ### Action sets pull request commit to pending status with comment, and updates check and adds comment at the end of the workflow 160 | 161 | ``` 162 | name: Test 163 | 164 | on: [pull_request] 165 | 166 | jobs: 167 | build: 168 | runs-on: ubuntu-latest 169 | steps: 170 | - uses: actions/checkout@v3 171 | - uses: ouzi-dev/commit-status-updater@v2 172 | with: 173 | addHoldComment: "true" 174 | - if: always() 175 | uses: ouzi-dev/commit-status-updater@v2 176 | with: 177 | addHoldComment: "true" 178 | status: "${{ job.status }}" 179 | ``` 180 | 181 | ### Action sets pull request commit to pending status with comment, and updates check and adds comment at the end of the workflow with specific permissions 182 | 183 | ``` 184 | name: Test 185 | 186 | on: [pull_request] 187 | 188 | permissions: 189 | statuses: write 190 | pull-requests: write 191 | 192 | jobs: 193 | build: 194 | runs-on: ubuntu-latest 195 | steps: 196 | - uses: actions/checkout@v3 197 | - uses: ouzi-dev/commit-status-updater@v2 198 | with: 199 | addHoldComment: "true" 200 | - if: always() 201 | uses: ouzi-dev/commit-status-updater@v2 202 | with: 203 | addHoldComment: "true" 204 | status: "${{ job.status }}" 205 | ``` 206 | 207 | ### Action with custom hold comments 208 | 209 | ``` 210 | name: Test 211 | 212 | on: [pull_request] 213 | 214 | jobs: 215 | build: 216 | runs-on: ubuntu-latest 217 | steps: 218 | - uses: actions/checkout@v3 219 | - uses: ouzi-dev/commit-status-updater@v2 220 | with: 221 | status: "pending" 222 | addHoldComment: "true" 223 | pendingComment: "action pending!" 224 | successComment: "action success!" 225 | failComment: "action failed!" 226 | ``` 227 | 228 | ### Action no comments, set commit to "error" status and set url, description and specific name 229 | 230 | ``` 231 | name: Test 232 | 233 | on: [pull_request] 234 | 235 | jobs: 236 | build: 237 | runs-on: ubuntu-latest 238 | steps: 239 | - uses: actions/checkout@v3 240 | - uses: ouzi-dev/commit-status-updater@v2 241 | with: 242 | status: "error" 243 | url: http://myurl.io/ 244 | description: "this is my status check" 245 | name: "name of my status check" 246 | ``` 247 | 248 | ### Action with specific token and setting status check in commits in forks 249 | 250 | ``` 251 | name: Test 252 | 253 | on: [pull_request] 254 | 255 | jobs: 256 | build: 257 | runs-on: ubuntu-latest 258 | steps: 259 | - uses: actions/checkout@v3 260 | - uses: ouzi-dev/commit-status-updater@v2 261 | with: 262 | token: "my_custom_token" 263 | ignoreForks: "false" 264 | ``` 265 | 266 | ## Integration with Prow 267 | 268 | An example is [Prow](https://github.com/kubernetes/test-infra/tree/master/prow#readme) which uses the Github Status API to read the status of a given commit. 269 | Using this actions you can tell tide to not skip optional contexts and effectively wait for a GitHub Workflow to pass before merging. 270 | 271 | ### Example with Tide 272 | 273 | ``` 274 | tide: 275 | context_options: 276 | # Treat unknown contexts as required 277 | skip-unknown-contexts: false 278 | ``` 279 | 280 | ## Development 281 | 282 | __Important: Before pushing your changes run `make release` to generate all the correct files and clean stuff, etc...__ -------------------------------------------------------------------------------- /__test__/inputs.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import {IParams} from '../lib/paramsHelper' 3 | 4 | // Late bind 5 | let inputsHelper: any 6 | 7 | // Mock @actions/core 8 | let inputs = {} as any 9 | const mockCore = jest.genMockFromModule('@actions/core') as any 10 | mockCore.getInput = (name: string) => { 11 | return inputs[name] 12 | } 13 | 14 | describe('inputsHelper tests', () => { 15 | beforeAll(() => { 16 | // Mocks 17 | jest.setMock('@actions/core', mockCore) 18 | 19 | // Now import 20 | inputsHelper = require('../lib/inputsHelper') 21 | }) 22 | 23 | beforeEach(() => { 24 | // Reset inputs 25 | inputs = {} 26 | }) 27 | 28 | afterAll(() => { 29 | // Reset modules 30 | jest.resetModules() 31 | }) 32 | 33 | it('requires token', async () => { 34 | expect.assertions(1) 35 | await expect(inputsHelper.getInputs()).rejects.toEqual( 36 | TypeError("token can't be an empty string") 37 | ) 38 | }) 39 | 40 | it('requires name', async () => { 41 | expect.assertions(1) 42 | 43 | inputs.token = '1234567abcdefg' 44 | 45 | await expect(inputsHelper.getInputs()).rejects.toEqual( 46 | TypeError("name can't be an empty string") 47 | ) 48 | }) 49 | 50 | it('requires status', async () => { 51 | expect.assertions(1) 52 | 53 | inputs.name = 'my_name' 54 | inputs.token = '1234567abcdefg' 55 | 56 | await expect(inputsHelper.getInputs()).rejects.toEqual( 57 | TypeError("status can't be an empty string") 58 | ) 59 | }) 60 | 61 | it('requires valid commit status', async () => { 62 | expect.assertions(1) 63 | 64 | inputs.name = 'my_name' 65 | inputs.token = '1234567abcdefg' 66 | inputs.status = 'bleh' 67 | 68 | await expect(inputsHelper.getInputs()).rejects.toEqual( 69 | TypeError('unknown commit or job status: bleh') 70 | ) 71 | }) 72 | 73 | it('sets correct default values', async () => { 74 | inputs.name = 'my_name' 75 | inputs.token = '1234567abcdefg' 76 | inputs.status = 'Success' 77 | 78 | const params: IParams = await inputsHelper.getInputs() 79 | 80 | expect(params).toBeTruthy() 81 | expect(params.name).toBe('my_name') 82 | expect(params.token).toBe('1234567abcdefg') 83 | expect(params.url).toBe('') 84 | expect(params.description).toBe('') 85 | expect(params.status).toBe('success') 86 | expect(params.pendingComment).toBe('') 87 | expect(params.successComment).toBe('') 88 | expect(params.failComment).toBe('') 89 | expect(params.selectedComment).toBe('') 90 | expect(params.addHoldComment).toBeFalsy() 91 | expect(params.ignoreForks).toBeTruthy() 92 | }) 93 | 94 | it('sets success status', async () => { 95 | inputs.singleShot = 'false' 96 | inputs.status = 'Success' 97 | inputs.token = 'my_token' 98 | inputs.url = 'my_url' 99 | inputs.description = 'my_description' 100 | inputs.name = 'my_name' 101 | inputs.ignoreForks = 'true' 102 | inputs.addHoldComment = 'false' 103 | inputs.pendingComment = '/hold' 104 | inputs.successComment = '/hold cancel' 105 | inputs.failComment = '/hold fail' 106 | 107 | const params: IParams = await inputsHelper.getInputs() 108 | 109 | expect(params).toBeTruthy() 110 | expect(params.token).toBe('my_token') 111 | expect(params.url).toBe('my_url') 112 | expect(params.description).toBe('my_description') 113 | expect(params.name).toBe('my_name') 114 | expect(params.status).toBe('success') 115 | expect(params.pendingComment).toBe('/hold') 116 | expect(params.successComment).toBe('/hold cancel') 117 | expect(params.failComment).toBe('/hold fail') 118 | expect(params.selectedComment).toBe('/hold cancel') 119 | expect(params.addHoldComment).toBeFalsy() 120 | expect(params.ignoreForks).toBeTruthy() 121 | }) 122 | 123 | it('sets pending status', async () => { 124 | inputs.singleShot = 'false' 125 | inputs.status = 'Pending' 126 | inputs.token = 'my_token' 127 | inputs.url = '' 128 | inputs.description = '' 129 | inputs.name = 'my_name' 130 | inputs.ignoreForks = 'false' 131 | inputs.addHoldComment = 'true' 132 | inputs.pendingComment = '/hold' 133 | inputs.successComment = '/hold cancel' 134 | inputs.failComment = '/hold fail' 135 | 136 | const params: IParams = await inputsHelper.getInputs() 137 | 138 | expect(params).toBeTruthy() 139 | expect(params.token).toBe('my_token') 140 | expect(params.url).toBe('') 141 | expect(params.description).toBe('') 142 | expect(params.name).toBe('my_name') 143 | expect(params.status).toBe('pending') 144 | expect(params.pendingComment).toBe('/hold') 145 | expect(params.successComment).toBe('/hold cancel') 146 | expect(params.failComment).toBe('/hold fail') 147 | expect(params.selectedComment).toBe('/hold') 148 | expect(params.addHoldComment).toBeTruthy() 149 | expect(params.ignoreForks).toBeFalsy() 150 | }) 151 | 152 | it('sets correct status and comment', async () => { 153 | inputs.singleShot = 'false' 154 | inputs.token = 'my_token' 155 | inputs.url = '' 156 | inputs.description = '' 157 | inputs.name = 'my_name' 158 | inputs.status = 'Success' 159 | inputs.ignoreForks = 'true' 160 | inputs.addHoldComment = 'false' 161 | inputs.pendingComment = '/hold' 162 | inputs.successComment = '/hold cancel' 163 | inputs.failComment = '/hold fail' 164 | 165 | let params: IParams = await inputsHelper.getInputs() 166 | 167 | expect(params).toBeTruthy() 168 | expect(params.status).toBe('success') 169 | expect(params.selectedComment).toBe('/hold cancel') 170 | 171 | inputs.status = 'pending' 172 | params = await inputsHelper.getInputs() 173 | expect(params).toBeTruthy() 174 | expect(params.status).toBe('pending') 175 | expect(params.selectedComment).toBe('/hold') 176 | 177 | inputs.status = 'failure' 178 | params = await inputsHelper.getInputs() 179 | expect(params).toBeTruthy() 180 | expect(params.status).toBe('failure') 181 | expect(params.selectedComment).toBe('/hold fail') 182 | 183 | inputs.status = 'cancelled' 184 | params = await inputsHelper.getInputs() 185 | expect(params).toBeTruthy() 186 | expect(params.status).toBe('failure') 187 | expect(params.selectedComment).toBe('/hold fail') 188 | 189 | inputs.status = 'error' 190 | params = await inputsHelper.getInputs() 191 | expect(params).toBeTruthy() 192 | expect(params.status).toBe('error') 193 | expect(params.selectedComment).toBe('/hold fail') 194 | }) 195 | }) 196 | -------------------------------------------------------------------------------- /__test__/runner.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import {IGithubHelper} from '../lib/githubHelper' 3 | import {IParams} from '../lib/paramsHelper' 4 | 5 | let runner: any 6 | 7 | const mockUtils = jest.genMockFromModule('../lib/utils') as any 8 | 9 | const mockInputsHelper = jest.genMockFromModule('../lib/inputsHelper') as any 10 | 11 | const mockGithubHelper = jest.genMockFromModule('../lib/githubHelper') as any 12 | mockGithubHelper.getInputs = jest.fn() 13 | 14 | const MockIGithubHelper = jest.fn(() => ({ 15 | isPullRequest: jest.fn(), 16 | isFork: jest.fn(), 17 | setStatus: jest.fn(), 18 | addComment: jest.fn() 19 | })) 20 | let mockIGithubHelper: any 21 | 22 | describe('runner tests', () => { 23 | beforeAll(() => { 24 | jest.setMock('../lib/utils', mockUtils) 25 | jest.setMock('../lib/inputsHelper', mockInputsHelper) 26 | jest.setMock('../lib/githubHelper', mockGithubHelper) 27 | 28 | mockIGithubHelper = new MockIGithubHelper() 29 | 30 | runner = require('../lib/runner') 31 | }) 32 | 33 | beforeEach(() => {}) 34 | 35 | afterAll(() => { 36 | jest.resetModules() 37 | }) 38 | 39 | it('on push run sets status', async () => { 40 | const params = {} as unknown as IParams 41 | params.token = 'bleh' 42 | params.ignoreForks = true 43 | params.addHoldComment = true 44 | params.selectedComment = 'my comment' 45 | mockInputsHelper.getInputs.mockReturnValue(params) 46 | mockGithubHelper.CreateGithubHelper.mockReturnValue(mockIGithubHelper) 47 | mockIGithubHelper.isFork.mockReturnValue(false) 48 | mockIGithubHelper.isPullRequest.mockReturnValue(false) 49 | 50 | await runner.run() 51 | 52 | expect(mockUtils.validateEventType).toHaveBeenCalled() 53 | expect(mockIGithubHelper.setStatus).toHaveBeenCalledWith(params) 54 | expect(mockIGithubHelper.addComment).toHaveBeenCalledTimes(0) 55 | }) 56 | 57 | it('on PR run sets status and comment', async () => { 58 | const params = {} as unknown as IParams 59 | params.token = 'bleh' 60 | params.ignoreForks = true 61 | params.addHoldComment = true 62 | params.selectedComment = 'my comment' 63 | mockInputsHelper.getInputs.mockReturnValue(params) 64 | mockGithubHelper.CreateGithubHelper.mockReturnValue(mockIGithubHelper) 65 | mockIGithubHelper.isFork.mockReturnValue(false) 66 | mockIGithubHelper.isPullRequest.mockReturnValue(true) 67 | 68 | await runner.run() 69 | 70 | expect(mockUtils.validateEventType).toHaveBeenCalled() 71 | expect(mockIGithubHelper.setStatus).toHaveBeenCalledWith(params) 72 | expect(mockIGithubHelper.addComment).toHaveBeenCalledWith( 73 | 'bleh', 74 | 'my comment' 75 | ) 76 | }) 77 | 78 | it('on PR run sets status and no comment', async () => { 79 | const params = {} as unknown as IParams 80 | params.token = 'bleh' 81 | params.ignoreForks = true 82 | params.addHoldComment = false 83 | params.selectedComment = 'my comment' 84 | mockInputsHelper.getInputs.mockReturnValue(params) 85 | mockGithubHelper.CreateGithubHelper.mockReturnValue(mockIGithubHelper) 86 | mockIGithubHelper.isFork.mockReturnValue(false) 87 | mockIGithubHelper.isPullRequest.mockReturnValue(true) 88 | 89 | await runner.run() 90 | 91 | expect(mockUtils.validateEventType).toHaveBeenCalled() 92 | expect(mockIGithubHelper.setStatus).toHaveBeenCalledWith(params) 93 | expect(mockIGithubHelper.addComment).toHaveBeenCalledTimes(0) 94 | }) 95 | 96 | it('on PR run does not set status or comment', async () => { 97 | const params = {} as unknown as IParams 98 | params.token = 'bleh' 99 | params.ignoreForks = true 100 | params.addHoldComment = false 101 | params.selectedComment = 'my comment' 102 | mockInputsHelper.getInputs.mockReturnValue(params) 103 | mockGithubHelper.CreateGithubHelper.mockReturnValue(mockIGithubHelper) 104 | mockIGithubHelper.isFork.mockReturnValue(true) 105 | mockIGithubHelper.isPullRequest.mockReturnValue(true) 106 | 107 | await runner.run() 108 | 109 | expect(mockUtils.validateEventType).toHaveBeenCalled() 110 | expect(mockIGithubHelper.setStatus).toHaveBeenCalledTimes(0) 111 | expect(mockIGithubHelper.addComment).toHaveBeenCalledTimes(0) 112 | }) 113 | 114 | it('on PR run does not set status or comment when it is a fork and add comment enabled', async () => { 115 | const params = {} as unknown as IParams 116 | params.token = 'bleh' 117 | params.ignoreForks = true 118 | params.addHoldComment = true 119 | params.selectedComment = 'my comment' 120 | mockInputsHelper.getInputs.mockReturnValue(params) 121 | mockGithubHelper.CreateGithubHelper.mockReturnValue(mockIGithubHelper) 122 | mockIGithubHelper.isFork.mockReturnValue(true) 123 | mockIGithubHelper.isPullRequest.mockReturnValue(true) 124 | 125 | await runner.run() 126 | 127 | expect(mockUtils.validateEventType).toHaveBeenCalled() 128 | expect(mockIGithubHelper.setStatus).toHaveBeenCalledTimes(0) 129 | expect(mockIGithubHelper.addComment).toHaveBeenCalledTimes(0) 130 | }) 131 | 132 | it('on PR run sets status if ignore fork false', async () => { 133 | const params = {} as unknown as IParams 134 | params.token = 'bleh' 135 | params.ignoreForks = false 136 | params.addHoldComment = true 137 | params.selectedComment = 'my comment' 138 | mockInputsHelper.getInputs.mockReturnValue(params) 139 | mockGithubHelper.CreateGithubHelper.mockReturnValue(mockIGithubHelper) 140 | mockIGithubHelper.isPullRequest.mockReturnValue(true) 141 | 142 | await runner.run() 143 | 144 | expect(mockUtils.validateEventType).toHaveBeenCalled() 145 | expect(mockIGithubHelper.isFork).toHaveBeenCalledTimes(0) 146 | expect(mockIGithubHelper.setStatus).toHaveBeenCalledWith(params) 147 | }) 148 | }) 149 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "commit-status-updater" 2 | description: "A simple Github Action that allows us to update the status of the last commit in a pull request" 3 | author: "Ouzi, ltd. Ouzi Team " 4 | branding: 5 | icon: "check-circle" 6 | color: "purple" 7 | inputs: 8 | token: 9 | description: > 10 | Auth token used to add status commits. 11 | default: "${{ github.token }}" 12 | required: true 13 | url: 14 | description: > 15 | URL for the status check. 16 | default: "" 17 | description: 18 | description: > 19 | Description for the status check. 20 | default: "" 21 | name: 22 | description: > 23 | The context for the status. Default is GithubActions - ${GITHUB_WORKFLOW} 24 | default: "GithubActions - ${{ github.workflow }}" 25 | required: true 26 | status: 27 | description: > 28 | Commit or job status, based on this the action will set the correct status in the commit: 29 | Accepted values are: 'error', 'failure', 'pending', 'success' and 'cancelled'. 30 | By default it will use the current job status. 31 | If the passed status is 'pending' it will set status commit 'pending' 32 | If the passed status is 'failure' or 'cancelled' it wil set status commit 'failure' 33 | If the passed status is 'success' it will set status commit 'success' 34 | If the passed status is 'error' it will set status commit 'error' 35 | default: "pending" 36 | required: true 37 | ignoreForks: 38 | description: > 39 | If the pull request is from a fork the action won't add a status by default. 40 | This is because the action won't have a token with permissions to add the status 41 | to the commit. You can disable this, but then you'll have to provide a token 42 | with enough permissions to add status to the commits in the forks! 43 | Will be used only for pull requests. 44 | default: "true" 45 | required: true 46 | addHoldComment: 47 | description: > 48 | If true the action will add a comment to the pull request. This is useful if you use prow and you get PRs from forks, 49 | you can use `/hold` and `/hold cancel` instead of the status check since the token won't have permissions to do that. 50 | Will be used only for pull requests. 51 | default: "false" 52 | pendingComment: 53 | description: > 54 | This is the message to add to the pull request when the status is pending. 55 | Will be used only for pull requests. 56 | default: "/hold" 57 | successComment: 58 | description: > 59 | This is the message to add to the pull request when the status is success. 60 | Will be used only for pull requests. 61 | default: "/hold cancel" 62 | failComment: 63 | description: > 64 | This is the message to add to the pull request when the status is 'failure', 'error' or 'cancelled'. 65 | Will be used only for pull requests. 66 | default: "/hold" 67 | runs: 68 | using: node20 69 | main: dist/index.js 70 | -------------------------------------------------------------------------------- /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 | coverageDirectory: 'coverage', 12 | collectCoverageFrom: ["**/*.{ts,js}", "!**/node_modules/**",] 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commit-status-updater", 3 | "version": "v1.1.0", 4 | "description": "commit status updater", 5 | "main": "lib/main.js", 6 | "scripts": { 7 | "semantic-release": "semantic-release", 8 | "build": "tsc", 9 | "format": "prettier --write **/*.ts", 10 | "format-check": "prettier --check **/*.ts", 11 | "lint": "eslint src/**/*.ts", 12 | "pack": "ncc build", 13 | "test": "jest", 14 | "coverage": "jest --coverage=true", 15 | "clean": "rm -rf node_modules && rm -rf lib && rm -rf coverage && rm -rf dist", 16 | "all": "npm run clean && npm install && npm run build && npm run format && npm run lint && npm run pack && npm test" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/ouzi-dev/commit-status-updater.git" 21 | }, 22 | "keywords": [ 23 | "github", 24 | "actions", 25 | "commit", 26 | "status", 27 | "updater" 28 | ], 29 | "author": "GitHub", 30 | "license": "Apache", 31 | "bugs": { 32 | "url": "https://github.com/ouzi-dev/commit-status-updater/issues" 33 | }, 34 | "homepage": "https://github.com/ouzi-dev/commit-status-updater#readme", 35 | "dependencies": { 36 | "@actions/core": "^1.10.0", 37 | "@actions/github": "^5.1.1", 38 | "@actions/http-client": ">=2.0.1", 39 | "@octokit/core": "^4.1.0", 40 | "@octokit/webhooks": "^10.3.0", 41 | "@octokit/webhooks-types": "^6.5.0", 42 | "@vercel/ncc": "^0.34.0", 43 | "npm-check-updates": "^16.3.14", 44 | "uuid": "^9.0.0" 45 | }, 46 | "devDependencies": { 47 | "@semantic-release/commit-analyzer": "^9.0.2", 48 | "@semantic-release/exec": "^6.0.3", 49 | "@semantic-release/github": "^8.0.6", 50 | "@semantic-release/release-notes-generator": "^10.0.3", 51 | "@types/jest": "^29.2.0", 52 | "@types/node": "^18.11.2", 53 | "@types/uuid": "^8.3.4", 54 | "@typescript-eslint/eslint-plugin": "^5.40.1", 55 | "@typescript-eslint/parser": "^5.40.1", 56 | "env-ci": "^7.3.0", 57 | "eslint": "^8.25.0", 58 | "eslint-plugin-github": "^4.4.0", 59 | "eslint-plugin-jest": "^27.1.3", 60 | "jest": "^29.2.1", 61 | "jest-circus": "^29.2.1", 62 | "js-yaml": "^4.1.0", 63 | "prettier": "^2.7.1", 64 | "semantic-release": "^19.0.5", 65 | "semver-regex": "^3.1.4", 66 | "ts-jest": "^29.0.3", 67 | "typescript": "^4.8.4" 68 | }, 69 | "release": { 70 | "plugins": [ 71 | [ 72 | "@semantic-release/commit-analyzer", 73 | { 74 | "releaseRules": [ 75 | { 76 | "type": "fix", 77 | "release": "patch" 78 | }, 79 | { 80 | "type": "refactor", 81 | "release": "patch" 82 | }, 83 | { 84 | "type": "feat", 85 | "release": "minor" 86 | }, 87 | { 88 | "type": "major", 89 | "release": "major" 90 | } 91 | ] 92 | } 93 | ], 94 | "@semantic-release/release-notes-generator", 95 | [ 96 | "@semantic-release/exec", 97 | { 98 | "successCmd": "make tag-major VERSION=v${nextRelease.version}" 99 | } 100 | ], 101 | [ 102 | "@semantic-release/github", 103 | { 104 | "successComment": "This ${issue.pull_request ? 'pull request' : 'issue'} is included in version ${nextRelease.version}", 105 | "failComment": "The release from ${branch} had failed due to the following errors:\n- ${errors.map(err => err.message).join('\\n- ')}" 106 | } 107 | ] 108 | ], 109 | "preset": "angular", 110 | "branches": [ 111 | "master", 112 | "feature/semantic_release" 113 | ], 114 | "tagFormat": "v${version}", 115 | "ci": false 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/githubHelper.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import {context, getOctokit} from '@actions/github' 3 | import {IParams} from './paramsHelper' 4 | 5 | export interface IGithubHelper { 6 | isFork(): Promise 7 | setStatus(params: IParams): Promise 8 | addComment(token: string, comment: string): Promise 9 | isPullRequest(): Promise 10 | } 11 | 12 | export async function CreateGithubHelper(): Promise { 13 | return await GithubHelper.createGithubHelper() 14 | } 15 | 16 | class GithubHelper { 17 | private owner 18 | private repo 19 | private sha 20 | private issueNumber 21 | private octokit 22 | private isPR 23 | private baseRepo 24 | private headRepo 25 | private baseOwner 26 | private baseRepoName 27 | 28 | private constructor() {} 29 | 30 | static async createGithubHelper(): Promise { 31 | const result = new GithubHelper() 32 | await result.initialize() 33 | return result 34 | } 35 | 36 | private async initialize(): Promise { 37 | if (context.eventName === 'pull_request') { 38 | this.isPR = true 39 | this.owner = context.payload?.pull_request?.head?.repo?.owner?.login 40 | this.repo = context.payload?.pull_request?.head?.repo?.name 41 | this.sha = context.payload?.pull_request?.head?.sha 42 | this.issueNumber = context.payload?.pull_request?.number 43 | this.baseRepo = context.payload?.pull_request?.base?.repo?.full_name 44 | this.headRepo = context.payload?.pull_request?.head?.repo?.full_name 45 | this.baseOwner = context.payload?.pull_request?.base?.repo?.owner?.login 46 | this.baseRepoName = context.payload?.pull_request?.base?.repo?.name 47 | } 48 | 49 | if (context.eventName === 'push') { 50 | this.isPR = false 51 | this.owner = context.payload?.repository?.owner?.login 52 | this.repo = context.payload?.repository?.name 53 | this.sha = context.sha 54 | } 55 | } 56 | 57 | async isPullRequest(): Promise { 58 | return this.isPR 59 | } 60 | 61 | async isFork(): Promise { 62 | return this.baseRepo !== this.headRepo 63 | } 64 | 65 | async setStatus(params: IParams): Promise { 66 | try { 67 | const octokit = getOctokit(params.token) 68 | await octokit.rest.repos.createCommitStatus({ 69 | context: params.name, 70 | description: params.description, 71 | owner: this.owner, 72 | repo: this.repo, 73 | sha: this.sha, 74 | state: params.status, 75 | target_url: params.url 76 | }) 77 | core.info(`Updated build status: ${params.status}`) 78 | } catch (error) { 79 | throw new Error( 80 | `error while setting context status: ${(error as Error).message}` 81 | ) 82 | } 83 | } 84 | 85 | async addComment(token: string, comment: string): Promise { 86 | // if we support forks, then we need to use the base, cause head will be the fork 87 | try { 88 | const octokit = getOctokit(token) 89 | await octokit.rest.issues.createComment({ 90 | owner: this.baseOwner, 91 | repo: this.baseRepoName, 92 | issue_number: this.issueNumber, 93 | body: comment 94 | }) 95 | core.info(`Comment added to pull request`) 96 | } catch (error) { 97 | throw new Error( 98 | `error while adding comment to pull request: ${ 99 | (error as Error).message 100 | }` 101 | ) 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/inputsHelper.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as paramsHelper from './paramsHelper' 3 | 4 | const STATUS_PARAM = 'status' 5 | const TOKEN_PARAM = 'token' 6 | const URL_PARAM = 'url' 7 | const DESCRIPTION_PARAM = 'description' 8 | const NAME_PARAM = 'name' 9 | const IGNORE_FORKS_PARAM = 'ignoreForks' 10 | const ADD_HOLD_COMMENT_PARAM = 'addHoldComment' 11 | const PENDING_COMMENT_PARAM = 'pendingComment' 12 | const SUCCESS_COMMENT_PARAM = 'successComment' 13 | const FAIL_COMMENT_PARAM = 'failComment' 14 | 15 | async function isEmptyString(str: string): Promise { 16 | return !str || str.length === 0 17 | } 18 | 19 | async function validateString(str: string, paramName: string): Promise { 20 | if (await isEmptyString(str)) { 21 | throw new TypeError(`${paramName} can't be an empty string`) 22 | } 23 | } 24 | 25 | export async function getInputs(): Promise { 26 | const result = {} as unknown as paramsHelper.IParams 27 | 28 | result.token = core.getInput(TOKEN_PARAM) 29 | 30 | await validateString(result.token, TOKEN_PARAM) 31 | 32 | result.name = core.getInput(NAME_PARAM) 33 | 34 | await validateString(result.name, NAME_PARAM) 35 | 36 | const status = core.getInput(STATUS_PARAM) 37 | 38 | await validateString(status, STATUS_PARAM) 39 | 40 | result.status = await paramsHelper.getStatus(status) 41 | 42 | result.url = core.getInput(URL_PARAM) || '' 43 | result.description = core.getInput(DESCRIPTION_PARAM) || '' 44 | 45 | const ignoreForks = core.getInput(IGNORE_FORKS_PARAM) 46 | 47 | if (await isEmptyString(ignoreForks)) { 48 | result.ignoreForks = true 49 | } else { 50 | result.ignoreForks = !(ignoreForks.toLowerCase() === 'false') 51 | } 52 | 53 | const addHoldComment = core.getInput(ADD_HOLD_COMMENT_PARAM) 54 | 55 | if (await isEmptyString(addHoldComment)) { 56 | result.addHoldComment = false 57 | } else { 58 | result.addHoldComment = addHoldComment.toLowerCase() === 'true' 59 | } 60 | 61 | result.pendingComment = core.getInput(PENDING_COMMENT_PARAM) || '' 62 | result.successComment = core.getInput(SUCCESS_COMMENT_PARAM) || '' 63 | result.failComment = core.getInput(FAIL_COMMENT_PARAM) || '' 64 | 65 | const selectedComment = await paramsHelper.getMessageForStatus( 66 | result.status, 67 | result 68 | ) 69 | 70 | result.selectedComment = selectedComment 71 | 72 | return result 73 | } 74 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as runner from './runner' 2 | 3 | runner.run() 4 | -------------------------------------------------------------------------------- /src/paramsHelper.ts: -------------------------------------------------------------------------------- 1 | const SUCCESS = 'success' 2 | const ERROR = 'error' 3 | const FAILURE = 'failure' 4 | const PENDING = 'pending' 5 | const CANCELLED = 'cancelled' 6 | 7 | type Status = 'error' | 'failure' | 'pending' | 'success' 8 | 9 | export interface IParams { 10 | token: string 11 | url: string 12 | description: string 13 | name: string 14 | status: Status 15 | ignoreForks: boolean 16 | addHoldComment: boolean 17 | pendingComment: string 18 | successComment: string 19 | failComment: string 20 | selectedComment: string 21 | } 22 | 23 | export async function getMessageForStatus( 24 | status: Status, 25 | params: IParams 26 | ): Promise { 27 | switch (status) { 28 | case PENDING: { 29 | return params.pendingComment 30 | } 31 | case SUCCESS: { 32 | return params.successComment 33 | } 34 | default: { 35 | return params.failComment 36 | } 37 | } 38 | } 39 | 40 | export async function getStatus(str: string): Promise { 41 | const toLower = str.toLowerCase() 42 | switch (toLower) { 43 | case ERROR: { 44 | return ERROR 45 | } 46 | case FAILURE: { 47 | return FAILURE 48 | } 49 | case PENDING: { 50 | return PENDING 51 | } 52 | case SUCCESS: { 53 | return SUCCESS 54 | } 55 | case CANCELLED: { 56 | return FAILURE 57 | } 58 | default: { 59 | throw new TypeError(`unknown commit or job status: ${str}`) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/runner.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as inputsHelper from './inputsHelper' 3 | import * as githubHelper from './githubHelper' 4 | import * as utils from './utils' 5 | import {IParams} from './paramsHelper' 6 | 7 | export async function run(): Promise { 8 | try { 9 | await utils.validateEventType() 10 | 11 | const params: IParams = await inputsHelper.getInputs() 12 | 13 | const ghHelper: githubHelper.IGithubHelper = 14 | await githubHelper.CreateGithubHelper() 15 | 16 | if (await ghHelper.isPullRequest()) { 17 | if (params.ignoreForks && (await ghHelper.isFork())) { 18 | core.info('ignoring PR from fork...') 19 | } else { 20 | await ghHelper.setStatus(params) 21 | // for now only add comments if it's not a fork or we explicitly say don't ignore forks 22 | // we should have a token with permissions in the fork for this 23 | if (params.addHoldComment) { 24 | await ghHelper.addComment(params.token, params.selectedComment) 25 | } 26 | } 27 | } else { 28 | await ghHelper.setStatus(params) 29 | } 30 | } catch (error) { 31 | core.setFailed((error as Error).message) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as github from '@actions/github' 2 | 3 | export async function validateEventType(): Promise { 4 | if ( 5 | github.context.eventName !== 'pull_request' && 6 | github.context.eventName !== 'push' 7 | ) { 8 | throw new Error('Error, action only works for pull_request or push events!') 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es6", 7 | "es2018", 8 | "esnext" 9 | ], 10 | "outDir": "./lib", 11 | "rootDir": "./src", 12 | "declaration": true, 13 | "strict": true, 14 | "noImplicitAny": false, 15 | "esModuleInterop": true 16 | }, 17 | "exclude": ["__test__", "lib", "node_modules"] 18 | } 19 | --------------------------------------------------------------------------------