GitHub Actions: First impressions
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:
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 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!