With open source software, it’s not an easy task to strike the right balance between producing stable programs and having a decent base of testing users.
This post explains how to best achieve this, and what conclusions resulted from the years I have been developing open source projects.
Triggers for image tags
- The Git
main
branch (akamaster
) should be your base branch, and commits added to it should trigger a build with the:latest
tag. - Pull requests should trigger a build with the
:pr-{pull-request-number}
image tag so that users can easily test the new feature or fix. - Publishing a Github release should trigger a build with the
:{release-tag}
image tag. If using semver, this should be the three tags:vX.Y.Z
,:vX.Y
and:vX
so that users can easily pin to a specific version but still get bug fixes or additional features.
Direct users to use the :latest
image tag
The foundation of stable software is to have a robust testing suite to avoid relying on users for testing.
However testing cannot cover all cases, especially with a large user base and when the software behaves differently depending on the environment.
For example, with my VPN client Docker image Gluetun, there are many aspects that made the software buggy such as the kernel version, routing, network setup, LXC container runtime etc.
As a consequence, users testing the software in their environment are very valuable and should be encouraged.
This is why I tend to direct users to use the Docker image without a release tag (aka :latest
) by default.
Release tags
Even if the general direction for users is to use the latest Docker image, release tags are also available and mentioned.
They should all be as stable as possible, and bug fix releases should be kept low.
The process to do a feature release is the following:
- 3 weeks after the previous feature release, stop merging/pushing commits to the
main
branch - Wait 1 week for users to report issues by testing the
:latest
Docker image. If any issue is due to a change made since the previous feature release:- Push a fix commit to the
main
branch - Ideally wait 1 week after the last fix commit to make sure no other issue is reported
- Push a fix commit to the
- Publish a release targetting the
main
branch
If there is still a somehow critical issue reported after the release is published, a bug fix release can be made:
git checkout <commit-hash>
to the commit hash of the current feature releasegit checkout -b vX.Y
to create a new branchvX.Y
(where the current feature release isvX.Y.0
)- Push a commit to fix the issue to the
vX.Y
branch - Publish a release targetting the
vX.Y
branch - You can later delete the
vX.Y
branch once the next feature release is published
Release tags are especially useful when the latest Docker image breaks for some users, and they can fallback to a previous release tag. It’s also a nice-to-have to users who don’t want to risk using the latest Docker image. On top of this, it really helps fixing issues reported by users since they would usually try different release tags and mention which ones work and which don’t for them.
Pull requests
Pull requests are a great way to test new features or fixes, and it’s important to make it easy for users to test them.
Any pull request should trigger a Docker image build with the :pr-{pull-request-number}
tag, so users can easily pull and run the image.
One can also restrict this build to be limited to pull requests originating a certain repository, and not forks, to avoid abuse and leaking the Docker image registry credentials.
And then also not trigger from the dependabot[bot]
user which has not access to the repository secrets.
Github actions
You must be wondering, how do I do all this on Github!?
The answer is Github actions, and here is a trimmed down example of a workflow file to achieve all this:
name: CI
on:
release:
types:
- published
push:
branches:
- main
pull_request:
jobs:
publish:
if: |
github.repository == 'github_username/github_repository' &&
(
github.event_name == 'push' ||
github.event_name == 'release' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
flavor: |
latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
images: |
docker_username/docker_repository
tags: |
type=ref,event=pr
type=semver,pattern=v{{major}}.{{minor}}.{{patch}}
type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern=v{{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
- uses: docker/login-action@v2
with:
username: docker_username
password: ${{ secrets.docker_password }}
- uses: docker/build-push-action@v4
with:
tags: ${{ steps.meta.outputs.tags }}
push: true
For production you might want to add more jobs and/or steps, a more concrete example would be Gluetun’s CI workflow file.
Conclusion
I hope this post helped you to better understand how to do Docker image tagging for open source projects. As a very fast bullet point summary:
- Use the
:latest
tag for themain
branch - Use the
:pr-{pull-request-number}
tag for pull requests - Use the
vX.Y.Z
,vX.Y
andvX
tags for releases - Direct users to the
:latest
tag by default, and mention how to fallback to release tags
Comments