DEV Community

loading...

Basic Github Actions setup for Phoenix apps

mnishiguchi profile image Masatoshi Nishiguchi ・6 min read

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

1

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
Enter fullscreen mode Exit fullscreen mode

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

2

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
Enter fullscreen mode Exit fullscreen mode

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

3

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

   ...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

D: With deployment

4

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

Enter fullscreen mode Exit fullscreen mode

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!

Discussion (0)

Forem Open with the Forem app