├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── __tests__ ├── events.test.ts ├── fixtures │ └── gotestoutput.txt ├── helpers.ts ├── inputs.test.ts ├── renderer.test.ts ├── results.test.ts └── runner.test.ts ├── action.yml ├── dist └── index.js ├── docs └── img │ ├── expanded.png │ └── overview.png ├── jest.config.ts ├── package-lock.json ├── package.json ├── script └── localenv ├── src ├── events.ts ├── index.ts ├── inputs.ts ├── renderer.ts ├── results.ts └── runner.ts └── tsconfig.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | groups: 8 | npm: 9 | patterns: 10 | - "*" 11 | - package-ecosystem: github-actions 12 | directory: "/" 13 | schedule: 14 | interval: weekly 15 | groups: 16 | actions: 17 | patterns: 18 | - "*" 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | jest: 9 | name: jest 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 20 16 | - run: npm ci 17 | - run: npm test 18 | - run: npm run build 19 | - name: check dist 20 | run: | 21 | changed_files_count=$(git status --porcelain | wc -l) 22 | if [ $changed_files_count -ne 0 ]; then 23 | echo 'mismatched files from ncc generation! did you forget to run `npm run build`?' | tee -a $GITHUB_STEP_SUMMARY 24 | echo '```diff' >> $GITHUB_STEP_SUMMARY 25 | git diff >> $GITHUB_STEP_SUMMARY 26 | echo '```' >> $GITHUB_STEP_SUMMARY 27 | exit 1 28 | fi 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | __tests__/output 3 | 4 | # Created by https://www.toptal.com/developers/gitignore/api/macos,node,go 5 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,node,go 6 | 7 | ### Go ### 8 | # If you prefer the allow list template instead of the deny list, see community template: 9 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 10 | # 11 | # Binaries for programs and plugins 12 | *.exe 13 | *.exe~ 14 | *.dll 15 | *.so 16 | *.dylib 17 | 18 | # Test binary, built with `go test -c` 19 | *.test 20 | 21 | # Output of the go coverage tool, specifically when used with LiteIDE 22 | *.out 23 | 24 | # Dependency directories (remove the comment below to include it) 25 | # vendor/ 26 | 27 | # Go workspace file 28 | go.work 29 | 30 | ### Go Patch ### 31 | /vendor/ 32 | /Godeps/ 33 | 34 | ### macOS ### 35 | # General 36 | .DS_Store 37 | .AppleDouble 38 | .LSOverride 39 | 40 | # Icon must end with two \r 41 | Icon 42 | 43 | 44 | # Thumbnails 45 | ._* 46 | 47 | # Files that might appear in the root of a volume 48 | .DocumentRevisions-V100 49 | .fseventsd 50 | .Spotlight-V100 51 | .TemporaryItems 52 | .Trashes 53 | .VolumeIcon.icns 54 | .com.apple.timemachine.donotpresent 55 | 56 | # Directories potentially created on remote AFP share 57 | .AppleDB 58 | .AppleDesktop 59 | Network Trash Folder 60 | Temporary Items 61 | .apdisk 62 | 63 | ### macOS Patch ### 64 | # iCloud generated files 65 | *.icloud 66 | 67 | ### Node ### 68 | # Logs 69 | logs 70 | *.log 71 | npm-debug.log* 72 | yarn-debug.log* 73 | yarn-error.log* 74 | lerna-debug.log* 75 | .pnpm-debug.log* 76 | 77 | # Diagnostic reports (https://nodejs.org/api/report.html) 78 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 79 | 80 | # Runtime data 81 | pids 82 | *.pid 83 | *.seed 84 | *.pid.lock 85 | 86 | # Directory for instrumented libs generated by jscoverage/JSCover 87 | lib-cov 88 | 89 | # Coverage directory used by tools like istanbul 90 | coverage 91 | *.lcov 92 | 93 | # nyc test coverage 94 | .nyc_output 95 | 96 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 97 | .grunt 98 | 99 | # Bower dependency directory (https://bower.io/) 100 | bower_components 101 | 102 | # node-waf configuration 103 | .lock-wscript 104 | 105 | # Compiled binary addons (https://nodejs.org/api/addons.html) 106 | build/Release 107 | 108 | # Dependency directories 109 | node_modules/ 110 | jspm_packages/ 111 | 112 | # Snowpack dependency directory (https://snowpack.dev/) 113 | web_modules/ 114 | 115 | # TypeScript cache 116 | *.tsbuildinfo 117 | 118 | # Optional npm cache directory 119 | .npm 120 | 121 | # Optional eslint cache 122 | .eslintcache 123 | 124 | # Optional stylelint cache 125 | .stylelintcache 126 | 127 | # Microbundle cache 128 | .rpt2_cache/ 129 | .rts2_cache_cjs/ 130 | .rts2_cache_es/ 131 | .rts2_cache_umd/ 132 | 133 | # Optional REPL history 134 | .node_repl_history 135 | 136 | # Output of 'npm pack' 137 | *.tgz 138 | 139 | # Yarn Integrity file 140 | .yarn-integrity 141 | 142 | # dotenv environment variable files 143 | .env 144 | .env.development.local 145 | .env.test.local 146 | .env.production.local 147 | .env.local 148 | 149 | # parcel-bundler cache (https://parceljs.org/) 150 | .cache 151 | .parcel-cache 152 | 153 | # Next.js build output 154 | .next 155 | out 156 | 157 | # Nuxt.js build / generate output 158 | .nuxt 159 | 160 | # Gatsby files 161 | .cache/ 162 | # Comment in the public line in if your project uses Gatsby and not Next.js 163 | # https://nextjs.org/blog/next-9-1#public-directory-support 164 | # public 165 | 166 | # vuepress build output 167 | .vuepress/dist 168 | 169 | # vuepress v2.x temp and cache directory 170 | .temp 171 | 172 | # Docusaurus cache and generated files 173 | .docusaurus 174 | 175 | # Serverless directories 176 | .serverless/ 177 | 178 | # FuseBox cache 179 | .fusebox/ 180 | 181 | # DynamoDB Local files 182 | .dynamodb/ 183 | 184 | # TernJS port file 185 | .tern-port 186 | 187 | # Stores VSCode versions used for testing VSCode extensions 188 | .vscode-test 189 | 190 | # yarn v2 191 | .yarn/cache 192 | .yarn/unplugged 193 | .yarn/build-state.yml 194 | .yarn/install-state.gz 195 | .pnp.* 196 | 197 | ### Node Patch ### 198 | # Serverless Webpack directories 199 | .webpack/ 200 | 201 | # Optional stylelint cache 202 | 203 | # SvelteKit build / generate output 204 | .svelte-kit 205 | 206 | # End of https://www.toptal.com/developers/gitignore/api/macos,node,go -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "printWidth": 80, 4 | "semi": false, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "es5", 8 | "useTabs": false 9 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Robert Herley 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 | # go-test-action 2 | 3 | - [go-test-action](#go-test-action) 4 | - [Quick start](#quick-start) 5 | - [Inputs](#inputs) 6 | - [Screenshots](#screenshots) 7 | - [Examples](#examples) 8 | - [Basic](#basic) 9 | - [Using existing test file](#using-existing-test-file) 10 | - [Omitting elements](#omitting-elements) 11 | 12 | GitHub Action for running `go test ./...` and getting rich summary and annotations as output. 13 | 14 | Powered by [Job Summaries](https://github.blog/2022-05-09-supercharging-github-actions-with-job-summaries/), this Action will generate a convenient interactive viewer for tests based on Go's [test2json](https://pkg.go.dev/cmd/test2json) output. If there are any errors during `go test`, the Action will report back the same exit code, which will fail the job. 15 | 16 | ## Quick start 17 | 18 | ```yaml 19 | - name: Test 20 | uses: robherley/go-test-action@v0 21 | ``` 22 | 23 | ## Inputs 24 | 25 | ```yaml 26 | - uses: robherley/go-test-action@v0 27 | with: 28 | # Relative path to the directory containing the go.mod of the module you wish to test. 29 | # Optional. Default is '.' 30 | moduleDirectory: 31 | 32 | # Arguments to pass to go test, -json will be prepended automatically. 33 | # Optional. Default is './...' 34 | testArguments: 35 | 36 | # Parse an exisiting [test2json](https://pkg.go.dev/cmd/test2json) file, instead of executing go test. 37 | # Will always exit(0) on successful test file parse. 38 | # Optional. No default 39 | fromJSONFile: 40 | 41 | # Whitespace separated list of renderable items to omit. 42 | # Valid options to omit are: 43 | # untested: packages that have no tests 44 | # successful: packages that are successful 45 | # pie: mermaid.js pie chart 46 | # pkg-tests: per-package test list 47 | # pkg-output: per-package test output 48 | # stderr: standard error output of `go test` subprocess 49 | # Optional. No default 50 | omit: 51 | ``` 52 | 53 | ## Screenshots 54 | 55 | Tests are organized per package, with a brief summary of individual test results: 56 | 57 | ![summary overview](docs/img/overview.png) 58 | 59 | Expand for per-test (with subtest) results and to view raw test output: 60 | 61 | ![summary expanded](docs/img/expanded.png) 62 | 63 | ## Examples 64 | 65 | ### Basic 66 | 67 | ```yaml 68 | name: Go 69 | 70 | on: 71 | push: 72 | 73 | jobs: 74 | test: 75 | runs-on: ubuntu-latest 76 | steps: 77 | - uses: actions/checkout@v4 78 | 79 | - name: Set up Go 80 | uses: actions/setup-go@v5 81 | with: 82 | go-version-file: go.mod 83 | 84 | - name: Build 85 | run: go build -v ./... 86 | 87 | - name: Test 88 | uses: robherley/go-test-action@v0 89 | ``` 90 | 91 | ### Using existing test file 92 | 93 | ```yaml 94 | - name: Test 95 | uses: robherley/go-test-action@v0 96 | with: 97 | fromJSONFile: /path/to/test2json.json 98 | ``` 99 | 100 | ### Omitting elements 101 | 102 | See [Inputs](#inputs) above for valid options 103 | 104 | ```yaml 105 | - name: Test 106 | uses: robherley/go-test-action@v0 107 | with: 108 | omit: | 109 | pie 110 | stderr 111 | ``` 112 | 113 | or 114 | 115 | ```yaml 116 | - name: Test 117 | uses: robherley/go-test-action@v0 118 | with: 119 | omit: 'pie' 120 | ``` 121 | -------------------------------------------------------------------------------- /__tests__/events.test.ts: -------------------------------------------------------------------------------- 1 | import { getTestStdout, mockActionsCoreLogging } from './helpers' 2 | import { parseTestEvents } from '../src/events' 3 | 4 | describe('events', () => { 5 | beforeEach(() => { 6 | mockActionsCoreLogging() 7 | }) 8 | 9 | it('correctly parses test2json output', async () => { 10 | const stdout = await getTestStdout() 11 | 12 | const testsEvents = parseTestEvents(stdout) 13 | 14 | expect(testsEvents).toHaveLength(59) 15 | expect(testsEvents[58]).toEqual({ 16 | time: new Date('2022-07-11T02:42:12.111Z'), 17 | action: 'fail', 18 | package: 'github.com/robherley/go-test-example/boom', 19 | test: undefined, 20 | elapsed: 0.103, 21 | output: undefined, 22 | isCached: false, 23 | isSubtest: false, 24 | isPackageLevel: true, 25 | isConclusive: true, 26 | }) 27 | }) 28 | 29 | it('correctly indicates a package level test', () => { 30 | const packageLevelStdout = 31 | '{"Time":"2022-07-10T22:42:11.92576-04:00","Action":"output","Package":"github.com/robherley/go-test-example","Output":"? \\tgithub.com/robherley/go-test-example\\t[no test files]\\n"}' 32 | 33 | const packageLevelTestEvents = parseTestEvents(packageLevelStdout) 34 | expect(packageLevelTestEvents[0]).toHaveProperty('isPackageLevel', true) 35 | 36 | const otherStdout = 37 | '{"Time":"2022-07-10T22:42:12.108346-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestFatal","Output":"=== RUN TestFatal\\n"}' 38 | 39 | const otherTestEvents = parseTestEvents(otherStdout) 40 | expect(otherTestEvents[0]).toHaveProperty('isPackageLevel', false) 41 | }) 42 | 43 | it('correctly indicates a subtest', () => { 44 | const subTestStdout = 45 | '{"Time":"2022-07-10T22:42:11.9313-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess/Subtest(2)","Output":" success_test.go:19: hello from subtest #2\\n"}' 46 | 47 | const subTestEvents = parseTestEvents(subTestStdout) 48 | expect(subTestEvents[0]).toHaveProperty('isSubtest', true) 49 | 50 | const topLevelTestStdout = 51 | '{"Time":"2022-07-10T22:42:11.931141-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess","Output":"=== RUN TestSuccess\\n"}' 52 | 53 | const topLevelTestEvents = parseTestEvents(topLevelTestStdout) 54 | expect(topLevelTestEvents[0]).toHaveProperty('isSubtest', false) 55 | }) 56 | 57 | it('correctly indicates conclusive tests', () => { 58 | const getStdout = (action: string) => 59 | `{"Time":"2022-07-10T22:42:12.108414-04:00","Action":"${action}","Package":"github.com/robherley/go-test-example/boom","Test":"TestFatal","Elapsed":0}` 60 | 61 | const testCases: [string, boolean][] = [ 62 | ['run', false], 63 | ['pause', false], 64 | ['cont', false], 65 | ['bench', false], 66 | ['output', false], 67 | ['pass', true], 68 | ['fail', true], 69 | ['skip', true], 70 | ] 71 | 72 | for (let [action, isConclusive] of testCases) { 73 | const stdout = getStdout(action) 74 | const testEvents = parseTestEvents(stdout) 75 | expect(testEvents[0]).toHaveProperty('isConclusive', isConclusive) 76 | } 77 | }) 78 | 79 | it('correctly indicates a cached test', () => { 80 | const cachedStdout = 81 | '{"Time":"2022-07-10T22:42:11.931552-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Output":"ok \\tgithub.com/robherley/go-test-example/success\\t(cached)\\n"}' 82 | 83 | const cachedTestEvents = parseTestEvents(cachedStdout) 84 | expect(cachedTestEvents[0]).toHaveProperty('isCached', true) 85 | 86 | const otherStdout = 87 | '{"Time":"2022-07-10T22:42:11.931552-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Output":"ok \\tgithub.com/robherley/go-test-example/success"}' 88 | 89 | const otherTestEvents = parseTestEvents(otherStdout) 90 | expect(otherTestEvents[0]).toHaveProperty('isCached', false) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /__tests__/fixtures/gotestoutput.txt: -------------------------------------------------------------------------------- 1 | {"Time":"2022-07-10T22:42:11.92576-04:00","Action":"output","Package":"github.com/robherley/go-test-example","Output":"? \tgithub.com/robherley/go-test-example\t[no test files]\n"} 2 | {"Time":"2022-07-10T22:42:11.926603-04:00","Action":"skip","Package":"github.com/robherley/go-test-example","Elapsed":0.001} 3 | {"Time":"2022-07-10T22:42:11.931066-04:00","Action":"run","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess"} 4 | {"Time":"2022-07-10T22:42:11.931141-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess","Output":"=== RUN TestSuccess\n"} 5 | {"Time":"2022-07-10T22:42:11.931166-04:00","Action":"run","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess/Subtest(1)"} 6 | {"Time":"2022-07-10T22:42:11.931185-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess/Subtest(1)","Output":"=== RUN TestSuccess/Subtest(1)\n"} 7 | {"Time":"2022-07-10T22:42:11.931204-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess/Subtest(1)","Output":" success_test.go:19: hello from subtest #1\n"} 8 | {"Time":"2022-07-10T22:42:11.931239-04:00","Action":"run","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess/Subtest(2)"} 9 | {"Time":"2022-07-10T22:42:11.931284-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess/Subtest(2)","Output":"=== RUN TestSuccess/Subtest(2)\n"} 10 | {"Time":"2022-07-10T22:42:11.9313-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess/Subtest(2)","Output":" success_test.go:19: hello from subtest #2\n"} 11 | {"Time":"2022-07-10T22:42:11.931315-04:00","Action":"run","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess/Subtest(3)"} 12 | {"Time":"2022-07-10T22:42:11.931332-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess/Subtest(3)","Output":"=== RUN TestSuccess/Subtest(3)\n"} 13 | {"Time":"2022-07-10T22:42:11.931347-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess/Subtest(3)","Output":" success_test.go:19: hello from subtest #3\n"} 14 | {"Time":"2022-07-10T22:42:11.931366-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess","Output":"--- PASS: TestSuccess (0.00s)\n"} 15 | {"Time":"2022-07-10T22:42:11.931383-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess/Subtest(1)","Output":" --- PASS: TestSuccess/Subtest(1) (0.00s)\n"} 16 | {"Time":"2022-07-10T22:42:11.93144-04:00","Action":"pass","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess/Subtest(1)","Elapsed":0} 17 | {"Time":"2022-07-10T22:42:11.931461-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess/Subtest(2)","Output":" --- PASS: TestSuccess/Subtest(2) (0.00s)\n"} 18 | {"Time":"2022-07-10T22:42:11.931477-04:00","Action":"pass","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess/Subtest(2)","Elapsed":0} 19 | {"Time":"2022-07-10T22:42:11.931492-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess/Subtest(3)","Output":" --- PASS: TestSuccess/Subtest(3) (0.00s)\n"} 20 | {"Time":"2022-07-10T22:42:11.931506-04:00","Action":"pass","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess/Subtest(3)","Elapsed":0} 21 | {"Time":"2022-07-10T22:42:11.931521-04:00","Action":"pass","Package":"github.com/robherley/go-test-example/success","Test":"TestSuccess","Elapsed":0} 22 | {"Time":"2022-07-10T22:42:11.931536-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Output":"PASS\n"} 23 | {"Time":"2022-07-10T22:42:11.931552-04:00","Action":"output","Package":"github.com/robherley/go-test-example/success","Output":"ok \tgithub.com/robherley/go-test-example/success\t(cached)\n"} 24 | {"Time":"2022-07-10T22:42:11.931572-04:00","Action":"pass","Package":"github.com/robherley/go-test-example/success","Elapsed":0.001} 25 | {"Time":"2022-07-10T22:42:11.933412-04:00","Action":"run","Package":"github.com/robherley/go-test-example/skipme","Test":"TestSkip"} 26 | {"Time":"2022-07-10T22:42:11.933502-04:00","Action":"output","Package":"github.com/robherley/go-test-example/skipme","Test":"TestSkip","Output":"=== RUN TestSkip\n"} 27 | {"Time":"2022-07-10T22:42:11.933535-04:00","Action":"output","Package":"github.com/robherley/go-test-example/skipme","Test":"TestSkip","Output":" skipme_test.go:6: skip me\n"} 28 | {"Time":"2022-07-10T22:42:11.933551-04:00","Action":"output","Package":"github.com/robherley/go-test-example/skipme","Test":"TestSkip","Output":"--- SKIP: TestSkip (0.00s)\n"} 29 | {"Time":"2022-07-10T22:42:11.933563-04:00","Action":"skip","Package":"github.com/robherley/go-test-example/skipme","Test":"TestSkip","Elapsed":0} 30 | {"Time":"2022-07-10T22:42:11.933575-04:00","Action":"output","Package":"github.com/robherley/go-test-example/skipme","Output":"PASS\n"} 31 | {"Time":"2022-07-10T22:42:11.933587-04:00","Action":"output","Package":"github.com/robherley/go-test-example/skipme","Output":"ok \tgithub.com/robherley/go-test-example/skipme\t(cached)\n"} 32 | {"Time":"2022-07-10T22:42:11.9336-04:00","Action":"pass","Package":"github.com/robherley/go-test-example/skipme","Elapsed":0} 33 | {"Time":"2022-07-10T22:42:12.108255-04:00","Action":"run","Package":"github.com/robherley/go-test-example/boom","Test":"TestFatal"} 34 | {"Time":"2022-07-10T22:42:12.108346-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestFatal","Output":"=== RUN TestFatal\n"} 35 | {"Time":"2022-07-10T22:42:12.108374-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestFatal","Output":" boom_test.go:6: this was a failure\n"} 36 | {"Time":"2022-07-10T22:42:12.108399-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestFatal","Output":"--- FAIL: TestFatal (0.00s)\n"} 37 | {"Time":"2022-07-10T22:42:12.108414-04:00","Action":"fail","Package":"github.com/robherley/go-test-example/boom","Test":"TestFatal","Elapsed":0} 38 | {"Time":"2022-07-10T22:42:12.108429-04:00","Action":"run","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic"} 39 | {"Time":"2022-07-10T22:42:12.108442-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"=== RUN TestPanic\n"} 40 | {"Time":"2022-07-10T22:42:12.108455-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"--- FAIL: TestPanic (0.00s)\n"} 41 | {"Time":"2022-07-10T22:42:12.110899-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"panic: this was a panic [recovered]\n"} 42 | {"Time":"2022-07-10T22:42:12.111001-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"\tpanic: this was a panic\n"} 43 | {"Time":"2022-07-10T22:42:12.111021-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"\n"} 44 | {"Time":"2022-07-10T22:42:12.111052-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"goroutine 35 [running]:\n"} 45 | {"Time":"2022-07-10T22:42:12.111084-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"testing.tRunner.func1.2({0x10fc2a0, 0x1147ec0})\n"} 46 | {"Time":"2022-07-10T22:42:12.111098-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"\t/usr/local/go/src/testing/testing.go:1389 +0x24e\n"} 47 | {"Time":"2022-07-10T22:42:12.111181-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"testing.tRunner.func1()\n"} 48 | {"Time":"2022-07-10T22:42:12.111198-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"\t/usr/local/go/src/testing/testing.go:1392 +0x39f\n"} 49 | {"Time":"2022-07-10T22:42:12.111214-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"panic({0x10fc2a0, 0x1147ec0})\n"} 50 | {"Time":"2022-07-10T22:42:12.111229-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"\t/usr/local/go/src/runtime/panic.go:838 +0x207\n"} 51 | {"Time":"2022-07-10T22:42:12.111277-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"github.com/robherley/go-test-example/boom_test.TestPanic(0x0?)\n"} 52 | {"Time":"2022-07-10T22:42:12.111293-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"\t/Users/robherley/dev/go-test-action/tmp/go-test-example/boom/boom_test.go:10 +0x27\n"} 53 | {"Time":"2022-07-10T22:42:12.111306-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"testing.tRunner(0xc0001196c0, 0x1126f60)\n"} 54 | {"Time":"2022-07-10T22:42:12.111318-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"\t/usr/local/go/src/testing/testing.go:1439 +0x102\n"} 55 | {"Time":"2022-07-10T22:42:12.111331-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"created by testing.(*T).Run\n"} 56 | {"Time":"2022-07-10T22:42:12.111344-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Output":"\t/usr/local/go/src/testing/testing.go:1486 +0x35f\n"} 57 | {"Time":"2022-07-10T22:42:12.111458-04:00","Action":"fail","Package":"github.com/robherley/go-test-example/boom","Test":"TestPanic","Elapsed":0} 58 | {"Time":"2022-07-10T22:42:12.111503-04:00","Action":"output","Package":"github.com/robherley/go-test-example/boom","Output":"FAIL\tgithub.com/robherley/go-test-example/boom\t0.103s\n"} 59 | {"Time":"2022-07-10T22:42:12.111534-04:00","Action":"fail","Package":"github.com/robherley/go-test-example/boom","Elapsed":0.103} 60 | -------------------------------------------------------------------------------- /__tests__/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs/promises' 2 | import * as core from '@actions/core' 3 | import path from 'path' 4 | 5 | export const testFixturesDirectory = path.join(__dirname, 'fixtures') 6 | export const testOutputDirectory = path.join(__dirname, 'output') 7 | export const testModuleDirectory = path.join( 8 | __dirname, 9 | 'output', 10 | 'go-test-example' 11 | ) 12 | export const testArguments = './...' 13 | export const testSummaryFilePath = path.join( 14 | testOutputDirectory, 15 | 'test-summary.md' 16 | ) 17 | export const testGoModContents = ` 18 | module github.com/robherley/go-test-example 19 | 20 | go 1.18 21 | ` 22 | 23 | export const createSummaryFile = async () => { 24 | process.env['GITHUB_STEP_SUMMARY'] = testSummaryFilePath 25 | await fs.writeFile(testSummaryFilePath, '', { encoding: 'utf8' }) 26 | core.summary.emptyBuffer() 27 | } 28 | 29 | export const removeSummaryFile = async () => { 30 | delete process.env['GITHUB_STEP_SUMMARY'] 31 | await fs.unlink(testSummaryFilePath) 32 | core.summary.emptyBuffer() 33 | } 34 | 35 | export const setupActionsInputs = () => { 36 | process.env['INPUT_MODULEDIRECTORY'] = testModuleDirectory 37 | process.env['INPUT_TESTARGUMENTS'] = testArguments 38 | process.env['INPUT_OMIT'] = '' 39 | } 40 | 41 | export const createFakeGoModule = async () => { 42 | await fs.mkdir(testModuleDirectory, { recursive: true }) 43 | await fs.writeFile( 44 | path.join(testModuleDirectory, 'go.mod'), 45 | testGoModContents, 46 | { 47 | encoding: 'utf8', 48 | } 49 | ) 50 | } 51 | 52 | export const mockProcessExit = (): jest.SpyInstance => { 53 | // @ts-ignore:next-line 54 | return jest.spyOn(process, 'exit').mockImplementationOnce(() => {}) 55 | } 56 | 57 | export const getTestStdout = async (): Promise => { 58 | const buf = await fs.readFile( 59 | path.join(testFixturesDirectory, 'gotestoutput.txt') 60 | ) 61 | return buf.toString() 62 | } 63 | 64 | export const mockActionsCoreLogging = (silent = true) => { 65 | type LogFuncs = 'debug' | 'error' | 'warning' | 'notice' | 'info' 66 | const logMethods: LogFuncs[] = ['debug', 'error', 'warning', 'notice', 'info'] 67 | logMethods.forEach(method => { 68 | jest 69 | .spyOn(core, method) 70 | .mockImplementation( 71 | (msg: string | Error, props?: core.AnnotationProperties) => { 72 | if (silent) return 73 | console.log( 74 | `[mock: core.${method}(${props ? JSON.stringify(props) : ''})]:`, 75 | msg 76 | ) 77 | } 78 | ) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /__tests__/inputs.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import { OmitOption, getInputs } from '../src/inputs' 3 | 4 | jest.mock('@actions/core') 5 | 6 | const mockGetInput = core.getInput as jest.MockedFunction 7 | const mockGetBooleanInput = core.getBooleanInput as jest.MockedFunction< 8 | typeof core.getBooleanInput 9 | > 10 | 11 | const mockInput = (name: string, value: string) => { 12 | mockGetInput.mockImplementation((n: string) => (n === name ? value : '')) 13 | } 14 | 15 | describe('renderer', () => { 16 | beforeEach(() => { 17 | jest.resetAllMocks() 18 | }) 19 | 20 | it('uses default values', () => { 21 | mockGetInput.mockReturnValue('') 22 | const inputs = getInputs() 23 | 24 | expect(inputs).toEqual({ 25 | moduleDirectory: '.', 26 | testArguments: ['./...'], 27 | fromJSONFile: null, 28 | omit: new Set(), 29 | }) 30 | }) 31 | 32 | it('parses moduleDirectory', () => { 33 | mockInput('moduleDirectory', 'foo') 34 | const inputs = getInputs() 35 | 36 | expect(inputs.moduleDirectory).toEqual('foo') 37 | }) 38 | 39 | it('parses testArguments', () => { 40 | mockInput('testArguments', 'foo bar') 41 | const inputs = getInputs() 42 | 43 | expect(inputs.testArguments).toEqual(['foo', 'bar']) 44 | }) 45 | 46 | it('parses fromJSONFile', () => { 47 | mockInput('fromJSONFile', 'foo.json') 48 | const inputs = getInputs() 49 | 50 | expect(inputs.fromJSONFile).toEqual('foo.json') 51 | }) 52 | 53 | it('parses omit', () => { 54 | mockInput( 55 | 'omit', 56 | [...Object.values(OmitOption), 'foo', 'bar', 'baz'].join('\n') 57 | ) 58 | const inputs = getInputs() 59 | 60 | expect(inputs.omit).toEqual(new Set(Object.values(OmitOption))) 61 | }) 62 | 63 | it('supports deprecated inputs', () => { 64 | mockGetInput.mockImplementation((name: string) => { 65 | switch (name) { 66 | case 'omitUntestedPackages': 67 | case 'omitSuccessfulPackages': 68 | case 'omitPie': 69 | return 'true' 70 | default: 71 | return '' 72 | } 73 | }) 74 | 75 | mockGetBooleanInput.mockReturnValue(true) 76 | 77 | const inputs = getInputs() 78 | expect(inputs.omit).toEqual( 79 | new Set([OmitOption.Untested, OmitOption.Successful, OmitOption.Pie]) 80 | ) 81 | expect(core.warning).toHaveBeenCalled() 82 | }) 83 | 84 | it('does not make an annotation if the deprecated inputs are not used', () => { 85 | mockGetInput.mockImplementation((name: string) => { 86 | switch (name) { 87 | case 'omitUntestedPackages': 88 | case 'omitSuccessfulPackages': 89 | case 'omitPie': 90 | return 'false' 91 | default: 92 | return '' 93 | } 94 | }) 95 | mockGetBooleanInput.mockReturnValue(false) 96 | 97 | getInputs() 98 | expect(core.warning).not.toHaveBeenCalled() 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /__tests__/renderer.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs/promises' 2 | import * as cheerio from 'cheerio' 3 | 4 | import { 5 | getTestStdout, 6 | mockActionsCoreLogging, 7 | createFakeGoModule, 8 | createSummaryFile, 9 | removeSummaryFile, 10 | testSummaryFilePath, 11 | } from './helpers' 12 | import { parseTestEvents } from '../src/events' 13 | import Renderer from '../src/renderer' 14 | import { SummaryTableCell } from '@actions/core/lib/summary' 15 | import { OmitOption } from '../src/inputs' 16 | 17 | const loadSummaryHTML = async (): Promise => { 18 | const file = await fs.readFile(testSummaryFilePath, { encoding: 'utf8' }) 19 | return cheerio.load(file) 20 | } 21 | 22 | const getRenderer = async (): Promise => { 23 | const stdout = await getTestStdout() 24 | const testEvents = parseTestEvents(stdout) 25 | 26 | return new Renderer( 27 | 'github.com/robherley/go-test-example', 28 | testEvents, 29 | '', // stderr 30 | new Set() 31 | ) 32 | } 33 | 34 | describe('renderer', () => { 35 | beforeAll(async () => { 36 | await createFakeGoModule() 37 | }) 38 | 39 | beforeEach(async () => { 40 | mockActionsCoreLogging() 41 | await createSummaryFile() 42 | }) 43 | 44 | afterEach(async () => { 45 | await removeSummaryFile() 46 | }) 47 | 48 | it('calculates package results and conclusions', async () => { 49 | const renderer = await getRenderer() 50 | 51 | expect(renderer.packageResults).toBeDefined() 52 | expect(renderer.packageResults).toHaveLength(4) 53 | 54 | expect(renderer.totalConclusions).toEqual({ 55 | pass: 4, 56 | fail: 2, 57 | skip: 1, 58 | }) 59 | 60 | const expected = [ 61 | { 62 | packageName: 'github.com/robherley/go-test-example', 63 | conclusions: { pass: 0, fail: 0, skip: 0 }, 64 | overallConclusion: 'skip', 65 | topLevelTestCount: 0, 66 | eventsCount: 0, 67 | }, 68 | { 69 | packageName: 'github.com/robherley/go-test-example/boom', 70 | conclusions: { pass: 0, fail: 2, skip: 0 }, 71 | overallConclusion: 'fail', 72 | topLevelTestCount: 2, 73 | eventsCount: 25, 74 | }, 75 | { 76 | packageName: 'github.com/robherley/go-test-example/skipme', 77 | conclusions: { pass: 0, fail: 0, skip: 1 }, 78 | overallConclusion: 'pass', 79 | topLevelTestCount: 1, 80 | eventsCount: 5, 81 | }, 82 | { 83 | packageName: 'github.com/robherley/go-test-example/success', 84 | conclusions: { pass: 4, fail: 0, skip: 0 }, 85 | overallConclusion: 'pass', 86 | topLevelTestCount: 1, 87 | eventsCount: 19, 88 | }, 89 | ] 90 | 91 | renderer.packageResults.forEach((result, i) => { 92 | expect(result.packageEvent.package).toEqual(expected[i].packageName) 93 | expect(result.conclusions).toEqual(expected[i].conclusions) 94 | expect(result.packageEvent.action).toEqual(expected[i].overallConclusion) 95 | expect(Object.entries(result.tests)).toHaveLength( 96 | expected[i].topLevelTestCount 97 | ) 98 | expect(result.events).toHaveLength(expected[i].eventsCount) 99 | }) 100 | }) 101 | 102 | it('renders nothing if there are no test events', async () => { 103 | const renderer = new Renderer( 104 | 'github.com/robherley/empty-module', 105 | [], 106 | '', 107 | new Set() 108 | ) 109 | await renderer.writeSummary() 110 | 111 | const file = await fs.readFile(testSummaryFilePath, { encoding: 'utf8' }) 112 | expect(file).toEqual('') 113 | }) 114 | 115 | it('renders heading', async () => { 116 | const renderer = await getRenderer() 117 | await renderer.writeSummary() 118 | const $ = await loadSummaryHTML() 119 | 120 | expect($('h2').text()).toEqual('📝 Test results') 121 | }) 122 | 123 | it('renders center div hack', async () => { 124 | const renderer = await getRenderer() 125 | await renderer.writeSummary() 126 | const $ = await loadSummaryHTML() 127 | 128 | expect($('div[align="center"]')).toHaveLength(1) 129 | }) 130 | 131 | it('renders module name', async () => { 132 | const renderer = await getRenderer() 133 | await renderer.writeSummary() 134 | const $ = await loadSummaryHTML() 135 | expect($('h3 code').text()).toEqual('github.com/robherley/go-test-example') 136 | }) 137 | 138 | it('renders fallback when missing module name', async () => { 139 | const renderer = await getRenderer() 140 | renderer.moduleName = null 141 | await renderer.writeSummary() 142 | const $ = await loadSummaryHTML() 143 | 144 | expect($('h3 code').text()).toEqual('go test') 145 | }) 146 | 147 | it('renders correct summary test', async () => { 148 | const renderer = await getRenderer() 149 | renderer.moduleName = null 150 | await renderer.writeSummary() 151 | const $ = await loadSummaryHTML() 152 | 153 | expect($.text()).toContain('7 tests (4 passed, 2 failed, 1 skipped)') 154 | }) 155 | 156 | it('renders pie', async () => { 157 | const renderer = await getRenderer() 158 | await renderer.writeSummary() 159 | const $ = await loadSummaryHTML() 160 | 161 | const pieData = `pie showData 162 | "Passed" : 4 163 | "Failed" : 2 164 | "Skipped" : 1` 165 | expect($.text()).toContain('```mermaid') 166 | expect($.text()).toContain(pieData) 167 | }) 168 | 169 | it('does not render pie when pie in omit', async () => { 170 | const renderer = await getRenderer() 171 | renderer.omit.add(OmitOption.Pie) 172 | await renderer.writeSummary() 173 | const $ = await loadSummaryHTML() 174 | 175 | expect($.text()).not.toContain('```mermaid') 176 | }) 177 | 178 | it('renders table headers', async () => { 179 | const renderer = await getRenderer() 180 | await renderer.writeSummary() 181 | const $ = await loadSummaryHTML() 182 | 183 | for (let header of renderer.headers) { 184 | const headerCell = header as SummaryTableCell 185 | expect($(`th:contains(${headerCell.data})`)).toHaveLength(1) 186 | } 187 | }) 188 | 189 | it('renders correct number of table rows', async () => { 190 | const renderer = await getRenderer() 191 | await renderer.writeSummary() 192 | const $ = await loadSummaryHTML() 193 | 194 | expect($('tr')).toHaveLength(9) 195 | 196 | renderer.packageResults.forEach(result => { 197 | expect($(`td:contains(${result.packageEvent.package})`)) 198 | }) 199 | }) 200 | 201 | it('renders correct number of table rows when untested is in omit', async () => { 202 | const renderer = await getRenderer() 203 | renderer.omit.add(OmitOption.Untested) 204 | await renderer.writeSummary() 205 | const $ = await loadSummaryHTML() 206 | 207 | expect($('tr')).toHaveLength(7) 208 | }) 209 | 210 | it('renders correct number of table rows when successful is in omit', async () => { 211 | const renderer = await getRenderer() 212 | renderer.omit.add(OmitOption.Successful) 213 | await renderer.writeSummary() 214 | const $ = await loadSummaryHTML() 215 | 216 | expect($('tr')).toHaveLength(5) 217 | }) 218 | 219 | it('does not render stderr when empty', async () => { 220 | const renderer = await getRenderer() 221 | renderer.stderr = '' 222 | await renderer.writeSummary() 223 | const $ = await loadSummaryHTML() 224 | 225 | expect($('summary:contains(Standard Error Output)')).toHaveLength(0) 226 | }) 227 | 228 | it('renders stderr when specified', async () => { 229 | const renderer = await getRenderer() 230 | renderer.stderr = 'hello world' 231 | await renderer.writeSummary() 232 | const $ = await loadSummaryHTML() 233 | 234 | expect($('summary:contains(Standard Error Output)')).toHaveLength(1) 235 | expect($('details:contains(hello world)')).toHaveLength(1) 236 | }) 237 | 238 | it('does not render stderr when in omit', async () => { 239 | const renderer = await getRenderer() 240 | renderer.omit.add(OmitOption.Stderr) 241 | renderer.stderr = 'i should not be rendered' 242 | await renderer.writeSummary() 243 | const $ = await loadSummaryHTML() 244 | 245 | expect($('summary:contains(Standard Error Output)')).toHaveLength(0) 246 | }) 247 | 248 | it('renders package test and output list', async () => { 249 | const renderer = await getRenderer() 250 | await renderer.writeSummary() 251 | const $ = await loadSummaryHTML() 252 | 253 | expect($('summary:contains(🧪 Tests)')).toHaveLength(4) 254 | expect($('summary:contains(🖨️ Output)')).toHaveLength(4) 255 | }) 256 | 257 | it('does not render package test list when in omit', async () => { 258 | const renderer = await getRenderer() 259 | renderer.omit.add(OmitOption.PackageTests) 260 | await renderer.writeSummary() 261 | const $ = await loadSummaryHTML() 262 | 263 | expect($('summary:contains(🧪 Tests)')).toHaveLength(0) 264 | }) 265 | 266 | it('does not render package output list when in omit', async () => { 267 | const renderer = await getRenderer() 268 | renderer.omit.add(OmitOption.PackageOutput) 269 | await renderer.writeSummary() 270 | const $ = await loadSummaryHTML() 271 | 272 | expect($('summary:contains(🖨️ Output)')).toHaveLength(0) 273 | }) 274 | 275 | it('scrubs ansi from stderr', async () => { 276 | const renderer = await getRenderer() 277 | const placeholder = 'no-ansi-please' 278 | renderer.stderr = `\u001b[31m${placeholder}\u001b[0m` 279 | await renderer.writeSummary() 280 | const $ = await loadSummaryHTML() 281 | 282 | expect($(`details:contains(${placeholder})`)).toHaveLength(1) 283 | $(`details:contains(${placeholder})`).each((_, el) => { 284 | const text = $(el).text() 285 | if (text.includes(placeholder)) { 286 | expect(text).not.toContain('\u001b') 287 | } 288 | }) 289 | }) 290 | 291 | it('scrubs ansi from test output', async () => { 292 | const renderer = await getRenderer() 293 | const placeholder = 'no-ansi-please' 294 | renderer.packageResults[1].events[0].output = `\u001b[31m${placeholder}\u001b[0m` 295 | await renderer.writeSummary() 296 | const $ = await loadSummaryHTML() 297 | 298 | expect($(`details:contains(${placeholder})`)).toHaveLength(1) 299 | $(`details:contains(${placeholder})`).each((_, el) => { 300 | const text = $(el).text() 301 | if (text.includes(placeholder)) { 302 | expect(text).not.toContain('\u001b') 303 | } 304 | }) 305 | }) 306 | }) 307 | -------------------------------------------------------------------------------- /__tests__/results.test.ts: -------------------------------------------------------------------------------- 1 | import { getTestStdout, mockActionsCoreLogging } from './helpers' 2 | import { TestEvent, parseTestEvents } from '../src/events' 3 | import PackageResult from '../src/results' 4 | 5 | const getPackageLevelEvent = (testEvents: TestEvent[]): TestEvent => { 6 | return testEvents.filter( 7 | event => 8 | event.package === 'github.com/robherley/go-test-example/success' && 9 | event.isPackageLevel && 10 | event.isConclusive 11 | )[0] 12 | } 13 | 14 | describe('results', () => { 15 | beforeEach(() => { 16 | mockActionsCoreLogging() 17 | }) 18 | 19 | it('converts events to results', async () => { 20 | const stdout = await getTestStdout() 21 | const testEvents = parseTestEvents(stdout) 22 | 23 | const packageEvent = getPackageLevelEvent(testEvents) 24 | const packageResult = new PackageResult(packageEvent, testEvents) 25 | 26 | expect(packageResult.testCount()).toEqual(4) 27 | expect(packageResult.hasTests()).toBeTruthy() 28 | expect(packageResult.tests).toEqual({ 29 | TestSuccess: { 30 | conclusion: 'pass', 31 | subtests: { 32 | 'TestSuccess/Subtest(1)': { 33 | conclusion: 'pass', 34 | }, 35 | 'TestSuccess/Subtest(2)': { 36 | conclusion: 'pass', 37 | }, 38 | 'TestSuccess/Subtest(3)': { 39 | conclusion: 'pass', 40 | }, 41 | }, 42 | }, 43 | }) 44 | }) 45 | 46 | it('counts conclusions correctly', async () => { 47 | const stdout = await getTestStdout() 48 | const testEvents = parseTestEvents(stdout) 49 | 50 | const packageEvent = getPackageLevelEvent(testEvents) 51 | const packageResult = new PackageResult(packageEvent, testEvents) 52 | 53 | expect(packageResult.conclusions).toEqual({ 54 | pass: 4, 55 | fail: 0, 56 | skip: 0, 57 | }) 58 | }) 59 | 60 | it('has correct output', async () => { 61 | const stdout = await getTestStdout() 62 | const testEvents = parseTestEvents(stdout) 63 | 64 | const packageEvent = getPackageLevelEvent(testEvents) 65 | const packageResult = new PackageResult(packageEvent, testEvents) 66 | 67 | expect(packageResult.output()).toEqual(`=== RUN TestSuccess 68 | === RUN TestSuccess/Subtest(1) 69 | success_test.go:19: hello from subtest #1 70 | === RUN TestSuccess/Subtest(2) 71 | success_test.go:19: hello from subtest #2 72 | === RUN TestSuccess/Subtest(3) 73 | success_test.go:19: hello from subtest #3 74 | --- PASS: TestSuccess (0.00s) 75 | --- PASS: TestSuccess/Subtest(1) (0.00s) 76 | --- PASS: TestSuccess/Subtest(2) (0.00s) 77 | --- PASS: TestSuccess/Subtest(3) (0.00s) 78 | `) 79 | }) 80 | 81 | it('filters out any mismatched events by package', async () => { 82 | const stdout = await getTestStdout() 83 | const testEvents = parseTestEvents(stdout) 84 | 85 | const packageEvent = getPackageLevelEvent(testEvents) 86 | const packageResult = new PackageResult(packageEvent, testEvents) 87 | 88 | expect(packageResult.events).toHaveLength(19) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /__tests__/runner.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs/promises' 2 | import path from 'path' 3 | import * as actionsExec from '@actions/exec' 4 | 5 | import { 6 | setupActionsInputs, 7 | testOutputDirectory, 8 | testModuleDirectory, 9 | mockProcessExit, 10 | createFakeGoModule, 11 | mockActionsCoreLogging, 12 | } from './helpers' 13 | import Runner from '../src/runner' 14 | import Renderer from '../src/renderer' 15 | 16 | describe('runner', () => { 17 | beforeAll(async () => { 18 | await createFakeGoModule() 19 | }) 20 | 21 | beforeEach(() => { 22 | mockActionsCoreLogging() 23 | setupActionsInputs() 24 | }) 25 | 26 | it('resolves module name from go.mod', async () => { 27 | const runner = new Runner() 28 | const modName = await runner.findModuleName() 29 | 30 | expect(modName).toBe('github.com/robherley/go-test-example') 31 | }) 32 | 33 | it('returns null if missing module or go.mod', async () => { 34 | const emptyModule = path.join(testOutputDirectory, 'empty-mod') 35 | await fs.mkdir(emptyModule, { recursive: true }) 36 | 37 | process.env['INPUT_MODULEDIRECTORY'] = emptyModule 38 | 39 | const runner = new Runner() 40 | const modName = await runner.findModuleName() 41 | 42 | expect(modName).toBeNull() 43 | }) 44 | 45 | it('invokes exec with correct arguments', async () => { 46 | const spyExit = mockProcessExit() 47 | 48 | jest 49 | .spyOn(Renderer.prototype, 'writeSummary') 50 | .mockImplementationOnce(async () => {}) 51 | 52 | const spy = jest 53 | .spyOn(actionsExec, 'exec') 54 | .mockImplementationOnce(async () => 0) 55 | 56 | const runner = new Runner() 57 | await runner.run() 58 | 59 | expect(spy).toHaveBeenCalledWith( 60 | 'go', 61 | ['test', '-json', './...'], 62 | expect.objectContaining({ 63 | cwd: testModuleDirectory, 64 | ignoreReturnCode: true, 65 | }) 66 | ) 67 | 68 | expect(spyExit).toBeCalledWith(0) 69 | }) 70 | 71 | it('exits the process with non-zero exit code on failure', async () => { 72 | const spyExit = mockProcessExit() 73 | 74 | jest 75 | .spyOn(Renderer.prototype, 'writeSummary') 76 | .mockImplementationOnce(async () => {}) 77 | 78 | jest.spyOn(actionsExec, 'exec').mockImplementationOnce(async () => 2) 79 | 80 | const runner = new Runner() 81 | await runner.run() 82 | 83 | expect(spyExit).toBeCalledWith(2) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Go Test Action' 2 | description: 'Run `go test` with rich summary output and annotations.' 3 | branding: 4 | icon: 'check-circle' 5 | color: 'green' 6 | inputs: 7 | moduleDirectory: 8 | description: 'Directory of go module to test' 9 | required: false 10 | default: '.' 11 | testArguments: 12 | description: 'Arguments to `go test`, `-json` will be prepended' 13 | required: false 14 | default: './...' 15 | fromJSONFile: 16 | description: 'Parse the specified JSON file, instead of executing go test' 17 | required: false 18 | omit: 19 | description: 'Whitespace separated list of renderable items to omit. See README.md for details.' 20 | required: false 21 | # deprecated, use `omit` with `untested` 22 | omitUntestedPackages: 23 | description: 'Omit any packages from summary output that do not have tests' 24 | required: false 25 | default: 'false' 26 | # deprecated, use `omit` with `pie` 27 | omitPie: 28 | description: 'Omit the pie chart from the summary output' 29 | required: false 30 | default: 'false' 31 | # deprecated, use `omit` with `successful` 32 | omitSuccessfulPackages: 33 | description: 'Omit any packages from summary output that are successful' 34 | required: false 35 | default: 'false' 36 | runs: 37 | using: 'node20' 38 | main: 'dist/index.js' 39 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={914:function(e,t,r){"use strict";var n=this&&this.__createBinding||(Object.create?function(e,t,r,n){if(n===undefined)n=r;var i=Object.getOwnPropertyDescriptor(t,r);if(!i||("get"in i?!t.__esModule:i.writable||i.configurable)){i={enumerable:true,get:function(){return t[r]}}}Object.defineProperty(e,n,i)}:function(e,t,r,n){if(n===undefined)n=r;e[n]=t[r]});var i=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:true,value:t})}:function(e,t){e["default"]=t});var s=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)if(r!=="default"&&Object.prototype.hasOwnProperty.call(e,r))n(t,e,r);i(t,e);return t};Object.defineProperty(t,"__esModule",{value:true});t.issue=t.issueCommand=void 0;const o=s(r(857));const a=r(302);function issueCommand(e,t,r){const n=new Command(e,t,r);process.stdout.write(n.toString()+o.EOL)}t.issueCommand=issueCommand;function issue(e,t=""){issueCommand(e,{},t)}t.issue=issue;const u="::";class Command{constructor(e,t,r){if(!e){e="missing.command"}this.command=e;this.properties=t;this.message=r}toString(){let e=u+this.command;if(this.properties&&Object.keys(this.properties).length>0){e+=" ";let t=true;for(const r in this.properties){if(this.properties.hasOwnProperty(r)){const n=this.properties[r];if(n){if(t){t=false}else{e+=","}e+=`${r}=${escapeProperty(n)}`}}}}e+=`${u}${escapeData(this.message)}`;return e}}function escapeData(e){return(0,a.toCommandValue)(e).replace(/%/g,"%25").replace(/\r/g,"%0D").replace(/\n/g,"%0A")}function escapeProperty(e){return(0,a.toCommandValue)(e).replace(/%/g,"%25").replace(/\r/g,"%0D").replace(/\n/g,"%0A").replace(/:/g,"%3A").replace(/,/g,"%2C")}},484:function(e,t,r){"use strict";var n=this&&this.__createBinding||(Object.create?function(e,t,r,n){if(n===undefined)n=r;var i=Object.getOwnPropertyDescriptor(t,r);if(!i||("get"in i?!t.__esModule:i.writable||i.configurable)){i={enumerable:true,get:function(){return t[r]}}}Object.defineProperty(e,n,i)}:function(e,t,r,n){if(n===undefined)n=r;e[n]=t[r]});var i=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:true,value:t})}:function(e,t){e["default"]=t});var s=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)if(r!=="default"&&Object.prototype.hasOwnProperty.call(e,r))n(t,e,r);i(t,e);return t};var o=this&&this.__awaiter||function(e,t,r,n){function adopt(e){return e instanceof r?e:new r((function(t){t(e)}))}return new(r||(r=Promise))((function(r,i){function fulfilled(e){try{step(n.next(e))}catch(e){i(e)}}function rejected(e){try{step(n["throw"](e))}catch(e){i(e)}}function step(e){e.done?r(e.value):adopt(e.value).then(fulfilled,rejected)}step((n=n.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:true});t.platform=t.toPlatformPath=t.toWin32Path=t.toPosixPath=t.markdownSummary=t.summary=t.getIDToken=t.getState=t.saveState=t.group=t.endGroup=t.startGroup=t.info=t.notice=t.warning=t.error=t.debug=t.isDebug=t.setFailed=t.setCommandEcho=t.setOutput=t.getBooleanInput=t.getMultilineInput=t.getInput=t.addPath=t.setSecret=t.exportVariable=t.ExitCode=void 0;const a=r(914);const u=r(753);const c=r(302);const l=s(r(857));const d=s(r(928));const f=r(306);var p;(function(e){e[e["Success"]=0]="Success";e[e["Failure"]=1]="Failure"})(p||(t.ExitCode=p={}));function exportVariable(e,t){const r=(0,c.toCommandValue)(t);process.env[e]=r;const n=process.env["GITHUB_ENV"]||"";if(n){return(0,u.issueFileCommand)("ENV",(0,u.prepareKeyValueMessage)(e,t))}(0,a.issueCommand)("set-env",{name:e},r)}t.exportVariable=exportVariable;function setSecret(e){(0,a.issueCommand)("add-mask",{},e)}t.setSecret=setSecret;function addPath(e){const t=process.env["GITHUB_PATH"]||"";if(t){(0,u.issueFileCommand)("PATH",e)}else{(0,a.issueCommand)("add-path",{},e)}process.env["PATH"]=`${e}${d.delimiter}${process.env["PATH"]}`}t.addPath=addPath;function getInput(e,t){const r=process.env[`INPUT_${e.replace(/ /g,"_").toUpperCase()}`]||"";if(t&&t.required&&!r){throw new Error(`Input required and not supplied: ${e}`)}if(t&&t.trimWhitespace===false){return r}return r.trim()}t.getInput=getInput;function getMultilineInput(e,t){const r=getInput(e,t).split("\n").filter((e=>e!==""));if(t&&t.trimWhitespace===false){return r}return r.map((e=>e.trim()))}t.getMultilineInput=getMultilineInput;function getBooleanInput(e,t){const r=["true","True","TRUE"];const n=["false","False","FALSE"];const i=getInput(e,t);if(r.includes(i))return true;if(n.includes(i))return false;throw new TypeError(`Input does not meet YAML 1.2 "Core Schema" specification: ${e}\n`+`Support boolean input list: \`true | True | TRUE | false | False | FALSE\``)}t.getBooleanInput=getBooleanInput;function setOutput(e,t){const r=process.env["GITHUB_OUTPUT"]||"";if(r){return(0,u.issueFileCommand)("OUTPUT",(0,u.prepareKeyValueMessage)(e,t))}process.stdout.write(l.EOL);(0,a.issueCommand)("set-output",{name:e},(0,c.toCommandValue)(t))}t.setOutput=setOutput;function setCommandEcho(e){(0,a.issue)("echo",e?"on":"off")}t.setCommandEcho=setCommandEcho;function setFailed(e){process.exitCode=p.Failure;error(e)}t.setFailed=setFailed;function isDebug(){return process.env["RUNNER_DEBUG"]==="1"}t.isDebug=isDebug;function debug(e){(0,a.issueCommand)("debug",{},e)}t.debug=debug;function error(e,t={}){(0,a.issueCommand)("error",(0,c.toCommandProperties)(t),e instanceof Error?e.toString():e)}t.error=error;function warning(e,t={}){(0,a.issueCommand)("warning",(0,c.toCommandProperties)(t),e instanceof Error?e.toString():e)}t.warning=warning;function notice(e,t={}){(0,a.issueCommand)("notice",(0,c.toCommandProperties)(t),e instanceof Error?e.toString():e)}t.notice=notice;function info(e){process.stdout.write(e+l.EOL)}t.info=info;function startGroup(e){(0,a.issue)("group",e)}t.startGroup=startGroup;function endGroup(){(0,a.issue)("endgroup")}t.endGroup=endGroup;function group(e,t){return o(this,void 0,void 0,(function*(){startGroup(e);let r;try{r=yield t()}finally{endGroup()}return r}))}t.group=group;function saveState(e,t){const r=process.env["GITHUB_STATE"]||"";if(r){return(0,u.issueFileCommand)("STATE",(0,u.prepareKeyValueMessage)(e,t))}(0,a.issueCommand)("save-state",{name:e},(0,c.toCommandValue)(t))}t.saveState=saveState;function getState(e){return process.env[`STATE_${e}`]||""}t.getState=getState;function getIDToken(e){return o(this,void 0,void 0,(function*(){return yield f.OidcClient.getIDToken(e)}))}t.getIDToken=getIDToken;var h=r(847);Object.defineProperty(t,"summary",{enumerable:true,get:function(){return h.summary}});var m=r(847);Object.defineProperty(t,"markdownSummary",{enumerable:true,get:function(){return m.markdownSummary}});var g=r(976);Object.defineProperty(t,"toPosixPath",{enumerable:true,get:function(){return g.toPosixPath}});Object.defineProperty(t,"toWin32Path",{enumerable:true,get:function(){return g.toWin32Path}});Object.defineProperty(t,"toPlatformPath",{enumerable:true,get:function(){return g.toPlatformPath}});t.platform=s(r(968))},753:function(e,t,r){"use strict";var n=this&&this.__createBinding||(Object.create?function(e,t,r,n){if(n===undefined)n=r;var i=Object.getOwnPropertyDescriptor(t,r);if(!i||("get"in i?!t.__esModule:i.writable||i.configurable)){i={enumerable:true,get:function(){return t[r]}}}Object.defineProperty(e,n,i)}:function(e,t,r,n){if(n===undefined)n=r;e[n]=t[r]});var i=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:true,value:t})}:function(e,t){e["default"]=t});var s=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)if(r!=="default"&&Object.prototype.hasOwnProperty.call(e,r))n(t,e,r);i(t,e);return t};Object.defineProperty(t,"__esModule",{value:true});t.prepareKeyValueMessage=t.issueFileCommand=void 0;const o=s(r(982));const a=s(r(896));const u=s(r(857));const c=r(302);function issueFileCommand(e,t){const r=process.env[`GITHUB_${e}`];if(!r){throw new Error(`Unable to find environment variable for file command ${e}`)}if(!a.existsSync(r)){throw new Error(`Missing file at path: ${r}`)}a.appendFileSync(r,`${(0,c.toCommandValue)(t)}${u.EOL}`,{encoding:"utf8"})}t.issueFileCommand=issueFileCommand;function prepareKeyValueMessage(e,t){const r=`ghadelimiter_${o.randomUUID()}`;const n=(0,c.toCommandValue)(t);if(e.includes(r)){throw new Error(`Unexpected input: name should not contain the delimiter "${r}"`)}if(n.includes(r)){throw new Error(`Unexpected input: value should not contain the delimiter "${r}"`)}return`${e}<<${r}${u.EOL}${n}${u.EOL}${r}`}t.prepareKeyValueMessage=prepareKeyValueMessage},306:function(e,t,r){"use strict";var n=this&&this.__awaiter||function(e,t,r,n){function adopt(e){return e instanceof r?e:new r((function(t){t(e)}))}return new(r||(r=Promise))((function(r,i){function fulfilled(e){try{step(n.next(e))}catch(e){i(e)}}function rejected(e){try{step(n["throw"](e))}catch(e){i(e)}}function step(e){e.done?r(e.value):adopt(e.value).then(fulfilled,rejected)}step((n=n.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:true});t.OidcClient=void 0;const i=r(844);const s=r(552);const o=r(484);class OidcClient{static createHttpClient(e=true,t=10){const r={allowRetries:e,maxRetries:t};return new i.HttpClient("actions/oidc-client",[new s.BearerCredentialHandler(OidcClient.getRequestToken())],r)}static getRequestToken(){const e=process.env["ACTIONS_ID_TOKEN_REQUEST_TOKEN"];if(!e){throw new Error("Unable to get ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable")}return e}static getIDTokenUrl(){const e=process.env["ACTIONS_ID_TOKEN_REQUEST_URL"];if(!e){throw new Error("Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable")}return e}static getCall(e){var t;return n(this,void 0,void 0,(function*(){const r=OidcClient.createHttpClient();const n=yield r.getJson(e).catch((e=>{throw new Error(`Failed to get ID Token. \n \n Error Code : ${e.statusCode}\n \n Error Message: ${e.message}`)}));const i=(t=n.result)===null||t===void 0?void 0:t.value;if(!i){throw new Error("Response json body do not have ID Token field")}return i}))}static getIDToken(e){return n(this,void 0,void 0,(function*(){try{let t=OidcClient.getIDTokenUrl();if(e){const r=encodeURIComponent(e);t=`${t}&audience=${r}`}(0,o.debug)(`ID token url is ${t}`);const r=yield OidcClient.getCall(t);(0,o.setSecret)(r);return r}catch(e){throw new Error(`Error message: ${e.message}`)}}))}}t.OidcClient=OidcClient},976:function(e,t,r){"use strict";var n=this&&this.__createBinding||(Object.create?function(e,t,r,n){if(n===undefined)n=r;var i=Object.getOwnPropertyDescriptor(t,r);if(!i||("get"in i?!t.__esModule:i.writable||i.configurable)){i={enumerable:true,get:function(){return t[r]}}}Object.defineProperty(e,n,i)}:function(e,t,r,n){if(n===undefined)n=r;e[n]=t[r]});var i=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:true,value:t})}:function(e,t){e["default"]=t});var s=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)if(r!=="default"&&Object.prototype.hasOwnProperty.call(e,r))n(t,e,r);i(t,e);return t};Object.defineProperty(t,"__esModule",{value:true});t.toPlatformPath=t.toWin32Path=t.toPosixPath=void 0;const o=s(r(928));function toPosixPath(e){return e.replace(/[\\]/g,"/")}t.toPosixPath=toPosixPath;function toWin32Path(e){return e.replace(/[/]/g,"\\")}t.toWin32Path=toWin32Path;function toPlatformPath(e){return e.replace(/[/\\]/g,o.sep)}t.toPlatformPath=toPlatformPath},968:function(e,t,r){"use strict";var n=this&&this.__createBinding||(Object.create?function(e,t,r,n){if(n===undefined)n=r;var i=Object.getOwnPropertyDescriptor(t,r);if(!i||("get"in i?!t.__esModule:i.writable||i.configurable)){i={enumerable:true,get:function(){return t[r]}}}Object.defineProperty(e,n,i)}:function(e,t,r,n){if(n===undefined)n=r;e[n]=t[r]});var i=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:true,value:t})}:function(e,t){e["default"]=t});var s=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)if(r!=="default"&&Object.prototype.hasOwnProperty.call(e,r))n(t,e,r);i(t,e);return t};var o=this&&this.__awaiter||function(e,t,r,n){function adopt(e){return e instanceof r?e:new r((function(t){t(e)}))}return new(r||(r=Promise))((function(r,i){function fulfilled(e){try{step(n.next(e))}catch(e){i(e)}}function rejected(e){try{step(n["throw"](e))}catch(e){i(e)}}function step(e){e.done?r(e.value):adopt(e.value).then(fulfilled,rejected)}step((n=n.apply(e,t||[])).next())}))};var a=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:true});t.getDetails=t.isLinux=t.isMacOS=t.isWindows=t.arch=t.platform=void 0;const u=a(r(857));const c=s(r(236));const getWindowsInfo=()=>o(void 0,void 0,void 0,(function*(){const{stdout:e}=yield c.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Version"',undefined,{silent:true});const{stdout:t}=yield c.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Caption"',undefined,{silent:true});return{name:t.trim(),version:e.trim()}}));const getMacOsInfo=()=>o(void 0,void 0,void 0,(function*(){var e,t,r,n;const{stdout:i}=yield c.getExecOutput("sw_vers",undefined,{silent:true});const s=(t=(e=i.match(/ProductVersion:\s*(.+)/))===null||e===void 0?void 0:e[1])!==null&&t!==void 0?t:"";const o=(n=(r=i.match(/ProductName:\s*(.+)/))===null||r===void 0?void 0:r[1])!==null&&n!==void 0?n:"";return{name:o,version:s}}));const getLinuxInfo=()=>o(void 0,void 0,void 0,(function*(){const{stdout:e}=yield c.getExecOutput("lsb_release",["-i","-r","-s"],{silent:true});const[t,r]=e.trim().split("\n");return{name:t,version:r}}));t.platform=u.default.platform();t.arch=u.default.arch();t.isWindows=t.platform==="win32";t.isMacOS=t.platform==="darwin";t.isLinux=t.platform==="linux";function getDetails(){return o(this,void 0,void 0,(function*(){return Object.assign(Object.assign({},yield t.isWindows?getWindowsInfo():t.isMacOS?getMacOsInfo():getLinuxInfo()),{platform:t.platform,arch:t.arch,isWindows:t.isWindows,isMacOS:t.isMacOS,isLinux:t.isLinux})}))}t.getDetails=getDetails},847:function(e,t,r){"use strict";var n=this&&this.__awaiter||function(e,t,r,n){function adopt(e){return e instanceof r?e:new r((function(t){t(e)}))}return new(r||(r=Promise))((function(r,i){function fulfilled(e){try{step(n.next(e))}catch(e){i(e)}}function rejected(e){try{step(n["throw"](e))}catch(e){i(e)}}function step(e){e.done?r(e.value):adopt(e.value).then(fulfilled,rejected)}step((n=n.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:true});t.summary=t.markdownSummary=t.SUMMARY_DOCS_URL=t.SUMMARY_ENV_VAR=void 0;const i=r(857);const s=r(896);const{access:o,appendFile:a,writeFile:u}=s.promises;t.SUMMARY_ENV_VAR="GITHUB_STEP_SUMMARY";t.SUMMARY_DOCS_URL="https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";class Summary{constructor(){this._buffer=""}filePath(){return n(this,void 0,void 0,(function*(){if(this._filePath){return this._filePath}const e=process.env[t.SUMMARY_ENV_VAR];if(!e){throw new Error(`Unable to find environment variable for $${t.SUMMARY_ENV_VAR}. Check if your runtime environment supports job summaries.`)}try{yield o(e,s.constants.R_OK|s.constants.W_OK)}catch(t){throw new Error(`Unable to access summary file: '${e}'. Check if the file has correct read/write permissions.`)}this._filePath=e;return this._filePath}))}wrap(e,t,r={}){const n=Object.entries(r).map((([e,t])=>` ${e}="${t}"`)).join("");if(!t){return`<${e}${n}>`}return`<${e}${n}>${t}`}write(e){return n(this,void 0,void 0,(function*(){const t=!!(e===null||e===void 0?void 0:e.overwrite);const r=yield this.filePath();const n=t?u:a;yield n(r,this._buffer,{encoding:"utf8"});return this.emptyBuffer()}))}clear(){return n(this,void 0,void 0,(function*(){return this.emptyBuffer().write({overwrite:true})}))}stringify(){return this._buffer}isEmptyBuffer(){return this._buffer.length===0}emptyBuffer(){this._buffer="";return this}addRaw(e,t=false){this._buffer+=e;return t?this.addEOL():this}addEOL(){return this.addRaw(i.EOL)}addCodeBlock(e,t){const r=Object.assign({},t&&{lang:t});const n=this.wrap("pre",this.wrap("code",e),r);return this.addRaw(n).addEOL()}addList(e,t=false){const r=t?"ol":"ul";const n=e.map((e=>this.wrap("li",e))).join("");const i=this.wrap(r,n);return this.addRaw(i).addEOL()}addTable(e){const t=e.map((e=>{const t=e.map((e=>{if(typeof e==="string"){return this.wrap("td",e)}const{header:t,data:r,colspan:n,rowspan:i}=e;const s=t?"th":"td";const o=Object.assign(Object.assign({},n&&{colspan:n}),i&&{rowspan:i});return this.wrap(s,r,o)})).join("");return this.wrap("tr",t)})).join("");const r=this.wrap("table",t);return this.addRaw(r).addEOL()}addDetails(e,t){const r=this.wrap("details",this.wrap("summary",e)+t);return this.addRaw(r).addEOL()}addImage(e,t,r){const{width:n,height:i}=r||{};const s=Object.assign(Object.assign({},n&&{width:n}),i&&{height:i});const o=this.wrap("img",null,Object.assign({src:e,alt:t},s));return this.addRaw(o).addEOL()}addHeading(e,t){const r=`h${t}`;const n=["h1","h2","h3","h4","h5","h6"].includes(r)?r:"h1";const i=this.wrap(n,e);return this.addRaw(i).addEOL()}addSeparator(){const e=this.wrap("hr",null);return this.addRaw(e).addEOL()}addBreak(){const e=this.wrap("br",null);return this.addRaw(e).addEOL()}addQuote(e,t){const r=Object.assign({},t&&{cite:t});const n=this.wrap("blockquote",e,r);return this.addRaw(n).addEOL()}addLink(e,t){const r=this.wrap("a",e,{href:t});return this.addRaw(r).addEOL()}}const c=new Summary;t.markdownSummary=c;t.summary=c},302:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:true});t.toCommandProperties=t.toCommandValue=void 0;function toCommandValue(e){if(e===null||e===undefined){return""}else if(typeof e==="string"||e instanceof String){return e}return JSON.stringify(e)}t.toCommandValue=toCommandValue;function toCommandProperties(e){if(!Object.keys(e).length){return{}}return{title:e.title,file:e.file,line:e.startLine,endLine:e.endLine,col:e.startColumn,endColumn:e.endColumn}}t.toCommandProperties=toCommandProperties},236:function(e,t,r){"use strict";var n=this&&this.__createBinding||(Object.create?function(e,t,r,n){if(n===undefined)n=r;Object.defineProperty(e,n,{enumerable:true,get:function(){return t[r]}})}:function(e,t,r,n){if(n===undefined)n=r;e[n]=t[r]});var i=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:true,value:t})}:function(e,t){e["default"]=t});var s=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)if(r!=="default"&&Object.hasOwnProperty.call(e,r))n(t,e,r);i(t,e);return t};var o=this&&this.__awaiter||function(e,t,r,n){function adopt(e){return e instanceof r?e:new r((function(t){t(e)}))}return new(r||(r=Promise))((function(r,i){function fulfilled(e){try{step(n.next(e))}catch(e){i(e)}}function rejected(e){try{step(n["throw"](e))}catch(e){i(e)}}function step(e){e.done?r(e.value):adopt(e.value).then(fulfilled,rejected)}step((n=n.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:true});t.getExecOutput=t.exec=void 0;const a=r(193);const u=s(r(665));function exec(e,t,r){return o(this,void 0,void 0,(function*(){const n=u.argStringToArray(e);if(n.length===0){throw new Error(`Parameter 'commandLine' cannot be null or empty.`)}const i=n[0];t=n.slice(1).concat(t||[]);const s=new u.ToolRunner(i,t,r);return s.exec()}))}t.exec=exec;function getExecOutput(e,t,r){var n,i;return o(this,void 0,void 0,(function*(){let s="";let o="";const u=new a.StringDecoder("utf8");const c=new a.StringDecoder("utf8");const l=(n=r===null||r===void 0?void 0:r.listeners)===null||n===void 0?void 0:n.stdout;const d=(i=r===null||r===void 0?void 0:r.listeners)===null||i===void 0?void 0:i.stderr;const stdErrListener=e=>{o+=c.write(e);if(d){d(e)}};const stdOutListener=e=>{s+=u.write(e);if(l){l(e)}};const f=Object.assign(Object.assign({},r===null||r===void 0?void 0:r.listeners),{stdout:stdOutListener,stderr:stdErrListener});const p=yield exec(e,t,Object.assign(Object.assign({},r),{listeners:f}));s+=u.end();o+=c.end();return{exitCode:p,stdout:s,stderr:o}}))}t.getExecOutput=getExecOutput},665:function(e,t,r){"use strict";var n=this&&this.__createBinding||(Object.create?function(e,t,r,n){if(n===undefined)n=r;Object.defineProperty(e,n,{enumerable:true,get:function(){return t[r]}})}:function(e,t,r,n){if(n===undefined)n=r;e[n]=t[r]});var i=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:true,value:t})}:function(e,t){e["default"]=t});var s=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)if(r!=="default"&&Object.hasOwnProperty.call(e,r))n(t,e,r);i(t,e);return t};var o=this&&this.__awaiter||function(e,t,r,n){function adopt(e){return e instanceof r?e:new r((function(t){t(e)}))}return new(r||(r=Promise))((function(r,i){function fulfilled(e){try{step(n.next(e))}catch(e){i(e)}}function rejected(e){try{step(n["throw"](e))}catch(e){i(e)}}function step(e){e.done?r(e.value):adopt(e.value).then(fulfilled,rejected)}step((n=n.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:true});t.argStringToArray=t.ToolRunner=void 0;const a=s(r(857));const u=s(r(434));const c=s(r(317));const l=s(r(928));const d=s(r(994));const f=s(r(207));const p=r(557);const h=process.platform==="win32";class ToolRunner extends u.EventEmitter{constructor(e,t,r){super();if(!e){throw new Error("Parameter 'toolPath' cannot be null or empty.")}this.toolPath=e;this.args=t||[];this.options=r||{}}_debug(e){if(this.options.listeners&&this.options.listeners.debug){this.options.listeners.debug(e)}}_getCommandString(e,t){const r=this._getSpawnFileName();const n=this._getSpawnArgs(e);let i=t?"":"[command]";if(h){if(this._isCmdFile()){i+=r;for(const e of n){i+=` ${e}`}}else if(e.windowsVerbatimArguments){i+=`"${r}"`;for(const e of n){i+=` ${e}`}}else{i+=this._windowsQuoteCmdArg(r);for(const e of n){i+=` ${this._windowsQuoteCmdArg(e)}`}}}else{i+=r;for(const e of n){i+=` ${e}`}}return i}_processLineBuffer(e,t,r){try{let n=t+e.toString();let i=n.indexOf(a.EOL);while(i>-1){const e=n.substring(0,i);r(e);n=n.substring(i+a.EOL.length);i=n.indexOf(a.EOL)}return n}catch(e){this._debug(`error processing line. Failed with error ${e}`);return""}}_getSpawnFileName(){if(h){if(this._isCmdFile()){return process.env["COMSPEC"]||"cmd.exe"}}return this.toolPath}_getSpawnArgs(e){if(h){if(this._isCmdFile()){let t=`/D /S /C "${this._windowsQuoteCmdArg(this.toolPath)}`;for(const r of this.args){t+=" ";t+=e.windowsVerbatimArguments?r:this._windowsQuoteCmdArg(r)}t+='"';return[t]}}return this.args}_endsWith(e,t){return e.endsWith(t)}_isCmdFile(){const e=this.toolPath.toUpperCase();return this._endsWith(e,".CMD")||this._endsWith(e,".BAT")}_windowsQuoteCmdArg(e){if(!this._isCmdFile()){return this._uvQuoteCmdArg(e)}if(!e){return'""'}const t=[" ","\t","&","(",")","[","]","{","}","^","=",";","!","'","+",",","`","~","|","<",">",'"'];let r=false;for(const n of e){if(t.some((e=>e===n))){r=true;break}}if(!r){return e}let n='"';let i=true;for(let t=e.length;t>0;t--){n+=e[t-1];if(i&&e[t-1]==="\\"){n+="\\"}else if(e[t-1]==='"'){i=true;n+='"'}else{i=false}}n+='"';return n.split("").reverse().join("")}_uvQuoteCmdArg(e){if(!e){return'""'}if(!e.includes(" ")&&!e.includes("\t")&&!e.includes('"')){return e}if(!e.includes('"')&&!e.includes("\\")){return`"${e}"`}let t='"';let r=true;for(let n=e.length;n>0;n--){t+=e[n-1];if(r&&e[n-1]==="\\"){t+="\\"}else if(e[n-1]==='"'){r=true;t+="\\"}else{r=false}}t+='"';return t.split("").reverse().join("")}_cloneExecOptions(e){e=e||{};const t={cwd:e.cwd||process.cwd(),env:e.env||process.env,silent:e.silent||false,windowsVerbatimArguments:e.windowsVerbatimArguments||false,failOnStdErr:e.failOnStdErr||false,ignoreReturnCode:e.ignoreReturnCode||false,delay:e.delay||1e4};t.outStream=e.outStream||process.stdout;t.errStream=e.errStream||process.stderr;return t}_getSpawnOptions(e,t){e=e||{};const r={};r.cwd=e.cwd;r.env=e.env;r["windowsVerbatimArguments"]=e.windowsVerbatimArguments||this._isCmdFile();if(e.windowsVerbatimArguments){r.argv0=`"${t}"`}return r}exec(){return o(this,void 0,void 0,(function*(){if(!f.isRooted(this.toolPath)&&(this.toolPath.includes("/")||h&&this.toolPath.includes("\\"))){this.toolPath=l.resolve(process.cwd(),this.options.cwd||process.cwd(),this.toolPath)}this.toolPath=yield d.which(this.toolPath,true);return new Promise(((e,t)=>o(this,void 0,void 0,(function*(){this._debug(`exec tool: ${this.toolPath}`);this._debug("arguments:");for(const e of this.args){this._debug(` ${e}`)}const r=this._cloneExecOptions(this.options);if(!r.silent&&r.outStream){r.outStream.write(this._getCommandString(r)+a.EOL)}const n=new ExecState(r,this.toolPath);n.on("debug",(e=>{this._debug(e)}));if(this.options.cwd&&!(yield f.exists(this.options.cwd))){return t(new Error(`The cwd: ${this.options.cwd} does not exist!`))}const i=this._getSpawnFileName();const s=c.spawn(i,this._getSpawnArgs(r),this._getSpawnOptions(this.options,i));let o="";if(s.stdout){s.stdout.on("data",(e=>{if(this.options.listeners&&this.options.listeners.stdout){this.options.listeners.stdout(e)}if(!r.silent&&r.outStream){r.outStream.write(e)}o=this._processLineBuffer(e,o,(e=>{if(this.options.listeners&&this.options.listeners.stdline){this.options.listeners.stdline(e)}}))}))}let u="";if(s.stderr){s.stderr.on("data",(e=>{n.processStderr=true;if(this.options.listeners&&this.options.listeners.stderr){this.options.listeners.stderr(e)}if(!r.silent&&r.errStream&&r.outStream){const t=r.failOnStdErr?r.errStream:r.outStream;t.write(e)}u=this._processLineBuffer(e,u,(e=>{if(this.options.listeners&&this.options.listeners.errline){this.options.listeners.errline(e)}}))}))}s.on("error",(e=>{n.processError=e.message;n.processExited=true;n.processClosed=true;n.CheckComplete()}));s.on("exit",(e=>{n.processExitCode=e;n.processExited=true;this._debug(`Exit code ${e} received from tool '${this.toolPath}'`);n.CheckComplete()}));s.on("close",(e=>{n.processExitCode=e;n.processExited=true;n.processClosed=true;this._debug(`STDIO streams have closed for tool '${this.toolPath}'`);n.CheckComplete()}));n.on("done",((r,n)=>{if(o.length>0){this.emit("stdline",o)}if(u.length>0){this.emit("errline",u)}s.removeAllListeners();if(r){t(r)}else{e(n)}}));if(this.options.input){if(!s.stdin){throw new Error("child process missing stdin")}s.stdin.end(this.options.input)}}))))}))}}t.ToolRunner=ToolRunner;function argStringToArray(e){const t=[];let r=false;let n=false;let i="";function append(e){if(n&&e!=='"'){i+="\\"}i+=e;n=false}for(let s=0;s0){t.push(i);i=""}continue}append(o)}if(i.length>0){t.push(i.trim())}return t}t.argStringToArray=argStringToArray;class ExecState extends u.EventEmitter{constructor(e,t){super();this.processClosed=false;this.processError="";this.processExitCode=0;this.processExited=false;this.processStderr=false;this.delay=1e4;this.done=false;this.timeout=null;if(!t){throw new Error("toolPath must not be empty")}this.options=e;this.toolPath=t;if(e.delay){this.delay=e.delay}}CheckComplete(){if(this.done){return}if(this.processClosed){this._setResult()}else if(this.processExited){this.timeout=p.setTimeout(ExecState.HandleTimeout,this.delay,this)}}_debug(e){this.emit("debug",e)}_setResult(){let e;if(this.processExited){if(this.processError){e=new Error(`There was an error when attempting to execute the process '${this.toolPath}'. This may indicate the process failed to start. Error: ${this.processError}`)}else if(this.processExitCode!==0&&!this.options.ignoreReturnCode){e=new Error(`The process '${this.toolPath}' failed with exit code ${this.processExitCode}`)}else if(this.processStderr&&this.options.failOnStdErr){e=new Error(`The process '${this.toolPath}' failed because one or more lines were written to the STDERR stream`)}}if(this.timeout){clearTimeout(this.timeout);this.timeout=null}this.done=true;this.emit("done",e,this.processExitCode)}static HandleTimeout(e){if(e.done){return}if(!e.processClosed&&e.processExited){const t=`The STDIO streams did not close within ${e.delay/1e3} seconds of the exit event from process '${e.toolPath}'. This may indicate a child process inherited the STDIO streams and has not yet exited.`;e._debug(t)}e._setResult()}}},552:function(e,t){"use strict";var r=this&&this.__awaiter||function(e,t,r,n){function adopt(e){return e instanceof r?e:new r((function(t){t(e)}))}return new(r||(r=Promise))((function(r,i){function fulfilled(e){try{step(n.next(e))}catch(e){i(e)}}function rejected(e){try{step(n["throw"](e))}catch(e){i(e)}}function step(e){e.done?r(e.value):adopt(e.value).then(fulfilled,rejected)}step((n=n.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:true});t.PersonalAccessTokenCredentialHandler=t.BearerCredentialHandler=t.BasicCredentialHandler=void 0;class BasicCredentialHandler{constructor(e,t){this.username=e;this.password=t}prepareRequest(e){if(!e.headers){throw Error("The request has no headers")}e.headers["Authorization"]=`Basic ${Buffer.from(`${this.username}:${this.password}`).toString("base64")}`}canHandleAuthentication(){return false}handleAuthentication(){return r(this,void 0,void 0,(function*(){throw new Error("not implemented")}))}}t.BasicCredentialHandler=BasicCredentialHandler;class BearerCredentialHandler{constructor(e){this.token=e}prepareRequest(e){if(!e.headers){throw Error("The request has no headers")}e.headers["Authorization"]=`Bearer ${this.token}`}canHandleAuthentication(){return false}handleAuthentication(){return r(this,void 0,void 0,(function*(){throw new Error("not implemented")}))}}t.BearerCredentialHandler=BearerCredentialHandler;class PersonalAccessTokenCredentialHandler{constructor(e){this.token=e}prepareRequest(e){if(!e.headers){throw Error("The request has no headers")}e.headers["Authorization"]=`Basic ${Buffer.from(`PAT:${this.token}`).toString("base64")}`}canHandleAuthentication(){return false}handleAuthentication(){return r(this,void 0,void 0,(function*(){throw new Error("not implemented")}))}}t.PersonalAccessTokenCredentialHandler=PersonalAccessTokenCredentialHandler},844:function(e,t,r){"use strict";var n=this&&this.__createBinding||(Object.create?function(e,t,r,n){if(n===undefined)n=r;Object.defineProperty(e,n,{enumerable:true,get:function(){return t[r]}})}:function(e,t,r,n){if(n===undefined)n=r;e[n]=t[r]});var i=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:true,value:t})}:function(e,t){e["default"]=t});var s=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)if(r!=="default"&&Object.hasOwnProperty.call(e,r))n(t,e,r);i(t,e);return t};var o=this&&this.__awaiter||function(e,t,r,n){function adopt(e){return e instanceof r?e:new r((function(t){t(e)}))}return new(r||(r=Promise))((function(r,i){function fulfilled(e){try{step(n.next(e))}catch(e){i(e)}}function rejected(e){try{step(n["throw"](e))}catch(e){i(e)}}function step(e){e.done?r(e.value):adopt(e.value).then(fulfilled,rejected)}step((n=n.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:true});t.HttpClient=t.isHttps=t.HttpClientResponse=t.HttpClientError=t.getProxyUrl=t.MediaTypes=t.Headers=t.HttpCodes=void 0;const a=s(r(611));const u=s(r(692));const c=s(r(988));const l=s(r(770));var d;(function(e){e[e["OK"]=200]="OK";e[e["MultipleChoices"]=300]="MultipleChoices";e[e["MovedPermanently"]=301]="MovedPermanently";e[e["ResourceMoved"]=302]="ResourceMoved";e[e["SeeOther"]=303]="SeeOther";e[e["NotModified"]=304]="NotModified";e[e["UseProxy"]=305]="UseProxy";e[e["SwitchProxy"]=306]="SwitchProxy";e[e["TemporaryRedirect"]=307]="TemporaryRedirect";e[e["PermanentRedirect"]=308]="PermanentRedirect";e[e["BadRequest"]=400]="BadRequest";e[e["Unauthorized"]=401]="Unauthorized";e[e["PaymentRequired"]=402]="PaymentRequired";e[e["Forbidden"]=403]="Forbidden";e[e["NotFound"]=404]="NotFound";e[e["MethodNotAllowed"]=405]="MethodNotAllowed";e[e["NotAcceptable"]=406]="NotAcceptable";e[e["ProxyAuthenticationRequired"]=407]="ProxyAuthenticationRequired";e[e["RequestTimeout"]=408]="RequestTimeout";e[e["Conflict"]=409]="Conflict";e[e["Gone"]=410]="Gone";e[e["TooManyRequests"]=429]="TooManyRequests";e[e["InternalServerError"]=500]="InternalServerError";e[e["NotImplemented"]=501]="NotImplemented";e[e["BadGateway"]=502]="BadGateway";e[e["ServiceUnavailable"]=503]="ServiceUnavailable";e[e["GatewayTimeout"]=504]="GatewayTimeout"})(d=t.HttpCodes||(t.HttpCodes={}));var f;(function(e){e["Accept"]="accept";e["ContentType"]="content-type"})(f=t.Headers||(t.Headers={}));var p;(function(e){e["ApplicationJson"]="application/json"})(p=t.MediaTypes||(t.MediaTypes={}));function getProxyUrl(e){const t=c.getProxyUrl(new URL(e));return t?t.href:""}t.getProxyUrl=getProxyUrl;const h=[d.MovedPermanently,d.ResourceMoved,d.SeeOther,d.TemporaryRedirect,d.PermanentRedirect];const m=[d.BadGateway,d.ServiceUnavailable,d.GatewayTimeout];const g=["OPTIONS","GET","DELETE","HEAD"];const v=10;const y=5;class HttpClientError extends Error{constructor(e,t){super(e);this.name="HttpClientError";this.statusCode=t;Object.setPrototypeOf(this,HttpClientError.prototype)}}t.HttpClientError=HttpClientError;class HttpClientResponse{constructor(e){this.message=e}readBody(){return o(this,void 0,void 0,(function*(){return new Promise((e=>o(this,void 0,void 0,(function*(){let t=Buffer.alloc(0);this.message.on("data",(e=>{t=Buffer.concat([t,e])}));this.message.on("end",(()=>{e(t.toString())}))}))))}))}}t.HttpClientResponse=HttpClientResponse;function isHttps(e){const t=new URL(e);return t.protocol==="https:"}t.isHttps=isHttps;class HttpClient{constructor(e,t,r){this._ignoreSslError=false;this._allowRedirects=true;this._allowRedirectDowngrade=false;this._maxRedirects=50;this._allowRetries=false;this._maxRetries=1;this._keepAlive=false;this._disposed=false;this.userAgent=e;this.handlers=t||[];this.requestOptions=r;if(r){if(r.ignoreSslError!=null){this._ignoreSslError=r.ignoreSslError}this._socketTimeout=r.socketTimeout;if(r.allowRedirects!=null){this._allowRedirects=r.allowRedirects}if(r.allowRedirectDowngrade!=null){this._allowRedirectDowngrade=r.allowRedirectDowngrade}if(r.maxRedirects!=null){this._maxRedirects=Math.max(r.maxRedirects,0)}if(r.keepAlive!=null){this._keepAlive=r.keepAlive}if(r.allowRetries!=null){this._allowRetries=r.allowRetries}if(r.maxRetries!=null){this._maxRetries=r.maxRetries}}}options(e,t){return o(this,void 0,void 0,(function*(){return this.request("OPTIONS",e,null,t||{})}))}get(e,t){return o(this,void 0,void 0,(function*(){return this.request("GET",e,null,t||{})}))}del(e,t){return o(this,void 0,void 0,(function*(){return this.request("DELETE",e,null,t||{})}))}post(e,t,r){return o(this,void 0,void 0,(function*(){return this.request("POST",e,t,r||{})}))}patch(e,t,r){return o(this,void 0,void 0,(function*(){return this.request("PATCH",e,t,r||{})}))}put(e,t,r){return o(this,void 0,void 0,(function*(){return this.request("PUT",e,t,r||{})}))}head(e,t){return o(this,void 0,void 0,(function*(){return this.request("HEAD",e,null,t||{})}))}sendStream(e,t,r,n){return o(this,void 0,void 0,(function*(){return this.request(e,t,r,n)}))}getJson(e,t={}){return o(this,void 0,void 0,(function*(){t[f.Accept]=this._getExistingOrDefaultHeader(t,f.Accept,p.ApplicationJson);const r=yield this.get(e,t);return this._processResponse(r,this.requestOptions)}))}postJson(e,t,r={}){return o(this,void 0,void 0,(function*(){const n=JSON.stringify(t,null,2);r[f.Accept]=this._getExistingOrDefaultHeader(r,f.Accept,p.ApplicationJson);r[f.ContentType]=this._getExistingOrDefaultHeader(r,f.ContentType,p.ApplicationJson);const i=yield this.post(e,n,r);return this._processResponse(i,this.requestOptions)}))}putJson(e,t,r={}){return o(this,void 0,void 0,(function*(){const n=JSON.stringify(t,null,2);r[f.Accept]=this._getExistingOrDefaultHeader(r,f.Accept,p.ApplicationJson);r[f.ContentType]=this._getExistingOrDefaultHeader(r,f.ContentType,p.ApplicationJson);const i=yield this.put(e,n,r);return this._processResponse(i,this.requestOptions)}))}patchJson(e,t,r={}){return o(this,void 0,void 0,(function*(){const n=JSON.stringify(t,null,2);r[f.Accept]=this._getExistingOrDefaultHeader(r,f.Accept,p.ApplicationJson);r[f.ContentType]=this._getExistingOrDefaultHeader(r,f.ContentType,p.ApplicationJson);const i=yield this.patch(e,n,r);return this._processResponse(i,this.requestOptions)}))}request(e,t,r,n){return o(this,void 0,void 0,(function*(){if(this._disposed){throw new Error("Client has already been disposed.")}const i=new URL(t);let s=this._prepareRequest(e,i,n);const o=this._allowRetries&&g.includes(e)?this._maxRetries+1:1;let a=0;let u;do{u=yield this.requestRaw(s,r);if(u&&u.message&&u.message.statusCode===d.Unauthorized){let e;for(const t of this.handlers){if(t.canHandleAuthentication(u)){e=t;break}}if(e){return e.handleAuthentication(this,s,r)}else{return u}}let t=this._maxRedirects;while(u.message.statusCode&&h.includes(u.message.statusCode)&&this._allowRedirects&&t>0){const o=u.message.headers["location"];if(!o){break}const a=new URL(o);if(i.protocol==="https:"&&i.protocol!==a.protocol&&!this._allowRedirectDowngrade){throw new Error("Redirect from HTTPS to HTTP protocol. This downgrade is not allowed for security reasons. If you want to allow this behavior, set the allowRedirectDowngrade option to true.")}yield u.readBody();if(a.hostname!==i.hostname){for(const e in n){if(e.toLowerCase()==="authorization"){delete n[e]}}}s=this._prepareRequest(e,a,n);u=yield this.requestRaw(s,r);t--}if(!u.message.statusCode||!m.includes(u.message.statusCode)){return u}a+=1;if(a{function callbackForResult(e,t){if(e){n(e)}else if(!t){n(new Error("Unknown error"))}else{r(t)}}this.requestRawWithCallback(e,t,callbackForResult)}))}))}requestRawWithCallback(e,t,r){if(typeof t==="string"){if(!e.options.headers){e.options.headers={}}e.options.headers["Content-Length"]=Buffer.byteLength(t,"utf8")}let n=false;function handleResult(e,t){if(!n){n=true;r(e,t)}}const i=e.httpModule.request(e.options,(e=>{const t=new HttpClientResponse(e);handleResult(undefined,t)}));let s;i.on("socket",(e=>{s=e}));i.setTimeout(this._socketTimeout||3*6e4,(()=>{if(s){s.end()}handleResult(new Error(`Request timeout: ${e.options.path}`))}));i.on("error",(function(e){handleResult(e)}));if(t&&typeof t==="string"){i.write(t,"utf8")}if(t&&typeof t!=="string"){t.on("close",(function(){i.end()}));t.pipe(i)}else{i.end()}}getAgent(e){const t=new URL(e);return this._getAgent(t)}_prepareRequest(e,t,r){const n={};n.parsedUrl=t;const i=n.parsedUrl.protocol==="https:";n.httpModule=i?u:a;const s=i?443:80;n.options={};n.options.host=n.parsedUrl.hostname;n.options.port=n.parsedUrl.port?parseInt(n.parsedUrl.port):s;n.options.path=(n.parsedUrl.pathname||"")+(n.parsedUrl.search||"");n.options.method=e;n.options.headers=this._mergeHeaders(r);if(this.userAgent!=null){n.options.headers["user-agent"]=this.userAgent}n.options.agent=this._getAgent(n.parsedUrl);if(this.handlers){for(const e of this.handlers){e.prepareRequest(n.options)}}return n}_mergeHeaders(e){if(this.requestOptions&&this.requestOptions.headers){return Object.assign({},lowercaseKeys(this.requestOptions.headers),lowercaseKeys(e||{}))}return lowercaseKeys(e||{})}_getExistingOrDefaultHeader(e,t,r){let n;if(this.requestOptions&&this.requestOptions.headers){n=lowercaseKeys(this.requestOptions.headers)[t]}return e[t]||n||r}_getAgent(e){let t;const r=c.getProxyUrl(e);const n=r&&r.hostname;if(this._keepAlive&&n){t=this._proxyAgent}if(this._keepAlive&&!n){t=this._agent}if(t){return t}const i=e.protocol==="https:";let s=100;if(this.requestOptions){s=this.requestOptions.maxSockets||a.globalAgent.maxSockets}if(r&&r.hostname){const e={maxSockets:s,keepAlive:this._keepAlive,proxy:Object.assign(Object.assign({},(r.username||r.password)&&{proxyAuth:`${r.username}:${r.password}`}),{host:r.hostname,port:r.port})};let n;const o=r.protocol==="https:";if(i){n=o?l.httpsOverHttps:l.httpsOverHttp}else{n=o?l.httpOverHttps:l.httpOverHttp}t=n(e);this._proxyAgent=t}if(this._keepAlive&&!t){const e={keepAlive:this._keepAlive,maxSockets:s};t=i?new u.Agent(e):new a.Agent(e);this._agent=t}if(!t){t=i?u.globalAgent:a.globalAgent}if(i&&this._ignoreSslError){t.options=Object.assign(t.options||{},{rejectUnauthorized:false})}return t}_performExponentialBackoff(e){return o(this,void 0,void 0,(function*(){e=Math.min(v,e);const t=y*Math.pow(2,e);return new Promise((e=>setTimeout((()=>e()),t)))}))}_processResponse(e,t){return o(this,void 0,void 0,(function*(){return new Promise(((r,n)=>o(this,void 0,void 0,(function*(){const i=e.message.statusCode||0;const s={statusCode:i,result:null,headers:{}};if(i===d.NotFound){r(s)}function dateTimeDeserializer(e,t){if(typeof t==="string"){const e=new Date(t);if(!isNaN(e.valueOf())){return e}}return t}let o;let a;try{a=yield e.readBody();if(a&&a.length>0){if(t&&t.deserializeDates){o=JSON.parse(a,dateTimeDeserializer)}else{o=JSON.parse(a)}s.result=o}s.headers=e.message.headers}catch(e){}if(i>299){let e;if(o&&o.message){e=o.message}else if(a&&a.length>0){e=a}else{e=`Failed request: (${i})`}const t=new HttpClientError(e,i);t.result=s.result;n(t)}else{r(s)}}))))}))}}t.HttpClient=HttpClient;const lowercaseKeys=e=>Object.keys(e).reduce(((t,r)=>(t[r.toLowerCase()]=e[r],t)),{})},988:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:true});t.checkBypass=t.getProxyUrl=void 0;function getProxyUrl(e){const t=e.protocol==="https:";if(checkBypass(e)){return undefined}const r=(()=>{if(t){return process.env["https_proxy"]||process.env["HTTPS_PROXY"]}else{return process.env["http_proxy"]||process.env["HTTP_PROXY"]}})();if(r){return new URL(r)}else{return undefined}}t.getProxyUrl=getProxyUrl;function checkBypass(e){if(!e.hostname){return false}const t=process.env["no_proxy"]||process.env["NO_PROXY"]||"";if(!t){return false}let r;if(e.port){r=Number(e.port)}else if(e.protocol==="http:"){r=80}else if(e.protocol==="https:"){r=443}const n=[e.hostname.toUpperCase()];if(typeof r==="number"){n.push(`${n[0]}:${r}`)}for(const e of t.split(",").map((e=>e.trim().toUpperCase())).filter((e=>e))){if(n.some((t=>t===e))){return true}}return false}t.checkBypass=checkBypass},207:function(e,t,r){"use strict";var n=this&&this.__createBinding||(Object.create?function(e,t,r,n){if(n===undefined)n=r;Object.defineProperty(e,n,{enumerable:true,get:function(){return t[r]}})}:function(e,t,r,n){if(n===undefined)n=r;e[n]=t[r]});var i=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:true,value:t})}:function(e,t){e["default"]=t});var s=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)if(r!=="default"&&Object.hasOwnProperty.call(e,r))n(t,e,r);i(t,e);return t};var o=this&&this.__awaiter||function(e,t,r,n){function adopt(e){return e instanceof r?e:new r((function(t){t(e)}))}return new(r||(r=Promise))((function(r,i){function fulfilled(e){try{step(n.next(e))}catch(e){i(e)}}function rejected(e){try{step(n["throw"](e))}catch(e){i(e)}}function step(e){e.done?r(e.value):adopt(e.value).then(fulfilled,rejected)}step((n=n.apply(e,t||[])).next())}))};var a;Object.defineProperty(t,"__esModule",{value:true});t.getCmdPath=t.tryGetExecutablePath=t.isRooted=t.isDirectory=t.exists=t.IS_WINDOWS=t.unlink=t.symlink=t.stat=t.rmdir=t.rename=t.readlink=t.readdir=t.mkdir=t.lstat=t.copyFile=t.chmod=void 0;const u=s(r(896));const c=s(r(928));a=u.promises,t.chmod=a.chmod,t.copyFile=a.copyFile,t.lstat=a.lstat,t.mkdir=a.mkdir,t.readdir=a.readdir,t.readlink=a.readlink,t.rename=a.rename,t.rmdir=a.rmdir,t.stat=a.stat,t.symlink=a.symlink,t.unlink=a.unlink;t.IS_WINDOWS=process.platform==="win32";function exists(e){return o(this,void 0,void 0,(function*(){try{yield t.stat(e)}catch(e){if(e.code==="ENOENT"){return false}throw e}return true}))}t.exists=exists;function isDirectory(e,r=false){return o(this,void 0,void 0,(function*(){const n=r?yield t.stat(e):yield t.lstat(e);return n.isDirectory()}))}t.isDirectory=isDirectory;function isRooted(e){e=normalizeSeparators(e);if(!e){throw new Error('isRooted() parameter "p" cannot be empty')}if(t.IS_WINDOWS){return e.startsWith("\\")||/^[A-Z]:/i.test(e)}return e.startsWith("/")}t.isRooted=isRooted;function tryGetExecutablePath(e,r){return o(this,void 0,void 0,(function*(){let n=undefined;try{n=yield t.stat(e)}catch(t){if(t.code!=="ENOENT"){console.log(`Unexpected error attempting to determine if executable file exists '${e}': ${t}`)}}if(n&&n.isFile()){if(t.IS_WINDOWS){const t=c.extname(e).toUpperCase();if(r.some((e=>e.toUpperCase()===t))){return e}}else{if(isUnixExecutable(n)){return e}}}const i=e;for(const s of r){e=i+s;n=undefined;try{n=yield t.stat(e)}catch(t){if(t.code!=="ENOENT"){console.log(`Unexpected error attempting to determine if executable file exists '${e}': ${t}`)}}if(n&&n.isFile()){if(t.IS_WINDOWS){try{const r=c.dirname(e);const n=c.basename(e).toUpperCase();for(const i of yield t.readdir(r)){if(n===i.toUpperCase()){e=c.join(r,i);break}}}catch(t){console.log(`Unexpected error attempting to determine the actual case of the file '${e}': ${t}`)}return e}else{if(isUnixExecutable(n)){return e}}}}return""}))}t.tryGetExecutablePath=tryGetExecutablePath;function normalizeSeparators(e){e=e||"";if(t.IS_WINDOWS){e=e.replace(/\//g,"\\");return e.replace(/\\\\+/g,"\\")}return e.replace(/\/\/+/g,"/")}function isUnixExecutable(e){return(e.mode&1)>0||(e.mode&8)>0&&e.gid===process.getgid()||(e.mode&64)>0&&e.uid===process.getuid()}function getCmdPath(){var e;return(e=process.env["COMSPEC"])!==null&&e!==void 0?e:`cmd.exe`}t.getCmdPath=getCmdPath},994:function(e,t,r){"use strict";var n=this&&this.__createBinding||(Object.create?function(e,t,r,n){if(n===undefined)n=r;Object.defineProperty(e,n,{enumerable:true,get:function(){return t[r]}})}:function(e,t,r,n){if(n===undefined)n=r;e[n]=t[r]});var i=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:true,value:t})}:function(e,t){e["default"]=t});var s=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)if(r!=="default"&&Object.hasOwnProperty.call(e,r))n(t,e,r);i(t,e);return t};var o=this&&this.__awaiter||function(e,t,r,n){function adopt(e){return e instanceof r?e:new r((function(t){t(e)}))}return new(r||(r=Promise))((function(r,i){function fulfilled(e){try{step(n.next(e))}catch(e){i(e)}}function rejected(e){try{step(n["throw"](e))}catch(e){i(e)}}function step(e){e.done?r(e.value):adopt(e.value).then(fulfilled,rejected)}step((n=n.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:true});t.findInPath=t.which=t.mkdirP=t.rmRF=t.mv=t.cp=void 0;const a=r(613);const u=s(r(317));const c=s(r(928));const l=r(23);const d=s(r(207));const f=l.promisify(u.exec);const p=l.promisify(u.execFile);function cp(e,t,r={}){return o(this,void 0,void 0,(function*(){const{force:n,recursive:i,copySourceDirectory:s}=readCopyOptions(r);const o=(yield d.exists(t))?yield d.stat(t):null;if(o&&o.isFile()&&!n){return}const a=o&&o.isDirectory()&&s?c.join(t,c.basename(e)):t;if(!(yield d.exists(e))){throw new Error(`no such file or directory: ${e}`)}const u=yield d.stat(e);if(u.isDirectory()){if(!i){throw new Error(`Failed to copy. ${e} is a directory, but tried to copy without recursive flag.`)}else{yield cpDirRecursive(e,a,0,n)}}else{if(c.relative(e,a)===""){throw new Error(`'${a}' and '${e}' are the same file`)}yield copyFile(e,a,n)}}))}t.cp=cp;function mv(e,t,r={}){return o(this,void 0,void 0,(function*(){if(yield d.exists(t)){let n=true;if(yield d.isDirectory(t)){t=c.join(t,c.basename(e));n=yield d.exists(t)}if(n){if(r.force==null||r.force){yield rmRF(t)}else{throw new Error("Destination already exists")}}}yield mkdirP(c.dirname(t));yield d.rename(e,t)}))}t.mv=mv;function rmRF(e){return o(this,void 0,void 0,(function*(){if(d.IS_WINDOWS){if(/[*"<>|]/.test(e)){throw new Error('File path must not contain `*`, `"`, `<`, `>` or `|` on Windows')}try{const t=d.getCmdPath();if(yield d.isDirectory(e,true)){yield f(`${t} /s /c "rd /s /q "%inputPath%""`,{env:{inputPath:e}})}else{yield f(`${t} /s /c "del /f /a "%inputPath%""`,{env:{inputPath:e}})}}catch(e){if(e.code!=="ENOENT")throw e}try{yield d.unlink(e)}catch(e){if(e.code!=="ENOENT")throw e}}else{let t=false;try{t=yield d.isDirectory(e)}catch(e){if(e.code!=="ENOENT")throw e;return}if(t){yield p(`rm`,[`-rf`,`${e}`])}else{yield d.unlink(e)}}}))}t.rmRF=rmRF;function mkdirP(e){return o(this,void 0,void 0,(function*(){a.ok(e,"a path argument must be provided");yield d.mkdir(e,{recursive:true})}))}t.mkdirP=mkdirP;function which(e,t){return o(this,void 0,void 0,(function*(){if(!e){throw new Error("parameter 'tool' is required")}if(t){const t=yield which(e,false);if(!t){if(d.IS_WINDOWS){throw new Error(`Unable to locate executable file: ${e}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.`)}else{throw new Error(`Unable to locate executable file: ${e}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.`)}}return t}const r=yield findInPath(e);if(r&&r.length>0){return r[0]}return""}))}t.which=which;function findInPath(e){return o(this,void 0,void 0,(function*(){if(!e){throw new Error("parameter 'tool' is required")}const t=[];if(d.IS_WINDOWS&&process.env["PATHEXT"]){for(const e of process.env["PATHEXT"].split(c.delimiter)){if(e){t.push(e)}}}if(d.isRooted(e)){const r=yield d.tryGetExecutablePath(e,t);if(r){return[r]}return[]}if(e.includes(c.sep)){return[]}const r=[];if(process.env.PATH){for(const e of process.env.PATH.split(c.delimiter)){if(e){r.push(e)}}}const n=[];for(const i of r){const r=yield d.tryGetExecutablePath(c.join(i,e),t);if(r){n.push(r)}}return n}))}t.findInPath=findInPath;function readCopyOptions(e){const t=e.force==null?true:e.force;const r=Boolean(e.recursive);const n=e.copySourceDirectory==null?true:Boolean(e.copySourceDirectory);return{force:t,recursive:r,copySourceDirectory:n}}function cpDirRecursive(e,t,r,n){return o(this,void 0,void 0,(function*(){if(r>=255)return;r++;yield mkdirP(t);const i=yield d.readdir(e);for(const s of i){const i=`${e}/${s}`;const o=`${t}/${s}`;const a=yield d.lstat(i);if(a.isDirectory()){yield cpDirRecursive(i,o,r,n)}else{yield copyFile(i,o,n)}}yield d.chmod(t,(yield d.stat(e)).mode)}))}function copyFile(e,t,r){return o(this,void 0,void 0,(function*(){if((yield d.lstat(e)).isSymbolicLink()){try{yield d.lstat(t);yield d.unlink(t)}catch(e){if(e.code==="EPERM"){yield d.chmod(t,"0666");yield d.unlink(t)}}const r=yield d.readlink(e);yield d.symlink(r,t,d.IS_WINDOWS?"junction":null)}else if(!(yield d.exists(t))||r){yield d.copyFile(e,t)}}))}},770:(e,t,r)=>{e.exports=r(218)},218:(e,t,r)=>{"use strict";var n=r(278);var i=r(756);var s=r(611);var o=r(692);var a=r(434);var u=r(613);var c=r(23);t.httpOverHttp=httpOverHttp;t.httpsOverHttp=httpsOverHttp;t.httpOverHttps=httpOverHttps;t.httpsOverHttps=httpsOverHttps;function httpOverHttp(e){var t=new TunnelingAgent(e);t.request=s.request;return t}function httpsOverHttp(e){var t=new TunnelingAgent(e);t.request=s.request;t.createSocket=createSecureSocket;t.defaultPort=443;return t}function httpOverHttps(e){var t=new TunnelingAgent(e);t.request=o.request;return t}function httpsOverHttps(e){var t=new TunnelingAgent(e);t.request=o.request;t.createSocket=createSecureSocket;t.defaultPort=443;return t}function TunnelingAgent(e){var t=this;t.options=e||{};t.proxyOptions=t.options.proxy||{};t.maxSockets=t.options.maxSockets||s.Agent.defaultMaxSockets;t.requests=[];t.sockets=[];t.on("free",(function onFree(e,r,n,i){var s=toOptions(r,n,i);for(var o=0,a=t.requests.length;o=this.maxSockets){i.requests.push(s);return}i.createSocket(s,(function(t){t.on("free",onFree);t.on("close",onCloseOrRemove);t.on("agentRemove",onCloseOrRemove);e.onSocket(t);function onFree(){i.emit("free",t,s)}function onCloseOrRemove(e){i.removeSocket(t);t.removeListener("free",onFree);t.removeListener("close",onCloseOrRemove);t.removeListener("agentRemove",onCloseOrRemove)}}))};TunnelingAgent.prototype.createSocket=function createSocket(e,t){var r=this;var n={};r.sockets.push(n);var i=mergeOptions({},r.proxyOptions,{method:"CONNECT",path:e.host+":"+e.port,agent:false,headers:{host:e.host+":"+e.port}});if(e.localAddress){i.localAddress=e.localAddress}if(i.proxyAuth){i.headers=i.headers||{};i.headers["Proxy-Authorization"]="Basic "+new Buffer(i.proxyAuth).toString("base64")}l("making CONNECT request");var s=r.request(i);s.useChunkedEncodingByDefault=false;s.once("response",onResponse);s.once("upgrade",onUpgrade);s.once("connect",onConnect);s.once("error",onError);s.end();function onResponse(e){e.upgrade=true}function onUpgrade(e,t,r){process.nextTick((function(){onConnect(e,t,r)}))}function onConnect(i,o,a){s.removeAllListeners();o.removeAllListeners();if(i.statusCode!==200){l("tunneling socket could not be established, statusCode=%d",i.statusCode);o.destroy();var u=new Error("tunneling socket could not be established, "+"statusCode="+i.statusCode);u.code="ECONNRESET";e.request.emit("error",u);r.removeSocket(n);return}if(a.length>0){l("got illegal response body from proxy");o.destroy();var u=new Error("got illegal response body from proxy");u.code="ECONNRESET";e.request.emit("error",u);r.removeSocket(n);return}l("tunneling connection has established");r.sockets[r.sockets.indexOf(n)]=o;return t(o)}function onError(t){s.removeAllListeners();l("tunneling socket could not be established, cause=%s\n",t.message,t.stack);var i=new Error("tunneling socket could not be established, "+"cause="+t.message);i.code="ECONNRESET";e.request.emit("error",i);r.removeSocket(n)}};TunnelingAgent.prototype.removeSocket=function removeSocket(e){var t=this.sockets.indexOf(e);if(t===-1){return}this.sockets.splice(t,1);var r=this.requests.shift();if(r){this.createSocket(r,(function(e){r.request.onSocket(e)}))}};function createSecureSocket(e,t){var r=this;TunnelingAgent.prototype.createSocket.call(r,e,(function(n){var s=e.request.getHeader("host");var o=mergeOptions({},r.options,{socket:n,servername:s?s.replace(/:.*$/,""):e.host});var a=i.connect(0,o);r.sockets[r.sockets.indexOf(n)]=a;t(a)}))}function toOptions(e,t,r){if(typeof e==="string"){return{host:e,port:t,localAddress:r}}return e}function mergeOptions(e){for(var t=1,r=arguments.length;te.length!==0));for(let e of n){try{const n=JSON.parse(e);r.push({time:n.Time&&new Date(n.Time),action:n.Action,package:n.Package,test:n.Test,elapsed:n.Elapsed,output:n.Output,isCached:n.Output?.includes("\t(cached)")||false,isSubtest:n.Test?.includes("/")||false,isPackageLevel:typeof n.Test==="undefined",isConclusive:t.conclusiveTestEvents.includes(n.Action)})}catch{o.debug(`unable to parse line: ${e}`);continue}}return r}},407:function(e,t,r){"use strict";var n=this&&this.__createBinding||(Object.create?function(e,t,r,n){if(n===undefined)n=r;var i=Object.getOwnPropertyDescriptor(t,r);if(!i||("get"in i?!t.__esModule:i.writable||i.configurable)){i={enumerable:true,get:function(){return t[r]}}}Object.defineProperty(e,n,i)}:function(e,t,r,n){if(n===undefined)n=r;e[n]=t[r]});var i=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:true,value:t})}:function(e,t){e["default"]=t});var s=this&&this.__importStar||function(){var ownKeys=function(e){ownKeys=Object.getOwnPropertyNames||function(e){var t=[];for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r))t[t.length]=r;return t};return ownKeys(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r=ownKeys(e),s=0;s{a.error(e);process.exit(1)}))},422:function(e,t,r){"use strict";var n=this&&this.__createBinding||(Object.create?function(e,t,r,n){if(n===undefined)n=r;var i=Object.getOwnPropertyDescriptor(t,r);if(!i||("get"in i?!t.__esModule:i.writable||i.configurable)){i={enumerable:true,get:function(){return t[r]}}}Object.defineProperty(e,n,i)}:function(e,t,r,n){if(n===undefined)n=r;e[n]=t[r]});var i=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:true,value:t})}:function(e,t){e["default"]=t});var s=this&&this.__importStar||function(){var ownKeys=function(e){ownKeys=Object.getOwnPropertyNames||function(e){var t=[];for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r))t[t.length]=r;return t};return ownKeys(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r=ownKeys(e),s=0;s({moduleDirectory:".",testArguments:["./..."],fromJSONFile:null,omit:new Set});t.defaultInputs=defaultInputs;function getInputs(){const e=(0,t.defaultInputs)();getDeprecatedOmitInputs().forEach((t=>{e.omit.add(t)}));const r=o.getInput("moduleDirectory");if(r){e.moduleDirectory=r}const n=o.getInput("testArguments");if(n){e.testArguments=n.split(/\s/).filter((e=>e.length))}const i=o.getInput("fromJSONFile");if(i){e.fromJSONFile=i}const s=o.getInput("omit");if(s){s.split(/\s/).filter((e=>Object.values(a).includes(e))).forEach((t=>e.omit.add(t)))}return e}function getDeprecatedOmitInputs(){const e=[];const t=[];const r=o.getInput("omitUntestedPackages");if(r){if(o.getBooleanInput("omitUntestedPackages")){t.push("omitUntestedPackages");e.push(a.Untested)}}const n=o.getInput("omitSuccessfulPackages");if(n){if(o.getBooleanInput("omitSuccessfulPackages")){t.push("omitSuccessfulPackages");e.push(a.Successful)}}const i=o.getInput("omitPie");if(i){if(o.getBooleanInput("omitPie")){t.push("omitPie");e.push(a.Pie)}}if(t.length>0){o.warning(`The following inputs are deprecated and will be removed in the next major version: ${Array.from(t).join(", ")}. Please use the \`omit\` input instead.`)}return e}},40:function(e,t,r){"use strict";var n=this&&this.__createBinding||(Object.create?function(e,t,r,n){if(n===undefined)n=r;var i=Object.getOwnPropertyDescriptor(t,r);if(!i||("get"in i?!t.__esModule:i.writable||i.configurable)){i={enumerable:true,get:function(){return t[r]}}}Object.defineProperty(e,n,i)}:function(e,t,r,n){if(n===undefined)n=r;e[n]=t[r]});var i=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:true,value:t})}:function(e,t){e["default"]=t});var s=this&&this.__importStar||function(){var ownKeys=function(e){ownKeys=Object.getOwnPropertyNames||function(e){var t=[];for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r))t[t.length]=r;return t};return ownKeys(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r=ownKeys(e),s=0;sthis.omit.has(c.OmitOption.Untested)?e.hasTests():true)).filter((e=>this.omit.has(c.OmitOption.Successful)?e.onlySuccessfulTests():true));if(e.length===0){a.debug("no packages with tests, skipping render");return}const t=[this.headers];for(let r of e){t.push(...this.renderPackageRows(r))}await a.summary.addHeading("📝 Test results",2).addRaw('
').addRaw(`

