diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..2f299e8 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,40 @@ +name: Build + +on: + push: + branches: + - 'develop' + - 'main' + - 'release/**' + pull_request: + branches: + - '**' + +jobs: + + build: + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + checks: write + pull-requests: write + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Publish Delta Coverage + id: render-delta-coverage + uses: gw-kit/delta-coverage-action@v1 + with: + summary-report-base-path: 'test/data/' + + - id: upload-badges + uses: actions/upload-artifact@v4 + with: + name: coverage-gen.svg + path: ${{ steps.render-delta-coverage.outputs.badges-dir }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ee94de5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Delta Coverage Action + +## 1.0 + +- Now the action generates coverage badges for each test view. + For details see [readme](./README.md#coverage-badges). diff --git a/README.md b/README.md index 01a77fd..c5815cf 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ Also, the action creates a comment in the pull request with links to the check r If `title` is not blank then the previous comment generated by this action will be updated with the new report, otherwise a new comment will be created. +## Outputs + +- `badges-dir` - The directory where the coverage badges are stored. + ## Pre-requisites Required permissions: @@ -40,3 +44,23 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} ``` + +## Coverage Badges + +The action generates coverage badges for each [coverage view](https://github.com/gw-kit/delta-coverage-plugin/blob/main/README.md#report-views). +The badges are generated based on _full coverage summary_ data files created by [Delta-Coverage plugin](https://github.com/gw-kit/delta-coverage-plugin). + +⚠️ Enabling of full coverage report is required: +```kts +deltaCoverageReport { + reports { + fullCoverageReport = true + } +} +``` + +### Badges example + +![aggregated.svg](https://raw.githubusercontent.com/gw-kit/coverage-badges/refs/heads/main/delta-coverage-plugin/badges/aggregated.svg) +![functionalTest.svg](https://raw.githubusercontent.com/gw-kit/coverage-badges/refs/heads/main/delta-coverage-plugin/badges/functionalTest.svg) +![test.svg](https://raw.githubusercontent.com/gw-kit/coverage-badges/refs/heads/main/delta-coverage-plugin/badges/test.svg) diff --git a/action.yaml b/action.yaml index 0479f27..469b32e 100644 --- a/action.yaml +++ b/action.yaml @@ -25,6 +25,11 @@ inputs: required: false default: 'delta-coverage' +outputs: + badges-dir: + description: 'Directory with generated badges.' + value: ${{ steps.generate-badges.outputs.badges-dir }} + runs: using: "composite" @@ -36,18 +41,28 @@ runs: with: github-token: ${{ inputs.github-token }} script: | - const fs = require('fs'); - const files = fs.readdirSync(`${{ inputs.summary-report-base-path }}`); - const summaryFiles = files.filter(file => file.includes('-summary.json')) - .filter(file => !file.includes('full-coverage-')); - - const summaries = summaryFiles.map(file => - JSON.parse(fs.readFileSync(`build/reports/coverage-reports/${file}`, 'utf8')) - ); - const allSummaries = JSON.stringify(summaries); - const allSummariesFile = 'all-summaries.json'; - fs.writeFileSync(allSummariesFile, allSummaries); - core.setOutput('file', allSummariesFile); + 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: Generate Badges + id: generate-badges + continue-on-error: true + shell: bash + run: | + npm install gradient-badge @actions/core + src_path="${{ github.action_path }}/src" + mv ./node_modules $src_path/ + node $src_path/generate-badges.js ${{ steps.all-summaries.outputs.full }} - name: Fetch PR Labels id: fetch-labels @@ -81,7 +96,7 @@ runs: script: | const createCheckRuns = require('${{ github.action_path }}/src/create-check-runs.js'); const checkRuns = await createCheckRuns({ - summaryReportPath: `${{ steps.all-summaries.outputs.file }}`, + summaryReportPath: `${{ steps.all-summaries.outputs.delta }}`, ignoreCoverageFailure: ${{ steps.check-suppress.outputs.suppress }}, core: core, context: context, diff --git a/src/generate-badges.js b/src/generate-badges.js new file mode 100644 index 0000000..64b57ca --- /dev/null +++ b/src/generate-badges.js @@ -0,0 +1,52 @@ +const fs = require('fs'); +const path = require('path'); +const gradientBadge = require('gradient-badge'); +const core = require('@actions/core'); + +const badgesOutputDir = 'badges/'; +fs.mkdirSync(badgesOutputDir, {recursive: true}); + +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)], + }; +}; + +const [, , summariesFile] = process.argv; +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/read-summaries.js b/src/read-summaries.js new file mode 100644 index 0000000..6b8dbab --- /dev/null +++ b/src/read-summaries.js @@ -0,0 +1,21 @@ +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/test/data/full-coverage-aggregated-summary.json b/test/data/full-coverage-aggregated-summary.json new file mode 100644 index 0000000..a3621b2 --- /dev/null +++ b/test/data/full-coverage-aggregated-summary.json @@ -0,0 +1,29 @@ +{ + "view": "aggregated", + "reportBound": "DELTA_REPORT", + "coverageRulesConfig": { + "failOnViolation": false, + "entitiesRules": {} + }, + "verifications": [], + "coverageInfo": [ + { + "coverageEntity": "INSTRUCTION", + "covered": 9406, + "total": 10775, + "percents": 87.29 + }, + { + "coverageEntity": "BRANCH", + "covered": 303, + "total": 377, + "percents": 80.37 + }, + { + "coverageEntity": "LINE", + "covered": 1943, + "total": 2209, + "percents": 87.96 + } + ] +} diff --git a/test/data/full-coverage-functionalTest-summary.json b/test/data/full-coverage-functionalTest-summary.json new file mode 100644 index 0000000..688f43d --- /dev/null +++ b/test/data/full-coverage-functionalTest-summary.json @@ -0,0 +1 @@ +{"view":"functionalTest","reportBound":"DELTA_REPORT","coverageRulesConfig":{"failOnViolation":false,"entitiesRules":{}},"verifications":[],"coverageInfo":[{"coverageEntity":"INSTRUCTION","covered":6843,"total":10775,"percents":63.51},{"coverageEntity":"BRANCH","covered":180,"total":377,"percents":47.75},{"coverageEntity":"LINE","covered":1395,"total":2196,"percents":63.52}]} \ No newline at end of file diff --git a/test/data/full-coverage-test-summary.json b/test/data/full-coverage-test-summary.json new file mode 100644 index 0000000..b4dac7a --- /dev/null +++ b/test/data/full-coverage-test-summary.json @@ -0,0 +1 @@ +{"view":"test","reportBound":"DELTA_REPORT","coverageRulesConfig":{"failOnViolation":false,"entitiesRules":{}},"verifications":[],"coverageInfo":[{"coverageEntity":"INSTRUCTION","covered":8860,"total":10775,"percents":82.23},{"coverageEntity":"BRANCH","covered":280,"total":371,"percents":75.47},{"coverageEntity":"LINE","covered":1800,"total":2187,"percents":82.3}]} \ No newline at end of file diff --git a/version.properties b/version.properties new file mode 100644 index 0000000..aef125e --- /dev/null +++ b/version.properties @@ -0,0 +1 @@ +version=1.0.0