Welcome to Github CI

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!