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 :)
先輩気づいてね〜!