Continuous Integration for a Phoenix LiveView Site (Without Postgres)

Dans le cadre de la construction et du maintien d'un site LiveView fiable, un pipeline CI solide est essentiel. Ci-dessous, je vais vous guider à travers la configuration CI que nous utilisons pour ce site, qui est construit avec Phoenix (Elixir), et déployé en utilisant Kamal (un autre article sera écrit à ce sujet !) et Docker. Au moment où nous écrivons ces lignes, l'application ne repose pas sur une base de données comme Postgres, mais nous avons laissé le support commenté pour une expansion future.

We use GitHub Actions pour CI, ce qui nous donne des environnements propres et isolés et une bonne intégration avec Docker Hub et les versions versionnées.

1. Triggers

on:
  push:
    branches: [main]
    tags:
      - "*"
  pull_request:
    branches: [main]
  

The CI pipeline runs automatically:

  • On any push to the main branche
  • When a new tag is pushed (used for Docker image release)
  • On pull requests targeting main

2. What is Mise?

Mise est un gestionnaire de versions moderne qui prend en charge plusieurs langages et outils — similaire à asdf, mais plus rapide et plus facile à configurer. En utilisant un seul fichier de configuration (`mise.toml`), vous pouvez définir les versions exactes d'outils comme Elixir, Erlang, Ruby, et même des environnements JavaScript comme Bun.

En développement local comme en CI, cela garantit que tout le monde — et chaque tâche — utilise les mêmes versions. No more surprises between your machine and GitHub Actions!

2.1 Defined Tool Versions

  [tools]
  bun = "1.2.11"
  elixir = "1.18.3"
  erlang = "27.3.4"
  ruby = "3.4.2"
  

This setup guarantees consistent runtime environments:

  • bun for asset management
  • elixir and erlang for the Phoenix backend
  • ruby for tools like Kamal used in deployment

2.2 Reusable Tasks

Mise vous permet également de définir des tâches CLI, vous pouvez donc standardiser les flux de travail courants comme la configuration, les nettoyages et les assistants de déploiement. These tasks are great for onboarding and automation.

  [tasks.reset]
  description = "Reset and recompile project"
  run = "rm -rf deps _build .elixir_ls && mix deps.get && mix && mix test"

  [tasks."clean:images"]
  description = "Delete images folders"
  run = "rm -rf priv/waffle"

  [tasks."setup:kamal"]
  description = "Install kamal to manage deployment"
  run = "gem install kamal"

  [tasks."assets:install"]
  description = "Install JS assets"
  run = "bun install --cwd assets"
  

2.3 Mise in CI

In the CI, we use the official `jdx/mise-action` to install and cache these tools quickly. Cela accélère les builds, évite les versions incompatibles et maintient des environnements CI miroirs des environnements de développement locaux.

En mettant en cache les artefacts de mixage et de construction par version d'outil et par environnement, nous gagnons également du temps sur les exécutions répétées de CI :

  key: ${{ env.MIX_ENV }}-mise-${{ hashFiles('**/mise.toml') }}-mix-${{ hashFiles('**/mix.lock') }}
  

3. Job Overview

The workflow defines four jobs. Let's dig into these !

3.1 Tools

This job initializes the CI environment by installing all required tools using mise. Il assure la cohérence entre le développement et la CI en alignant les versions des outils définies dans mise.toml.

    tools:
    name: TOOLS
    runs-on: ubuntu-24.04
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Set up tools
        uses: jdx/mise-action@v2

    

3.2 Lint

Le job LINT applique les règles de qualité de code et de sécurité. Il vérifie le formatage, les dépendances inutilisées, les vulnérabilités de sécurité et les problèmes stylistiques. Tools like credo, sobelow, hex.audit, et mix deps.unlock --check-unused are used to ensure the codebase stays clean and secure.

    lint:
    name: LINT
    needs: [tools]
    runs-on: ubuntu-24.04
    env:
      MIX_ENV: dev
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Set up tools
        uses: jdx/mise-action@v2
      - name: Cache
        uses: actions/cache@v4
        with:
          path: |
            deps
            _build
          key: ${{ env.MIX_ENV }}-mise-${{ hashFiles('**/mise.toml') }}-mix-${{ hashFiles('**/mix.lock') }}
          restore-keys: |
            ${{ env.MIX_ENV }}-mise-${{ hashFiles('**/mise.toml') }}-mix-
            ${{ env.MIX_ENV }}-mise-
      - name: Install dependencies
        run: mix deps.get
      - name: Compile without warnings
        run: mix compile --warnings-as-errors
      - name: Check code format
        run: mix format --dry-run --check-formatted
      - name: Check unused deps
        run: mix deps.unlock --check-unused
      - name: Hex audit
        run: mix hex.audit
      - name: Deps audit
        run: mix deps.audit
      - name: Run credo
        run: mix credo
      - name: Run sobelow
        run: mix sobelow --config

    

