Basic Github Actions setup for Phoenix apps
Tags: elixir, phoenix, github, ci
Published: 2021-04-27 01:21:06

I had held off setting up CI/CD (continuous integration and continuous deployment) for my personal Phoenix server. As it turned out, it was a lot easier than I expected. Today, I will briefly write up what I learned for future reference. I used [Github Actions](https://docs.github.com/en/actions) as a CI/CD platform among others. I made four patterns so that I can digest the concepts easily.

[にほんご](https://qiita.com/mnishiguchi/items/8e709ef617a3c3deedaa)

## A: Simple Phoenix test

![1](https://user-images.githubusercontent.com/7563926/116171851-98670580-a6d7-11eb-9f7f-de96a83dff73.png)

A minimalistic simple example can be found in [erlef/setup-beam](https://github.com/erlef/setup-beam#phoenix-example)'s repos. All I needed to to was make a `.github/workflows` directory where I created a workflow YAML file and pasted in the example workflow. The forkflow YAML file can be named as you want like `.github/workflows/ci.yml`.

```yaml
on: push

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      db:
        image: postgres:latest
        ports: ['5432:5432']
        env:
          POSTGRES_PASSWORD: postgres
        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
    steps:
      - uses: actions/checkout@v2
      - uses: erlef/setup-beam@v1
        with:
          otp-version: '22.2'
          elixir-version: '1.9.4'
      - run: mix deps.get
      - run: mix test
```

This is nice and simple and can be good enough for a small Phoenix app, but one issue is that we need to install dependencies every run. It may take a few minutes to complete a workflow even for a small Phoenix app. So it nice to cache dependencies.

## B: With caching

![2](https://user-images.githubusercontent.com/7563926/116171846-9604ab80-a6d7-11eb-8706-8cc9ccbb9731.png)

This is a predfined action [actions/cache](https://github.com/actions/cache) and it has [an example for Elixir](https://github.com/actions/cache/blob/main/examples.md#elixir---mix). In order to [skip](https://github.com/actions/cache#skipping-steps-based-on-cache-hit) the installation of dependencies and the build, we use `if: steps.mix-cache.outputs.cache-hit != 'true'` clause in the "Install dependencies" step. We use cache key that contains otp version and Elixir verion like `Linux-23.3.1-1.11.3-35a9` so that we could add more versions to the `matrix` field.

```yaml
on: push

jobs:
  dependencies:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        elixir: ['1.11.3']
        otp: ['23.3.1']
    steps:
      - name: Cancel previous runs
        uses: styfle/cancel-workflow-action@0.9.0
        with:
          access_token: ${{ github.token }}
      - name: Checkout Github repo
        uses: actions/checkout@v2
      - name: Sets up an Erlang/OTP environment
        uses: erlef/setup-beam@v1
        with:
          elixir-version: ${{ matrix.elixir }}
          otp-version: ${{ matrix.otp }}
      - name: Retrieve cached dependencies
        uses: actions/cache@v2
        id: mix-cache
        with:
          path: |
            deps
            _build
          key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }}
      - name: Install dependencies
        if: steps.mix-cache.outputs.cache-hit != 'true'
        run: |
          mix local.rebar --force
          mix local.hex --force
          mix deps.get
          mix deps.compile

  mix-test:
    needs: dependencies
    runs-on: ubuntu-latest
    strategy:
      matrix:
        elixir: ['1.11.3']
        otp: ['23.3.1']
    services:
      db:
        image: postgres:latest
        ports: ['5432:5432']
        env:
          POSTGRES_PASSWORD: postgres
        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
    steps:
      - name: Cancel previous runs
        uses: styfle/cancel-workflow-action@0.9.0
        with:
          access_token: ${{ github.token }}
      - name: Checkout Github repo
        uses: actions/checkout@v2
      - name: Sets up an Erlang/OTP environment
        uses: erlef/setup-beam@v1
        with:
          elixir-version: ${{ matrix.elixir }}
          otp-version: ${{ matrix.otp }}
      - name: Retrieve cached dependencies
        uses: actions/cache@v2
        id: mix-cache
        with:
          path: |
            deps
            _build
          key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }}
      - run: mix test --trace --slowest 10
```

Other than adding [actions/cache](https://github.com/actions/cache), I gave each step descriptive names. Also add `--trace` and `--slowest 10` options to the `mix test` command so that we can get some useful extra information.

## C: With static code analysis

![3](https://user-images.githubusercontent.com/7563926/116171840-93a25180-a6d7-11eb-9dfc-9bc528fa990a.png)

Now that we have efficient and fast workflow taking advantage of caching, we could run more in parallel. It would be nice to have static code analysis. This post  [Github actions for Elixir & Phoenix app with cache](https://medium.com/@gottfrois/github-actions-for-elixir-phoenix-app-with-cache-6ca33f628459) by Pierre-Louis Gottfrois has a nice example for it. Bacially we install credo and dialyxir, and run them in the workflow. dialyxir particularly can take 10 minutes to complete but once the result is cached, it will be fast as long as the dependencies are unchanged.

Before editing the CI configuration, we add credo and dialyxir in ` mix.exs` file, then run `mix deps.get` as usual.

```diff
 defmodule Mnishiguchi.MixProject do
   use Mix.Project

   ...

   defp deps do
     [
       {:phoenix, "~> 1.5.7"},
       ...
+      {:credo, "~> 1.4", only: [:dev, :test], runtime: false},
+      {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false}
     ]
   end

   ...
```

Here is the updated workflow file.

```yaml
on: push

jobs:
  dependencies:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        elixir: ['1.11.3']
        otp: ['23.3.1']
    steps:
      - name: Cancel previous runs
        uses: styfle/cancel-workflow-action@0.9.0
        with:
          access_token: ${{ github.token }}
      - name: Checkout Github repo
        uses: actions/checkout@v2
      - name: Sets up an Erlang/OTP environment
        uses: erlef/setup-beam@v1
        with:
          elixir-version: ${{ matrix.elixir }}
          otp-version: ${{ matrix.otp }}
      - name: Retrieve cached dependencies
        uses: actions/cache@v2
        id: mix-cache
        with:
          path: |
            deps
            _build
            priv/plts
          key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }}
      - name: Install dependencies
        if: steps.mix-cache.outputs.cache-hit != 'true'
        run: |
          mkdir -p priv/plts
          mix local.rebar --force
          mix local.hex --force
          mix deps.get
          mix deps.compile
          mix dialyzer --plt

  static-code-analysis:
    needs: dependencies
    runs-on: ubuntu-latest
    strategy:
      matrix:
        elixir: ['1.11.3']
        otp: ['23.3.1']
    steps:
      - name: Cancel previous runs
        uses: styfle/cancel-workflow-action@0.9.0
        with:
          access_token: ${{ github.token }}
      - name: Checkout Github repo
        uses: actions/checkout@v2
      - name: Sets up an Erlang/OTP environment
        uses: erlef/setup-beam@v1
        with:
          elixir-version: ${{ matrix.elixir }}
          otp-version: ${{ matrix.otp }}
      - name: Retrieve cached dependencies
        uses: actions/cache@v2
        id: mix-cache
        with:
          path: |
            deps
            _build
            priv/plts
          key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }}
      - run: mix format --check-formatted
      - run: mix credo
      - run: mix dialyzer --no-check --ignore-exit-status

  mix-test:
    runs-on: ubuntu-latest
    needs: dependencies
    strategy:
      matrix:
        elixir: ['1.11.3']
        otp: ['23.3.1']
    services:
      db:
        image: postgres:latest
        ports: ['5432:5432']
        env:
          POSTGRES_PASSWORD: postgres
        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
    steps:
      - name: Cancel previous runs
        uses: styfle/cancel-workflow-action@0.9.0
        with:
          access_token: ${{ github.token }}
      - name: Checkout Github repo
        uses: actions/checkout@v2
      - name: Sets up an Erlang/OTP environment
        uses: erlef/setup-beam@v1
        with:
          elixir-version: ${{ matrix.elixir }}
          otp-version: ${{ matrix.otp }}
      - name: Retrieve cached dependencies
        uses: actions/cache@v2
        id: mix-cache
        with:
          path: |
            deps
            _build
            priv/plts
          key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }}
      - run: mix test --trace --slowest 10
```

## D: With deployment

![4](https://user-images.githubusercontent.com/7563926/116171834-91d88e00-a6d7-11eb-905e-8c62f157726f.png)

This is optional but since I deploy my Phoenix app to [Gigalixir](https://www.gigalixir.com/), it would be nice if I can automate the deployment after the CI process. This post [Elixir/PhoenixアプリをGitHub ActionsでGigalixirに継続的デプロイする](https://qiita.com/mokichi/items/efe4e87763bfdf589d28) by [@mokichi](https://qiita.com/mokichi) did a fantastic job. He points out it is so simple to set up we do not need any third-party library at all.

The basic ideas are explained in Gigalixir's [How to Set Up Continuous Integration (CI/CD)?](https://gigalixir.readthedocs.io/en/latest/deploy.html#how-to-set-up-continuous-integration-ci-cd) documentation.


```yaml
name: CI/CD
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  dependencies:
    ...

  static-code-analysis:
    ...

  mix-test:
    ...

  deploy:
    needs:
      - static-code-analysis
      - mix-test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Github repo
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: Deploy to Gigalixir
        run: |
          git remote add gigalixir https://${{ secrets.GIGALIXIR_EMAIL }}:${{ secrets.GIGALIXIR_API_KEY }}@git.gigalixir.com/${{ secrets.GIGALIXIR_APP_NAME }}.git
          git push -f gigalixir HEAD:refs/heads/master

```

For the three secret values (`GIGALIXIR_EMAIL`, `GIGALIXIR_API_KEY` and `GIGALIXIR_APP_NAME`), we can assign in the project's Github repo. Github's [Creating encrypted secrets for a repository](https://docs.github.com/en/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository) documentation explains it. For `GIGALIXIR_EMAIL` environment variable, we need do the URI encoding e.g. `foo%40gigalixir.com`.

TODO:

- Database migration

That's it!