GitHub Actions can automate semantic versioned releases by orchestrating the process of bumping versions, creating Git tags, and publishing release artifacts.
Let’s look at a real-world example of how this might play out. Imagine a Node.js project where the package.json file holds the current version.
{
"name": "my-awesome-package",
"version": "1.2.0",
"description": "A package that does awesome things",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
When a new feature is merged into the main branch, we want to trigger a release. This release should be a "minor" version bump because we’re adding a new, backward-compatible feature. The version should go from 1.2.0 to 1.3.0.
Here’s a simplified GitHub Actions workflow that accomplishes this:
name: Semantic Release
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0 # Crucial for git history
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Get latest tag
id: get_tag
run: |
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
- name: Determine next version and tag
id: semantic_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LAST_TAG: ${{ steps.get_tag.outputs.latest_tag }}
run: |
# This script would contain logic to analyze commits since LAST_TAG
# and determine if it's a patch, minor, or major bump based on commit messages (e.g., conventional commits).
# For demonstration, let's assume a minor bump.
NEXT_VERSION="1.3.0" # In a real scenario, this is dynamically calculated
NEW_TAG="v${NEXT_VERSION}"
echo "next_version=$NEXT_VERSION" >> $GITHUB_OUTPUT
echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT
- name: Update package.json version
run: |
npm version ${{ steps.semantic_release.outputs.next_version }} --no-git-tag-version
git add package.json
git commit -m "chore(release): bump version to ${{ steps.semantic_release.outputs.next_version }}"
- name: Create Git Tag
run: |
git tag ${{ steps.semantic_release.outputs.new_tag }}
git push origin ${{ steps.semantic_release.outputs.new_tag }}
- name: Publish Release Notes
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.semantic_release.outputs.new_tag }}
body: |
Release ${{ steps.semantic_release.outputs.new_tag }}
Changes:
- Added new feature (minor bump)
# draft: true # Uncomment to create a draft release
# prerelease: true # Uncomment for pre-release versions
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
This workflow breaks down the release process:
- Checkout code: Fetches the repository’s content.
fetch-depth: 0is essential because we need the full Git history to analyze commits. - Set up Node.js: Configures the environment to run Node.js commands.
- Get latest tag: Uses
git describeto find the most recent Git tag. If no tags exist, it defaults tov0.0.0. This is the baseline for determining what has changed. - Determine next version and tag: This is where the core semantic versioning logic lives. In a real system, you’d integrate a tool like
semantic-releaseor a custom script that inspects commit messages (following conventions like Conventional Commits) since theLAST_TAG. It then calculates whether the next version should be a patch, minor, or major bump and constructs the new tag (e.g.,v1.3.0). - Update package.json version: The
npm versioncommand is used to update theversionfield inpackage.json. We use--no-git-tag-versionbecause we’re handling the tag creation manually later. A commit is made to record this version bump in the history. - Create Git Tag: A Git tag is created pointing to the commit that bumped the version. This tag is then pushed to the remote repository.
- Publish Release Notes: The
softprops/action-gh-releaseaction creates a release on GitHub, using the generated tag and providing release notes. These notes would typically be generated dynamically based on the commit messages since the last release.
The power of this approach lies in its automation and adherence to a defined versioning scheme. By analyzing commit messages, the system can automatically determine the type of change (bug fix, new feature, breaking change) and apply the corresponding version bump (patch, minor, major). This provides predictability for consumers of your library or application.
A common pitfall is not fetching enough Git history. If fetch-depth is not set to 0 (or a sufficiently large number), the git describe command might not find the last tag, leading to incorrect version calculations or failures.
The next concept you’ll likely encounter is how to handle different types of releases, such as pre-releases (alpha, beta, rc) or how to integrate this with dependency management tools beyond npm, like Maven for Java or pip for Python.