Jun 09, 2019

Automating my open-source projects with CI/CD workflows

When you maintain more than a couple of projects, managing dependency upgrades, code quality checks, tests, and publishing releases becomes a chore you'll inevitably face during a project's lifecycle. In addition to this being neither fun, nor a rewarding task, it consumes a lot of time to pull the latest changes, double-check that everything works fine and then publish to different release targets.

Luckily, I'm not the first one to have experienced said problem and quickly found out it's more than easy to automate. I've documented a list of steps I've taken so far to go from zero to "all I have to do is review code and merge PRs" in less than thirty minutes.

My procedure for automating a repository

Let's start off with some basic refresh of your (and my) git knowledge: Let's talk tags. Git Tags (published on GitHub) will become very important in this workflow since the main tool we'll use will decide which version to publish based on those.

A short guide to git tags

I haven't manually used the tagging feature git provides in a long time, which is why I had to refresh my knowledge on this. As some sort of cheat sheet, here are my most used tagging commands:

# create signed tag
git tag -s <TAG NAME> -m "<TAG MESSAGE>"

# delete tag
git tag -d <TAG NAME>

# show details about tag
git show <TAG NAME>

# push to remote
git push origin <TAG NAME>

🔗 Further resources can be found here.

Add Renovate(bot) to automate dependency upgrades

Keeping track of dependency-versions can be a hassle at times, which is why I decided to start off my automation journey by installing and configuring renovatebot for all of my open-source projects. After installing the renovate integration from GitHub's marketplace, it automatically started opening setup PRs including a default configuration, I haven't had an onboarding this straightforward and smooth in a while.

My typical renovate config looks like this:

{
  // We don't modify anything here
  // since the standard works just fine 👌
  "extends": ["config:base"],
  // Since I don't want to clutter up my repositories with
  // dependency-upgrade pull requests, let renovatebot merge
  // all upgrade requests automatically (except major changes)
  "automerge": true,
  "major": {
    "automerge": false
  }
}

Add semantic-release to automate publishing workflows

After adding renovatebot to take care of the chore of manually upgrading versions, I continued with the next step down the maintenance road: Publishing new versions. This can be quite a drag when a project grows larger, having to double-check if all code quality checks passed, signing in to the package registry and waiting for a new release to be built and published isn't the best use of anyone's time, hence I started using semantic-release to manage publishing new versions of my projects.

For the biggest part (so everything excluding the usage CircleCI which I'll describe in the next point), I didn't have to configure semantic-release in greater detail than simply adding it to my package file which now roughly contains the following settings to make everything work as expected:

{
  // We can pretty much choose any version here
  // since it won't be used anyway
  "version": "0.0.0-development",
  "devDependencies": {
    "prettier": "...",
    "semantic-release": "...",
    "tslint": "...",
    "typescript": "..."
  },
  // The collection of yarn scripts
  // available here should be important
  // for the CI configuration later on
  "scripts": {
    "build": "...",
    "lint": "...",
    "test": "...",
    // We need this one to release new versions 🚀
    "semantic-release": "semantic-release"
  },
  // Sometimes the default options are just fine 👍
  "release": {
    "plugins": [
      "@semantic-release/commit-analyzer",
      "@semantic-release/release-notes-generator",
      "@semantic-release/npm",
      "@semantic-release/github"
    ]
  }
}

Add CircleCI to automatically publish changes

This may be the most important part of my automation setup, CircleCI will continuously run jobs to test and deploy all changes made to the project by checking quality tests (mostly linting & code style), actual tests and then publishing the built files to npm (for my Node.js-based projects, that is), Docker Hub, etc.

Most of my CircleCI configuration files (for Node.js projects) look similar to the following:

# Required Environment variables:
# GIT_AUTHOR_NAME - Name of tag author
# GIT_AUTHOR_EMAIL - Email of tag author
# GIT_COMMITTER_NAME - Name of tag committer
# GIT_COMMITTER_EMAIL - Email of tag committer
# GH_TOKEN - Token for GitHub actions (tags, releases)
# NPM_TOKEN - Token for npm publishing workflow

version: 2.1
jobs:
  test:
    docker:
      - image: 'circleci/node:latest'
    steps:
      - checkout
      # install dependencies
      - run:
          name: install
          command: yarn
      # lint code
      - run:
          name: lint
          command: yarn lint
      # run tests
      - run:
          name: test
          command: yarn test
  release:
    docker:
      - image: 'circleci/node:latest'
    steps:
      - checkout
      # install dependencies
      - run:
          name: install
          command: yarn
      # lint code
      - run:
          name: lint
          command: yarn lint
      # run tests
      - run:
          name: test
          command: yarn test
      # release
      - run:
          name: release
          command: yarn semantic-release
workflows:
  version: 2
  default-workflow:
    jobs:
      # run test job on every change except pushes to master branch
      - test:
          filters:
            branches:
              ignore:
                - master
      # run release job only for pushes to master
      - release:
          filters:
            branches:
              only:
                - master

Checklist for everything to run

Once you've added all required configuration files and set everything up, you should check on the following points to prevent any unwanted surprises and failing builds after the push to version control:

  • Generate access tokens for GitHub (or your version control of choice) and npm
  • Give write access to a separate account for managing releases to package registry
  • Add required environment variables (listed in CI configuration file) to CircleCI
  • Check if previous versions already exist and bump the git tag so future releases won't collide (important for npm)
  • Configure branch protection so all checks have to run (required reviews will break renovatebot automerging currently, so either you have to turn off automerge or disable required reviews)

That should be about it, once you've configured and set everything up, you can push your changes and observe everything just working 🚀 If this is not the case, take a look at the checklist again, maybe you've overlooked some configuration part 🤞

If you've got any questions, suggestions or other feedback, don't hesitate to send a mail.