Coverage Badge with GitHub Actions - FINALLY!

The Jared Wilcurt - Dec 28 '20 - - Dev Community

This is the only documented way to get coverage badges with GitHub Actions. It took a few months of research, trial, and error; but eventually I got it to work, with the help of a GitHub user by the name of Schneegans.

Snoogans - Jay and Silent Bob

The following is for Node.js and Jest, but you can tweak it to work with anything (if you are comfortable doing some shell script googling). Here is what the end result looks like:

PR with working badge

Yep, just a simple coverage badge. At the top of your PR or README. There's a lot of setup required for this to work, but once in place it's pretty minor to set up other repos. Here's the instructions:


  1. Go to gist.github.com and create a new gist. You will need the ID of the gist (this is the long alphanumerical part of its URL) later.
    • Create a gist
  2. Go to github.com/settings/tokens and create a new token with the gist scope.
    • Create a token
    • Copy token
  3. Go to the Secrets page of the settings of your repo and add this token as a new secret with the name GIST_SECRET.
    • Create a new Secret in your repo
    • Paste token and name it GIST_SECRET
  4. Create your workflow file like this (comments to explain the code)

    • your-repo/.github/workflows/node.js.yml
    # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
    # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
    
    name: Build Status
    
    on:
      push:
        branches: [ main ]
      pull_request:
        branches: [ main ]
    
    jobs:
      build:
    
        runs-on: ${{ matrix.os }}
    
        strategy:
          matrix:
            os: [ubuntu-latest]
            node-version: [14.x]
    
        steps:
        - uses: actions/checkout@v2
        - name: Use Node.js ${{ matrix.node-version }} on ${{ matrix.os }}
          uses: actions/setup-node@v1
          with:
            node-version: ${{ matrix.node-version }}
    
        # basically npm install but only installs from package-lock
        - run: npm ci
        - run: npm run lint
        - run: npm t
    
        # Only run the coverage once
        - if: ${{ matrix.node-version == '14.x' }}
          name: Get Coverage for badge
          run: |
            # var SUMMARY = [
            #   '',
            #   '=============================== Coverage summary ===============================',
            #   'Statements   : 32.5% ( 39/120 )',
            #   'Branches     : 38.89% ( 21/54 )',
            #   'Functions    : 21.74% ( 5/23 )',
            #   'Lines        : 31.93% ( 38/119 )',
            #   '================================================================================',
            #   ''
            # ];
            # SUMMARY = SUMMARY.split('\n')[5]; // 'Lines        : 31.93% ( 38/119 )'
            # SUMMARY = SUMMARY.split(':')[1].split('(')[0].trim(); // '31.93%'
            SUMMARY="$(npm test -- --coverageReporters='text-summary' | tail -2 | head -1)"
            TOKENS=($SUMMARY)
            # process.env.COVERAGE = '31.93%';
            echo "COVERAGE=$(echo ${TOKENS[2]})" >> $GITHUB_ENV
    
            # var REF = 'refs/pull/27/merge.json';
            REF=${{ github.ref }}
            # console.log('github.ref: ' + REF);
            echo "github.ref: $REF"
            # var PATHS = REF.split('/');
            IFS='/' read -ra PATHS <<< "$REF"
            # var BRANCH_NAME = PATHS[1] + '_' + PATHS[2];
            BRANCH_NAME="${PATHS[1]}_${PATHS[2]}"
            # console.log(BRANCH_NAME); // 'pull_27'
            echo $BRANCH_NAME
            # process.env.BRANCH = 'pull_27';
            echo "BRANCH=$(echo ${BRANCH_NAME})" >> $GITHUB_ENV
        - if: ${{ matrix.node-version == '14.x' }}
          name: Create the Badge
          uses: schneegans/dynamic-badges-action@v1.0.0
          with:
            auth: ${{ secrets.GIST_SECRET }}
            gistID: 7d4c25ef2e97e8de523ef7c1fee26e8e
            filename: your-repo-name__${{ env.BRANCH }}.json
            label: Test Coverage
            message: ${{ env.COVERAGE }}
            color: green
            namedLogo: jest
    
  5. The above will run npm test, which for me is jest --coverage, then it does a double dash -- which says the next arguments will be passed down and appended to the end of the command, then --coverageReporters='text-summary'. The result is the GitHub Actions CI will run jest --coverage --coverageReporters='text-summary'. The reporter being set to "text-summary" is important, as it will give us the correct string output to parse to get the coverage percent.

  6. We do some shell script magic to grab the correct value from the result of the coverage command (comments written in JavaScript to help explain what the variables are equal to and what the shell script magic is doing).

  7. We then store the coverage string in a secure GitHub Environment Variable.

  8. Unfortunately, GitHub actions does not offer a way to get the current branch name from a PR, instead it gives the Pull Request ID (except sometimes it actually gives you the branch name, but... it doesn't really matter, just know that this is very annoying)

  9. So we use more shell script nonsense to do string manipulation to get a usable representation of the branch or PR, and store that in an environment variable too.

  10. Finally we use Schneegans' plugin to create a JSON file stored on the Gist we created earlier (Make sure you change the Gist ID from the above code to your own). Also change the your-repo-name to the name of your repo.

  11. Then you can use this code to help set up your PR's.

    • your-repo/.github/PULL_REQUEST_TEMPLATE.md
    <!-- Change the ## to your pull request number -->
    ![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/<YourUsername>/<gist_id>/raw/<your-repo>__pull_##.json)
    
    **Notes for reviewer:**
    
    *
    
  12. Change out the 3 items above wrapped in <>

  13. From now on, every PR you make for this repo will come with a badge (though you will still have to create the PR first, then edit it to set the PR number in the badge), but it works!

    • PR with working badge
  14. If you want one for your main branch to put at the top of the README.md you can use this:

    • your-repo/README.md
    [![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/<YourUsername>/<gist_id>/raw/<your-repo>__heads_main.json)]
    
  15. Now all you need to do to set this up in other repos is to add the GIST_SECRET to each, copy/paste your CI config and change the repo name in it. Since the JSON files created in the gist contain the repo name, it can be reused if you want.

    • JSON files stored in gist

Yes, this is very hacky, but I haven't found a better way yet, and I spent months trying different approaches. This is the first thing I've found that works. Still hoping that GitHub just adds this feature in, like every other major CI already does.

If you do not care about the badge itself, there is a simpler way of displaying coverage on PR's by adding this to your GitHub Actions file:

    # Main doesn't have a PR for comments so skip that branch
    # We don't want multiple comments about code coverage, just just run it once on 14.x on Linux
    - if: ${{ github.ref != 'refs/heads/main' && matrix.node-version == '14.x' && matrix.os == 'ubuntu-latest' }}
      uses: romeovs/lcov-reporter-action@v0.2.16
      with:
        github-token: ${{ secrets.GITHUB_TOKEN }}
        lcov-file: ./tests/coverage/lcov.info
Enter fullscreen mode Exit fullscreen mode

This results in a comment being added to the PR by a bot with the coverage percent and a expandable hidden table of all uncovered lines. Example. Though more detailed, this is often overkill, and can be spammy when pushing changes to a PR. These details can just as easily be seen from the results of the actions being ran from the "Checks" tab of a PR. Though these check logs may get deleted over time, based on retention settings. So the comments approach is better from a historical perspective.


All IDs/Tokens in screenshots were modified in Photoshop.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .