An Elixir/Phoenix release workflow for GitHub CI

Screenshot showing Octocat

This article details an example configuration for running an Elixir/Phoenix workflow on GitHub CI (aka GitHub actions).

This is an update to my previous article which used the (now redundant) beta version of GitHub workflows.

GitHub has made actions available to anyone that requests it and is planning on launching in November so now is a good time to start moving your projects across.

There are a lot more features available now including matrix builds and attached services. GitHub also detects an Elixir repository and gives you a starting point for your workflow.

image

The default workflow looks like this:

name: Elixir CI

on: push

jobs:
  build:
    runs-on: ubuntu-latest

    container:
      image: elixir:1.9.1-slim

    steps:
      - uses: actions/checkout@v1
      - name: Install Dependencies
        run: |
          mix local.rebar --force
          mix local.hex --force
          mix deps.get
      - name: Run Tests
        run: mix test

This works great for very basic setups, unfortunately for me the first stumbling block was the container image doesn’t have any build tools (eg. make) so it wasn’t able to compile bcrypt_elixir among other dependencies.

Hovever, the host does have build tools available so instead of running it in a container we can install elixir and erlang (OTP) on the host. GitHub even have an action to do this which is handy!

This makes the basic configuration look like this:

name: Elixir CI

on: push

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v1
    - uses: actions/setup-elixir@v1.0.0
        with:
          otp-version: 22.x
          elixir-version: 1.9.x
    - name: Install Dependencies
      run: |
        mix local.rebar --force
        mix local.hex --force
        mix deps.get
    - name: Run Tests
      run: mix test

This will work as a simple CI for a lot of Elixir projects, but for Phoenix projects we need a bit more.

This configuration sets up a standard Elixir/Phoenix environment (OTP, Elixir and Node) with a Postgres container. It then compiles the code and assets, runs the tests and builds & publishes a release to GitHub releases.

name: Elixir CI

on: push

jobs:
  test:
    runs-on: ${{ matrix.os }}
    name: OTP ${{ matrix.otp }} | Elixir ${{ matrix.elixir }} | Node ${{ matrix.node }} | OS ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-18.04]
        otp: [22.x]
        elixir: [1.9.x]
        node: [12.x]

    services:
      db:
        image: postgres:11
        ports: ["5432:5432"]
        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5

    steps:
      - uses: actions/checkout@v1.0.0
      - uses: actions/setup-elixir@v1.0.0
        with:
          otp-version: ${{ matrix.otp }}
          elixir-version: ${{ matrix.elixir }}
      - uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node }}

      - name: Install dependencies
        run: |
          mix local.rebar --force
          mix local.hex --force
          mix deps.get
          yarn --cwd assets install

      - name: Run tests
        run: |
          mix compile --warnings-as-errors
          mix format --check-formatted
          mix test
        env:
          MIX_ENV: test

      - name: Prepare release
        run: |
          mix compile
          yarn --cwd assets deploy
          mix phx.digest
          mix release
        env:
          MIX_ENV: prod

      - name: Publish release
        uses: moomerman/actions/bin/ghr@master
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          RELEASE_PATH: _build/prod/rel
          APPLICATION: <<APP NAME>>

We’ve also added the strategy/matrix configuration above which isn’t entirely necessary in this example but I like how it ends up having all the versions of dependencies clearly labelled in one place and it allows for extending the matrix to other versions very easily.

A couple of gotchas I found so far (which could be user error) are:

  • I couldn’t find a way of specifying the version of an image in the matrix, eg. v11 of postgres above

  • I tried to use the ofiicial nodejs docker image to execute the yarn commands rather than running the setup-node action on the host, but this resulted in assets that had permissions that meant the host couldn’t modify them which seems like a bug.

The newer format is definitely a lot easier to follow and is extremely powerful. It still takes a while to run, there’s no official caching support (though there are a number of optimisations behind the scene) but I expect that to improve soon.

You can see an example of an phoenix project running an adaptation of this configuration on GitHub at moomerman/httping.

If you have any tips or feedback please get in touch via Twitter.