${this.moduleName||"go test"}

`).addRaw(this.renderSummaryText()).addRaw(this.renderPie()).addTable(t).addRaw("
").addRaw(this.renderStderr()).write()}calculatePackageResults(){const e=this.testEvents.filter((e=>e.isConclusive&&e.isPackageLevel)).sort(((e,t)=>e.package.localeCompare(t.package)));const t=[];for(let r of e){const e=this.testEvents.filter((e=>e.package===r.package&&!e.isPackageLevel));const n=new u.default(r,e);for(let[e,t]of Object.entries(n.conclusions)){this.totalConclusions[e]+=t}t.push(n)}return t}emojiFor(e){switch(e){case"pass":return"🟢";case"fail":return"🔴";case"skip":return"🟡";default:return"❓"}}renderSummaryText(){const e=Object.values(this.totalConclusions).reduce(((e,t)=>e+t));let t=`${e} test${e===1?"":"s"}`;const r=l.conclusiveTestEvents.filter((e=>this.totalConclusions[e])).map((e=>`${this.totalConclusions[e]} ${e==="skip"?"skipp":e}ed`)).join(", ");if(r.length!==0){t+=` (${r})`}return t}renderPackageRows(e){const t=Object.entries(e.tests).sort(((e,t)=>e[0].localeCompare(t[0])));let r="";for(let[e,n]of t){if(r.length===0){r+="
    "}r+=`
  • ${this.emojiFor(n.conclusion)}${e}
  • `;if(n.subtests&&Object.entries(n.subtests).length){const e=Object.entries(n.subtests).sort(((e,t)=>e[0].localeCompare(t[0])));r+="
      ";for(let[t,n]of e){r+=`
    • ${this.emojiFor(n.conclusion)}${t}
    • `}r+="
    "}}if(r.length!==0){r+="
"}let n="";if(!this.omit.has(c.OmitOption.PackageTests)){n+=`
🧪 Tests${r||"(none)"}
`}if(!this.omit.has(c.OmitOption.PackageOutput)){n+=`
🖨️ Output
${this.scrubAnsi(e.output())||"(none)"}
`}const i=`${this.emojiFor(e.packageEvent.action)} ${e.packageEvent.package}${e.packageEvent.package===this.moduleName?" (main)":""}`;const s=[[i,e.conclusions.pass.toString(),e.conclusions.fail.toString(),e.conclusions.skip.toString(),`${(e.packageEvent.elapsed||0)*1e3}ms`]];if(n){s.push([{data:n,colspan:"5"}])}return s}renderPie(){if(this.omit.has(c.OmitOption.Pie)){return"

"}const e={theme:"base",themeVariables:{fontFamily:"monospace",pieSectionTextSize:"24px",darkMode:true}};const t={pass:{color:"#2da44e",word:"Passed"},fail:{color:"#cf222e",word:"Failed"},skip:{color:"#dbab0a",word:"Skipped"}};let r=1;const n=l.conclusiveTestEvents.map((n=>{if(!this.totalConclusions[n]){return""}e.themeVariables[`pie${r}`]=t[n].color;r++;return`"${t[n].word}" : ${this.totalConclusions[n]}\n`})).join("");return`\n\n\`\`\`mermaid\n%%{init: ${JSON.stringify(e)}}%%\npie showData\n${n}\n\`\`\`\n\n`}renderStderr(){if(this.omit.has(c.OmitOption.Stderr)||!this.stderr){return""}return`
\n🚨 Standard Error Output\n\n\`\`\`\n${this.scrubAnsi(this.stderr)}\n\`\`\`\n\n
`}scrubAnsi(e){return e.replace(/\x1b\[[0-9;]*[a-zA-Z]/g,"")}}t["default"]=Renderer},385:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:true});class PackageResult{constructor(e,t){this.tests={};this.conclusions={pass:0,fail:0,skip:0};this.packageEvent=e;this.events=t.filter((e=>!e.isPackageLevel&&e.package===this.packageEvent.package));this.eventsToResults()}testCount(){return Object.values(this.conclusions).reduce(((e,t)=>e+t))}hasTests(){return this.testCount()!==0}onlySuccessfulTests(){return this.conclusions.skip===0&&this.conclusions.fail===0}output(){return this.events.map((e=>e.output)).join("")}eventsToResults(){for(let e of this.events){if(!e.isConclusive){continue}const t=e.action;this.conclusions[t]+=1;if(e.isSubtest){const r=e.test.split("/")[0];this.tests[r]={...this.tests[r]||{},subtests:{[e.test]:{conclusion:t},...this.tests[r]?.subtests}}}else{this.tests[e.test]={conclusion:e.action,subtests:this.tests[e.test]?.subtests||{}}}}}}t["default"]=PackageResult},813:function(e,t,r){"use strict";var n=this&&this.__createBinding||(Object.create?function(e,t,r,n){if(n===undefined)n=r;var i=Object.getOwnPropertyDescriptor(t,r);if(!i||("get"in i?!t.__esModule:i.writable||i.configurable)){i={enumerable:true,get:function(){return t[r]}}}Object.defineProperty(e,n,i)}:function(e,t,r,n){if(n===undefined)n=r;e[n]=t[r]});var i=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:true,value:t})}:function(e,t){e["default"]=t});var s=this&&this.__importStar||function(){var ownKeys=function(e){ownKeys=Object.getOwnPropertyNames||function(e){var t=[];for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r))t[t.length]=r;return t};return ownKeys(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r=ownKeys(e),s=0;s0){c.error(`\`go test\` returned nonzero exit code: ${t}`)}const i=(0,f.parseTestEvents)(r);const s=new d.default(e,i,n,this.inputs.omit);await s.writeSummary();process.exit(t)}}async findModuleName(){const e=a.join(a.resolve(this.inputs.moduleDirectory),"go.mod");try{const t=await(0,u.readFile)(e);const r=t.toString().match(/module\s+.*/);if(!r){throw"no matching module line found"}return r[0].split(/module\s/)[1]}catch(e){c.debug(`unable to parse module from go.mod: ${e}`);return null}}async goTest(){let e="";let t="";const r={cwd:this.inputs.moduleDirectory,ignoreReturnCode:true,listeners:{stdout:t=>{e+=t.toString()},stderr:e=>{t+=e.toString()}}};const n=await(0,l.exec)("go",["test","-json",...this.inputs.testArguments],r);return{retCode:n,stdout:e,stderr:t}}}t["default"]=Runner},613:e=>{"use strict";e.exports=require("assert")},317:e=>{"use strict";e.exports=require("child_process")},982:e=>{"use strict";e.exports=require("crypto")},434:e=>{"use strict";e.exports=require("events")},896:e=>{"use strict";e.exports=require("fs")},943:e=>{"use strict";e.exports=require("fs/promises")},611:e=>{"use strict";e.exports=require("http")},692:e=>{"use strict";e.exports=require("https")},278:e=>{"use strict";e.exports=require("net")},857:e=>{"use strict";e.exports=require("os")},928:e=>{"use strict";e.exports=require("path")},193:e=>{"use strict";e.exports=require("string_decoder")},557:e=>{"use strict";e.exports=require("timers")},756:e=>{"use strict";e.exports=require("tls")},23:e=>{"use strict";e.exports=require("util")}};var t={};function __nccwpck_require__(r){var n=t[r];if(n!==undefined){return n.exports}var i=t[r]={exports:{}};var s=true;try{e[r].call(i.exports,i,i.exports,__nccwpck_require__);s=false}finally{if(s)delete t[r]}return i.exports}if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=__dirname+"/";var r=__nccwpck_require__(407);module.exports=r})(); -------------------------------------------------------------------------------- /docs/img/expanded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robherley/go-test-action/b19f6aadabfb1ad85079065b21aa2af132466468/docs/img/expanded.png -------------------------------------------------------------------------------- /docs/img/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robherley/go-test-action/b19f6aadabfb1ad85079065b21aa2af132466468/docs/img/overview.png -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types' 2 | 3 | const config: Config.InitialOptions = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | testPathIgnorePatterns: ['helpers.ts'], 7 | reporters: ['default', 'github-actions'], 8 | clearMocks: true, 9 | } 10 | 11 | export default config 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "go-test-action", 3 | "version": "0.5.0", 4 | "description": "GitHub Action to run go tests with rich summary output and annotations.", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "local": ". script/localenv && ts-node src/index.ts", 8 | "build": "ncc build -m -o dist src/index.ts", 9 | "test": "jest" 10 | }, 11 | "author": "Rob Herley (https://reb.gg)", 12 | "license": "MIT", 13 | "dependencies": { 14 | "@actions/core": "^1.11.1", 15 | "@actions/exec": "^1.1.1", 16 | "@actions/glob": "^0.5.0" 17 | }, 18 | "devDependencies": { 19 | "@types/jest": "^29.5.14", 20 | "@types/node": "^22.10.1", 21 | "@vercel/ncc": "^0.38.3", 22 | "cheerio": "^1.0.0", 23 | "jest": "^29.7.0", 24 | "ts-jest": "^29.2.5", 25 | "ts-node": "^10.9.2", 26 | "typescript": "^5.7.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /script/localenv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # fake environment & files for local development 4 | 5 | export GITHUB_STEP_SUMMARY="tmp/summary.md" 6 | mkdir -p tmp/ 7 | echo -n "" > $GITHUB_STEP_SUMMARY 8 | 9 | export INPUT_MODULEDIRECTORY="tmp/go-test-example" 10 | if [[ ! -d "$INPUT_MODULEDIRECTORY" ]]; then 11 | git clone https://github.com/robherley/go-test-example $INPUT_MODULEDIRECTORY 12 | fi 13 | 14 | export INPUT_TESTARGUMENTS="./..." -------------------------------------------------------------------------------- /src/events.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | 3 | // https://cs.opensource.google/go/go/+/master:src/cmd/test2json/main.go;l=46-55 4 | export type TestEventAction = 5 | | 'run' // the test has started running 6 | | 'pause' // the test has been paused 7 | | 'cont' // the test has continued running 8 | | 'bench' // the benchmark printed log output but did not fail 9 | | 'output' // the test printed output 10 | | TestEventActionConclusion 11 | 12 | // Specific actions that are "conclusive", they mark the end result of a test 13 | export type TestEventActionConclusion = 14 | | 'pass' // the test passed 15 | | 'fail' // the test or benchmark failed 16 | | 'skip' // the test was skipped or the package contained no tests 17 | 18 | // Specific test actions that mark the conclusive state of a test 19 | export const conclusiveTestEvents: TestEventActionConclusion[] = [ 20 | 'pass', 21 | 'fail', 22 | 'skip', 23 | ] 24 | 25 | // https://cs.opensource.google/go/go/+/master:src/cmd/test2json/main.go;l=34-41 26 | export interface TestEvent { 27 | // parsed fields from go's test event 28 | time?: Date 29 | action: TestEventAction 30 | package: string 31 | test: string 32 | elapsed?: number // seconds 33 | output?: string 34 | // added fields 35 | isSubtest: boolean 36 | isPackageLevel: boolean 37 | isConclusive: boolean 38 | isCached: boolean 39 | } 40 | 41 | /** 42 | * Convert test2json raw JSON output to TestEvent 43 | * @param stdout raw stdout of go test process 44 | * @returns parsed test events 45 | */ 46 | export function parseTestEvents(stdout: string): TestEvent[] { 47 | const events: TestEvent[] = [] 48 | 49 | const lines = stdout.split('\n').filter(line => line.length !== 0) 50 | for (let line of lines) { 51 | try { 52 | const json = JSON.parse(line) 53 | events.push({ 54 | time: json.Time && new Date(json.Time), 55 | action: json.Action as TestEventAction, 56 | package: json.Package, 57 | test: json.Test, 58 | elapsed: json.Elapsed, 59 | output: json.Output, 60 | isCached: json.Output?.includes('\t(cached)') || false, 61 | isSubtest: json.Test?.includes('/') || false, // afaik there isn't a better indicator in test2json 62 | isPackageLevel: typeof json.Test === 'undefined', 63 | isConclusive: conclusiveTestEvents.includes(json.Action), 64 | }) 65 | } catch { 66 | core.debug(`unable to parse line: ${line}`) 67 | continue 68 | } 69 | } 70 | 71 | return events 72 | } 73 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import Runner from './runner' 3 | 4 | new Runner().run().catch(err => { 5 | core.error(err) 6 | process.exit(1) 7 | }) 8 | -------------------------------------------------------------------------------- /src/inputs.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | 3 | export interface Inputs { 4 | moduleDirectory: string 5 | testArguments: string[] 6 | fromJSONFile: string | null 7 | omit: Set 8 | } 9 | 10 | export enum OmitOption { 11 | // Omit untested packages from the summary 12 | Untested = 'untested', 13 | // Omit successful packages from the summary 14 | Successful = 'successful', 15 | // Omit the pie chart from the summary 16 | Pie = 'pie', 17 | // Omit the package test output 18 | PackageOutput = 'pkg-output', 19 | // Omit the package test list 20 | PackageTests = 'pkg-tests', 21 | // Omit stderr 22 | Stderr = 'stderr', 23 | } 24 | 25 | export const defaultInputs = (): Inputs => ({ 26 | moduleDirectory: '.', 27 | testArguments: ['./...'], 28 | fromJSONFile: null, 29 | omit: new Set(), 30 | }) 31 | 32 | /** 33 | * Parses the action inputs from the environment 34 | * @returns the parsed inputs 35 | */ 36 | export function getInputs(): Inputs { 37 | const inputs = defaultInputs() 38 | 39 | getDeprecatedOmitInputs().forEach(option => { 40 | inputs.omit.add(option) 41 | }) 42 | 43 | const moduleDirectory = core.getInput('moduleDirectory') 44 | if (moduleDirectory) { 45 | inputs.moduleDirectory = moduleDirectory 46 | } 47 | 48 | const testArguments = core.getInput('testArguments') 49 | if (testArguments) { 50 | inputs.testArguments = testArguments.split(/\s/).filter(arg => arg.length) 51 | } 52 | 53 | const fromJSONFile = core.getInput('fromJSONFile') 54 | if (fromJSONFile) { 55 | inputs.fromJSONFile = fromJSONFile 56 | } 57 | 58 | const omit = core.getInput('omit') 59 | if (omit) { 60 | omit 61 | .split(/\s/) 62 | .filter(option => 63 | Object.values(OmitOption).includes(option as OmitOption) 64 | ) 65 | .forEach(option => inputs.omit.add(option as OmitOption)) 66 | } 67 | 68 | return inputs 69 | } 70 | 71 | /** 72 | * Parses the deprecated omit inputs 73 | * @returns the parsed omit options 74 | */ 75 | function getDeprecatedOmitInputs(): OmitOption[] { 76 | const omitOptions: OmitOption[] = [] 77 | const usedDeprecated: string[] = [] 78 | 79 | const omitUntestedPackages = core.getInput('omitUntestedPackages') 80 | if (omitUntestedPackages) { 81 | if (core.getBooleanInput('omitUntestedPackages')) { 82 | usedDeprecated.push('omitUntestedPackages') 83 | omitOptions.push(OmitOption.Untested) 84 | } 85 | } 86 | 87 | const omitSuccessfulPackages = core.getInput('omitSuccessfulPackages') 88 | if (omitSuccessfulPackages) { 89 | if (core.getBooleanInput('omitSuccessfulPackages')) { 90 | usedDeprecated.push('omitSuccessfulPackages') 91 | omitOptions.push(OmitOption.Successful) 92 | } 93 | } 94 | 95 | const omitPie = core.getInput('omitPie') 96 | if (omitPie) { 97 | if (core.getBooleanInput('omitPie')) { 98 | usedDeprecated.push('omitPie') 99 | omitOptions.push(OmitOption.Pie) 100 | } 101 | } 102 | 103 | if (usedDeprecated.length > 0) { 104 | core.warning( 105 | `The following inputs are deprecated and will be removed in the next major version: ${Array.from( 106 | usedDeprecated 107 | ).join(', ')}. Please use the \`omit\` input instead.` 108 | ) 109 | } 110 | 111 | return omitOptions 112 | } 113 | -------------------------------------------------------------------------------- /src/renderer.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import type { SummaryTableRow } from '@actions/core/lib/summary' 3 | 4 | import type { ConclusionResults } from './results' 5 | import PackageResult from './results' 6 | import { OmitOption } from './inputs' 7 | 8 | import type { 9 | TestEvent, 10 | TestEventAction, 11 | TestEventActionConclusion, 12 | } from './events' 13 | import { conclusiveTestEvents } from './events' 14 | 15 | class Renderer { 16 | moduleName: string | null 17 | testEvents: TestEvent[] 18 | stderr: string 19 | omit: Set 20 | packageResults: PackageResult[] 21 | headers: SummaryTableRow = [ 22 | { data: '📦 Package', header: true }, 23 | { data: '🟢 Passed', header: true }, 24 | { data: '🔴 Failed', header: true }, 25 | { data: '🟡 Skipped', header: true }, 26 | { data: '⏳ Duration', header: true }, 27 | ] 28 | totalConclusions: ConclusionResults = { 29 | pass: 0, 30 | fail: 0, 31 | skip: 0, 32 | } 33 | 34 | constructor( 35 | moduleName: string | null, 36 | testEvents: TestEvent[], 37 | stderr: string, 38 | omit: Set 39 | ) { 40 | this.moduleName = moduleName 41 | this.testEvents = testEvents 42 | this.stderr = stderr 43 | this.omit = omit 44 | this.packageResults = this.calculatePackageResults() 45 | } 46 | 47 | /** 48 | * Generates a GitHub Actions job summary for the parsed test events 49 | * https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary 50 | */ 51 | async writeSummary() { 52 | const resultsToRender = this.packageResults 53 | .filter(result => 54 | this.omit.has(OmitOption.Untested) ? result.hasTests() : true 55 | ) 56 | .filter(result => 57 | this.omit.has(OmitOption.Successful) 58 | ? result.onlySuccessfulTests() 59 | : true 60 | ) 61 | 62 | if (resultsToRender.length === 0) { 63 | core.debug('no packages with tests, skipping render') 64 | return 65 | } 66 | 67 | const rows: SummaryTableRow[] = [this.headers] 68 | for (let packageResult of resultsToRender) { 69 | rows.push(...this.renderPackageRows(packageResult)) 70 | } 71 | 72 | await core.summary 73 | .addHeading('📝 Test results', 2) 74 | .addRaw('
') // center alignment hack 75 | .addRaw(`

