Photo by Edward Howell on Unsplash
Introduction
Professional API workflows use versioning. However, this is not specific to APIs. Version control is a core principle in software engineering and is found wherever software is.
Principles do not change as frequently (if ever) as best practices do. In fact, principles inform best practices.
When working with APIs (or published libraries, for that matter), a best practice is to use semantic versioning (semver). This has become a common practice for adding features, fixes, and upgrading software. In addition, it communicates changes in the codebase between the development team and users.
Therefore, I wanted to add semantic versioning and releases to a private Github repository of mine. But, I wasn't quite sure how to do it with Python and make it easy with Github. I knew that other people have set up workflows that helped with versioning in Github, so I set out to implement something I could use.
This article explains a workflow combining semver, Conventional Commits, and Github actions. I'm delighted with the setup and think that it saves me time.
Semantic Versioning
To explain why the workflow solves the problem and how the pieces fit together, I'll start with semver.
I won't explain semver in detail because there is already a website that does that. However, I will copy the summary section over to give you an idea.
Given a version number MAJOR.MINOR.PATCH, increment the:
1. MAJOR version when you make incompatible API changes,
2. MINOR version when you add functionality in a backwards compatible manner, and
3. PATCH version when you make backwards compatible bug fixes.
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
If you have spent any time downloading libraries, this format is recognizable. For example, ReactJS is currently in version 17.0.2. Python's current version is 3.9.6.
Conventional Commits
In the past, I was confused when I saw other peoples' commits prefixed with words like chore and fix. It seemed trivial.
This was before I learned about Convention Commits. Conventional commits outline a series of prefixes for code commits that categorize what is being done.
Why?
As stated on the website for Conventional Commits, using these prefixes can help with:
- Automatically generating CHANGELOGs.
- Automatically determining a semantic version bump (based on the types of commits landed).
- Communicating the nature of changes to teammates, the public, and other stakeholders.
- Triggering build and publish processes.
- Making it easier for people to contribute to your projects, by allowing them to explore a more structured commit history.
If you combine your understanding of semver and why you should use conventional commits, it's apparent (I hope) the usefulness in combining the two.
It's now time to talk about Github because you can't talk about version control without Git.
Github Actions and Releases
Repositories in Github have release data. Let's use React as an example.
To record a release in Github, you use tagging. Tags mark points in the repositories history. Take a look at the tags and corresponding releases for React.
Notice, all the tags use semver.
There are ways in Git to create tags manually. Then, you could draft a release and changelog notes manually. However, with a bigger project and many releases, this is a lot of manual work. Additionally, it's difficult to keep track of all the things that changed: what did that commit do, exactly?
Here's the cool part. You can use Github Actions (with conventional commits) to automate your semantic versioning, tagging, and releasing.
How does this work?
1. Use Conventional Commits
When committing code, use the prefixes described in conventional commits. There is a cheat sheet here, and I'll paste the descriptions below.
feat
- Features
A new feature
fix
- Bug Fixes
A bug fix
docs
- Documentation
Documentation only changes
style
- Styles
Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc.)
refactor
- Code Refactoring
A code change that neither fixes a bug nor adds a feature
perf
- Performance Improvements
A code change that improves performance
test
- Tests
Adding missing tests or correcting existing tests
build
- Builds
Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
ci
- Continuous Integrations
Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
chore
- Chores
Other changes that don't modify src or test files
revert
- Reverts
Reverts a previous commit
One of the most important syntax requirements is to append an exclamation point on any prefix that commits breaking changes. (i.e., feat!: commit message
)
2. Add this Github Action To Your Repo
***
I pulled this Github Action code from Lukasz Gornicki's article that's published on the AsyncAPI blog. It's a two-part series and includes more context and information than I include below.
***
Github actions are included in repositories with the folder structure /.github/workflows/.
The .github folder needs to be at the root of the project. We define actions in a YAML file.
Create a new file in /.github/workflows/ named release.yml.
Paste in the following code:
name: Release
on:
push:
branches:
- master
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 13
- name: Add plugin for conventional commits
run: npm install conventional-changelog-conventionalcommits
working-directory: ./.github/workflows
- name: Release to GitHub
working-directory: ./.github/workflows
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_AUTHOR_NAME: enter author name
GIT_AUTHOR_EMAIL: person@example.com
GIT_COMMITTER_NAME: enter name
GIT_COMMITTER_EMAIL: committer@example.com
run: npx semantic-release
This Github Action runs when we push commits up to the master branch.
It checks our commits and decides whether it needs to bump the version. If it needs to bump the version, it collects the commits (based on the prefix category), creates a tag for the new version, and publishes the release.
If the commits have a fix prefix, the version is patched. If they have a feat prefix, the version is bumped by 1 minor increment. Finally, if any prefix has an exclamation point, an entire major version is released (1.0.0 -> 2.0.0).
Another cool feature is the commit messages are organized in the release notes.
When I tested this action out, my repository bumped the release version because I made two fix commits before getting the Github Action to work.
I didn't have to organize the commit messages and decide whether to bump the version or create a tag. It all just happened.
The two bug fix commits are redundant. I was testing out the Github Action and it failed a couple of times because my tests weren't set up properly.
I fixing the same problem, but in different files and separating the commits so my commits weren't just 'test' commits.
Conclusion
It's easy for me to get frustrated when things don't work properly when programming. It's harder to appreciate when something works better than you could expect it to work. Fortunately, this time, I can marvel at how cool and useful all these tools are.
As always, thanks for reading!