3.3 Test

This job runs the test suite with mix test. Il garantit que l'application se comporte comme prévu. PostgreSQL est commenté pour l'instant car le projet ne nécessite pas actuellement de base de données, mais la configuration est prête pour une utilisation future.

    test:
    name: TEST
    needs: [tools]
    runs-on: ubuntu-24.04
    env:
      MIX_ENV: test
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Set up tools
        uses: jdx/mise-action@v2
      - name: Cache
        uses: actions/cache@v4
        with:
          path: |
            deps
            _build
          key: ${{ env.MIX_ENV }}-mise-${{ hashFiles('**/mise.toml') }}-mix-${{ hashFiles('**/mix.lock') }}
          restore-keys: |
            ${{ env.MIX_ENV }}-mise-${{ hashFiles('**/mise.toml') }}-mix-
            ${{ env.MIX_ENV }}-mise-
      - name: Install dependencies
        run: mix deps.get
      - name: Run tests
        run: mix test --max-failures=1 --color

    

3.4 Build

Le job BUILD compile l'application, construit les assets et génère une release. Il utilise les paramètres de production pour s'assurer que la sortie compilée correspond à ce qui sera déployé. Si un tag Git est poussé, il construit également et pousse une image Docker vers Docker Hub en utilisant les secrets configurés.

    build:
    name: BUILD
    needs: [lint, test]
    runs-on: ubuntu-24.04
    env:
      MIX_ENV: prod
      PHX_SERVER: true
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Set up tools
        uses: jdx/mise-action@v2
        with:
          cache_key_prefix: mise-${{ runner.os }}
      - name: Cache
        uses: actions/cache@v4
        with:
          path: |
            deps
            _build
          key: ${{ runner.os }}-${{ env.MIX_ENV }}-mise-${{ hashFiles('**/mise.toml') }}-mix-${{ hashFiles('**/mix.lock') }}
          restore-keys: |
            ${{ runner.os }}-${{ env.MIX_ENV }}-mise-${{ hashFiles('**/mise.toml') }}-mix-
            ${{ runner.os }}-${{ env.MIX_ENV }}-mise-
      - name: Install web dependencies
        run: mise run assets:install
      - name: Install backend dependencies
        run: mix deps.get --only prod
      - name: Build
        run: MIX_ENV=${{ env.MIX_ENV }} mix do assets.setup, assets.deploy, compile --warnings-as-errors, release --overwrite
      - name: Docker meta
        if: ${{ contains(github.ref, 'refs/tags/') }}
        uses: docker/metadata-action@v5
        id: meta
        with:
          images: ${{ env.DOCKER_IMAGE }}
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
      - name: Set up docker buildx
        if: ${{ contains(github.ref, 'refs/tags/') }}
        id: buildx
        uses: docker/setup-buildx-action@v3
      - name: Login to docker hub
        if: ${{ contains(github.ref, 'refs/tags/') }}
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USER }}
          password: ${{ secrets.DOCKER_TOKEN }}
      - name: Release
        if: ${{ contains(github.ref, 'refs/tags/') }}
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          cache-from: type=gha
          cache-to: type=gha,mode=max
          builder: ${{ steps.buildx.outputs.name }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

    

4. Docker Image Release

On each tagged commit, the workflow:

  • Builds the Phoenix release with static assets
  • Generates Docker metadata and tags
  • Logs into Docker Hub using secrets
  • Builds and pushes the image using build-push-action

5. Conclusion

This setup gives us confidence in code quality and makes production releases seamless Si vous construisez un site Phoenix LiveView sans base de données, c'est une bonne base. When you're ready to enable Postgres, the foundation is already there.