Skip to main content
  1. Posts/

Automated Versioning in GitHub Actions

··4 mins
devops github ci/cd tips & tricks
Table of Contents
Patch Note:
After publishing this post, I decided to simplify the CalVer format for my project. The original idea of the post remains intact, but see the Changelog section for details on the format change and the reasoning behind it.

Context
#

I’ve been working on a personal project that is a simple Python web app (Flask) packaged in a lightweight Docker container. After several occasions of building the Docker image, getting distracted, and then forgetting if I actually deployed it, I decided to add basic versioning to keep track of which build was running.

In this case, the versioning serves two simple purposes—adding a quick, visual reference to the web UI and also adding a unique version tag to the Docker image. I also wanted the versioning to be added automatically during the build process without ever having to think about it (e.g. using a git tag, etc.).

There are many different versioning techniques, but two common conventions are Semantic Versioning (SemVer) and Calendar Versioning (CalVer).

With the above conditions in mind, I chose to use the CalVer convention for its simplicity, flexibility, and ease of automating.

CalVer is a versioning convention based on your project’s release calendar, instead of arbitrary numbers.
calver.org

Specifically, I decided on a CalVer format of YYMMDD.mmss (i.e. 250614.0159). This format not only provided a date reference, but also produced a unique suffix for each build. This way, if multiple builds are performed on the same day there is still a unique version.

Automating CalVer with GitHub Actions
#

As it turns out, automating CalVer was relatively simple. I am using GitHub Actions for this project and already had a GitHub Actions workflow in place.

Adding CalVer to the existing GitHub Actions workflow was as simple as…

  • Creating a new build step that…
    • Runs the date command to get the date string in the desired format
    • Sets the formatted date string as the step’s output parameter
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Generate Calendar Versioning String
        id: calver
        run: |
          # YYMMDD.mmss format
          CALVER=$(date +"%y%m%d.%M%S")
          echo "Generated CalVer Tag: $CALVER"
          echo "version=$CALVER" >> $GITHUB_OUTPUT          

Once stored as an output parameter, the CalVer date string can then be referenced in other steps. In this case it’s used…

  • As a Docker build argument (passed to Flask for the web UI version string)
  • As a Docker tag for a unique image version
- name: Build and push
  uses: docker/build-push-action@v6
  with:
    context: .
    platforms: linux/amd64,linux/arm64
    push: true
    build-args: |
      APP_VERSION=${{ steps.calver.outputs.version }}      
    tags: |
      ghcr.io/${{ github.repository }}:latest
      ghcr.io/${{ github.repository }}:${{ github.sha }}
      ghcr.io/${{ github.repository }}:${{ steps.calver.outputs.version }}      

Unintended Consequences
#

As an aside…
Shortly after implementing this CalVer format, I ran into an unexpected “issue”. The format I chose is 10 digits long—and you know what else is? A U.S. phone number. Consequently, the web browser mistook the version string for a phone number and added the tel: hyperlink to it. This was easily solved with some CSS fixes, but it was something I definitely didn’t consider when deciding on the format.

Conclusion
#

As someone who doesn’t have a development background, I’ve never given much thought to something so seemingly trivial as release versioning. This exercise definitely taught me a thing or two about how much really goes into it. They say “the devil is in the details” and this particular detail certainly lives up to that statement.

With this simple addition to my project, I now have automated versioning baked right into each build—no more “did I push that?” moments.

This may not fix my (lack of) short-term memory, but at least now I don’t have to pretend I can recognize a Docker image by its SHA hash.

Changelog
#

After implementing the initial CalVer format (YYMMDD.mmss), I realized I didn’t love how it looked. While the format wasn’t technically flawed and served its intended purpose, the visual result felt cluttered—the numbers ran together, making builds harder to quickly recognize.

The two original purposes for versioning still held true: to provide a quick visual indicator in the web UI and to generate a unique version tag for Docker images. In an effort to maintain the purpose while improving readability, I switched to a more streamlined format of YYYY.MM.bN (where N = build number). This structure is more aligned with the CalVer scheme, easier to read, and still guarantees a unique version for multiple builds on the same day.

The GitHub Actions workflow only required a slight modification to accommodate the change. For the build number, I referenced the GitHub Actions context github.run_number in the build step. This number automatically increments with each workflow run; providing a unique build number for the version string.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Generate Calendar Version for Tagging
        id: calver
        run: |
          # YYYY.MM format
          DATE=$(date +"%Y.%m")
          # Unique build run number
          BUILD=${{ github.run_number }}
          # CALVER = YYYY.MM.bN
          # N = build number
          CALVER=$DATE.b$BUILD
          echo "Generated CalVer Tag: $CALVER"
          echo "version=$CALVER" >> $GITHUB_OUTPUT          
Josh Jaggard
Author
Josh Jaggard