GitHub Actions are coming on strong—in my team, almost everyone who has applied for a beta program, me included, had recently got access to GitHub’s latest “killer feature” that threatens to make life harder for Travis CI and CircleCI: Continuous Integration with GitHub Actions. Here are my first impressions.

For the ultimate test, I decided to reuse the documentation generator setup from my previous article: “Keeping OSS documentation with Docsify, Lefthook, and friends”). To lint a documentation website for AnyCable, I used Lefthook locally and CircleCI for production. For my next documentation project, the one for TestProf, I decided to move all the CI functionality to GitHub Actions (sorry, Travis and CircleCI, of course I still love you).

Warming up: dealing with stale issues

The first thing I found impressive about GitHub Actions is that they could be used not only to deal with code pushes and pull requests but also react on other GitHub events or run on schedule!

One of the actions that GitHub offers you when you first open the “Actions” tab of your project is Stale: it allows you to mark issues and pull requests with a “stale” label and close them. That’s what I usually did by hand (and was hoping to automate with the help of the Stale GitHub App).

It took me a few minutes to add this action:

# .github/workflows/stale.yml
name: Mark stale issues

on:
  schedule:
  - cron: "0 * * * *"

jobs:
  stale:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/stale@v1
      with:
        repo-token: ${{ secrets.GITHUB_TOKEN }}
        stale-issue-message: >
          ⚠ Marking this issue as stale since there has been no activity in the last 30 days.
          Remove stale label or comment or this issue will be closed in 15 days ⌛️
        stale-issue-label: stale
        days-before-stale: 30
        days-before-close: 15

After some time, I received a lot of notifications—the action took action:

Stale comment example

Stale issue comment

I quickly realized that it wasn’t a good idea: GitHub marked all the issues intentionally kept open (for discussion) as stale and spammed all the participants with the comment notification. I’m sorry, guys 😿.

It turned out that there is no ignore mechanism (e.g., by labels). There is one now, but still be careful: it is easy to get burned.

Migrating Markdown linters

Reimplementing the Markdown linting configuration I’ve already had for CircleCI was a pretty straightforward task:

name: Lint Docs

on:
  push:
    branches:
    - master
    paths:
    - "**/*.md"
  pull_request:
    paths:
    - "**/*.md"

jobs:
  markdownlint:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: Set up Ruby 2.6
      uses: actions/setup-ruby@v1
      with:
        ruby-version: 2.6.x
    - name: Run Markdown linter
      run: |
        gem install mdl
        mdl docs
  rubocop:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: Set up Ruby 2.6
      uses: actions/setup-ruby@v1
      with:
        ruby-version: 2.6.x
    - name: Lint Markdown files with RuboCop
      run: |
        gem install bundler
        bundle install --gemfile gemfiles/rubocop.gemfile --jobs 4 --retry 3
        bundle exec --gemfile gemfiles/rubocop.gemfile rubocop -c .rubocop-md.yml
  forspell:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: Install Hunspell
      run: |
        sudo apt-get install hunspell
    - name: Set up Ruby 2.6
      uses: actions/setup-ruby@v1
      with:
        ruby-version: 2.6.x
    - name: Run Forspell
      run: |
        gem install forspell
        forspell docs/
  liche:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: Set up Go
      uses: actions/setup-go@v1
      with:
        go-version: 1.12.x
    - name: Run liche
      # see https://github.com/actions/setup-go/issues/14
      run: |
        export PATH=$PATH:$(go env GOPATH)/bin
        go get -u github.com/raviqqe/liche
        liche -r docs -d docs

A few noticeable differences compared to a CircleCI config:

  • Ability to trigger actions only when matching files have changed (See the paths: ["**/*.md"] declaration).
  • Inability to share the setup (checkout, dependency installation) between the jobs from the same action; there is no cache, or anything akin to CircleCI “workspaces.”

Adding Neo and Trinity for RSpec

Adding the “Lint Docs” action went smoothly, so I decided to continue with migrating RSpec tests.

I’m running gems tests against multiple Ruby and frameworks versions to make sure most of the users are covered. I’ve been doing this successfully for years with Travis’ Build Matrix feature:

