GitHub Actions: First impressions
Tags: github, ruby, ci
Published: 2019-09-06 10:16:33

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](https://github.com/features/actions). Here are my first impressions.

{% twitter 1164301670429409281 %}

For the ultimate test, I decided to reuse the documentation generator setup from my previous article: ["Keeping OSS documentation with Docsify, Lefthook, and friends"](/evilmartians/keeping-oss-documentation-with-docsify-lefthook-and-friends-11e5)). To lint a documentation website for [AnyCable](https://anycable.io/), I used [Lefthook](https://evilmartians.com/chronicles/lefthook-knock-your-teams-code-back-into-shape) locally and CircleCI for production. For my next documentation project, the one for [TestProf](https://test-prof.evilmartians.io/), I decided to move all the CI functionality to GitHub Actions (sorry, Travis and CircleCI, [of course I still love you](https://en.wikipedia.org/wiki/Autonomous_spaceport_drone_shiphttps://en.wikipedia.org/wiki/Autonomous_spaceport_drone_ship)).

> Check the PR with the final implementation: [palkan/test-prof#156](https://github.com/palkan/test-prof/pull/156)

## Warming up: dealing with the 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](https://help.github.com/en/articles/events-that-trigger-workflows) or run on schedule!

One of the _actions_ that GitHub offers you when you first open the "Actions" tab of your project is [Stale](https://github.com/actions/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](https://github.com/apps/stale) GitHub App).

It took me a few minutes to add this action:

```yml
# .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](https://thepracticaldev.s3.amazonaws.com/i/qlr9sbagvo4r9pdf4x1j.png)

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](https://github.com/actions/stale/pull/11) now, but still be careful: it is easy to get burned.

## Migrating Markdown linters

Reimplementing the Markdown [linting configuration](https://github.com/palkan/docs-example/blob/master/.circleci/config.yml) I've already had for CircleCI was a pretty straightforward task:

```yml
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](https://github.com/palkan/docs-example/blob/master/.circleci/config.yml):

- 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](https://stackoverflow.com/questions/55110729/how-do-i-cache-steps-in-github-action)__, 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](https://docs.travis-ci.com/user/build-matrix/) feature:

```yml
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`](https://help.github.com/en/articles/workflow-syntax-for-github-actions#jobsjob_idstrategy). 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](https://github.community/t5/How-to-use-Git-and-GitHub/GitHub-Actions-Matrix-options-dont-work-as-documented/td-p/29558).

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

```yml
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 exactly 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:

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

Also, there is no `allow_failures` option. There is a [`steps.continue-on-failure`](https://help.github.com/en/articles/workflow-syntax-for-github-actions#jobsjob_idstepscontinue-on-error) 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](https://hub.docker.com/_/jruby) and added a separate job:

```yml
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](https://github.com/palkan/test-prof/pull/156) 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](https://www.reddit.com/r/github/comments/csehoc/github_actions_official_status_badges/) 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!

![Badges](https://thepracticaldev.s3.amazonaws.com/i/rljeqyj1f935jkd54ro7.png)

---

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!

---

Read more dev articles on [https://evilmartians.com/chronicles](https://evilmartians.com/chronicles)!