Continuous Deployment with Semantic Release and GitHub Actions
If you find the approach in this article interesting, check out Code Genie. Starting a new software project? Code Genie is a Full Stack App Generator that generates source code based on your project’s data model. Including:
- A React Next.js Web App hosted on Amplify Hosting
- Serverless Express REST API running on API Gateway and Lambda
- Cognito User Pools for Identity/Authentication
- DynamoDB Database
- Cloud Development Kit (CDK) for Infrastructure as Code (IAC)
- Continuous Integration/Delivery (CI/CD) with GitHub Actions
- And more!
Operational excellence is critical for all software projects, from startups to enterprise. This includes test and release automation as a core pillar of DevOps. In this article, we’ll cover how to use GitHub Actions and Semantic Release to automatically test, build, version, tag and release software packages on GitHub and NPM. We’ll then explore how we can expand on this foundation to continuously deploy software services to the cloud.
Semantic Release
Semantic Release is an Open-Source Software tool for automatically versioning your software with Semantic Versions based on your Git commit messages. It then releases/deploys the new version to the channel(s) you specify, for example GitHub Release, NPM, PyPI, etc.
By default, Semantic Release expects commits to be in the Conventional Commit format. In its simplest form, this looks like feat: add feature X
or fix: fix bug Y
that perform Minor
and Patch
version bumps respectively.
Since Semantic Release also generates release notes and maintains a CHANGELOG.md
for you, adding quality git commit messages — including a detailed body — becomes increasingly valuable.
Check out the Conventional Commit website for additional details such as marking a commit as a breaking change (resulting in a
Major
version bump).
To set up Semantic Release in your project, you’ll first need to install semantic-release and its core plugins:
npm i -D semantic-release \
@semantic-release/commit-analyzer \
@semantic-release/release-notes-generator \
@semantic-release/changelog \
@semantic-release/npm \
@semantic-release/github \
@semantic-release/git
Next, inside your package.json
specify the list of files
that should be bundled up and distributed as part of the release. If you don’t want the package to be published to NPM make sure you also add "private": true
.
"private": true,
"files": [
"index.js",
"CHANGELOG.md",
"package.json",
"package-lock.json"
]
Finally, create a release.config.js
in your repository root to instruct Semantic Release on how to release our software.
module.exports = {
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
["@semantic-release/npm", {
"tarballDir": "release"
}],
["@semantic-release/github", {
"assets": "release/*.tgz"
}],
"@semantic-release/git"
],
"preset": "angular"
}
Based on this configuration, Semantic Release performs the following steps:
- @semantic-release/commit-analyzer analyzes your commit messages to determine the next semantic version.
- @semantic-release/release-notes-generator generates release notes based on the commit messages since the last release.
- @semantic-release/changelog creates and updates a
CHANGELOG.md
file based on the release notes generated. - @semantic-release/npm Updates the
version
inpackage.json
and creates a tarball in therelease
directory based on thefiles
specified inpackage.json
. If the package isn’t marked asprivate
inpackage.json
, the new version of the package is published to NPM. - @semantic-release/github creates a GitHub release titled and tagged with the new version. The release notes are used in the description and the tarball created in the previous step is included as the release binary. It also adds a comment to any Issues and Pull Requests linked in the commit message.
- @semantic-release/git commits the files modified in the previous steps (
CHANGELOG.md
,package.json
, andpackage-lock.json
) back to the repository. The commit is tagged withvMAJOR.MINOR.PATCH
and the commit message body includes the generated release notes. Perform agit pull --rebase
to get that commit locally.
The base Conventional Commit spec allows for feat
and fix
commit types, but the configuration above is using the angular preset. The Angular flavor of Conventional Commit adds support for additional commit types: build, chore, ci, docs, style, refactor, perf, and test. If you prefer not to use those additional commit types, simply remove that final line in the config.
If you want to test your configuration, create a GitHub personal access token and run
GITHUB_TOKEN=your-token npx semantic-release --dry-run
.
You now have Semantic Release set up for your project, but it’s not yet running in response to changes to your repository — let’s do that now.
GitHub Actions
GitHub Actions allows software developers to run actions in response to events in a GitHub repository. While there are plenty of useful events for automating your GitHub projects, the most common use case is running tests when commits are pushed to the repository or when Pull Requests are opened. Now that we have Semantic Release set up we can take it a step further and perform automated releases.
I’ve annotated the GitHub Actions workflow file below with code comments that hopefully provide enough explanation. Store this file in .github/workflows/test-release.yaml
from your root directory.
name: Test and release
# Run the workflow when a Pull Request is opened or when changes are pushed to master
on:
pull_request:
push:
branches: [ master ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
# Run the steps below with the following versions of Node.js
node-version: [8.x, 10.x, 12.x]
steps:
# Fetch the latest commit
- name: Checkout
uses: actions/checkout@v2
# Setup Node.js using the appropriate version
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
# Install package dependencies
- name: Install
run: npm ci
# Run tests
- name: Test
run: npm test
release:
# Only release on push to master
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
# Waits for test jobs for each Node.js version to complete
needs: [test]
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Install
run: npm ci
- name: Release
run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
After pushing this file to your repository, GitHub will automatically create the Actions for you.
That’s it! Your GitHub project is now set up to automatically run tests on every Pull Request and release changes pushed to the master
branch.
Let’s review
We installed Semantic Release and its plugins, updated our package.json
, and added release.config.js
to get our project ready for automated release versioning based on Conventional Commits. We then added automation by defining our GitHub Action in .github/workflows/test-release.yaml
.
Now, all Pull Requests have tests run against them to ensure changes don’t break our package. When changes are pushed to our master
branch or when Pull Requests are merged, we get versioned releases published to GitHub and NPM with release notes and a changelog. All Issues and PRs referenced in commit messages are automatically commented on to announce they’ve been addressed in a release. Not bad considering the minimal effort involved.
Deploying services to the cloud
What we’ve walked through so far is great for releasing software packages such as libraries and frameworks to GitHub and NPM, but what about deploying services and applications to the cloud? In Part 2, we’ll build on the foundation we’ve established to do just that. We’ll create a new GitHub Action workflow that:
- Is triggered when a new GitHub release is published.
- Downloads the assets of the latest release.
- And deploys the build artifacts to AWS.