matrix:
  fast_finish: true
  include:
    - rvm: 2.6.3
      gemfile: gemfiles/railsmaster.gemfile
    - rvm: jruby-9.2.7.0
      gemfile: gemfiles/jruby.gemfile
    - rvm: 2.6.3
      gemfile: gemfiles/activerecord6.gemfile
    - rvm: 2.6.3
      gemfile: Gemfile
    - rvm: 2.5.3
      gemfile: Gemfile
    - rvm: 2.4.2
      gemfile: gemfiles/default_factory_girl.gemfile
    - rvm: 2.4.2
      gemfile: gemfiles/rspec35.gemfile
    - rvm: 2.4.2
      gemfile: gemfiles/activerecord42.gemfile
   allow_failures:
    - rvm: 2.6.2
      gemfile: gemfiles/railsmaster.gemfile
    - rvm: jruby-9.2.7.0
      gemfile: gemfiles/jruby.gemfile

GitHub Actions have a similar feature—strategy.matrix. And it’s even stated in the documentation that the include-only variant works the same way as in Travis. However, that’s not exactly true yet.

So, I had to take a detour and use the exclude option:

strategy:
  matrix:
    ruby: ["2.5.x", "2.6.x", "2.4.x"]
    gemfile: [
       "gemfiles/railsmaster.gemfile",
       "gemfiles/activerecord6.gemfile",
       "gemfiles/activerecord42.gemfile",
       "gemfiles/default_factory_girl.gemfile",
       "gemfiles/rspec35.gemfile"
    ]
    exclude:
    - ruby: "2.6.x"
      gemfile: "gemfiles/activerecord42.gemfile"
    - ruby: "2.6.x"
      gemfile: "gemfiles/rspec35.gemfile"
    - ruby: "2.6.x"
      gemfile: "gemfiles/default_factory_girl.gemfile"
    - ruby: "2.5.x"
      gemfile: "gemfiles/railsmaster.gemfile"
    - ruby: "2.5.x"
      gemfile: "gemfiles/activerecord42.gemfile"
    - ruby: "2.5.x"
      gemfile: "gemfiles/rspec35.gemfile"
    - ruby: "2.5.x"
      gemfile: "gemfiles/default_factory_girl.gemfile"
    - ruby: "2.4.x"
      gemfile: "gemfiles/railsmaster.gemfile"
    - ruby: "2.4.x"
      gemfile: "gemfiles/activerecord6.gemfile"

This configuration works precisely the same as the one from Travis (except the missing JRuby, we’ll discuss it later), but unfortunately is much less readable.

Another thing I’d like to point out is that with GitHub Actions (compared to Travis) you have to deal with setting up a correct Gemfile yourself:

- name: Configure Gemfile
  run: |
    bundle config --global gemfile ${{ matrix.gemfile }}

Also, there is no allow_failures option. There is a steps.continue-on-failure toggle which could be used to achieve something similar, but with the strategy matrix.

Dealing with JRuby

You cannot use JRuby with the actions/setup-ruby action (or you can, but I couldn’t find how?). It requires special treatment.

Here I applied another GitHub Actions feature—ability to use Docker containers to perform actions. That makes it works very similar to CircleCI.

I took the official JRuby image and added a separate job:

rspec-jruby:
  runs-on: ubuntu-latest
  container:
    image: jruby:9.2.8
    env:
      BUNDLE_GEMFILE: gemfiles/jruby.gemfile
  steps:
  - uses: actions/checkout@v1
  # I need git 'cause I use `git ls-files` in my .gemspec
  # to generate the list of the gem's files
  - name: Install git
    run: |
      apt-get update
      apt-get install -y --no-install-recommends git
  - name: Install deps and run RSpec
    run: |
      gem install bundler
      bundle install --jobs 4 --retry 3
      bundle exec rspec

And it just works!

Bonus: multiple badges

After merging the PR I’ve started looking for a way to add a build status badge to Readme (since I remove the old one from Travis).

The answer was found on Reddit pretty soon: https://github.com/{github_id}/{repository}/workflows/{workflow_name}/badge.svg.

What’s interesting here is that you have a separate badge for each workflow. That means that, for example, you shouldn’t be afraid of a “red” status due to the yet-another minor RuboCop release.

Another useful application of this “feature” is an ability to show that your library supports specific runtimes. For example, I split rspec workflow into two parts: one for different MRI version and another for JRuby. Now it’s clear from the README that TestProf has been tested on JRuby and it’s (hopefully) green!

GitHub Actions badges

GitHub Actions badges

GitHub Actions look very promising, especially for open source projects. The lack of some features (e.g., caching) would stop from using it as the primary CI/CD tool for commercial projects for now.

But that is just the beginning; we will see what’s coming in the final release!

Join our email newsletter

Get all the new posts delivered directly to your inbox. Unsubscribe anytime.