When setting up a dev org for a new product, we tend to prioritize code-related tasks and workflows. After all, what can be more important than clean and efficient code, testing-driven development practices, and scalable architecture? Not to underestimate all of these, but another thing you should be thinking about is the delivery pipeline.
At Wilco, we’re big fans of continuous integration and deployment. And while we’re a relatively new company, our process has changed quite a bit over the past year — as we learned more and more about what works for us and what doesn’t.
How It Began
It all started with our take on Git-Flow: a developer creates a feature branch out of develop, works on their code, then opens a pull request back to develop, which is continuously deployed to our staging environment. After several features were merged into develop, we would open another pull request from develop to main, which was then deployed to production once merged.
This workflow sounds fairly simple but raises a couple of issues; first and foremost, keeping a clean commit history. It might sound minor but is very useful to dissect issues and understand the scope of changes each feature introduces.
Our merge strategy was to squash the feature branches into develop and merge commit develop into main. This way we had a clear view of what feature was added and when, but without all the mess of the work-in-progress commits. This, however, had a couple of major downsides:
We then began looking for a new development and deployment cycle, with the following requirements in mind:
What we did
With our requirements defined, we decided to use trunk-based development.
Having a single branch (main) means we could no longer use Heroku’s automatic deployment like we did when we had two branches. This meant we couldn’t test features before shipping them. The solution was to implement our deployment pipeline using the releases mechanism: git tags with fancy readme and GitHub actions with a few nifty actions that increase our deployment visibility.
In a nutshell, this is our flow:
Release Drafter
We use the Release Drafter action in two workflows. When a pull request is opened, we trigger the Auto Labeler functionality, which adds a label based on a predefined rule: feature, fix, chore, or any other required label.
name: Auto Labeler
on:
pull_request:
types: [opened]
jobs:
auto-labeler:
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v5
with:
config-name: release-drafter-config.yml
disable-releaser: true # only run auto-labeler for PRs
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Once a pull request is merged into main, we trigger the Release Drafter functionality, which automatically creates or updates a release draft in GitHub with the changelog from a previous release.
name: Release Drafter
on:
push:
branches:
- main
jobs:
update-release-draft:
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v5
with:
config-name: release-drafter-config.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
This is what a release draft looks like:
Publishing a new release is two clicks: edit a release draft, and click Publish.
Deployment to Heroku
Once a release is published, we want to deploy it to our production environment in Heroku. We run several integration and lint tests, and use the Deploy to Heroku action to deploy the new release.
name: Continuous Deployment
# This action works when creating a tag or release
on:
release:
types:
- "released"
push:
tags:
- "release-*"
- "rollback-*"
jobs:
ci-checks:
# some ci-checks here
lint:
# some lint checks there
deploy-production:
needs:
- ci-checks
- lint
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v2
- uses: akhileshns/heroku-deploy@v3.12.12
with:
heroku_api_key: ${{secrets.HEROKU_API_KEY}}
heroku_app_name: "<your app name here>"
heroku_email: "<your email address>"
team: "<team name>"
dontautocreate: true # do not create the app if it doesn't exist
Note: you need to generate a Heroku API key and store it as a secret in your GitHub repository or organization.
Revert and Rollback
Once the deployment flow is up and running, it’s time to address the unhappy path: rollbacks and reverts.
The easiest way is to create a new release, pointing to a different commit, using the GitHub UI. You can select a commit (it has to be on the base branch), release it, and the deployment workflow will automatically deploy that release to production.
Want to deploy a different branch? You’ve probably already noticed that the deployment workflow isn’t triggered only by publishing a new release, but also when a tag is pushed. This allows us to add a rollback tag on any commit (no matter the branch), push it to the repository, and deploy it as a temporary version to production—while our engineers fix any problem that might have happened.
What comes next
We are now several months after implementing this new flow, and couldn’t have been happier about the results. Most of our pain points are resolved, without the need for additional maintenance. Onboarding new employees is quick, easy, and requires no complicated training on our CI processes.
Like all things in a startup, we evaluate our flow and code constantly, introduce changes and iterate fast. Our flow can be further extended - add Slack notifications, automatic rollbacks and more, but for now, this serves us well 🙂