🚀  Our new eBook is out – “CI/CD for Monorepos.” Learn how to effectively build, test, and deploy code with monorepos. Download now →

Setting Up Continuous Integration for an Elixir Project Using Semaphore

This tutorial was initially published on DailyDrip. Read the original post here.


What’s the point of writing tests if you aren’t going to ensure they’re always passing? It’s important to set up a continuous integration service very early in a project. In this tutorial, you will learn how to set up continuous integration for an Elixir project using Semaphore. The project we’ll be working on is Firestorm, an open-source forum engine, with an Elixir Phoenix backend, and an Elm frontend.

You can either watch the video tutorial, or keep reading for a text version.

The Project

We’re starting with the dailydrip/firestorm repo tagged before this episode.

We’ll make a new branch, for later:

git checkout -b feature/semaphore_test
git push origin feature/semaphore_test

Setting Up Continuous Integration

Setting up Semaphore is not particularly difficult. First, we’ll sign up or log into our Semaphore account, where we’re presented with a list of our projects. We’ve already connected our GitHub account – you would have to go through that step first.

Semaphore Projects List

Next, we’ll click Add new project to add Firestorm:

Add Project

Then we’ll pick the branch to build – we want to build the semaphore_test branch:

Choose Branch

Finally, we pick the owner of the project:

Choose Owner

Now, Semaphore will analyze the project, identify it as an Elixir project, and ask us to confirm the build steps:

Build Steps

We’ll ensure the latest version of Elixir is being used and apply these build steps. If we look, we can see that it hangs because there’s no rebar3 and it wants us to confirm that it should install it, which of course we can’t do as this is not interactive.

Could not find "rebar3", which is needed to build dependency :idna
I can install a local copy which is just used by Mix

Let’s modify our build steps to first install rebar3, by adding the following step:

mix local.rebar --force

Now, if we run the build again, it will fail because it can’t connect to the database. Semaphore gives us a couple of environment variables that are meant to help here. First, let’s move them to environment variable names of our choosing in Semaphore itself, in the build steps:


We’ll also need to set some environment variables, in the appropriate section in Semaphore’s admin:

export AWS_ACCESS_KEY_ID=xxxxxxxx
export AWS_SECRET_ACCESS_KEY=xxxxxxxxx
export AWS_S3_BUCKET=test-firestormforum-org
export AWS_S3_REGION=us-west-2
mix test

Then, we’ll update firestorm’s test configuration to support using these environment variables for the repo’s database setup:

vim config/test.exs
# Configure your database
config :firestorm_web, FirestormWeb.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: System.get_env("POSTGRES_USER") || "postgres",
  password: System.get_env("POSTGRES_PASSWORD") || "postgres",
  database: "firestorm_web_test",
  hostname: "localhost",
  pool: Ecto.Adapters.SQL.Sandbox

If we push this up, we’ll see a new build triggered. This build will likely fail due to a race condition involving our Notifications worker. Let’s modify the application to avoid starting our Notifications process in test mode:

vim lib/firestorm_web/application.ex
defmodule FirestormWeb.Application do
  # We need the whole module to have access to the supervisor spec functions
  import Supervisor.Spec

  # ...
  def start(_type, _args) do
    opts = [strategy: :one_for_one, name: FirestormWeb.Supervisor]
    Supervisor.start_link(children(Mix.env), opts)

  defp default_children() do
      # Start the Ecto repository
      supervisor(FirestormWeb.Repo, []),
      # Start the endpoint when the application starts
      supervisor(FirestormWeb.Web.Endpoint, [])
  defp children(:test) do
  defp children(_) do
    default_children() ++
      # Start the notifications server
      worker(FirestormWeb.Notifications, [])

We’d like to start it for the duration of the notifications test, however:

vim test/notifications_test.exs
defmodule FirestormWeb.NotificationsTest do
  # ...
  setup do
    {:ok, _} = FirestormWeb.Notifications.start_link()

  # ...

With that, our tests should pass. They still don’t quite, on the CI server. This seems to happen because of an issue when we redirect. This is probably due to a bug in the version of phantomjs that is being used on Semaphore at present. When phantomjs redirects, it fails to send the appropriate headers to keep us in the right Ecto sandbox. For now, we’ll skip the thread feature test’s watch tests, as those are what are triggering the redirects.

vim test/feature/threads_test.exs
# ...
  @tag :pending
  test "creating a new thread", %{session: session} do
# ...
  @tag :pending
  test "creating a new thread when unauthenticated", %{session: session} do
# ...
  @tag :pending
  test "watching a thread", %{session: session} do
# ...

And we’ll configure ExUnit to skip pending tests:

vim test/test_helper.exs
ExUnit.configure(exclude: [pending: true])
# ...

We’ll push this up, and the tests should pass.


In this tutorial, we saw how to set up continuous integration using Semaphore CI for the Firestorm project. Aside from a simple tweak to support the environment variables that we’re provided by Semaphore, it was completely trivial.

Feel free to leave any comments or questions you may have in the comment section below.

Have a comment? Join the discussion on the forum