Create Badges with GitHub Actions!

Everybody loves badges. With GitHub actions you can even create dynamic badges which change with every commit. In this guide I will show you how!

The Goal

For the README.md of Fly-Pie’s GitHub repository I wanted to have badges which show the current lines of code and the percentage of comment lines. These numbers can change very frequently, so I needed a system for dynamic badges without polluting my git history. And here is the result:

loc comments

These badges will always show the current numbers and can be embedded everywhere, also in the README.md of GitHub! In this post I will show you how these badges are created.

1. Calculate the Lines of Code

First you will need a script which calculates the number of code lines and comment lines for your project. I have created the script below which you may use as a starting point for your project. It uses the awesome cloc commandline tool, hence it can be adapted for almost all programming languages.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#!/bin/bash

# This scripts counts the lines of code and comments in all source files
# and prints the results to the command line. It uses the commandline tool
# "cloc". You can either pass --loc, --comments or --percentage to show the
# respective values only.
# Some parts below need to be adapted to your project!

# Get the location of this script.
SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"

# Run cloc - this counts code lines, blank lines and comment lines
# for the specified languages. You will need to change this accordingly.
# For C++, you could use "C++,C/C++ Header" for example.
# We are only interested in the summary, therefore the tail -1
SUMMARY="$(cloc "${SCRIPT_DIR}" --include-lang="JavaScript" --md | tail -1)"

# The $SUMMARY is one line of a markdown table and looks like this:
# SUM:|101|3123|2238|10783
# We use the following command to split it into an array.
IFS='|' read -r -a TOKENS <<< "$SUMMARY"

# Store the individual tokens for better readability.
NUMBER_OF_FILES=${TOKENS[1]}
COMMENT_LINES=${TOKENS[3]}
LINES_OF_CODE=${TOKENS[4]}

# To make the estimate of commented lines more accurate, we have to
# subtract any copyright header which is included in each file.
# For Fly-Pie, this header has the length of five lines.
# All dumb comments like those /////////// or those // ------------ 
# are also subtracted. As cloc does not count inline comments,
# the overall estimate should be rather conservative.
# Change the lines below according to your project.
DUMB_COMMENTS="$(grep -r -E '//////|// -----' "${SCRIPT_DIR}" | wc -l)"
COMMENT_LINES=$(($COMMENT_LINES - 5 * $NUMBER_OF_FILES - $DUMB_COMMENTS))

# Print all results if no arguments are given.
if [[ $# -eq 0 ]] ; then
  awk -v a=$LINES_OF_CODE \
      'BEGIN {printf "Lines of source code: %6.1fk\n", a/1000}'
  awk -v a=$COMMENT_LINES \
      'BEGIN {printf "Lines of comments:    %6.1fk\n", a/1000}'
  awk -v a=$COMMENT_LINES -v b=$LINES_OF_CODE \
      'BEGIN {printf "Comment Percentage:   %6.1f%\n", 100*a/(a+b)}'
  exit 0
fi

# Show lines of code if --loc is given.
if [[ $* == *--loc* ]]
then
  awk -v a=$LINES_OF_CODE \
      'BEGIN {printf "%.1fk\n", a/1000}'
fi

# Show lines of comments if --comments is given.
if [[ $* == *--comments* ]]
then
  awk -v a=$COMMENT_LINES \
      'BEGIN {printf "%.1fk\n", a/1000}'
fi

# Show precentage of comments if --percentage is given.
if [[ $* == *--percentage* ]]
then
  awk -v a=$COMMENT_LINES -v b=$LINES_OF_CODE \
      'BEGIN {printf "%.1f\n", 100*a/(a+b)}'
fi

Save this as cloc.sh in the root directory of your repository and make it executable:

$> chmod +x cloc.sh

Then, when you execute the script, it will show you the number of code lines, comment lines and the percentage of comment lines.

$> ./cloc.sh 
Lines of source code:    4.2k
Lines of comments:       1.3k
Comment Percentage:     32.1%
$> ./cloc.sh --loc
4.2k
$> ./cloc.sh --percentage
32.1

2. Use the Dynamic Badges Action

Next you have to setup a workflow which creates badges based on these numbers. To allow this, I have created a GitHub Action which can be used to create arbitrary badges for your README.md from within a GitHub Actions Workflow. This action creates a JSON description of the badge and uploads it to a gist. This JSON description can then be used with shields.io/endpoint to create the final badge.

Please read the configuration instructions of the Dynamic Badges Action. You will have to create a public gist and add a secret to your repository allowing the action to push to this gist.

Below you will find an exemplary workflow which will execute the cloc.sh script on each commit to the main branch and update the badges accordingly. This workflow has one job with five steps.

  1. Checkout the repository.
  2. Download the cloc tool.
  3. Execute the cloc.sh script twice to get the required numbers.
  4. Execute the Dynamic Badges Action once to upload the JSON description of the lines-of-code badge.
  5. Execute the Dynamic Badges Action once more to upload the JSON description of the comment-percentage badge.

You will have to replace <gist-ID> by the ID of the gist you created earlier. The badge showing the lines of code will have a fixed lightgrey color, the comment-percentage badge will have an automatic color: It will be red for 0% comments and green for 50% comments.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
name: Badges

on:
  push:
    branches:
      - main

jobs:
  update-badges:
    name: Update Badges
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2
      - name: Download cloc
        run: sudo apt-get update -y && sudo apt-get install -y cloc
      - name: Get the Numbers
        run: |
          echo "CODE_LINES=$(   ./cloc.sh --loc)" >> $GITHUB_ENV
          echo "COMMENT_PERCENTAGE=$(./cloc.sh --percentage)" >> $GITHUB_ENV
      - name: Create Lines-of-Code-Badge
        uses: schneegans/dynamic-badges-action@v1.3.0
        with:
          auth: ${{ secrets.GIST_SECRET }}
          gistID: <gist-ID>
          filename: loc.json
          label: Lines of Code
          message: ${{ env.CODE_LINES }}
          color: lightgrey
      - name: Create Comments-Badge
        uses: schneegans/dynamic-badges-action@v1.3.0
        with:
          auth: ${{ secrets.GIST_SECRET }}
          gistID: <gist-ID>
          filename: comments.json
          label: Comments
          message: ${{ env.COMMENT_PERCENTAGE }}%
          valColorRange: ${{ env.COMMENT_PERCENTAGE }}
          maxColorRange: 50
          minColorRange: 0

3. Embed the Badges

Once this workflow has run for the first time, you will find new files (loc.json and comments.json in the example above) in the gist you created before. Embedding the badges is now straight-forward.

     ![loc](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/<user>/<gist-ID>/raw/loc.json)
![comments](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/<user>/<gist-ID>/raw/comments.json)

Shields.io caches the these JSON files for at least 300 seconds, so it may take up to five minutes for your badges to refresh when you re-execute the workflow.

Final Thoughts

Of course, this cannot be used for lines-of-code badges only. In fact you can create badges for basically anything which may change from commit to commit! Let me know if you find another cool use case!

Comments

blog comments powered by Disqus