Here is a complete action that can be put into .github/workflows/ci.yml so that tests and other checks (formatter, Credo, Dialyzer) are ran automatically on each push and each pull request:
name: CI
on: [push, pull_request]
env:
MIX_ENV: test
jobs:
build-and-test:
name: Build and Tests
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-20.04]
elixir: [1.11.2]
otp: [23.3.4]
services:
postgres:
image: postgres:12.6
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: kiz_test
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Check Out Repository
uses: actions/checkout@v2
- name: Prepare Application
run: cp config/test.secret.ci.exs config/test.secret.exs
- name: Setup Elixir
uses: actions/setup-elixir@v1
with:
elixir-version: ${{ matrix.elixir }}
otp-version: ${{ matrix.otp }}
experimental-otp: true
- name: Restore Dependencies, Build & Dialyzer PLTs Cache
uses: actions/cache@v2
with:
path: |
deps
_build
priv/plts
key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}
- name: Install Dependencies
run: mix deps.get
- name: Setup DB
run: mix ecto.setup
- name: Run Tests
run: mix test
- name: Check Formatting
run: mix format --check-formatted
- name: Review Code With Credo
run: mix credo
- name: Typecheck With Dialyzer
run: mix dialyzer
Few things to note with that setup.
- We use build matrix, but only set one value for each of OS, Elixir and OTP versions, this way it runs normally (as if without the matrix) but we can enable it by adding a farther version of anything, e.g. os: [ubuntu-18.04, ubuntu-20.04] would make it run on both systems.
- Specifying the full OTP version is necessary for Dialyzer to work properly, otherwise minor or bugfix version numbers will change and Dialyzer run would fail. If not using Dialyzer, setting major OTP version should be enough.
- The flag experimental-otp is needed for Ubuntu 20.04 support at the time of writing, otherwise the “OpenSSL might not be installed on this system.” would occur. Soon it should become default so the flag will not be needed, here’s the relevant issue.
- Postgres is a service which means it will spawn a separate container so we map ports like that 5432:5432 – that means we map the port 5432 (which is the default Postgres port) inside the container to the same port outside of it so that the app can connect to the DB using the standard port.
- We use config system with secret configs and keep CI config in the config/test.secret.ci.exs in project repository (this file gets DB settings only and the password is “postgres”, no big secret), new apps initially made with Elixir 1.10 and later should switch to using runtime config, this way all the config options can simply be listed as env variables at the beginning of the action (where MIX_ENV is set)
- We cache deps and _build directories as well as priv/plts, first two will enable incremental builds which will significantly speed up consecutive runs, last one is only needed for Dialyzer step and can be skipped otherwise.
- As cache key we include OS as well as both Elixir and OTP versions, this way adding them to the Matrix or upgrading them will bust the cache and enforce new build so we get no weird behavior.
- If you want to use Dialyzer then it must be installed and configured to use “priv/plts” as core path as well as “priv/plts/dialyzer.plt” as plt file (last one should not be necessary but without it I got no cache hits). Installing Dialyzer for Elixir means adding something like that to deps: {:dialyxir, “~> 1.0”, only: [:dev, :test], runtime: false} and configuring it requires adding dialyzer key to project function in mix.exs, example below.
def project do
[
...
dialyzer: [
plt_core_path: "priv/plts",
plt_file: {:no_warn, "priv/plts/dialyzer.plt"}
],
...
]
end