diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b549ec9..0f7cc17 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,10 +17,7 @@ jobs: node bin/only-covered main1.js main2.js - name: Check totals 🛡 - run: node bin/check-total --min 90 - - - name: Update code coverage badge 🥇 - run: node bin/update-badge + run: node bin/check-total --min 30 - name: Set commit status using REST # https://developer.github.com/v3/repos/statuses/ @@ -40,6 +37,14 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Check coverage change from README 📫 + run: node bin/set-gh-status --check-against-readme + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Update code coverage badge 🥇 + run: node bin/update-badge + - name: Semantic Release 🚀 uses: cycjimmy/semantic-release-action@v2 env: diff --git a/README.md b/README.md index 9b5a048..e72415a 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,17 @@ Which should show a commit status message like: This script reads the code coverage summary from `coverage/coverage-summary.json` by default (you can specific a different file name using `--from` option) and posts the commit status, always passing for now. +If there is a coverage badge in the README file, you can add 2nd status check. This check will read the code coverage from the README file (by parsing the badge text), then will set a failing status check if the coverage dropped more than 1 percent. **Tip:** use this check on pull requests to ensure tests and code are updated together before merging. + +```yaml +- name: Ensure coverage has not dropped 📈 + run: npx set-gh-status --check-against-readme + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` + +![Coverage diff](images/coverage-diff.png) + ## Debug To see verbose log messages, run with `DEBUG=check-code-coverage` environment variable diff --git a/bin/set-gh-status.js b/bin/set-gh-status.js index 5543f53..d87bfd4 100755 --- a/bin/set-gh-status.js +++ b/bin/set-gh-status.js @@ -3,12 +3,13 @@ const got = require('got') const debug = require('debug')('check-code-coverage') -const {readCoverage, toPercent} = require('..') +const {readCoverage, toPercent, badge} = require('..') const arg = require('arg') const args = arg({ - '--from': String // input json-summary filename, by default "coverage/coverage-summary.json" + '--from': String, // input json-summary filename, by default "coverage/coverage-summary.json" + '--check-against-readme': Boolean }) debug('args: %o', args) @@ -46,6 +47,55 @@ async function setGitHubCommitStatus(options, envOptions) { } }) console.log('response status: %d %s', res.statusCode, res.statusMessage) + + if (options.checkAgainstReadme) { + const readmePercent = badge.getCoverageFromReadme() + if (typeof readmePercent !== 'number') { + console.error('Could not get code coverage percentage from README') + process.exit(1) + } + + if (pct > readmePercent) { + console.log('coverage 📈 from %d% to %d%', readmePercent, pct) + // @ts-ignore + await got.post(url, { + headers: { + authorization: `Bearer ${envOptions.token}` + }, + json: { + context: 'code-coverage Δ', + state: 'success', + description: `went up from ${readmePercent}% to ${pct}%` + } + }) + } else if (Math.abs(pct - readmePercent) < 1) { + console.log('coverage stayed the same %d% ~ %d%', readmePercent, pct) + // @ts-ignore + await got.post(url, { + headers: { + authorization: `Bearer ${envOptions.token}` + }, + json: { + context: 'code-coverage Δ', + state: 'success', + description: `stayed the same at ${pct}%` + } + }) + } else { + console.log('coverage 📉 from %d% to %d%', readmePercent, pct) + // @ts-ignore + await got.post(url, { + headers: { + authorization: `Bearer ${envOptions.token}` + }, + json: { + context: 'code-coverage Δ', + state: 'failure', + description: `decreased from ${readmePercent}% to ${pct}%` + } + }) + } + } } function checkEnvVariables(env) { @@ -68,7 +118,8 @@ function checkEnvVariables(env) { checkEnvVariables(process.env) const options = { - filename: args['--file'] + filename: args['--file'], + checkAgainstReadme: args['--check-against-readme'] } const envOptions = { token: process.env.GITHUB_TOKEN, diff --git a/bin/update-badge.js b/bin/update-badge.js index 0e5e245..fdcac41 100755 --- a/bin/update-badge.js +++ b/bin/update-badge.js @@ -6,7 +6,7 @@ const path = require('path') const fs = require('fs') const os = require('os') const arg = require('arg') -const {readCoverage, toPercent} = require('..') +const {readCoverage, toPercent, badge} = require('..') const args = arg({ '--from': String, // input json-summary filename, by default "coverage/coverage-summary.json" @@ -14,23 +14,6 @@ const args = arg({ }) debug('args: %o', args) -const availableColors = ['red', 'yellow', 'green', 'brightgreen'] - -const availableColorsReStr = '(:?' + availableColors.join('|') + ')' - -function getColor(coveredPercent) { - if (coveredPercent < 60) { - return 'red' - } - if (coveredPercent < 80) { - return 'yellow' - } - if (coveredPercent < 90) { - return 'green' - } - return 'brightgreen' -} - function updateBadge(args) { let pct = 0 if (args['--set']) { @@ -47,21 +30,15 @@ function updateBadge(args) { const readmeText = fs.readFileSync(readmeFilename, 'utf8') function replaceShield() { - const color = getColor(pct) - debug('for coverage %d% badge color "%s"', pct, color) - if (!availableColors.includes(color)) { - console.error('cannot pick code coverage color for %d%', pct) - console.error('color "%s" is invalid', color) - return readmeText - } - - // note, Shields.io escaped '-' with '--' - const coverageRe = new RegExp( - `https://img\\.shields\\.io/badge/code--coverage-\\d+%25-${availableColorsReStr}`, - ) - const coverageBadge = `https://img.shields.io/badge/code--coverage-${pct}%25-${color}` + const coverageRe = badge.getCoverageRe() debug('coverage regex: "%s"', coverageRe) + + const coverageBadge = badge.getCoverageBadge(pct) debug('new coverage badge: "%s"', coverageBadge) + if (!coverageBadge) { + console.error('cannot form new badge for %d%', pct) + return readmeText + } let found let updatedReadmeText = readmeText.replace( diff --git a/images/coverage-diff.png b/images/coverage-diff.png new file mode 100644 index 0000000..b4ec11b Binary files /dev/null and b/images/coverage-diff.png differ diff --git a/src/index.js b/src/index.js index a8a8eda..4e8a675 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ // @ts-check const path = require('path') +const fs = require('fs') const debug = require('debug')('check-code-coverage') /** @@ -32,7 +33,66 @@ function toPercent(x) { return x } +const availableColors = ['red', 'yellow', 'green', 'brightgreen'] + +const availableColorsReStr = '(:?' + availableColors.join('|') + ')' + +function getCoverageRe() { + // note, Shields.io escaped '-' with '--' + const coverageRe = new RegExp( + `https://img\\.shields\\.io/badge/code--coverage-\\d+%25-${availableColorsReStr}`, + ) + return coverageRe +} + +function getColor(coveredPercent) { + if (coveredPercent < 60) { + return 'red' + } + if (coveredPercent < 80) { + return 'yellow' + } + if (coveredPercent < 90) { + return 'green' + } + return 'brightgreen' +} + +function getCoverageBadge(pct) { + const color = getColor(pct) || 'lightgrey' + debug('for coverage %d% badge color "%s"', pct, color) + + const coverageBadge = `https://img.shields.io/badge/code--coverage-${pct}%25-${color}` + return coverageBadge +} + +function getCoverageFromReadme() { + const readmeFilename = path.join(process.cwd(), 'README.md') + const readmeText = fs.readFileSync(readmeFilename, 'utf8') + + const coverageRe = new RegExp( + `https://img\\.shields\\.io/badge/code--coverage-(\\d+)%25-${availableColorsReStr}`, + ) + const matches = coverageRe.exec(readmeText) + + if (!matches) { + console.log('Could not find coverage badge in README') + return + } + debug('coverage badge "%s" percentage "%s"', matches[0], matches[1]) + const pct = toPercent(parseFloat(matches[1])) + debug('parsed percentage: %d', pct) + return pct +} + module.exports = { toPercent, - readCoverage + readCoverage, + badge: { + availableColors, + availableColorsReStr, + getCoverageFromReadme, + getCoverageRe, + getCoverageBadge + } }