├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── action.yml ├── assets ├── coverage.png └── fail.png ├── dist └── index.js ├── package-lock.json ├── package.json ├── sample-results.json ├── src ├── action.ts ├── failing-tests │ └── failing.test.ts ├── run.ts └── tests │ └── action.test.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | lib/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | es6: true, 6 | jest: true 7 | }, 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | ecmaVersion: 2018, 11 | sourceType: 'module' 12 | }, 13 | extends: ['eslint:recommended', 'plugin:prettier/recommended'], 14 | overrides: [ 15 | { 16 | files: ['**/*.ts'], 17 | extends: [ 18 | 'eslint:recommended', 19 | 'plugin:@typescript-eslint/eslint-recommended', 20 | 'plugin:@typescript-eslint/recommended', 21 | 'prettier/@typescript-eslint', 22 | 'plugin:jest/recommended' 23 | ], 24 | plugins: ['@typescript-eslint', 'jest'], 25 | rules: { 26 | '@typescript-eslint/explicit-function-return-type': [ 27 | 'warn', 28 | { 29 | allowExpressions: true 30 | } 31 | ], 32 | '@typescript-eslint/no-unused-vars': 'warn', 33 | '@typescript-eslint/no-use-before-define': 'off', 34 | 'no-console': 'error', 35 | // nededed for mixins to avoid unecessary warnings 36 | '@typescript-eslint/no-empty-interface': 'off' 37 | } 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | - "releases/*" 8 | 9 | jobs: 10 | # unit tests 11 | tests: 12 | name: "Unit tests" 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v1 16 | - run: npm ci 17 | - run: npm test 18 | 19 | # Failing tests on purpose 20 | failing-tests: 21 | name: "Failing tests (on purpose)" 22 | runs-on: ubuntu-latest 23 | continue-on-error: true 24 | steps: 25 | - uses: actions/checkout@v1 26 | - run: npm ci 27 | - uses: ./ 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | jest.results.json 3 | lib/ 4 | 5 | # Editors 6 | .vscode 7 | .DS_Store 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Other Dependency directories 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | 67 | # next.js build output 68 | .next 69 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "all", 3 | tabWidth: 2, 4 | semi: false, 5 | printWidth: 90, 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Matthias Etienne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jest Github Action 2 | 3 | Main features: 4 | 5 | - Add status checks with code annotations to your pull requests 6 | - Comment your pull requests with code coverage table (if tests succeeded) 7 | 8 | ## Coverage example 9 | 10 | ![Coverage](assets/coverage.png) 11 | 12 | ## Check annotations example 13 | 14 | ![Fail](assets/fail.png) 15 | 16 | ## Usage 17 | 18 | You can now consume the action by referencing the v1 branch 19 | 20 | ```yaml 21 | uses: mattallty/jest-github-action@v1 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | ``` 25 | 26 | ### Overriding the test command 27 | 28 | By default, this action will execute `npm test` to run your tests. 29 | You can change this behavior by providing a custom `test-command` like this: 30 | 31 | ```yaml 32 | uses: mattallty/jest-github-action@v1 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | with: 36 | # this is just an example, this could be any command that will trigger jest 37 | test-command: "yarn test" 38 | ``` 39 | 40 | ### Running tests only on changed files 41 | 42 | ```yaml 43 | uses: mattallty/jest-github-action@v1 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | with: 47 | # Runs tests related to the changes since the base branch of your pull request 48 | # Default to false if not set 49 | changes-only: true 50 | ``` 51 | 52 | ### Silencing the code coverage comment 53 | 54 | ```yaml 55 | uses: mattallty/jest-github-action@v1 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | with: 59 | # To avoid reporting code coverage, set this variable to false 60 | coverage-comment: false 61 | ``` 62 | 63 | ### Running tests in a subdirectory 64 | 65 | For running tests in folders other than root, supply a working-directory. 66 | 67 | ```yaml 68 | uses: mattallty/jest-github-action@v1 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | with: 72 | working-directory: "frontend" 73 | ``` 74 | 75 | 76 | See the [actions tab](https://github.com/mattallty/jest-github-action/actions) for runs of this action! :rocket: 77 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "Jest Annotations & Coverage" 2 | description: "Jest action adding checks and annotations to your pull requests and comment them with code coverage results." 3 | branding: 4 | icon: "check" 5 | color: "blue" 6 | inputs: 7 | test-command: 8 | description: "The test command to run" 9 | required: false 10 | default: "npm test" 11 | working-directory: 12 | description: "Subdirectory to run tests in" 13 | required: false 14 | default: "" 15 | coverage-comment: 16 | description: "Comment PRs with code coverage" 17 | required: false 18 | default: "true" 19 | changes-only: 20 | description: "Only run tests on changed files (over base branch)" 21 | required: false 22 | default: "false" 23 | check-name: 24 | description: "Status check name" 25 | required: true 26 | 27 | runs: 28 | using: "node12" 29 | main: "dist/index.js" 30 | -------------------------------------------------------------------------------- /assets/coverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattallty/jest-github-action/12c8c9a48ae4543fdcf5faa4d126e922d69783a8/assets/coverage.png -------------------------------------------------------------------------------- /assets/fail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattallty/jest-github-action/12c8c9a48ae4543fdcf5faa4d126e922d69783a8/assets/fail.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-github-action", 3 | "version": "1.0.0", 4 | "description": "Jest action adding checks to your pull requests", 5 | "main": "lib/run.js", 6 | "scripts": { 7 | "lint": "eslint src/**/*.ts", 8 | "lint:fix": "eslint --fix src/**/*.ts", 9 | "build": "tsc", 10 | "pack": "ncc build lib/run.js -m", 11 | "prepack": "npm run build", 12 | "failing-test": "jest failing-tests", 13 | "test": "jest /tests/" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/mattallty/jest-github-action.git" 18 | }, 19 | "keywords": [ 20 | "GitHub", 21 | "Actions", 22 | "Jest", 23 | "Pull request" 24 | ], 25 | "author": "Matthias Etienne ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/mattallty/jest-github-action/issues" 29 | }, 30 | "homepage": "https://github.com/mattallty/jest-github-action#readme", 31 | "jest": { 32 | "clearMocks": true, 33 | "moduleFileExtensions": [ 34 | "js", 35 | "ts" 36 | ], 37 | "testEnvironment": "node", 38 | "testMatch": [ 39 | "**/*.test.ts" 40 | ], 41 | "transform": { 42 | "^.+\\.ts$": "ts-jest" 43 | }, 44 | "verbose": true 45 | }, 46 | "dependencies": { 47 | "@actions/core": "^1.1.1", 48 | "@actions/exec": "^1.0.3", 49 | "@actions/github": "^2.1.1", 50 | "lodash": "^4.17.15", 51 | "markdown-table": "^2.0.0", 52 | "strip-ansi": "^6.0.0" 53 | }, 54 | "devDependencies": { 55 | "@octokit/types": "^2.11.1", 56 | "@types/jest": "^25.2.1", 57 | "@types/lodash": "^4.14.150", 58 | "@types/markdown-table": "^2.0.0", 59 | "@types/node": "^13.13.1", 60 | "@typescript-eslint/parser": "^2.29.0", 61 | "@zeit/ncc": "^0.20.5", 62 | "eslint": "^6.3.0", 63 | "eslint-config-prettier": "^6.10.1", 64 | "eslint-plugin-jest": "^23.8.2", 65 | "eslint-plugin-prettier": "^3.1.3", 66 | "jest": "^25.4.0", 67 | "prettier": "^2.0.4", 68 | "ts-jest": "^25.4.0", 69 | "typescript": "^3.8.3" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /sample-results.json: -------------------------------------------------------------------------------- 1 | {"numFailedTestSuites":0,"numFailedTests":0,"numPassedTestSuites":1,"numPassedTests":3,"numPendingTestSuites":0,"numPendingTests":0,"numRuntimeErrorTestSuites":0,"numTodoTests":0,"numTotalTestSuites":1,"numTotalTests":3,"openHandles":[],"snapshot":{"added":0,"didUpdate":false,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0},"startTime":1587478594322,"success":true,"testResults":[{"assertionResults":[{"ancestorTitles":[],"failureMessages":[],"fullName":"throws invalid number","location":{"column":1,"line":3},"status":"passed","title":"throws invalid number"},{"ancestorTitles":[],"failureMessages":[],"fullName":"wait 500 ms","location":{"column":1,"line":7},"status":"passed","title":"wait 500 ms"},{"ancestorTitles":[],"failureMessages":[],"fullName":"action should be a function","location":{"column":1,"line":11},"status":"passed","title":"action should be a function"}],"endTime":1587478596660,"message":"","name":"/Volumes/Home/matt/dev/jest-github-action/src/tests/index.test.ts","startTime":1587478594374,"status":"passed","summary":""}],"wasInterrupted":false,"coverageMap":{"/Volumes/Home/matt/dev/jest-github-action/src/action.ts":{"path":"/Volumes/Home/matt/dev/jest-github-action/src/action.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":32}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":33}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":36}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":32}},"4":{"start":{"line":5,"column":0},"end":{"line":5,"column":49}},"5":{"start":{"line":7,"column":0},"end":{"line":7,"column":36}},"6":{"start":{"line":8,"column":0},"end":{"line":8,"column":34}},"7":{"start":{"line":9,"column":0},"end":{"line":9,"column":28}},"8":{"start":{"line":10,"column":0},"end":{"line":10,"column":30}},"9":{"start":{"line":11,"column":0},"end":{"line":11,"column":34}},"10":{"start":{"line":12,"column":0},"end":{"line":12,"column":51}},"11":{"start":{"line":15,"column":20},"end":{"line":15,"column":40}},"12":{"start":{"line":19,"column":14},"end":{"line":19,"column":33}},"13":{"start":{"line":20,"column":23},"end":{"line":20,"column":53}},"14":{"start":{"line":22,"column":2},"end":{"line":59,"column":null}},"15":{"start":{"line":23,"column":18},"end":{"line":23,"column":42}},"16":{"start":{"line":24,"column":4},"end":{"line":28,"column":null}},"17":{"start":{"line":25,"column":6},"end":{"line":25,"column":null}},"18":{"start":{"line":26,"column":6},"end":{"line":26,"column":null}},"19":{"start":{"line":27,"column":6},"end":{"line":27,"column":12}},"20":{"start":{"line":30,"column":16},"end":{"line":30,"column":44}},"21":{"start":{"line":32,"column":4},"end":{"line":32,"column":null}},"22":{"start":{"line":35,"column":20},"end":{"line":35,"column":37}},"23":{"start":{"line":38,"column":20},"end":{"line":38,"column":46}},"24":{"start":{"line":41,"column":25},"end":{"line":41,"column":54}},"25":{"start":{"line":42,"column":18},"end":{"line":42,"column":59}},"26":{"start":{"line":43,"column":4},"end":{"line":43,"column":null}},"27":{"start":{"line":46,"column":4},"end":{"line":51,"column":null}},"28":{"start":{"line":47,"column":22},"end":{"line":47,"column":52}},"29":{"start":{"line":48,"column":29},"end":{"line":48,"column":55}},"30":{"start":{"line":49,"column":18},"end":{"line":49,"column":68}},"31":{"start":{"line":50,"column":6},"end":{"line":50,"column":null}},"32":{"start":{"line":53,"column":4},"end":{"line":55,"column":null}},"33":{"start":{"line":54,"column":6},"end":{"line":54,"column":null}},"34":{"start":{"line":57,"column":4},"end":{"line":57,"column":null}},"35":{"start":{"line":58,"column":4},"end":{"line":58,"column":null}},"36":{"start":{"line":63,"column":2},"end":{"line":63,"column":null}},"37":{"start":{"line":67,"column":2},"end":{"line":69,"column":null}},"38":{"start":{"line":68,"column":4},"end":{"line":68,"column":null}},"39":{"start":{"line":70,"column":17},"end":{"line":70,"column":53}},"40":{"start":{"line":71,"column":15},"end":{"line":71,"column":77}},"41":{"start":{"line":73,"column":2},"end":{"line":82,"column":null}},"42":{"start":{"line":74,"column":30},"end":{"line":74,"column":46}},"43":{"start":{"line":75,"column":4},"end":{"line":81,"column":null}},"44":{"start":{"line":83,"column":2},"end":{"line":83,"column":null}},"45":{"start":{"line":87,"column":15},"end":{"line":90,"column":null}},"46":{"start":{"line":92,"column":2},"end":{"line":92,"column":null}},"47":{"start":{"line":96,"column":15},"end":{"line":110,"column":null}},"48":{"start":{"line":112,"column":2},"end":{"line":112,"column":null}},"49":{"start":{"line":113,"column":2},"end":{"line":113,"column":null}},"50":{"start":{"line":117,"column":12},"end":{"line":117,"column":62}},"51":{"start":{"line":118,"column":22},"end":{"line":118,"column":82}},"52":{"start":{"line":119,"column":16},"end":{"line":119,"column":62}},"53":{"start":{"line":120,"column":2},"end":{"line":120,"column":null}},"54":{"start":{"line":122,"column":2},"end":{"line":122,"column":null}},"55":{"start":{"line":124,"column":2},"end":{"line":124,"column":null}},"56":{"start":{"line":128,"column":18},"end":{"line":128,"column":64}},"57":{"start":{"line":129,"column":2},"end":{"line":129,"column":null}},"58":{"start":{"line":130,"column":2},"end":{"line":130,"column":null}},"59":{"start":{"line":134,"column":2},"end":{"line":139,"column":null}},"60":{"start":{"line":135,"column":4},"end":{"line":135,"column":null}},"61":{"start":{"line":136,"column":4},"end":{"line":136,"column":null}},"62":{"start":{"line":138,"column":4},"end":{"line":138,"column":null}},"63":{"start":{"line":143,"column":2},"end":{"line":143,"column":null}},"64":{"start":{"line":147,"column":2},"end":{"line":151,"column":null}},"65":{"start":{"line":148,"column":4},"end":{"line":148,"column":null}},"66":{"start":{"line":150,"column":4},"end":{"line":150,"column":null}},"67":{"start":{"line":154,"column":23},"end":{"line":171,"column":1}},"68":{"start":{"line":158,"column":2},"end":{"line":160,"column":null}},"69":{"start":{"line":159,"column":4},"end":{"line":159,"column":null}},"70":{"start":{"line":161,"column":2},"end":{"line":170,"column":null}},"71":{"start":{"line":162,"column":4},"end":{"line":169,"column":null}},"72":{"start":{"line":162,"column":84},"end":{"line":169,"column":7}},"73":{"start":{"line":173,"column":22},"end":{"line":179,"column":1}},"74":{"start":{"line":174,"column":2},"end":{"line":176,"column":null}},"75":{"start":{"line":175,"column":4},"end":{"line":175,"column":10}},"76":{"start":{"line":177,"column":18},"end":{"line":177,"column":75}},"77":{"start":{"line":177,"column":57},"end":{"line":177,"column":73}},"78":{"start":{"line":178,"column":2},"end":{"line":178,"column":null}},"79":{"start":{"line":182,"column":2},"end":{"line":182,"column":null}},"80":{"start":{"line":185,"column":0},"end":{"line":185,"column":null}},"81":{"start":{"line":187,"column":0},"end":{"line":189,"column":null}},"82":{"start":{"line":188,"column":2},"end":{"line":188,"column":null}}},"fnMap":{"0":{"name":"run","decl":{"start":{"line":18,"column":15},"end":{"line":18,"column":18}},"loc":{"start":{"line":18,"column":18},"end":{"line":60,"column":null}}},"1":{"name":"shouldCommentCoverage","decl":{"start":{"line":62,"column":9},"end":{"line":62,"column":30}},"loc":{"start":{"line":62,"column":30},"end":{"line":64,"column":1}}},"2":{"name":"getCoverageTable","decl":{"start":{"line":66,"column":9},"end":{"line":66,"column":25}},"loc":{"start":{"line":66,"column":68},"end":{"line":84,"column":1}}},"3":{"name":"getCommentPayload","decl":{"start":{"line":86,"column":9},"end":{"line":86,"column":26}},"loc":{"start":{"line":86,"column":39},"end":{"line":93,"column":1}}},"4":{"name":"getCheckPayload","decl":{"start":{"line":95,"column":9},"end":{"line":95,"column":24}},"loc":{"start":{"line":95,"column":67},"end":{"line":114,"column":1}}},"5":{"name":"getJestCommand","decl":{"start":{"line":116,"column":9},"end":{"line":116,"column":23}},"loc":{"start":{"line":116,"column":43},"end":{"line":125,"column":1}}},"6":{"name":"parseResults","decl":{"start":{"line":127,"column":9},"end":{"line":127,"column":21}},"loc":{"start":{"line":127,"column":41},"end":{"line":131,"column":1}}},"7":{"name":"execJest","decl":{"start":{"line":133,"column":15},"end":{"line":133,"column":23}},"loc":{"start":{"line":133,"column":35},"end":{"line":140,"column":null}}},"8":{"name":"getPullId","decl":{"start":{"line":142,"column":9},"end":{"line":142,"column":18}},"loc":{"start":{"line":142,"column":18},"end":{"line":144,"column":1}}},"9":{"name":"getSha","decl":{"start":{"line":146,"column":9},"end":{"line":146,"column":15}},"loc":{"start":{"line":146,"column":15},"end":{"line":152,"column":1}}},"10":{"name":"(anonymous_20)","decl":{"start":{"line":154,"column":23},"end":{"line":154,"column":null}},"loc":{"start":{"line":157,"column":51},"end":{"line":171,"column":1}}},"11":{"name":"(anonymous_21)","decl":{"start":{"line":161,"column":38},"end":{"line":161,"column":39}},"loc":{"start":{"line":161,"column":49},"end":{"line":170,"column":3}}},"12":{"name":"(anonymous_22)","decl":{"start":{"line":162,"column":69},"end":{"line":162,"column":70}},"loc":{"start":{"line":162,"column":83},"end":{"line":169,"column":7}}},"13":{"name":"(anonymous_23)","decl":{"start":{"line":173,"column":22},"end":{"line":173,"column":23}},"loc":{"start":{"line":173,"column":56},"end":{"line":179,"column":1}}},"14":{"name":"(anonymous_24)","decl":{"start":{"line":177,"column":50},"end":{"line":177,"column":51}},"loc":{"start":{"line":177,"column":57},"end":{"line":177,"column":73}}},"15":{"name":"asMarkdownCode","decl":{"start":{"line":181,"column":9},"end":{"line":181,"column":23}},"loc":{"start":{"line":181,"column":35},"end":{"line":183,"column":1}}}},"branchMap":{"0":{"loc":{"start":{"line":24,"column":4},"end":{"line":28,"column":null}},"type":"if","locations":[{"start":{"line":24,"column":4},"end":{"line":28,"column":null}},{"start":{"line":24,"column":4},"end":{"line":28,"column":null}}]},"1":{"loc":{"start":{"line":46,"column":4},"end":{"line":51,"column":null}},"type":"if","locations":[{"start":{"line":46,"column":4},"end":{"line":51,"column":null}},{"start":{"line":46,"column":4},"end":{"line":51,"column":null}}]},"2":{"loc":{"start":{"line":53,"column":4},"end":{"line":55,"column":null}},"type":"if","locations":[{"start":{"line":53,"column":4},"end":{"line":55,"column":null}},{"start":{"line":53,"column":4},"end":{"line":55,"column":null}}]},"3":{"loc":{"start":{"line":67,"column":2},"end":{"line":69,"column":null}},"type":"if","locations":[{"start":{"line":67,"column":2},"end":{"line":69,"column":null}},{"start":{"line":67,"column":2},"end":{"line":69,"column":null}}]},"4":{"loc":{"start":{"line":73,"column":48},"end":{"line":73,"column":59}},"type":"binary-expr","locations":[{"start":{"line":73,"column":48},"end":{"line":73,"column":59}},{"start":{"line":73,"column":63},"end":{"line":73,"column":65}}]},"5":{"loc":{"start":{"line":101,"column":34},"end":{"line":101,"column":43}},"type":"cond-expr","locations":[{"start":{"line":101,"column":34},"end":{"line":101,"column":43}},{"start":{"line":101,"column":46},"end":{"line":101,"column":55}}]},"6":{"loc":{"start":{"line":103,"column":31},"end":{"line":103,"column":50}},"type":"cond-expr","locations":[{"start":{"line":103,"column":31},"end":{"line":103,"column":50}},{"start":{"line":103,"column":53},"end":{"line":103,"column":72}}]},"7":{"loc":{"start":{"line":106,"column":10},"end":{"line":106,"column":84}},"type":"cond-expr","locations":[{"start":{"line":106,"column":10},"end":{"line":106,"column":84}},{"start":{"line":107,"column":10},"end":{"line":107,"column":149}}]},"8":{"loc":{"start":{"line":119,"column":16},"end":{"line":119,"column":37}},"type":"binary-expr","locations":[{"start":{"line":119,"column":16},"end":{"line":119,"column":37}},{"start":{"line":119,"column":41},"end":{"line":119,"column":62}}]},"9":{"loc":{"start":{"line":120,"column":18},"end":{"line":120,"column":24}},"type":"cond-expr","locations":[{"start":{"line":120,"column":18},"end":{"line":120,"column":24}},{"start":{"line":120,"column":27},"end":{"line":120,"column":30}}]},"10":{"loc":{"start":{"line":143,"column":45},"end":{"line":143,"column":49}},"type":"cond-expr","locations":[{"start":{"line":143,"column":45},"end":{"line":143,"column":49}},{"start":{"line":143,"column":49},"end":{"line":143,"column":50}}]},"11":{"loc":{"start":{"line":143,"column":2},"end":{"line":143,"column":49}},"type":"binary-expr","locations":[{"start":{"line":143,"column":2},"end":{"line":143,"column":49}},{"start":{"line":143,"column":45},"end":{"line":143,"column":49}}]},"12":{"loc":{"start":{"line":143,"column":37},"end":{"line":143,"column":39}},"type":"cond-expr","locations":[{"start":{"line":143,"column":37},"end":{"line":143,"column":39}},{"start":{"line":143,"column":37},"end":{"line":143,"column":45}}]},"13":{"loc":{"start":{"line":143,"column":2},"end":{"line":143,"column":39}},"type":"binary-expr","locations":[{"start":{"line":143,"column":2},"end":{"line":143,"column":39}},{"start":{"line":143,"column":37},"end":{"line":143,"column":39}}]},"14":{"loc":{"start":{"line":148,"column":39},"end":{"line":148,"column":41}},"type":"cond-expr","locations":[{"start":{"line":148,"column":39},"end":{"line":148,"column":41}},{"start":{"line":148,"column":39},"end":{"line":148,"column":49}}]},"15":{"loc":{"start":{"line":148,"column":4},"end":{"line":148,"column":41}},"type":"binary-expr","locations":[{"start":{"line":148,"column":4},"end":{"line":148,"column":41}},{"start":{"line":148,"column":39},"end":{"line":148,"column":41}}]},"16":{"loc":{"start":{"line":158,"column":2},"end":{"line":160,"column":null}},"type":"if","locations":[{"start":{"line":158,"column":2},"end":{"line":160,"column":null}},{"start":{"line":158,"column":2},"end":{"line":160,"column":null}}]},"17":{"loc":{"start":{"line":164,"column":42},"end":{"line":164,"column":46}},"type":"cond-expr","locations":[{"start":{"line":164,"column":42},"end":{"line":164,"column":46}},{"start":{"line":164,"column":46},"end":{"line":164,"column":47}}]},"18":{"loc":{"start":{"line":164,"column":16},"end":{"line":164,"column":46}},"type":"binary-expr","locations":[{"start":{"line":164,"column":16},"end":{"line":164,"column":46}},{"start":{"line":164,"column":42},"end":{"line":164,"column":46}}]},"19":{"loc":{"start":{"line":164,"column":36},"end":{"line":164,"column":38}},"type":"cond-expr","locations":[{"start":{"line":164,"column":36},"end":{"line":164,"column":38}},{"start":{"line":164,"column":36},"end":{"line":164,"column":42}}]},"20":{"loc":{"start":{"line":164,"column":16},"end":{"line":164,"column":38}},"type":"binary-expr","locations":[{"start":{"line":164,"column":16},"end":{"line":164,"column":38}},{"start":{"line":164,"column":36},"end":{"line":164,"column":38}}]},"21":{"loc":{"start":{"line":165,"column":40},"end":{"line":165,"column":44}},"type":"cond-expr","locations":[{"start":{"line":165,"column":40},"end":{"line":165,"column":44}},{"start":{"line":165,"column":44},"end":{"line":165,"column":45}}]},"22":{"loc":{"start":{"line":165,"column":14},"end":{"line":165,"column":44}},"type":"binary-expr","locations":[{"start":{"line":165,"column":14},"end":{"line":165,"column":44}},{"start":{"line":165,"column":40},"end":{"line":165,"column":44}}]},"23":{"loc":{"start":{"line":165,"column":34},"end":{"line":165,"column":36}},"type":"cond-expr","locations":[{"start":{"line":165,"column":34},"end":{"line":165,"column":36}},{"start":{"line":165,"column":34},"end":{"line":165,"column":40}}]},"24":{"loc":{"start":{"line":165,"column":14},"end":{"line":165,"column":36}},"type":"binary-expr","locations":[{"start":{"line":165,"column":14},"end":{"line":165,"column":36}},{"start":{"line":165,"column":34},"end":{"line":165,"column":36}}]},"25":{"loc":{"start":{"line":168,"column":59},"end":{"line":168,"column":64}},"type":"cond-expr","locations":[{"start":{"line":168,"column":59},"end":{"line":168,"column":64}},{"start":{"line":168,"column":64},"end":{"line":168,"column":66}}]},"26":{"loc":{"start":{"line":168,"column":20},"end":{"line":168,"column":64}},"type":"binary-expr","locations":[{"start":{"line":168,"column":20},"end":{"line":168,"column":64}},{"start":{"line":168,"column":59},"end":{"line":168,"column":64}}]},"27":{"loc":{"start":{"line":168,"column":46},"end":{"line":168,"column":48}},"type":"cond-expr","locations":[{"start":{"line":168,"column":46},"end":{"line":168,"column":48}},{"start":{"line":168,"column":46},"end":{"line":168,"column":64}}]},"28":{"loc":{"start":{"line":168,"column":20},"end":{"line":168,"column":48}},"type":"binary-expr","locations":[{"start":{"line":168,"column":20},"end":{"line":168,"column":48}},{"start":{"line":168,"column":46},"end":{"line":168,"column":48}}]},"29":{"loc":{"start":{"line":174,"column":2},"end":{"line":176,"column":null}},"type":"if","locations":[{"start":{"line":174,"column":2},"end":{"line":176,"column":null}},{"start":{"line":174,"column":2},"end":{"line":176,"column":null}}]},"30":{"loc":{"start":{"line":187,"column":0},"end":{"line":189,"column":null}},"type":"if","locations":[{"start":{"line":187,"column":0},"end":{"line":189,"column":null}},{"start":{"line":187,"column":0},"end":{"line":189,"column":null}}]}},"s":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,"30":0,"31":0,"32":0,"33":0,"34":0,"35":0,"36":0,"37":0,"38":0,"39":0,"40":0,"41":0,"42":0,"43":0,"44":0,"45":0,"46":0,"47":0,"48":0,"49":0,"50":0,"51":0,"52":0,"53":0,"54":0,"55":0,"56":0,"57":0,"58":0,"59":0,"60":0,"61":0,"62":0,"63":0,"64":0,"65":0,"66":0,"67":1,"68":0,"69":0,"70":0,"71":0,"72":0,"73":1,"74":0,"75":0,"76":0,"77":0,"78":0,"79":0,"80":1,"81":1,"82":0},"f":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0},"b":{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0],"6":[0,0],"7":[0,0],"8":[0,0],"9":[0,0],"10":[0,0],"11":[0,0],"12":[0,0],"13":[0,0],"14":[0,0],"15":[0,0],"16":[0,0],"17":[0,0],"18":[0,0],"19":[0,0],"20":[0,0],"21":[0,0],"22":[0,0],"23":[0,0],"24":[0,0],"25":[0,0],"26":[0,0],"27":[0,0],"28":[0,0],"29":[0,0],"30":[0,1]}}}} -------------------------------------------------------------------------------- /src/action.ts: -------------------------------------------------------------------------------- 1 | import { sep, join, resolve } from "path" 2 | import { readFileSync } from "fs" 3 | import { exec } from "@actions/exec" 4 | import * as core from "@actions/core" 5 | import { GitHub, context } from "@actions/github" 6 | import type { Octokit } from "@octokit/rest" 7 | import flatMap from "lodash/flatMap" 8 | import filter from "lodash/filter" 9 | import map from "lodash/map" 10 | import strip from "strip-ansi" 11 | import table from "markdown-table" 12 | import { createCoverageMap, CoverageMapData } from "istanbul-lib-coverage" 13 | import type { FormattedTestResults } from "@jest/test-result/build/types" 14 | 15 | const ACTION_NAME = "jest-github-action" 16 | const COVERAGE_HEADER = ":loop: **Code coverage**\n\n" 17 | 18 | export async function run() { 19 | let workingDirectory = core.getInput("working-directory", { required: false }) 20 | let cwd = workingDirectory ? resolve(workingDirectory) : process.cwd() 21 | const CWD = cwd + sep 22 | const RESULTS_FILE = join(CWD, "jest.results.json") 23 | 24 | try { 25 | const token = process.env.GITHUB_TOKEN 26 | if (token === undefined) { 27 | core.error("GITHUB_TOKEN not set.") 28 | core.setFailed("GITHUB_TOKEN not set.") 29 | return 30 | } 31 | 32 | const cmd = getJestCommand(RESULTS_FILE) 33 | 34 | await execJest(cmd, CWD) 35 | 36 | // octokit 37 | const octokit = new GitHub(token) 38 | 39 | // Parse results 40 | const results = parseResults(RESULTS_FILE) 41 | 42 | // Checks 43 | const checkPayload = getCheckPayload(results, CWD) 44 | await octokit.checks.create(checkPayload) 45 | 46 | // Coverage comments 47 | if (getPullId() && shouldCommentCoverage()) { 48 | const comment = getCoverageTable(results, CWD) 49 | if (comment) { 50 | await deletePreviousComments(octokit) 51 | const commentPayload = getCommentPayload(comment) 52 | await octokit.issues.createComment(commentPayload) 53 | } 54 | } 55 | 56 | if (!results.success) { 57 | core.setFailed("Some jest tests failed.") 58 | } 59 | } catch (error) { 60 | console.error(error) 61 | core.setFailed(error.message) 62 | } 63 | } 64 | 65 | async function deletePreviousComments(octokit: GitHub) { 66 | const { data } = await octokit.issues.listComments({ 67 | ...context.repo, 68 | per_page: 100, 69 | issue_number: getPullId(), 70 | }) 71 | return Promise.all( 72 | data 73 | .filter( 74 | (c) => 75 | c.user.login === "github-actions[bot]" && c.body.startsWith(COVERAGE_HEADER), 76 | ) 77 | .map((c) => octokit.issues.deleteComment({ ...context.repo, comment_id: c.id })), 78 | ) 79 | } 80 | 81 | function shouldCommentCoverage(): boolean { 82 | return Boolean(JSON.parse(core.getInput("coverage-comment", { required: false }))) 83 | } 84 | 85 | function shouldRunOnlyChangedFiles(): boolean { 86 | return Boolean(JSON.parse(core.getInput("changes-only", { required: false }))) 87 | } 88 | 89 | export function getCoverageTable( 90 | results: FormattedTestResults, 91 | cwd: string, 92 | ): string | false { 93 | if (!results.coverageMap) { 94 | return "" 95 | } 96 | const covMap = createCoverageMap((results.coverageMap as unknown) as CoverageMapData) 97 | const rows = [["Filename", "Statements", "Branches", "Functions", "Lines"]] 98 | 99 | if (!Object.keys(covMap.data).length) { 100 | console.error("No entries found in coverage data") 101 | return false 102 | } 103 | 104 | for (const [filename, data] of Object.entries(covMap.data || {})) { 105 | const { data: summary } = data.toSummary() 106 | rows.push([ 107 | filename.replace(cwd, ""), 108 | summary.statements.pct + "%", 109 | summary.branches.pct + "%", 110 | summary.functions.pct + "%", 111 | summary.lines.pct + "%", 112 | ]) 113 | } 114 | 115 | return COVERAGE_HEADER + table(rows, { align: ["l", "r", "r", "r", "r"] }) 116 | } 117 | 118 | function getCommentPayload(body: string) { 119 | const payload: Octokit.IssuesCreateCommentParams = { 120 | ...context.repo, 121 | body, 122 | issue_number: getPullId(), 123 | } 124 | return payload 125 | } 126 | 127 | function getCheckPayload(results: FormattedTestResults, cwd: string) { 128 | const payload: Octokit.ChecksCreateParams = { 129 | ...context.repo, 130 | head_sha: getSha(), 131 | name: core.getInput("check-name", { required: false }) || ACTION_NAME, 132 | status: "completed", 133 | conclusion: results.success ? "success" : "failure", 134 | output: { 135 | title: results.success ? "Jest tests passed" : "Jest tests failed", 136 | text: getOutputText(results), 137 | summary: results.success 138 | ? `${results.numPassedTests} tests passing in ${ 139 | results.numPassedTestSuites 140 | } suite${results.numPassedTestSuites > 1 ? "s" : ""}.` 141 | : `Failed tests: ${results.numFailedTests}/${results.numTotalTests}. Failed suites: ${results.numFailedTests}/${results.numTotalTestSuites}.`, 142 | 143 | annotations: getAnnotations(results, cwd), 144 | }, 145 | } 146 | console.debug("Check payload: %j", payload) 147 | return payload 148 | } 149 | 150 | function getJestCommand(resultsFile: string) { 151 | let cmd = core.getInput("test-command", { required: false }) 152 | const jestOptions = `--testLocationInResults --json ${ 153 | shouldCommentCoverage() ? "--coverage" : "" 154 | } ${ 155 | shouldRunOnlyChangedFiles() && context.payload.pull_request?.base.ref 156 | ? "--changedSince=" + context.payload.pull_request?.base.ref 157 | : "" 158 | } --outputFile=${resultsFile}` 159 | const shouldAddHyphen = cmd.startsWith("npm") || cmd.startsWith("npx") || cmd.startsWith("pnpm") || cmd.startsWith("pnpx") 160 | cmd += (shouldAddHyphen ? " -- " : " ") + jestOptions 161 | core.debug("Final test command: " + cmd) 162 | return cmd 163 | } 164 | 165 | function parseResults(resultsFile: string): FormattedTestResults { 166 | const results = JSON.parse(readFileSync(resultsFile, "utf-8")) 167 | console.debug("Jest results: %j", results) 168 | return results 169 | } 170 | 171 | async function execJest(cmd: string, cwd?: string) { 172 | try { 173 | await exec(cmd, [], { silent: true, cwd }) 174 | console.debug("Jest command executed") 175 | } catch (e) { 176 | console.error("Jest execution failed. Tests have likely failed.", e) 177 | } 178 | } 179 | 180 | function getPullId(): number { 181 | return context.payload.pull_request?.number ?? 0 182 | } 183 | 184 | function getSha(): string { 185 | return context.payload.pull_request?.head.sha ?? context.sha 186 | } 187 | 188 | const getAnnotations = ( 189 | results: FormattedTestResults, 190 | cwd: string, 191 | ): Octokit.ChecksCreateParamsOutputAnnotations[] => { 192 | if (results.success) { 193 | return [] 194 | } 195 | return flatMap(results.testResults, (result) => { 196 | return filter(result.assertionResults, ["status", "failed"]).map((assertion) => ({ 197 | path: result.name.replace(cwd, ""), 198 | start_line: assertion.location?.line ?? 0, 199 | end_line: assertion.location?.line ?? 0, 200 | annotation_level: "failure", 201 | title: assertion.ancestorTitles.concat(assertion.title).join(" > "), 202 | message: strip(assertion.failureMessages?.join("\n\n") ?? ""), 203 | })) 204 | }) 205 | } 206 | 207 | const getOutputText = (results: FormattedTestResults) => { 208 | if (results.success) { 209 | return 210 | } 211 | const entries = filter(map(results.testResults, (r) => strip(r.message))) 212 | return asMarkdownCode(entries.join("\n")) 213 | } 214 | 215 | export function asMarkdownCode(str: string) { 216 | return "```\n" + str.trimRight() + "\n```" 217 | } 218 | -------------------------------------------------------------------------------- /src/failing-tests/failing.test.ts: -------------------------------------------------------------------------------- 1 | describe("My test suite", () => { 2 | test("this one should fail", () => { 3 | expect(1).toBe(2) 4 | }) 5 | test("this another one should also fail", () => { 6 | const str = "I'm in the kitchen." 7 | expect(str).toBe("I'm in the garden.") 8 | }) 9 | test("this one should work", () => { 10 | expect(1).toBeTruthy() 11 | }) 12 | test("this one should also work", () => { 13 | expect(true).toBeTruthy() 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/run.ts: -------------------------------------------------------------------------------- 1 | import {run} from "./action" 2 | run() -------------------------------------------------------------------------------- /src/tests/action.test.ts: -------------------------------------------------------------------------------- 1 | import { getCoverageTable, asMarkdownCode } from "../action" 2 | 3 | test("throws invalid number", () => { 4 | expect(1).toBeTruthy() 5 | }) 6 | 7 | test("wait 500 ms", async () => { 8 | expect(500).toBeGreaterThan(450) 9 | }) 10 | 11 | describe("getCoverageTable()", () => { 12 | it("should return a markdown table", () => { 13 | const results = require("../../sample-results.json") 14 | expect( 15 | getCoverageTable(results, "/Volumes/Home/matt/dev/jest-github-action/"), 16 | ).toStrictEqual(expect.any(String)) 17 | }) 18 | }) 19 | 20 | describe("asMarkdownCode()", () => { 21 | it("should return a markdown formated code", () => { 22 | expect(asMarkdownCode("hello")).toStrictEqual("```\nhello\n```") 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 4 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 5 | "outDir": "./lib" /* Redirect output structure to the directory. */, 6 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 7 | "strict": true /* Enable all strict type-checking options. */, 8 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 9 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 10 | }, 11 | "exclude": ["node_modules", "lib"] 12 | } 13 | --------------------------------------------------------------------------------