${this.moduleName || 'go test'}

`) 76 | .addRaw(this.renderSummaryText()) 77 | .addRaw(this.renderPie()) 78 | .addTable(rows) 79 | .addRaw('
') 80 | .addRaw(this.renderStderr()) 81 | .write() 82 | } 83 | 84 | /** 85 | * Filter through test events and calculate the results per package 86 | * @returns list of package results 87 | */ 88 | private calculatePackageResults(): PackageResult[] { 89 | const pkgLevelConclusiveEvents = this.testEvents 90 | .filter(event => event.isConclusive && event.isPackageLevel) 91 | .sort((a, b) => a.package.localeCompare(b.package)) 92 | 93 | const packageResults: PackageResult[] = [] 94 | for (let pkgEvent of pkgLevelConclusiveEvents) { 95 | const otherPackageEvents = this.testEvents.filter( 96 | e => e.package === pkgEvent.package && !e.isPackageLevel 97 | ) 98 | const packageResult = new PackageResult(pkgEvent, otherPackageEvents) 99 | for (let [key, value] of Object.entries(packageResult.conclusions)) { 100 | this.totalConclusions[key as TestEventActionConclusion] += value 101 | } 102 | packageResults.push(packageResult) 103 | } 104 | 105 | return packageResults 106 | } 107 | 108 | /** 109 | * Displayed emoji for a specific test event 110 | * @param action test event action 111 | * @returns an emoji 112 | */ 113 | private emojiFor(action: TestEventAction): string { 114 | switch (action) { 115 | case 'pass': 116 | return '🟢' 117 | case 'fail': 118 | return '🔴' 119 | case 'skip': 120 | return '🟡' 121 | default: 122 | return '❓' 123 | } 124 | } 125 | 126 | /** 127 | * Renders out results text (ie: "4 tests (2 passed, 1 failed, 1 skipped)") 128 | * @returns results summary test 129 | */ 130 | private renderSummaryText(): string { 131 | const totalTestCount = Object.values(this.totalConclusions).reduce( 132 | (a, b) => a + b 133 | ) 134 | 135 | let summarized = `${totalTestCount} test${totalTestCount === 1 ? '' : 's'}` 136 | 137 | const conclusionText = conclusiveTestEvents 138 | .filter(c => this.totalConclusions[c]) 139 | .map(c => `${this.totalConclusions[c]} ${c === 'skip' ? 'skipp' : c}ed`) 140 | .join(', ') 141 | 142 | if (conclusionText.length !== 0) { 143 | summarized += ` (${conclusionText})` 144 | } 145 | 146 | return summarized 147 | } 148 | 149 | /** 150 | * For a given package event, renders the results, tests and subtests into a SummaryTableRow 151 | * @param packageResult the package result 152 | * @returns summary table row 153 | */ 154 | private renderPackageRows(packageResult: PackageResult): SummaryTableRow[] { 155 | const sortedTests = Object.entries(packageResult.tests).sort((a, b) => 156 | a[0].localeCompare(b[0]) 157 | ) 158 | let testList = '' 159 | for (let [name, result] of sortedTests) { 160 | if (testList.length === 0) { 161 | testList += '
    ' 162 | } 163 | testList += `
  • ${this.emojiFor( 164 | result.conclusion! 165 | )}${name}
  • ` 166 | 167 | if (result.subtests && Object.entries(result.subtests).length) { 168 | const sortedSubtests = Object.entries(result.subtests).sort((a, b) => 169 | a[0].localeCompare(b[0]) 170 | ) 171 | testList += '
      ' 172 | for (let [subtestName, subTestResult] of sortedSubtests) { 173 | testList += `
    • ${this.emojiFor( 174 | subTestResult.conclusion! 175 | )}${subtestName}
    • ` 176 | } 177 | testList += '
    ' 178 | } 179 | } 180 | if (testList.length !== 0) { 181 | testList += '
' 182 | } 183 | 184 | let details = '' 185 | 186 | if (!this.omit.has(OmitOption.PackageTests)) { 187 | details += `
🧪 Tests${ 188 | testList || '(none)' 189 | }
` 190 | } 191 | 192 | if (!this.omit.has(OmitOption.PackageOutput)) { 193 | details += `
🖨️ Output
${
194 |         this.scrubAnsi(packageResult.output()) || '(none)'
195 |       }
` 196 | } 197 | 198 | const pkgName = `${this.emojiFor( 199 | packageResult.packageEvent.action 200 | )} ${packageResult.packageEvent.package}${ 201 | packageResult.packageEvent.package === this.moduleName ? ' (main)' : '' 202 | }` 203 | 204 | const packageRows: SummaryTableRow[] = [ 205 | [ 206 | pkgName, 207 | packageResult.conclusions.pass.toString(), 208 | packageResult.conclusions.fail.toString(), 209 | packageResult.conclusions.skip.toString(), 210 | `${(packageResult.packageEvent.elapsed || 0) * 1000}ms`, 211 | ], 212 | ] 213 | 214 | if (details) { 215 | packageRows.push([{ data: details, colspan: '5' }]) 216 | } 217 | 218 | return packageRows 219 | } 220 | 221 | /** 222 | * Returns a pie chart summarizing all test results 223 | * @returns stringified markdown for mermaid.js pie chart 224 | */ 225 | private renderPie(): string { 226 | if (this.omit.has(OmitOption.Pie)) { 227 | return '

' // just return double break instead 228 | } 229 | 230 | const pieConfig: any = { 231 | theme: 'base', 232 | themeVariables: { 233 | fontFamily: 'monospace', 234 | pieSectionTextSize: '24px', 235 | darkMode: true, 236 | }, 237 | } 238 | 239 | const keys: { 240 | [key in TestEventActionConclusion]: { word: string; color: string } 241 | } = { 242 | pass: { color: '#2da44e', word: 'Passed' }, 243 | fail: { color: '#cf222e', word: 'Failed' }, 244 | skip: { color: '#dbab0a', word: 'Skipped' }, 245 | } 246 | 247 | let pieIndex = 1 248 | const pieData = conclusiveTestEvents 249 | .map(conclusion => { 250 | if (!this.totalConclusions[conclusion]) { 251 | return '' 252 | } 253 | 254 | pieConfig.themeVariables[`pie${pieIndex}`] = keys[conclusion].color 255 | pieIndex++ 256 | return `"${keys[conclusion].word}" : ${this.totalConclusions[conclusion]}\n` 257 | }) 258 | .join('') 259 | 260 | return ` 261 | \n\`\`\`mermaid 262 | %%{init: ${JSON.stringify(pieConfig)}}%% 263 | pie showData 264 | ${pieData} 265 | \`\`\`\n 266 | ` 267 | } 268 | 269 | private renderStderr(): string { 270 | if (this.omit.has(OmitOption.Stderr) || !this.stderr) { 271 | return '' 272 | } 273 | 274 | return `
275 | 🚨 Standard Error Output 276 | 277 | \`\`\` 278 | ${this.scrubAnsi(this.stderr)} 279 | \`\`\` 280 | 281 |
` 282 | } 283 | 284 | private scrubAnsi(input: string): string { 285 | return input.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '') 286 | } 287 | } 288 | 289 | export default Renderer 290 | -------------------------------------------------------------------------------- /src/results.ts: -------------------------------------------------------------------------------- 1 | import type { TestEvent, TestEventActionConclusion } from './events' 2 | 3 | export type TestResults = { [testName: string]: TestResult } 4 | export type ConclusionResults = { [key in TestEventActionConclusion]: number } 5 | 6 | export interface TestResult { 7 | conclusion?: TestEventActionConclusion 8 | subtests?: TestResults 9 | } 10 | 11 | class PackageResult { 12 | packageEvent: TestEvent 13 | events: TestEvent[] 14 | tests: TestResults = {} 15 | conclusions: ConclusionResults = { 16 | pass: 0, 17 | fail: 0, 18 | skip: 0, 19 | } 20 | 21 | constructor(packageEvent: TestEvent, events: TestEvent[]) { 22 | this.packageEvent = packageEvent 23 | this.events = events.filter( 24 | e => !e.isPackageLevel && e.package === this.packageEvent.package 25 | ) 26 | 27 | this.eventsToResults() 28 | } 29 | 30 | public testCount(): number { 31 | return Object.values(this.conclusions).reduce((a, b) => a + b) 32 | } 33 | 34 | public hasTests(): boolean { 35 | return this.testCount() !== 0 36 | } 37 | 38 | public onlySuccessfulTests(): boolean { 39 | return this.conclusions.skip === 0 && this.conclusions.fail === 0 40 | } 41 | 42 | public output(): string { 43 | return this.events.map(e => e.output).join('') 44 | } 45 | 46 | /** 47 | * Iterate through test events, find anything that is a conclusive results and record it 48 | */ 49 | private eventsToResults() { 50 | for (let event of this.events) { 51 | if (!event.isConclusive) { 52 | // if the event doesn't have a conclusion action, we don't need anything else from it 53 | continue 54 | } 55 | 56 | const conclusion = event.action as TestEventActionConclusion 57 | this.conclusions[conclusion] += 1 58 | 59 | if (event.isSubtest) { 60 | const parentEvent = event.test.split('/')[0] 61 | 62 | this.tests[parentEvent] = { 63 | ...(this.tests[parentEvent] || {}), 64 | subtests: { 65 | [event.test]: { conclusion }, 66 | ...this.tests[parentEvent]?.subtests, 67 | }, 68 | } 69 | } else { 70 | this.tests[event.test] = { 71 | conclusion: event.action as TestEventActionConclusion, 72 | subtests: this.tests[event.test]?.subtests || {}, 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | export default PackageResult 80 | -------------------------------------------------------------------------------- /src/runner.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { readFile } from 'fs/promises' 3 | 4 | import * as core from '@actions/core' 5 | import { exec } from '@actions/exec' 6 | 7 | import Renderer from './renderer' 8 | import { parseTestEvents } from './events' 9 | import { Inputs, getInputs } from './inputs' 10 | 11 | class Runner { 12 | inputs: Inputs 13 | 14 | constructor() { 15 | this.inputs = getInputs() 16 | } 17 | 18 | /** 19 | * Runs the go tests, captures any output, builds annotations and write the summary 20 | */ 21 | async run() { 22 | const moduleName = await this.findModuleName() 23 | 24 | if (this.inputs.fromJSONFile) { 25 | const stdout = await readFile(this.inputs.fromJSONFile) 26 | const testEvents = parseTestEvents(stdout.toString()) 27 | 28 | const renderer = new Renderer( 29 | moduleName, 30 | testEvents, 31 | '', 32 | this.inputs.omit 33 | ) 34 | 35 | await renderer.writeSummary() 36 | process.exit(0) 37 | } else { 38 | const { retCode, stdout, stderr } = await this.goTest() 39 | if (retCode > 0) { 40 | core.error(`\`go test\` returned nonzero exit code: ${retCode}`) 41 | } 42 | 43 | const testEvents = parseTestEvents(stdout) 44 | 45 | const renderer = new Renderer( 46 | moduleName, 47 | testEvents, 48 | stderr, 49 | this.inputs.omit 50 | ) 51 | 52 | await renderer.writeSummary() 53 | process.exit(retCode) 54 | } 55 | } 56 | 57 | /** 58 | * Deduces go module name from go.mod in working directory 59 | * @returns go module name 60 | */ 61 | async findModuleName(): Promise { 62 | const modulePath = path.join( 63 | path.resolve(this.inputs.moduleDirectory), 64 | 'go.mod' 65 | ) 66 | 67 | try { 68 | const contents = await readFile(modulePath) 69 | const match = contents.toString().match(/module\s+.*/) 70 | if (!match) { 71 | throw 'no matching module line found' 72 | } 73 | return match[0].split(/module\s/)[1] 74 | } catch (err) { 75 | core.debug(`unable to parse module from go.mod: ${err}`) 76 | return null 77 | } 78 | } 79 | 80 | /** 81 | * Execs `go test` with specified arguments, capturing the output 82 | * @returns return code, stdout, stderr of `go test` 83 | */ 84 | private async goTest(): Promise<{ 85 | retCode: number 86 | stdout: string 87 | stderr: string 88 | }> { 89 | let stdout = '' 90 | let stderr = '' 91 | 92 | const opts = { 93 | cwd: this.inputs.moduleDirectory, 94 | ignoreReturnCode: true, 95 | listeners: { 96 | stdout: (data: Buffer) => { 97 | stdout += data.toString() 98 | }, 99 | stderr: (data: Buffer) => { 100 | stderr += data.toString() 101 | }, 102 | }, 103 | } 104 | 105 | const retCode = await exec( 106 | 'go', 107 | ['test', '-json', ...this.inputs.testArguments], 108 | opts 109 | ) 110 | 111 | return { 112 | retCode, 113 | stdout, 114 | stderr, 115 | } 116 | } 117 | } 118 | 119 | export default Runner 120 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "esModuleInterop": true 9 | }, 10 | "exclude": [ 11 | "node_modules", 12 | "__tests__", 13 | "jest.config.ts" 14 | ] 15 | } 16 | --------------------------------------------------------------------------------