diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2f299e8..8ae99eb 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -27,9 +27,23 @@ jobs: with: fetch-depth: 0 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Build + run: npm run build + - name: Publish Delta Coverage id: render-delta-coverage - uses: gw-kit/delta-coverage-action@v1 + uses: ./ with: summary-report-base-path: 'test/data/' diff --git a/.gitignore b/.gitignore index 485dee6..8a0091d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ .idea +node_modules/ +coverage/ +.DS_Store diff --git a/action.yaml b/action.yaml index f2bdcbe..174c641 100644 --- a/action.yaml +++ b/action.yaml @@ -33,159 +33,8 @@ inputs: outputs: badges-dir: description: 'Directory with generated badges.' - value: ${{ steps.generate-badges.outputs.badges-dir }} + value: 'badges/' runs: - using: "composite" - - steps: - - - name: Build All Summaries - id: all-summaries - uses: actions/github-script@v7 - with: - github-token: ${{ inputs.github-token }} - script: | - const readSummaries = require('${{ github.action_path }}/src/read-summaries.js'); - const summariesBaseDir = `${{ inputs.summary-report-base-path }}`; - const deltaSummariesFile = await readSummaries({ - isFullCoverageMode: false, - baseSummariesPath: summariesBaseDir, - }); - const fullCovSummariesFile = await readSummaries({ - isFullCoverageMode: true, - baseSummariesPath: summariesBaseDir, - }); - core.setOutput('delta', deltaSummariesFile); - core.setOutput('full', fullCovSummariesFile); - - - name: Install Badge Dependencies - continue-on-error: true - shell: bash - run: | - npm install gradient-badge - mv ./node_modules "${{ github.action_path }}/src/" - - - name: Generate Badges - id: generate-badges - continue-on-error: true - uses: actions/github-script@v7 - with: - script: | - const generateBadges = require('${{ github.action_path }}/src/generate-badges.js'); - await generateBadges({ - summariesFile: '${{ steps.all-summaries.outputs.full }}', - core: core - }); - - - name: Fetch PR Labels - id: fetch-labels - if: ${{ github.event_name == 'pull_request' }} - shell: 'bash' - env: - GH_TOKEN: ${{ inputs.github-token }} - PR_ENDPOINT: "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" - run: | - LABELS=$(gh api ${PR_ENDPOINT} --jq '.labels[].name' || echo "") - LABELS_CLEAN=$(echo "$LABELS" | tr '\n' ',' | sed 's/,$//') - echo "labels=$LABELS_CLEAN" >> $GITHUB_OUTPUT - - - name: Check for Suppression - id: check-suppress - uses: actions/github-script@v7 - with: - script: | - const labels = '${{ steps.fetch-labels.outputs.labels }}'; - const suppressedByLabel = labels.includes('suppress-coverage'); - const suppressedByInput = '${{ inputs.suppress-check-failures }}' === 'true'; - const resolution = suppressedByLabel || suppressedByInput; - core.info(`Is suppress=${resolution} : by label=${suppressedByLabel}, by input=${suppressedByInput}`); - core.setOutput('suppress', resolution); - - - name: Create Check Run - id: create-check-runs - uses: actions/github-script@v7 - with: - github-token: ${{ inputs.github-token }} - script: | - const createCheckRuns = require('${{ github.action_path }}/src/create-check-runs.js'); - const extraRenderScript = (view) => { - const script = ${{ inputs.check-run-extra-render-script || '(v) => ""' }}; - try { return script(view) } catch(e) { - return `Error in custom script: ${e}` - }; - }; - const checkRuns = await createCheckRuns({ - summaryReportPath: `${{ steps.all-summaries.outputs.delta }}`, - ignoreCoverageFailure: ${{ steps.check-suppress.outputs.suppress }}, - core: core, - context: context, - github: github, - headSha: `${{ github.event.pull_request.head.sha || github.sha }}`, - externalId: `${{ inputs.external-id }}`, - summaryExtraFun: extraRenderScript - }); - core.setOutput('check-runs', checkRuns); - - - name: Create Comment Marker - id: comment-marker - if: ${{ inputs.title != '' }} - shell: bash - run: echo 'result=${{ format('', inputs.title) }}' >> $GITHUB_OUTPUT - - - name: Find Existing Comment In PR - id: find-comment - if: ${{ steps.comment-marker.outputs.result && github.event_name == 'pull_request' }} - uses: actions/github-script@v7 - with: - github-token: ${{ inputs.github-token }} - script: | - const response = await github.rest.issues.listComments({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - }) - const comment = response.data.find(it => - it.body.includes('${{ steps.comment-marker.outputs.result }}') - ) - if (comment) { - console.log(`Comment found: ${comment.id}`); - return comment.id - } - - - id: build-message - if: ${{ github.event_name == 'pull_request' }} - uses: actions/github-script@v7 - with: - github-token: ${{ inputs.github-token }} - script: | - const createCommentMsg = require('${{ github.action_path }}/src/build-comment-body.js'); - return createCommentMsg({ - checkRunsContent: `${{ steps.create-check-runs.outputs.check-runs }}`, - commentTitle: '${{ inputs.title }}', - commentMarker: `${{ steps.comment-marker.outputs.result }}`, - core: core - }); - - - name: Update or Create Comment - uses: actions/github-script@v7 - if: ${{ github.event_name == 'pull_request' }} - with: - github-token: ${{ inputs.github-token }} - script: | - if ('${{ steps.find-comment.outputs.result }}') { - github.rest.issues.updateComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: '${{ steps.find-comment.outputs.result }}', - body: ${{ steps.build-message.outputs.result }} - }) - } else { - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: ${{ steps.build-message.outputs.result }} - }) - } + using: 'node20' + main: 'dist/index.js' diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d9fdf95 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2558 @@ +{ + "name": "delta-coverage-action", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "delta-coverage-action", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/github": "^6.0.0", + "gradient-badge": "^1.3.0" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "@vercel/ncc": "^0.38.3", + "@vitest/coverage-v8": "^2.1.0", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + } + }, + "node_modules/@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "license": "MIT", + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "license": "MIT", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/github": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz", + "integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==", + "license": "MIT", + "dependencies": { + "@actions/http-client": "^2.2.0", + "@octokit/core": "^5.0.1", + "@octokit/plugin-paginate-rest": "^9.2.2", + "@octokit/plugin-rest-endpoint-methods": "^10.4.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "undici": "^5.28.5" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "license": "MIT" + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", + "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vercel/ncc": { + "version": "0.38.4", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz", + "integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==", + "dev": true, + "license": "MIT", + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", + "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.7", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.12", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.9", + "vitest": "2.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/badgen": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/badgen/-/badgen-3.2.3.tgz", + "integrity": "sha512-svDuwkc63E/z0ky3drpUppB83s/nlgDciH9m+STwwQoWyq7yCgew1qEfJ+9axkKdNq7MskByptWUN9j1PGMwFA==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "license": "Apache-2.0" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "license": "ISC" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gradient-badge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/gradient-badge/-/gradient-badge-1.3.1.tgz", + "integrity": "sha512-OvTFHZJSQ+fR7zWl6xz5PvniAfKRsHBy98NZ+kAWOnfmP27lt8wMA4oUvsQp0ppNET+2qE0oEnna2s3SiiqaVA==", + "license": "MIT", + "dependencies": { + "badgen": "^3.2.1" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f8c18e2 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "delta-coverage-action", + "version": "1.2.0", + "description": "GitHub Action for Delta Coverage reports", + "main": "dist/index.js", + "scripts": { + "build": "rm -rf dist && ncc build src/main.ts -o dist --source-map --license licenses.txt", + "compile": "tsc", + "test": "vitest run --coverage", + "test:watch": "vitest" + }, + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/github": "^6.0.0", + "gradient-badge": "^1.3.0" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "@vercel/ncc": "^0.38.3", + "@vitest/coverage-v8": "^2.1.0", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + }, + "license": "MIT" +} diff --git a/src/build-comment-body.js b/src/build-comment-body.js deleted file mode 100644 index f1d9c33..0000000 --- a/src/build-comment-body.js +++ /dev/null @@ -1,153 +0,0 @@ -const { GITHUB_SERVER_URL, GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_RUN_NUMBER, GITHUB_RUN_ATTEMPT } = process.env; - -module.exports = (ctx) => { - - const NO_VALUE = -1; - const ENTITIES = ['INSTRUCTION', 'BRANCH', 'LINE']; - const HEADERS = ['Check', 'Expected', 'Entity', 'Actual']; - - const NO_COVERAGE_TEXT = 'No%20diff'; - const SUCCESS_COLOR = '7AB56D'; - const FAILURE_COLOR = 'C4625A'; - const NO_COVERAGE_COLOR = '777777'; - - const TOOLTIPS = new Map([ - ['INSTRUCTION', 'The Java bytecode instructions executed during testing'], - ['BRANCH', 'The branches in conditional statements like if, switch, or loops that are executed.'], - ['LINE', 'The source code lines covered by the tests.'] - ]); - - const workflowUrl = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`; - const workflowNum = `${GITHUB_RUN_NUMBER}.${GITHUB_RUN_ATTEMPT}` - - const buildViewSummaryData = (checkRun) => { - const entitiesRules = checkRun.coverageRules?.entitiesRules || []; - const entityToExpectedPercents = new Map(); - for (const [entityName, entityConfig] of Object.entries(entitiesRules)) { - const percents = entityConfig.minCoverageRatio * 100; - entityToExpectedPercents.set(entityName, percents); - } - - const entityToActualPercents = checkRun.coverageInfo.reduce((acc, it) => { - if (it.total !== 0) { - acc.set(it.coverageEntity, it.percents); - } - return acc; - }, new Map()); - - const shouldFailOnViolations = checkRun.coverageRules?.failOnViolation || false; - return ENTITIES.map((entity) => { - const expectedPercents = entityToExpectedPercents.get(entity) || NO_VALUE; - - const actualPercents = entityToActualPercents.get(entity) !== undefined - ? entityToActualPercents.get(entity) - : NO_VALUE; - - const isFailed = shouldFailOnViolations - && actualPercents > NO_VALUE - && actualPercents < expectedPercents; - return { - entity, - isFailed, - expected: expectedPercents, - actual: actualPercents - } - }); - }; - - const buildCheckRunForViewText = (checkRun) => { - const buildProgressImg = (entityData) => { - let imageLink; - if (entityData.actual > NO_VALUE ) { - const color = entityData.actual < entityData.expected ? FAILURE_COLOR : SUCCESS_COLOR; - const actualInteger = Math.floor(entityData.actual); - imageLink = `https://progress-bar.xyz/${actualInteger}/?progress_color=${color}`; - } else { - imageLink = `https://progress-bar.xyz/100/?show_text=false&width=38` - +`&title=${NO_COVERAGE_TEXT}&color=${NO_COVERAGE_COLOR}&progress_color=${NO_COVERAGE_COLOR}`; - } - return ``; - } - - const buildExpectedValue = (entityData) => { - return (entityData.expected > NO_VALUE) ? `🎯 ${entityData.expected}% 🎯` : ''; - } - - const buildCoverageValueColumnHtml = (entityData, entityIndex, shouldFold, valueProvider) => { - if (shouldFold && entityIndex > 0) { - return ''; - } - const value = valueProvider(entityData); - const rowSpanAttr = (shouldFold && entityIndex === 0) ? `rowspan=3` : ''; - return `${value}`; - } - - const obtainUniqueValuesSet = (viewSummaryData, valueProvider) => { - const allExpected = viewSummaryData.map(it => valueProvider(it)); - return new Set(allExpected); - } - - const viewSummaryData = buildViewSummaryData(checkRun); - - const hasFailure = viewSummaryData.some(it => it.isFailed); - const statusSymbol = hasFailure ? 'πŸ”΄' : '🟒'; - const viewCellValue = ` - ${statusSymbol} ${checkRun.viewName} - `.trim(); - - const foldExpectedColumn = obtainUniqueValuesSet(viewSummaryData, it => it.expected).size === 1; - - const actualUniqueValues = obtainUniqueValuesSet(viewSummaryData, it => it.actual); - const foldActualColumn = actualUniqueValues.size === 1 - && (foldExpectedColumn || actualUniqueValues.has(NO_VALUE)); - - return viewSummaryData.map((entityData, index) => { - const viewCellInRow = (index === 0) ? viewCellValue : ''; - - const actualColumnHtml = buildCoverageValueColumnHtml(entityData, index, foldActualColumn, buildProgressImg); - const ruleColumnHtml = buildCoverageValueColumnHtml(entityData, index, foldExpectedColumn, buildExpectedValue); - - const toolTipText = TOOLTIPS.get(entityData.entity) || ''; - - return ` - ${viewCellInRow} - ${ruleColumnHtml} - ${entityData.entity} - ${actualColumnHtml} - `.trim().replace(/^ +/gm, ''); - }).join('\n'); - } - - const renderHeaders = () => { - return '' + HEADERS.map(it => `${it}`).join('\n') + ''; - } - - const buildRunMetaText = () => { - const workflowRunDate = new Date(); - const options = { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }; - - const workflowRunLink = `[Run ${workflowNum}](${workflowUrl})`; - const formattedDate = workflowRunDate.toLocaleString('en-US', options); - return `${workflowRunLink} | \`${formattedDate}\``; - }; - - const checkRuns = JSON.parse(ctx.checkRunsContent); - let summaryBuffer = ctx.core.summary - .addHeading(ctx.commentTitle, '2') - .addRaw(ctx.commentMarker, true) - .addEOL() - .addRaw(``) - .addRaw(renderHeaders()); - - checkRuns.forEach(checkRun => { - const runText = buildCheckRunForViewText(checkRun); - summaryBuffer = summaryBuffer.addRaw(runText, true); - }); - - return summaryBuffer - .addRaw(`
`) - .addEOL() - .addEOL() - .addRaw(buildRunMetaText()) - .stringify() -}; diff --git a/src/build-comment-body.ts b/src/build-comment-body.ts new file mode 100644 index 0000000..0d64337 --- /dev/null +++ b/src/build-comment-body.ts @@ -0,0 +1,170 @@ +import { CheckRunResult, CoverageEntity } from './types/coverage'; + +const NO_VALUE = -1; +const ENTITIES: CoverageEntity[] = ['INSTRUCTION', 'BRANCH', 'LINE']; +const HEADERS = ['Check', 'Expected', 'Entity', 'Actual']; + +const NO_COVERAGE_TEXT = 'No%20diff'; +const SUCCESS_COLOR = '7AB56D'; +const FAILURE_COLOR = 'C4625A'; +const NO_COVERAGE_COLOR = '777777'; + +const TOOLTIPS = new Map([ + ['INSTRUCTION', 'The Java bytecode instructions executed during testing'], + ['BRANCH', 'The branches in conditional statements like if, switch, or loops that are executed.'], + ['LINE', 'The source code lines covered by the tests.'], +]); + +interface EntityData { + entity: CoverageEntity; + isFailed: boolean; + expected: number; + actual: number; +} + +interface SummaryBuilder { + addHeading(text: string, level: string): SummaryBuilder; + addRaw(text: string, addEOL?: boolean): SummaryBuilder; + addEOL(): SummaryBuilder; + stringify(): string; +} + +export interface BuildCommentBodyParams { + checkRunsContent: string; + commentTitle: string; + commentMarker: string; + core: { summary: SummaryBuilder }; + env?: Record; +} + +function buildViewSummaryData(checkRun: CheckRunResult): EntityData[] { + const entitiesRules = checkRun.coverageRules?.entitiesRules || {}; + const entityToExpectedPercents = new Map(); + for (const [entityName, entityConfig] of Object.entries(entitiesRules)) { + const percents = entityConfig.minCoverageRatio * 100; + entityToExpectedPercents.set(entityName, percents); + } + + const entityToActualPercents = checkRun.coverageInfo.reduce((acc, it) => { + if (it.total !== 0) { + acc.set(it.coverageEntity, it.percents); + } + return acc; + }, new Map()); + + const shouldFailOnViolations = checkRun.coverageRules?.failOnViolation || false; + return ENTITIES.map((entity) => { + const expectedPercents = entityToExpectedPercents.get(entity) || NO_VALUE; + const actualPercents = entityToActualPercents.get(entity) !== undefined + ? entityToActualPercents.get(entity)! + : NO_VALUE; + + const isFailed = shouldFailOnViolations + && actualPercents > NO_VALUE + && actualPercents < expectedPercents; + return { entity, isFailed, expected: expectedPercents, actual: actualPercents }; + }); +} + +function buildProgressImg(entityData: EntityData): string { + let imageLink: string; + if (entityData.actual > NO_VALUE) { + const color = entityData.actual < entityData.expected ? FAILURE_COLOR : SUCCESS_COLOR; + const actualInteger = Math.floor(entityData.actual); + imageLink = `https://progress-bar.xyz/${actualInteger}/?progress_color=${color}`; + } else { + imageLink = `https://progress-bar.xyz/100/?show_text=false&width=38` + + `&title=${NO_COVERAGE_TEXT}&color=${NO_COVERAGE_COLOR}&progress_color=${NO_COVERAGE_COLOR}`; + } + return ``; +} + +function buildExpectedValue(entityData: EntityData): string { + return (entityData.expected > NO_VALUE) ? `🎯 ${entityData.expected}% 🎯` : ''; +} + +function buildCoverageValueColumnHtml( + entityData: EntityData, + entityIndex: number, + shouldFold: boolean, + valueProvider: (data: EntityData) => string, +): string { + if (shouldFold && entityIndex > 0) { + return ''; + } + const value = valueProvider(entityData); + const rowSpanAttr = (shouldFold && entityIndex === 0) ? 'rowspan=3' : ''; + return `${value}`; +} + +function obtainUniqueValuesSet(viewSummaryData: EntityData[], valueProvider: (data: EntityData) => T): Set { + const allValues = viewSummaryData.map(it => valueProvider(it)); + return new Set(allValues); +} + +function buildCheckRunForViewText(checkRun: CheckRunResult): string { + const viewSummaryData = buildViewSummaryData(checkRun); + + const hasFailure = viewSummaryData.some(it => it.isFailed); + const statusSymbol = hasFailure ? 'πŸ”΄' : '🟒'; + const viewCellValue = `${statusSymbol} ${checkRun.viewName}`; + + const foldExpectedColumn = obtainUniqueValuesSet(viewSummaryData, it => it.expected).size === 1; + const actualUniqueValues = obtainUniqueValuesSet(viewSummaryData, it => it.actual); + const foldActualColumn = actualUniqueValues.size === 1 + && (foldExpectedColumn || actualUniqueValues.has(NO_VALUE)); + + return viewSummaryData.map((entityData, index) => { + const viewCellInRow = (index === 0) ? viewCellValue : ''; + const actualColumnHtml = buildCoverageValueColumnHtml(entityData, index, foldActualColumn, buildProgressImg); + const ruleColumnHtml = buildCoverageValueColumnHtml(entityData, index, foldExpectedColumn, buildExpectedValue); + const toolTipText = TOOLTIPS.get(entityData.entity) || ''; + + return ` +${viewCellInRow} +${ruleColumnHtml} +${entityData.entity} +${actualColumnHtml} +`.trim().replace(/^ +/gm, ''); + }).join('\n'); +} + +function renderHeaders(): string { + return '' + HEADERS.map(it => `${it}`).join('\n') + ''; +} + +export function buildCommentBody(params: BuildCommentBodyParams): string { + const env = params.env || process.env; + const { GITHUB_SERVER_URL, GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_RUN_NUMBER, GITHUB_RUN_ATTEMPT } = env; + + const workflowUrl = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`; + const workflowNum = `${GITHUB_RUN_NUMBER}.${GITHUB_RUN_ATTEMPT}`; + + const buildRunMetaText = (): string => { + const workflowRunDate = new Date(); + const options: Intl.DateTimeFormatOptions = { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }; + const workflowRunLink = `[Run ${workflowNum}](${workflowUrl})`; + const formattedDate = workflowRunDate.toLocaleString('en-US', options); + return `${workflowRunLink} | \`${formattedDate}\``; + }; + + const checkRuns: CheckRunResult[] = JSON.parse(params.checkRunsContent); + let summaryBuffer = params.core.summary + .addHeading(params.commentTitle, '2') + .addRaw(params.commentMarker, true) + .addEOL() + .addRaw('') + .addRaw(renderHeaders()); + + checkRuns.forEach(checkRun => { + const runText = buildCheckRunForViewText(checkRun); + summaryBuffer = summaryBuffer.addRaw(runText, true); + }); + + return summaryBuffer + .addRaw('
') + .addEOL() + .addEOL() + .addRaw(buildRunMetaText()) + .stringify(); +} diff --git a/src/create-check-runs.js b/src/create-check-runs.js deleted file mode 100644 index 67b05e0..0000000 --- a/src/create-check-runs.js +++ /dev/null @@ -1,79 +0,0 @@ -module.exports = async (ctx) => { - const fs = require('fs'); - - const buildPathToReport = (viewName) => { - return `build/reports/coverage-reports/delta-coverage/${viewName}/report.md`; - }; - - const viewHasViolations = (view) => { - return view.verifications.length > 0; - }; - - const readViewMarkdownReport = (view) => { - const reportPath = buildPathToReport(view.view); - try { - return fs.readFileSync(reportPath, 'utf8'); - } catch (e) { - return `NO REPORT by path: ${reportPath}`; - } - } - - const capitalize = (s) => { - return s.charAt(0).toUpperCase() + s.slice(1); - } - - const computeViewConclusion = (view) => { - return !ctx.ignoreCoverageFailure && viewHasViolations(view) - ? 'failure' - : 'success'; - } - - const createCheckRun = async (view) => { - const conclusion = computeViewConclusion(view); - const viewName = capitalize(view.view); - const summaryExtra = ctx.summaryExtraFun ? ctx.summaryExtraFun(view) : ''; - const summary = `${readViewMarkdownReport(view)}\n\n\n${summaryExtra}`; - - const response = await ctx.github.rest.checks.create({ - owner: ctx.context.repo.owner, - repo: ctx.context.repo.repo, - name: `πŸ“ˆ${viewName} Coverage`, - head_sha: ctx.headSha, - status: 'completed', - external_id: ctx.externalId, - conclusion: conclusion, - output: { - title: `${viewName} Coverage`, - summary: summary, - } - }); - return { - viewName: viewName, - verifications: view.verifications, - coverageRules: view.coverageRulesConfig, - url: response.data.html_url, - conclusion: conclusion, - coverageInfo: view.coverageInfo - } - } - - const createAnnotations = (view) => { - const hasViolations = viewHasViolations(view); - if (hasViolations) { - const viewName = capitalize(view.view); - const violations = view.verifications.map(it => it.violation).join(';\n'); - const msg = `[${viewName}]: Code Coverage check failed:\n${violations}`; - ctx.core.error(msg); - } - } - - const reportContent = fs.readFileSync(ctx.summaryReportPath); - const summaryArray = JSON.parse(reportContent); - const checkRuns = []; - for (const view of summaryArray) { - createAnnotations(view); - const checkRun = await createCheckRun(view); - checkRuns.push(checkRun); - } - return checkRuns; -}; diff --git a/src/create-check-runs.ts b/src/create-check-runs.ts new file mode 100644 index 0000000..86cda13 --- /dev/null +++ b/src/create-check-runs.ts @@ -0,0 +1,111 @@ +import * as fs from 'fs'; +import { CoverageSummary, CheckRunResult } from './types/coverage'; + +export interface GitHubClient { + rest: { + checks: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + create(params: any): Promise<{ data: { html_url: string | null } }>; + }; + }; +} + +export interface GitHubContext { + repo: { owner: string; repo: string }; +} + +export interface CoreLogger { + error(message: string): void; +} + +export interface CreateCheckRunsParams { + summaryReportPath: string; + ignoreCoverageFailure: boolean; + core: CoreLogger; + context: GitHubContext; + github: GitHubClient; + headSha: string; + externalId: string; + summaryExtraFun?: (view: CoverageSummary) => string; +} + +function buildPathToReport(viewName: string): string { + return `build/reports/coverage-reports/delta-coverage/${viewName}/report.md`; +} + +function viewHasViolations(view: CoverageSummary): boolean { + return view.verifications.length > 0; +} + +function readViewMarkdownReport(view: CoverageSummary): string { + const reportPath = buildPathToReport(view.view); + try { + return fs.readFileSync(reportPath, 'utf8'); + } catch { + return `NO REPORT by path: ${reportPath}`; + } +} + +function capitalize(s: string): string { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +function computeViewConclusion(view: CoverageSummary, ignoreCoverageFailure: boolean): 'success' | 'failure' { + return !ignoreCoverageFailure && viewHasViolations(view) ? 'failure' : 'success'; +} + +async function createCheckRun( + view: CoverageSummary, + params: CreateCheckRunsParams, +): Promise { + const conclusion = computeViewConclusion(view, params.ignoreCoverageFailure); + const viewName = capitalize(view.view); + const summaryExtra = params.summaryExtraFun ? params.summaryExtraFun(view) : ''; + const summary = `${readViewMarkdownReport(view)}\n\n\n${summaryExtra}`; + + const response = await params.github.rest.checks.create({ + owner: params.context.repo.owner, + repo: params.context.repo.repo, + name: `πŸ“ˆ${viewName} Coverage`, + head_sha: params.headSha, + status: 'completed', + external_id: params.externalId, + conclusion, + output: { + title: `${viewName} Coverage`, + summary, + }, + }); + + return { + viewName, + verifications: view.verifications, + coverageRules: view.coverageRulesConfig, + url: response.data.html_url || '', + conclusion, + coverageInfo: view.coverageInfo, + }; +} + +function createAnnotations(view: CoverageSummary, core: CoreLogger): void { + if (viewHasViolations(view)) { + const viewName = capitalize(view.view); + const violations = view.verifications.map(it => it.violation).join(';\n'); + const msg = `[${viewName}]: Code Coverage check failed:\n${violations}`; + core.error(msg); + } +} + +export async function createCheckRuns(params: CreateCheckRunsParams): Promise { + const reportContent = fs.readFileSync(params.summaryReportPath, 'utf8'); + const summaryArray: CoverageSummary[] = JSON.parse(reportContent); + const checkRuns: CheckRunResult[] = []; + + for (const view of summaryArray) { + createAnnotations(view, params.core); + const checkRun = await createCheckRun(view, params); + checkRuns.push(checkRun); + } + + return checkRuns; +} diff --git a/src/generate-badges.js b/src/generate-badges.js deleted file mode 100644 index 6a27fa7..0000000 --- a/src/generate-badges.js +++ /dev/null @@ -1,55 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const gradientBadge = require('gradient-badge'); - -const badgesOutputDir = 'badges/'; - -const secondColor = '#117efa'; // blue -const firstColors = [ - '#ea00ff', // purple - '#16a41f', // green - '#16019f', // blue - '#ff1500', // red - '#ffcc00', // yellow -]; - -const normalizeColor = (color) => color.replace('#', ''); - -const mapToBadgeInputs = (index, summary) => { - const lineCoverage = summary.coverageInfo.find(coverage => coverage.coverageEntity === 'LINE'); - const firstColor = firstColors[index % firstColors.length]; - return { - subject: summary.view, - status: `${lineCoverage.percents}%`, - gradient: [normalizeColor(firstColor), normalizeColor(secondColor)], - }; -}; - -module.exports = async (ctx) => { - const { core, summariesFile } = ctx; - - fs.mkdirSync(badgesOutputDir, {recursive: true}); - - const summaries = JSON.parse(fs.readFileSync(summariesFile, 'utf8')); - summaries - .sort((a, b) => a.view.localeCompare(b.view)) - .map((summary, index) => { - return { - view: summary.view, - badgeInputs: mapToBadgeInputs(index, summary), - } - }) - .map(viewBadgeData => { - return { - view: viewBadgeData.view, - file: path.join(badgesOutputDir, `${viewBadgeData.view}.svg`), - badgeContent: gradientBadge(viewBadgeData.badgeInputs), - }; - }).forEach(badge => { - fs.writeFileSync(badge.file, badge.badgeContent); - core.info(`🏷️ Generated badge for ${badge.view} at ${badge.file}`); - core.setOutput(badge.view, badge.file); - }); - - core.setOutput('badges-dir', badgesOutputDir); -}; diff --git a/src/generate-badges.ts b/src/generate-badges.ts new file mode 100644 index 0000000..5272d36 --- /dev/null +++ b/src/generate-badges.ts @@ -0,0 +1,68 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { CoverageSummary } from './types/coverage'; + +const BADGES_OUTPUT_DIR = 'badges/'; + +const SECOND_COLOR = '#117efa'; // blue +const FIRST_COLORS = [ + '#ea00ff', // purple + '#16a41f', // green + '#16019f', // dark blue + '#ff1500', // red + '#ffcc00', // yellow +]; + +export interface GenerateBadgesParams { + summariesFile: string; + core: { + info(message: string): void; + setOutput(name: string, value: string): void; + }; + outputDir?: string; +} + +interface BadgeInputs { + subject: string; + status: string; + gradient: [string, string]; +} + +const normalizeColor = (color: string): string => color.replace('#', ''); + +function mapToBadgeInputs(index: number, summary: CoverageSummary): BadgeInputs { + const lineCoverage = summary.coverageInfo.find(c => c.coverageEntity === 'LINE'); + const firstColor = FIRST_COLORS[index % FIRST_COLORS.length]; + return { + subject: summary.view, + status: `${lineCoverage!.percents}%`, + gradient: [normalizeColor(firstColor), normalizeColor(SECOND_COLOR)], + }; +} + +export function generateBadges(params: GenerateBadgesParams): void { + const { core, summariesFile, outputDir = BADGES_OUTPUT_DIR } = params; + const gradientBadge = require('gradient-badge'); + + fs.mkdirSync(outputDir, { recursive: true }); + + const summaries: CoverageSummary[] = JSON.parse(fs.readFileSync(summariesFile, 'utf8')); + summaries + .sort((a, b) => a.view.localeCompare(b.view)) + .map((summary, index) => ({ + view: summary.view, + badgeInputs: mapToBadgeInputs(index, summary), + })) + .map(viewBadgeData => ({ + view: viewBadgeData.view, + file: path.join(outputDir, `${viewBadgeData.view}.svg`), + badgeContent: gradientBadge(viewBadgeData.badgeInputs), + })) + .forEach(badge => { + fs.writeFileSync(badge.file, badge.badgeContent); + core.info(`Generated badge for ${badge.view} at ${badge.file}`); + core.setOutput(badge.view, badge.file); + }); + + core.setOutput('badges-dir', outputDir); +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..ef25609 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,111 @@ +import * as core from '@actions/core'; +import * as github from '@actions/github'; +import { readSummaries } from './read-summaries'; +import { generateBadges } from './generate-badges'; +import { checkSuppression } from './suppression'; +import { createCheckRuns } from './create-check-runs'; +import { buildCommentBody } from './build-comment-body'; +import { buildCommentMarker, findExistingComment, upsertComment } from './pr-comments'; +import { CoverageSummary } from './types/coverage'; + +async function run(): Promise { + try { + const token = core.getInput('github-token'); + const octokit = github.getOctokit(token); + const context = github.context; + + const summariesBasePath = core.getInput('summary-report-base-path'); + const title = core.getInput('title'); + const suppressInput = core.getInput('suppress-check-failures') === 'true'; + const externalId = core.getInput('external-id'); + const extraRenderScriptInput = core.getInput('check-run-extra-render-script'); + + // Step 1: Read summaries + const deltaSummariesFile = readSummaries({ + isFullCoverageMode: false, + baseSummariesPath: summariesBasePath, + }); + const fullCovSummariesFile = readSummaries({ + isFullCoverageMode: true, + baseSummariesPath: summariesBasePath, + }); + + // Step 2: Generate badges (continue on error) + try { + generateBadges({ + summariesFile: fullCovSummariesFile, + core, + }); + } catch (e) { + core.warning(`Badge generation failed: ${e}`); + } + + // Step 3: Check suppression + const pullNumber = context.payload.pull_request?.number ?? 0; + const suppress = await checkSuppression({ + github: octokit, + context, + pullNumber, + suppressByInput: suppressInput, + core, + }); + + // Step 4: Create check runs + const extraRenderScript = (view: CoverageSummary): string => { + if (!extraRenderScriptInput) return ''; + try { + const script = new Function('return ' + extraRenderScriptInput)(); + return script(view); + } catch (e) { + return `Error in custom script: ${e}`; + } + }; + + const headSha = context.payload.pull_request?.head?.sha ?? context.sha; + const checkRuns = await createCheckRuns({ + summaryReportPath: deltaSummariesFile, + ignoreCoverageFailure: suppress, + core, + context, + github: octokit, + headSha, + externalId, + summaryExtraFun: extraRenderScript, + }); + + // Step 5: Post PR comment (only for pull requests) + if (context.eventName === 'pull_request') { + const marker = title ? buildCommentMarker(title) : ''; + const body = buildCommentBody({ + checkRunsContent: JSON.stringify(checkRuns), + commentTitle: title, + commentMarker: marker, + core, + }); + + let existingCommentId: number | undefined; + if (marker) { + existingCommentId = await findExistingComment({ + github: octokit, + context, + marker, + }); + } + + await upsertComment({ + github: octokit, + context, + existingCommentId, + body, + }); + } + } catch (error) { + if (error instanceof Error) { + core.setFailed(error.message); + } else { + core.setFailed(String(error)); + } + } +} + +run(); diff --git a/src/pr-comments.ts b/src/pr-comments.ts new file mode 100644 index 0000000..40c53eb --- /dev/null +++ b/src/pr-comments.ts @@ -0,0 +1,73 @@ +export interface PrCommentsGitHubClient { + rest: { + issues: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + listComments(params: any): Promise<{ data: Array<{ id: number; body?: string }> }>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createComment(params: any): Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updateComment(params: any): Promise; + }; + }; +} + +export interface FindExistingCommentParams { + github: PrCommentsGitHubClient; + context: { + issue: { number: number }; + repo: { owner: string; repo: string }; + }; + marker: string; +} + +export async function findExistingComment(params: FindExistingCommentParams): Promise { + const { github, context, marker } = params; + + const response = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + }); + + const comment = response.data.find(it => it.body?.includes(marker)); + if (comment) { + console.log(`Comment found: ${comment.id}`); + return comment.id; + } + return undefined; +} + +export interface UpsertCommentParams { + github: PrCommentsGitHubClient; + context: { + issue: { number: number }; + repo: { owner: string; repo: string }; + }; + existingCommentId?: number; + body: string; +} + +export async function upsertComment(params: UpsertCommentParams): Promise { + const { github, context, existingCommentId, body } = params; + + if (existingCommentId) { + await github.rest.issues.updateComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingCommentId, + body, + }); + } else { + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body, + }); + } +} + +export function buildCommentMarker(title: string): string { + return ``; +} diff --git a/src/read-summaries.js b/src/read-summaries.js deleted file mode 100644 index 6b8dbab..0000000 --- a/src/read-summaries.js +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = async (ctx) => { - const fs = require('fs'); - - const fullCoverageFilter = file => file.includes('full-coverage-'); - const deltaCoverageFilter = file => !fullCoverageFilter(file); - - const allSummariesFile = ctx.isFullCoverageMode ? 'full-cov-summaries.json' : 'delta-cov-summaries.json'; - - const chosenFilter = ctx.isFullCoverageMode ? fullCoverageFilter : deltaCoverageFilter; - - const files = fs.readdirSync(ctx.baseSummariesPath); - const summaryFiles = files.filter(file => file.includes('-summary.json')).filter(chosenFilter); - console.log(`Reading summaries from ${ctx.baseSummariesPath}: ${JSON.stringify(summaryFiles, null, 2)}`); - - const summaries = summaryFiles.map(file => - JSON.parse(fs.readFileSync(`${ctx.baseSummariesPath}/${file}`, 'utf8')) - ); - fs.writeFileSync(allSummariesFile, JSON.stringify(summaries)); - - return allSummariesFile; -}; diff --git a/src/read-summaries.ts b/src/read-summaries.ts new file mode 100644 index 0000000..a49611f --- /dev/null +++ b/src/read-summaries.ts @@ -0,0 +1,32 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { CoverageSummary } from './types/coverage'; + +export interface ReadSummariesParams { + isFullCoverageMode: boolean; + baseSummariesPath: string; +} + +export function readSummaries(params: ReadSummariesParams): string { + const { isFullCoverageMode, baseSummariesPath } = params; + + const fullCoverageFilter = (file: string) => file.includes('full-coverage-'); + const deltaCoverageFilter = (file: string) => !fullCoverageFilter(file); + + const allSummariesFile = isFullCoverageMode ? 'full-cov-summaries.json' : 'delta-cov-summaries.json'; + const chosenFilter = isFullCoverageMode ? fullCoverageFilter : deltaCoverageFilter; + + const files = fs.readdirSync(baseSummariesPath); + const summaryFiles = files + .filter(file => file.includes('-summary.json')) + .filter(chosenFilter); + console.log(`Reading summaries from ${baseSummariesPath}: ${JSON.stringify(summaryFiles, null, 2)}`); + + const summaries: CoverageSummary[] = summaryFiles.map(file => + JSON.parse(fs.readFileSync(path.join(baseSummariesPath, file), 'utf8')) + ); + fs.writeFileSync(allSummariesFile, JSON.stringify(summaries)); + + return allSummariesFile; +} + diff --git a/src/suppression.ts b/src/suppression.ts new file mode 100644 index 0000000..c837d7d --- /dev/null +++ b/src/suppression.ts @@ -0,0 +1,37 @@ +export interface SuppressionGitHubClient { + rest: { + pulls: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + get(params: any): Promise<{ data: { labels: Array<{ name: string }> } }>; + }; + }; +} + +export interface CheckSuppressionParams { + github: SuppressionGitHubClient; + context: { + repo: { owner: string; repo: string }; + }; + pullNumber: number; + suppressByInput: boolean; + core: { info(message: string): void }; +} + +export async function checkSuppression(params: CheckSuppressionParams): Promise { + const { github, context, pullNumber, suppressByInput, core } = params; + + let suppressedByLabel = false; + if (pullNumber > 0) { + const response = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pullNumber, + }); + const labels = response.data.labels.map(l => l.name); + suppressedByLabel = labels.includes('suppress-coverage'); + } + + const resolution = suppressedByLabel || suppressByInput; + core.info(`Is suppress=${resolution} : by label=${suppressedByLabel}, by input=${suppressByInput}`); + return resolution; +} diff --git a/src/types/coverage.ts b/src/types/coverage.ts new file mode 100644 index 0000000..7717655 --- /dev/null +++ b/src/types/coverage.ts @@ -0,0 +1,38 @@ +export type CoverageEntity = 'INSTRUCTION' | 'BRANCH' | 'LINE'; + +export interface CoverageInfo { + coverageEntity: CoverageEntity; + covered: number; + total: number; + percents: number; +} + +export interface EntityRule { + minCoverageRatio: number; +} + +export interface CoverageRulesConfig { + failOnViolation: boolean; + entitiesRules: Record; +} + +export interface Verification { + violation: string; +} + +export interface CoverageSummary { + view: string; + reportBound: string; + coverageRulesConfig: CoverageRulesConfig; + verifications: Verification[]; + coverageInfo: CoverageInfo[]; +} + +export interface CheckRunResult { + viewName: string; + verifications: Verification[]; + coverageRules: CoverageRulesConfig; + url: string; + conclusion: 'success' | 'failure'; + coverageInfo: CoverageInfo[]; +} diff --git a/test/build-comment-body.test.ts b/test/build-comment-body.test.ts new file mode 100644 index 0000000..dfc46ed --- /dev/null +++ b/test/build-comment-body.test.ts @@ -0,0 +1,274 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { buildCommentBody, BuildCommentBodyParams } from '../src/build-comment-body'; +import { CheckRunResult } from '../src/types/coverage'; + +function createMockSummaryBuilder() { + let buffer = ''; + const builder: any = { + addHeading: vi.fn((text: string, _level: string) => { buffer += `

${text}

`; return builder; }), + addRaw: vi.fn((text: string, _addEOL?: boolean) => { buffer += text; return builder; }), + addEOL: vi.fn(() => { buffer += '\n'; return builder; }), + stringify: vi.fn(() => buffer), + }; + return builder; +} + +function makeCheckRun(overrides: Partial = {}): CheckRunResult { + return { + viewName: 'Test', + verifications: [], + coverageRules: { failOnViolation: false, entitiesRules: {} }, + url: 'https://github.com/owner/repo/runs/123', + conclusion: 'success', + coverageInfo: [ + { coverageEntity: 'INSTRUCTION', covered: 80, total: 100, percents: 80 }, + { coverageEntity: 'BRANCH', covered: 50, total: 100, percents: 50 }, + { coverageEntity: 'LINE', covered: 70, total: 100, percents: 70 }, + ], + ...overrides, + }; +} + +const defaultEnv = { + GITHUB_SERVER_URL: 'https://github.com', + GITHUB_REPOSITORY: 'owner/repo', + GITHUB_RUN_ID: '12345', + GITHUB_RUN_NUMBER: '42', + GITHUB_RUN_ATTEMPT: '1', +}; + +describe('buildCommentBody', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should render table with check run data', () => { + const checkRun = makeCheckRun(); + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([checkRun]), + commentTitle: 'Coverage Report', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain(''); + expect(result).toContain('
'); + expect(result).toContain('Test'); + expect(result).toContain('INSTRUCTION'); + expect(result).toContain('BRANCH'); + expect(result).toContain('LINE'); + }); + + it('should include comment marker', () => { + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([makeCheckRun()]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain(''); + }); + + it('should show success status symbol for passing checks', () => { + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([makeCheckRun({ conclusion: 'success' })]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('🟒'); + expect(result).not.toContain('πŸ”΄'); + }); + + it('should show failure status symbol when violations exist and failOnViolation is true', () => { + const checkRun = makeCheckRun({ + coverageRules: { + failOnViolation: true, + entitiesRules: { + INSTRUCTION: { minCoverageRatio: 0.9 }, + BRANCH: { minCoverageRatio: 0.9 }, + LINE: { minCoverageRatio: 0.9 }, + }, + }, + }); + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([checkRun]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('πŸ”΄'); + }); + + it('should use progress bar with success color when actual >= expected', () => { + const checkRun = makeCheckRun({ + coverageRules: { + failOnViolation: true, + entitiesRules: { LINE: { minCoverageRatio: 0.5 } }, + }, + }); + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([checkRun]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('progress_color=7AB56D'); + }); + + it('should use progress bar with failure color when actual < expected', () => { + const checkRun = makeCheckRun({ + coverageRules: { + failOnViolation: true, + entitiesRules: { + INSTRUCTION: { minCoverageRatio: 0.99 }, + }, + }, + }); + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([checkRun]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('progress_color=C4625A'); + }); + + it('should show no-coverage indicator when total is 0', () => { + const checkRun = makeCheckRun({ + coverageInfo: [ + { coverageEntity: 'INSTRUCTION', covered: 0, total: 0, percents: 0 }, + { coverageEntity: 'BRANCH', covered: 0, total: 0, percents: 0 }, + { coverageEntity: 'LINE', covered: 0, total: 0, percents: 0 }, + ], + }); + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([checkRun]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('No%20diff'); + expect(result).toContain('777777'); + }); + + it('should fold expected column when all values are the same', () => { + const checkRun = makeCheckRun({ + coverageRules: { + failOnViolation: true, + entitiesRules: { + INSTRUCTION: { minCoverageRatio: 0.8 }, + BRANCH: { minCoverageRatio: 0.8 }, + LINE: { minCoverageRatio: 0.8 }, + }, + }, + }); + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([checkRun]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('rowspan=3'); + }); + + it('should include workflow metadata link', () => { + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([makeCheckRun()]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('Run 42.1'); + expect(result).toContain('https://github.com/owner/repo/actions/runs/12345'); + }); + + it('should include entity tooltips', () => { + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([makeCheckRun()]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('title="The Java bytecode instructions executed during testing"'); + expect(result).toContain('title="The source code lines covered by the tests."'); + }); + + it('should render multiple check runs', () => { + const checkRuns = [ + makeCheckRun({ viewName: 'Test' }), + makeCheckRun({ viewName: 'FunctionalTest' }), + ]; + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify(checkRuns), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('Test'); + expect(result).toContain('FunctionalTest'); + }); + + it('should show expected percentage with target emoji', () => { + const checkRun = makeCheckRun({ + coverageRules: { + failOnViolation: true, + entitiesRules: { LINE: { minCoverageRatio: 0.75 } }, + }, + }); + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([checkRun]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('🎯 75% 🎯'); + }); +}); diff --git a/test/create-check-runs.test.ts b/test/create-check-runs.test.ts new file mode 100644 index 0000000..92b97e6 --- /dev/null +++ b/test/create-check-runs.test.ts @@ -0,0 +1,205 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import * as fs from 'fs'; +import { CoverageSummary } from '../src/types/coverage'; + +vi.mock('fs', () => ({ + readFileSync: vi.fn(), +})); + +const mockedFs = vi.mocked(fs); + +import { createCheckRuns, CreateCheckRunsParams } from '../src/create-check-runs'; + +function makeSummary(overrides: Partial = {}): CoverageSummary { + return { + view: 'test', + reportBound: 'DELTA_REPORT', + coverageRulesConfig: { failOnViolation: false, entitiesRules: {} }, + verifications: [], + coverageInfo: [ + { coverageEntity: 'INSTRUCTION', covered: 80, total: 100, percents: 80 }, + { coverageEntity: 'BRANCH', covered: 50, total: 100, percents: 50 }, + { coverageEntity: 'LINE', covered: 70, total: 100, percents: 70 }, + ], + ...overrides, + }; +} + +function makeParams(overrides: Partial = {}): CreateCheckRunsParams { + return { + summaryReportPath: 'delta-cov-summaries.json', + ignoreCoverageFailure: false, + core: { error: vi.fn() }, + context: { repo: { owner: 'testOwner', repo: 'testRepo' } }, + github: { + rest: { + checks: { + create: vi.fn().mockResolvedValue({ + data: { html_url: 'https://github.com/testOwner/testRepo/runs/456' }, + }), + }, + }, + }, + headSha: 'abc123', + externalId: 'delta-coverage', + ...overrides, + }; +} + +describe('createCheckRuns', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should create a check run for each view', async () => { + const summaries = [makeSummary({ view: 'test' }), makeSummary({ view: 'functionalTest' })]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + return 'Report content'; + }); + + const params = makeParams(); + const result = await createCheckRuns(params); + + expect(result).toHaveLength(2); + expect(result[0].viewName).toBe('Test'); + expect(result[1].viewName).toBe('FunctionalTest'); + expect(params.github.rest.checks.create).toHaveBeenCalledTimes(2); + }); + + it('should set conclusion to success when no violations', async () => { + const summaries = [makeSummary({ verifications: [] })]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + return 'Report content'; + }); + + const params = makeParams(); + const result = await createCheckRuns(params); + + expect(result[0].conclusion).toBe('success'); + }); + + it('should set conclusion to failure when violations exist', async () => { + const summaries = [makeSummary({ verifications: [{ violation: 'LINE: min=80%, actual=50%' }] })]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + return 'Report content'; + }); + + const params = makeParams({ ignoreCoverageFailure: false }); + const result = await createCheckRuns(params); + + expect(result[0].conclusion).toBe('failure'); + }); + + it('should suppress failures when ignoreCoverageFailure is true', async () => { + const summaries = [makeSummary({ verifications: [{ violation: 'LINE: min=80%, actual=50%' }] })]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + return 'Report content'; + }); + + const params = makeParams({ ignoreCoverageFailure: true }); + const result = await createCheckRuns(params); + + expect(result[0].conclusion).toBe('success'); + }); + + it('should log error annotations for violations', async () => { + const summaries = [makeSummary({ + view: 'test', + verifications: [{ violation: 'LINE: min=80%, actual=50%' }], + })]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + return 'Report content'; + }); + + const params = makeParams(); + await createCheckRuns(params); + + expect(params.core.error).toHaveBeenCalledWith( + expect.stringContaining('[Test]: Code Coverage check failed:') + ); + }); + + it('should handle missing report file gracefully', async () => { + const summaries = [makeSummary({ view: 'test' })]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + throw new Error('ENOENT'); + }); + + const params = makeParams(); + const result = await createCheckRuns(params); + + expect(result[0]).toBeDefined(); + expect(params.github.rest.checks.create).toHaveBeenCalledWith( + expect.objectContaining({ + output: expect.objectContaining({ + summary: expect.stringContaining('NO REPORT by path:'), + }), + }) + ); + }); + + it('should pass correct parameters to checks.create', async () => { + const summaries = [makeSummary({ view: 'test' })]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + return 'Report content'; + }); + + const params = makeParams(); + await createCheckRuns(params); + + expect(params.github.rest.checks.create).toHaveBeenCalledWith({ + owner: 'testOwner', + repo: 'testRepo', + name: 'πŸ“ˆTest Coverage', + head_sha: 'abc123', + status: 'completed', + external_id: 'delta-coverage', + conclusion: 'success', + output: { + title: 'Test Coverage', + summary: expect.stringContaining('Report content'), + }, + }); + }); + + it('should include summaryExtraFun output in check run', async () => { + const summaries = [makeSummary({ view: 'test' })]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + return 'Report content'; + }); + + const extraFun = vi.fn().mockReturnValue('data'); + const params = makeParams({ summaryExtraFun: extraFun }); + await createCheckRuns(params); + + expect(extraFun).toHaveBeenCalledWith(summaries[0]); + expect(params.github.rest.checks.create).toHaveBeenCalledWith( + expect.objectContaining({ + output: expect.objectContaining({ + summary: expect.stringContaining('data'), + }), + }) + ); + }); + + it('should return check run URL from API response', async () => { + const summaries = [makeSummary()]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + return 'Report content'; + }); + + const params = makeParams(); + const result = await createCheckRuns(params); + + expect(result[0].url).toBe('https://github.com/testOwner/testRepo/runs/456'); + }); +}); diff --git a/test/data/delta-coverage-functionalTest-summary.json b/test/data/delta-coverage-functionalTest-summary.json new file mode 100644 index 0000000..21c27fb --- /dev/null +++ b/test/data/delta-coverage-functionalTest-summary.json @@ -0,0 +1 @@ +{"view":"functionalTest","reportBound":"DELTA_REPORT","coverageRulesConfig":{"failOnViolation":true,"entitiesRules":{"INSTRUCTION":{"minCoverageRatio":0.6},"BRANCH":{"minCoverageRatio":0.4},"LINE":{"minCoverageRatio":0.6}}},"verifications":[{"violation":"BRANCH: expectedMin=40.0%, actual=35.0%"}],"coverageInfo":[{"coverageEntity":"INSTRUCTION","covered":80,"total":120,"percents":66.67},{"coverageEntity":"BRANCH","covered":14,"total":40,"percents":35.0},{"coverageEntity":"LINE","covered":70,"total":100,"percents":70.0}]} diff --git a/test/data/delta-coverage-test-summary.json b/test/data/delta-coverage-test-summary.json new file mode 100644 index 0000000..a4c5cf1 --- /dev/null +++ b/test/data/delta-coverage-test-summary.json @@ -0,0 +1 @@ +{"view":"test","reportBound":"DELTA_REPORT","coverageRulesConfig":{"failOnViolation":true,"entitiesRules":{"INSTRUCTION":{"minCoverageRatio":0.8},"BRANCH":{"minCoverageRatio":0.5},"LINE":{"minCoverageRatio":0.8}}},"verifications":[],"coverageInfo":[{"coverageEntity":"INSTRUCTION","covered":120,"total":150,"percents":80.0},{"coverageEntity":"BRANCH","covered":30,"total":50,"percents":60.0},{"coverageEntity":"LINE","covered":100,"total":120,"percents":83.33}]} diff --git a/test/generate-badges.test.ts b/test/generate-badges.test.ts new file mode 100644 index 0000000..a82c16e --- /dev/null +++ b/test/generate-badges.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import * as fs from 'fs'; +import { CoverageSummary } from '../src/types/coverage'; + +vi.mock('fs', () => ({ + mkdirSync: vi.fn(), + readFileSync: vi.fn(), + writeFileSync: vi.fn(), +})); + +vi.mock('gradient-badge', () => ({ + default: vi.fn((inputs: any) => `${inputs.subject}: ${inputs.status}`), +})); + +const mockedFs = vi.mocked(fs); + +import { generateBadges } from '../src/generate-badges'; + +describe('generateBadges', () => { + const mockCore = { + info: vi.fn(), + setOutput: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + const makeSummary = (view: string, linePercent: number): CoverageSummary => ({ + view, + reportBound: 'DELTA_REPORT', + coverageRulesConfig: { failOnViolation: false, entitiesRules: {} }, + verifications: [], + coverageInfo: [ + { coverageEntity: 'INSTRUCTION', covered: 80, total: 100, percents: 80 }, + { coverageEntity: 'BRANCH', covered: 50, total: 100, percents: 50 }, + { coverageEntity: 'LINE', covered: 70, total: 100, percents: linePercent }, + ], + }); + + it('should create badges directory', () => { + mockedFs.readFileSync.mockReturnValue('[]' as any); + + generateBadges({ summariesFile: 'test.json', core: mockCore }); + + expect(mockedFs.mkdirSync).toHaveBeenCalledWith('badges/', { recursive: true }); + }); + + it('should generate badges for each view sorted alphabetically', () => { + const summaries = [ + makeSummary('test', 82.3), + makeSummary('aggregated', 87.96), + ]; + mockedFs.readFileSync.mockReturnValue(JSON.stringify(summaries) as any); + + generateBadges({ summariesFile: 'full-cov-summaries.json', core: mockCore }); + + expect(mockedFs.writeFileSync).toHaveBeenCalledTimes(2); + // Sorted alphabetically: aggregated first, then test + expect(mockedFs.writeFileSync).toHaveBeenCalledWith( + 'badges/aggregated.svg', + expect.stringContaining('aggregated') + ); + expect(mockedFs.writeFileSync).toHaveBeenCalledWith( + 'badges/test.svg', + expect.stringContaining('test') + ); + }); + + it('should set output for each badge and badges-dir', () => { + const summaries = [makeSummary('test', 82.3)]; + mockedFs.readFileSync.mockReturnValue(JSON.stringify(summaries) as any); + + generateBadges({ summariesFile: 'test.json', core: mockCore }); + + expect(mockCore.setOutput).toHaveBeenCalledWith('test', 'badges/test.svg'); + expect(mockCore.setOutput).toHaveBeenCalledWith('badges-dir', 'badges/'); + }); + + it('should use custom output directory', () => { + mockedFs.readFileSync.mockReturnValue('[]' as any); + + generateBadges({ summariesFile: 'test.json', core: mockCore, outputDir: 'custom/' }); + + expect(mockedFs.mkdirSync).toHaveBeenCalledWith('custom/', { recursive: true }); + expect(mockCore.setOutput).toHaveBeenCalledWith('badges-dir', 'custom/'); + }); + + it('should cycle through colors for many views', () => { + const summaries = [ + makeSummary('a', 80), + makeSummary('b', 70), + makeSummary('c', 60), + makeSummary('d', 50), + makeSummary('e', 40), + makeSummary('f', 30), // cycles back to first color + ]; + mockedFs.readFileSync.mockReturnValue(JSON.stringify(summaries) as any); + + generateBadges({ summariesFile: 'test.json', core: mockCore }); + + expect(mockedFs.writeFileSync).toHaveBeenCalledTimes(6); + }); + + it('should handle empty summaries', () => { + mockedFs.readFileSync.mockReturnValue('[]' as any); + + generateBadges({ summariesFile: 'test.json', core: mockCore }); + + expect(mockedFs.writeFileSync).not.toHaveBeenCalled(); + expect(mockCore.setOutput).toHaveBeenCalledWith('badges-dir', 'badges/'); + }); +}); diff --git a/test/main.test.ts b/test/main.test.ts new file mode 100644 index 0000000..ecde9c7 --- /dev/null +++ b/test/main.test.ts @@ -0,0 +1,190 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock('@actions/core'); +vi.mock('@actions/github'); +vi.mock('../src/read-summaries'); +vi.mock('../src/generate-badges'); +vi.mock('../src/suppression'); +vi.mock('../src/create-check-runs'); +vi.mock('../src/build-comment-body'); +vi.mock('../src/pr-comments'); + +import * as core from '@actions/core'; +import * as github from '@actions/github'; +import { readSummaries } from '../src/read-summaries'; +import { generateBadges } from '../src/generate-badges'; +import { checkSuppression } from '../src/suppression'; +import { createCheckRuns } from '../src/create-check-runs'; +import { buildCommentBody } from '../src/build-comment-body'; +import { buildCommentMarker, findExistingComment, upsertComment } from '../src/pr-comments'; + +const mockedCore = vi.mocked(core); +const mockedGithub = vi.mocked(github); +const mockedReadSummaries = vi.mocked(readSummaries); +const mockedGenerateBadges = vi.mocked(generateBadges); +const mockedCheckSuppression = vi.mocked(checkSuppression); +const mockedCreateCheckRuns = vi.mocked(createCheckRuns); +const mockedBuildCommentBody = vi.mocked(buildCommentBody); +const mockedBuildCommentMarker = vi.mocked(buildCommentMarker); +const mockedFindExistingComment = vi.mocked(findExistingComment); +const mockedUpsertComment = vi.mocked(upsertComment); + +describe('main', () => { + const mockOctokit = {} as any; + + beforeEach(() => { + vi.clearAllMocks(); + + mockedCore.getInput.mockImplementation((name: string) => { + const inputs: Record = { + 'github-token': 'test-token', + 'summary-report-base-path': 'test/data/', + 'title': 'Coverage Report', + 'suppress-check-failures': 'false', + 'external-id': 'delta-coverage', + 'check-run-extra-render-script': '', + }; + return inputs[name] || ''; + }); + + mockedGithub.getOctokit.mockReturnValue(mockOctokit); + Object.defineProperty(mockedGithub, 'context', { + value: { + eventName: 'pull_request', + sha: 'abc123', + payload: { + pull_request: { number: 42, head: { sha: 'pr-sha-456' } }, + }, + issue: { number: 42 }, + repo: { owner: 'testOwner', repo: 'testRepo' }, + }, + writable: true, + }); + + mockedReadSummaries.mockReturnValueOnce('delta-cov-summaries.json'); + mockedReadSummaries.mockReturnValueOnce('full-cov-summaries.json'); + mockedCheckSuppression.mockResolvedValue(false); + mockedCreateCheckRuns.mockResolvedValue([]); + mockedBuildCommentBody.mockReturnValue('body'); + mockedBuildCommentMarker.mockReturnValue(''); + mockedFindExistingComment.mockResolvedValue(undefined); + mockedUpsertComment.mockResolvedValue(undefined); + }); + + async function runMain() { + // Dynamically import to trigger the run() call + // We need to reset the module cache so it re-executes + vi.resetModules(); + // Re-mock after resetModules + vi.doMock('@actions/core', () => mockedCore); + vi.doMock('@actions/github', () => mockedGithub); + vi.doMock('../src/read-summaries', () => ({ readSummaries: mockedReadSummaries })); + vi.doMock('../src/generate-badges', () => ({ generateBadges: mockedGenerateBadges })); + vi.doMock('../src/suppression', () => ({ checkSuppression: mockedCheckSuppression })); + vi.doMock('../src/create-check-runs', () => ({ createCheckRuns: mockedCreateCheckRuns })); + vi.doMock('../src/build-comment-body', () => ({ buildCommentBody: mockedBuildCommentBody })); + vi.doMock('../src/pr-comments', () => ({ + buildCommentMarker: mockedBuildCommentMarker, + findExistingComment: mockedFindExistingComment, + upsertComment: mockedUpsertComment, + })); + await import('../src/main'); + // Allow microtasks to complete + await new Promise(resolve => setTimeout(resolve, 10)); + } + + it('should read both delta and full coverage summaries', async () => { + await runMain(); + + expect(mockedReadSummaries).toHaveBeenCalledWith({ + isFullCoverageMode: false, + baseSummariesPath: 'test/data/', + }); + expect(mockedReadSummaries).toHaveBeenCalledWith({ + isFullCoverageMode: true, + baseSummariesPath: 'test/data/', + }); + }); + + it('should generate badges from full coverage summaries', async () => { + await runMain(); + + expect(mockedGenerateBadges).toHaveBeenCalledWith({ + summariesFile: 'full-cov-summaries.json', + core: mockedCore, + }); + }); + + it('should check suppression with correct params', async () => { + await runMain(); + + expect(mockedCheckSuppression).toHaveBeenCalledWith({ + github: mockOctokit, + context: expect.objectContaining({ repo: { owner: 'testOwner', repo: 'testRepo' } }), + pullNumber: 42, + suppressByInput: false, + core: mockedCore, + }); + }); + + it('should create check runs with correct params', async () => { + await runMain(); + + expect(mockedCreateCheckRuns).toHaveBeenCalledWith( + expect.objectContaining({ + summaryReportPath: 'delta-cov-summaries.json', + ignoreCoverageFailure: false, + headSha: 'pr-sha-456', + externalId: 'delta-coverage', + }) + ); + }); + + it('should build and post PR comment', async () => { + await runMain(); + + expect(mockedBuildCommentBody).toHaveBeenCalled(); + expect(mockedUpsertComment).toHaveBeenCalledWith( + expect.objectContaining({ + body: 'body', + }) + ); + }); + + it('should find existing comment when marker exists', async () => { + await runMain(); + + expect(mockedFindExistingComment).toHaveBeenCalledWith( + expect.objectContaining({ + marker: '', + }) + ); + }); + + it('should not post comment when not a pull request', async () => { + Object.defineProperty(mockedGithub, 'context', { + value: { + eventName: 'push', + sha: 'abc123', + payload: {}, + repo: { owner: 'testOwner', repo: 'testRepo' }, + }, + writable: true, + }); + + await runMain(); + + expect(mockedUpsertComment).not.toHaveBeenCalled(); + }); + + it('should continue when badge generation fails', async () => { + mockedGenerateBadges.mockImplementation(() => { + throw new Error('badge error'); + }); + + await runMain(); + + expect(mockedCore.warning).toHaveBeenCalledWith(expect.stringContaining('badge error')); + expect(mockedCheckSuppression).toHaveBeenCalled(); + }); +}); diff --git a/test/pr-comments.test.ts b/test/pr-comments.test.ts new file mode 100644 index 0000000..b35f8c4 --- /dev/null +++ b/test/pr-comments.test.ts @@ -0,0 +1,153 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + findExistingComment, + upsertComment, + buildCommentMarker, + FindExistingCommentParams, + UpsertCommentParams, +} from '../src/pr-comments'; + +const mockContext = { + issue: { number: 42 }, + repo: { owner: 'testOwner', repo: 'testRepo' }, +}; + +describe('findExistingComment', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return comment id when marker found', async () => { + const params: FindExistingCommentParams = { + github: { + rest: { + issues: { + listComments: vi.fn().mockResolvedValue({ + data: [ + { id: 1, body: 'some other comment' }, + { id: 2, body: 'has in it' }, + ], + }), + createComment: vi.fn(), + updateComment: vi.fn(), + }, + }, + }, + context: mockContext, + marker: '', + }; + + const result = await findExistingComment(params); + expect(result).toBe(2); + }); + + it('should return undefined when marker not found', async () => { + const params: FindExistingCommentParams = { + github: { + rest: { + issues: { + listComments: vi.fn().mockResolvedValue({ + data: [{ id: 1, body: 'no marker here' }], + }), + createComment: vi.fn(), + updateComment: vi.fn(), + }, + }, + }, + context: mockContext, + marker: '', + }; + + const result = await findExistingComment(params); + expect(result).toBeUndefined(); + }); + + it('should return undefined for empty comments list', async () => { + const params: FindExistingCommentParams = { + github: { + rest: { + issues: { + listComments: vi.fn().mockResolvedValue({ data: [] }), + createComment: vi.fn(), + updateComment: vi.fn(), + }, + }, + }, + context: mockContext, + marker: '', + }; + + const result = await findExistingComment(params); + expect(result).toBeUndefined(); + }); +}); + +describe('upsertComment', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should update existing comment when id is provided', async () => { + const updateComment = vi.fn(); + const createComment = vi.fn(); + const params: UpsertCommentParams = { + github: { + rest: { + issues: { + listComments: vi.fn(), + updateComment, + createComment, + }, + }, + }, + context: mockContext, + existingCommentId: 123, + body: 'updated body', + }; + + await upsertComment(params); + + expect(updateComment).toHaveBeenCalledWith({ + issue_number: 42, + owner: 'testOwner', + repo: 'testRepo', + comment_id: 123, + body: 'updated body', + }); + expect(createComment).not.toHaveBeenCalled(); + }); + + it('should create new comment when no existing id', async () => { + const updateComment = vi.fn(); + const createComment = vi.fn(); + const params: UpsertCommentParams = { + github: { + rest: { + issues: { + listComments: vi.fn(), + updateComment, + createComment, + }, + }, + }, + context: mockContext, + body: 'new comment body', + }; + + await upsertComment(params); + + expect(createComment).toHaveBeenCalledWith({ + issue_number: 42, + owner: 'testOwner', + repo: 'testRepo', + body: 'new comment body', + }); + expect(updateComment).not.toHaveBeenCalled(); + }); +}); + +describe('buildCommentMarker', () => { + it('should build marker from title', () => { + expect(buildCommentMarker('My Title')).toBe(''); + }); +}); diff --git a/test/read-summaries.test.ts b/test/read-summaries.test.ts new file mode 100644 index 0000000..70c288c --- /dev/null +++ b/test/read-summaries.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import * as fs from 'fs'; + +vi.mock('fs', () => ({ + readdirSync: vi.fn(), + readFileSync: vi.fn(), + writeFileSync: vi.fn(), +})); + +const mockedFs = vi.mocked(fs); + +// Import after mock setup +import { readSummaries } from '../src/read-summaries'; + +describe('readSummaries', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should filter and aggregate full-coverage summaries', () => { + mockedFs.readdirSync.mockReturnValue([ + 'full-coverage-test-summary.json', + 'full-coverage-functionalTest-summary.json', + 'delta-coverage-test-summary.json', + ] as any); + + const testSummary = { view: 'test', coverageInfo: [] }; + const funcSummary = { view: 'functionalTest', coverageInfo: [] }; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath).includes('full-coverage-test')) return JSON.stringify(testSummary); + if (String(filePath).includes('full-coverage-functionalTest')) return JSON.stringify(funcSummary); + throw new Error(`Unexpected file: ${filePath}`); + }); + + const result = readSummaries({ + isFullCoverageMode: true, + baseSummariesPath: '/some/path', + }); + + expect(result).toBe('full-cov-summaries.json'); + expect(mockedFs.writeFileSync).toHaveBeenCalledWith( + 'full-cov-summaries.json', + JSON.stringify([testSummary, funcSummary]) + ); + }); + + it('should filter and aggregate delta-coverage summaries', () => { + mockedFs.readdirSync.mockReturnValue([ + 'full-coverage-test-summary.json', + 'delta-coverage-test-summary.json', + 'delta-coverage-functionalTest-summary.json', + ] as any); + + const deltaSummary = { view: 'test', coverageInfo: [] }; + const deltaFuncSummary = { view: 'functionalTest', coverageInfo: [] }; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath).includes('delta-coverage-test')) return JSON.stringify(deltaSummary); + if (String(filePath).includes('delta-coverage-functionalTest')) return JSON.stringify(deltaFuncSummary); + throw new Error(`Unexpected file: ${filePath}`); + }); + + const result = readSummaries({ + isFullCoverageMode: false, + baseSummariesPath: '/some/path', + }); + + expect(result).toBe('delta-cov-summaries.json'); + expect(mockedFs.writeFileSync).toHaveBeenCalledWith( + 'delta-cov-summaries.json', + JSON.stringify([deltaSummary, deltaFuncSummary]) + ); + }); + + it('should handle empty directory', () => { + mockedFs.readdirSync.mockReturnValue([] as any); + + const result = readSummaries({ + isFullCoverageMode: true, + baseSummariesPath: '/empty/dir', + }); + + expect(result).toBe('full-cov-summaries.json'); + expect(mockedFs.writeFileSync).toHaveBeenCalledWith( + 'full-cov-summaries.json', + '[]' + ); + }); + + it('should ignore non-summary files', () => { + mockedFs.readdirSync.mockReturnValue([ + 'full-coverage-test-summary.json', + 'some-other-file.txt', + 'report.json', + ] as any); + + const testSummary = { view: 'test' }; + mockedFs.readFileSync.mockReturnValue(JSON.stringify(testSummary) as any); + + readSummaries({ + isFullCoverageMode: true, + baseSummariesPath: '/path', + }); + + expect(mockedFs.readFileSync).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/suppression.test.ts b/test/suppression.test.ts new file mode 100644 index 0000000..b6f6af3 --- /dev/null +++ b/test/suppression.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { checkSuppression, CheckSuppressionParams } from '../src/suppression'; + +function makeParams(overrides: Partial = {}): CheckSuppressionParams { + return { + github: { + rest: { + pulls: { + get: vi.fn().mockResolvedValue({ + data: { labels: [] }, + }), + }, + }, + }, + context: { repo: { owner: 'testOwner', repo: 'testRepo' } }, + pullNumber: 42, + suppressByInput: false, + core: { info: vi.fn() }, + ...overrides, + }; +} + +describe('checkSuppression', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return false when no suppression conditions met', async () => { + const params = makeParams(); + const result = await checkSuppression(params); + expect(result).toBe(false); + }); + + it('should return true when suppress-coverage label is present', async () => { + const params = makeParams({ + github: { + rest: { + pulls: { + get: vi.fn().mockResolvedValue({ + data: { labels: [{ name: 'suppress-coverage' }, { name: 'other' }] }, + }), + }, + }, + }, + }); + + const result = await checkSuppression(params); + expect(result).toBe(true); + }); + + it('should return true when suppressByInput is true', async () => { + const params = makeParams({ suppressByInput: true }); + const result = await checkSuppression(params); + expect(result).toBe(true); + }); + + it('should return true when both label and input are true', async () => { + const params = makeParams({ + suppressByInput: true, + github: { + rest: { + pulls: { + get: vi.fn().mockResolvedValue({ + data: { labels: [{ name: 'suppress-coverage' }] }, + }), + }, + }, + }, + }); + + const result = await checkSuppression(params); + expect(result).toBe(true); + }); + + it('should skip label check when pullNumber is 0', async () => { + const params = makeParams({ pullNumber: 0 }); + const result = await checkSuppression(params); + expect(result).toBe(false); + expect(params.github.rest.pulls.get).not.toHaveBeenCalled(); + }); + + it('should call pulls.get with correct parameters', async () => { + const params = makeParams(); + await checkSuppression(params); + + expect(params.github.rest.pulls.get).toHaveBeenCalledWith({ + owner: 'testOwner', + repo: 'testRepo', + pull_number: 42, + }); + }); + + it('should log suppression status', async () => { + const params = makeParams(); + await checkSuppression(params); + + expect(params.core.info).toHaveBeenCalledWith( + 'Is suppress=false : by label=false, by input=false' + ); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6507d57 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "test"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..4b7b711 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + coverage: { + provider: 'v8', + reporter: ['text', 'lcov', 'json-summary'], + include: ['src/**/*.ts'], + exclude: ['src/types/**/*.ts'], + }, + }, +});