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 as a CI/CD platform among others. I made four patterns so that I can digest the concepts easily.
A: Simple Phoenix test
A minimalistic simple example can be found in erlef/setup-beam'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
.
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
This is a predfined action actions/cache and it has an example for Elixir. In order to skip 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.
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, 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
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 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.
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.
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
This is optional but since I deploy my Phoenix app to Gigalixir, it would be nice if I can automate the deployment after the CI process. This post Elixir/PhoenixアプリをGitHub ActionsでGigalixirに継続的デプロイする by @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)? documentation.
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 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!
Top comments (1)
thank you! it was so helpful :)
先輩気づいてね〜!