An Elixir/Phoenix release workflow for GitHub CI
Tags: elixir, phoenix, github, ci
Published: 2019-10-27 10:34:37

GitHub has made actions available to [anyone that requests it](https://twitter.com/natfriedman/status/1174158823877042177) 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](//images.ctfassets.net/qnx6g1z4xbnr/4l63Fc26k8inP1M3gA5zBl/299950f263ca1e353d180c53593da458/image.png)

The default workflow looks like this:

```yaml
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](https://github.com/actions/setup-elixir) to do this which is handy!

This makes the basic configuration look like this:

```yaml
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.

```yaml
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](https://github.com/moomerman/httping/blob/master/.github/workflows/elixir.yml) of this configuration on GitHub at [moomerman/httping](https://github.com/moomerman/httping/actions).

If you have any tips or feedback please get in touch via [Twitter](https://twitter.com/moomerman/status/1180957149570187267).