Use Github CI for Elixir Projects
Github CI
Github launched actions which make it possible to perform CI without leaving the place where the code belongs. It is indeed quite handy. Once somebody pushes, or makes a pull request, or whatever (the list when to apply an action might be found in the official documentation,) the build is started. Scheduled cron-like tasks are also supported.
One might produce pipelines of actions, named workflows. And all that is great, save for the documentation.
It took me almost an hour to figure out how to spawn a container with third-party services to test the application against. Here is what I have learned. Please note, that the official documentation is yet clumsy, incomplete and sometimes wrong.
The standard CI action uses the configuration files with the syntax quite similar to the one used by CircleCI. It’s plain old good YAML, allowing to set up the target OS, environment, commands to execute, etc. Actions are named what allows to refer to other actions and depend on them.
Also, the configuration allows to specify services. Services are to be run somewhere in the cloud and GH would map ports of the container to the ports these services expose, according to config. That part is feebly covered in the official documentation and even that what is covered contains errors.
Here is the working example of the configuration for the Elixir project, requiring RabbitMQ and Redis services for testing.
name: Tests for My Project
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
container:
image: elixir:1.9.1-slim
services:
rabbitmq:
image: rabbitmq
ports:
- 5672:5672
env:
RABBITMQ_USER: guest
RABBITMQ_PASSWORD: guest
RABBITMQ_VHOST: "/"
redis:
image: redis
ports:
- 6379:6379
steps:
- uses: actions/checkout@v1
- name: Install Dependencies
run: |
MIX_ENV=ci mix local.rebar --force
MIX_ENV=ci mix local.hex --force
MIX_ENV=ci mix deps.get
- name: Run All Tests
run: |
MIX_ENV=ci mix test
env:
RABBITMQ_HOST: rabbitmq
RABBITMQ_PORT: $❴❴ job.services.rabbitmq.ports[5672] ❵❵
REDIS_HOST: redis
REDIS_PORT: $❴❴ job.services.redis.ports[6379] ❵❵
NB Curly brackets above should be normal ones, I uses these because my templating engine drives bonkers seeing two opening curlies, sorry for that.
As one might see, tests are to be run on Ubuntu, using Elixir v1.9.1. Services are described under the services
key, and here is a trick. The port, the service port will be mapped to, is randomly chosen by the container engine in the runtime and stored in the internal shell variable with a name job.services.rabbitmq.ports[5672]
. rabbitmq
here is the name of the service, as specified in this file in services
section and 5672
is the original port. The internal variable has a syntax $❴❴ foo ❵❵
and is being passed to the environment variable RABBITMQ_PORT
. RABBITMQ_HOST
there must be set to the service name. Now your application might read the environment variables as usual.
import Config
config :my_app,
rabbitmq: [
host: System.get_env("RABBITMQ_HOST"),
password: "guest",
port: String.to_integer(System.get_env("RABBITMQ_PORT", "5672")),
username: "guest",
virtual_host: "/",
x_message_ttl: "4000"
]
I have created a dedicated mix
environment, called :ci
to distinguish configuration for tests running in local vs. tests running there in the cloud.
Besides the CI I do run dialyzer
on my sources. Since it’s running in a container, the task takes a while, because it needs to rebuild plts
from the scratch every time. That is why I do it once a day, using schedule
config.
name: Dialyzer for My Project
on:
schedule:
- cron: "* 1 * * *"
jobs:
build:
runs-on: ubuntu-latest
container:
image: elixir:1.9.1-slim
steps:
- uses: actions/checkout@v1
- name: Install Dependencies
run: |
MIX_ENV=ci mix local.rebar --force
MIX_ENV=ci mix local.hex --force
MIX_ENV=ci mix deps.get
- name: Run All Tests
run: |
MIX_ENV=ci mix code_quality
where code_quality
task is an alias declared in mix.exs
as
defp aliases do
[
code_quality: ["format", "credo --strict", "dialyzer"]
]
end
That is basically all we need to happily test the project with external dependencies in new Github Workflow.
Happy continiously intergrating!