Intégration continue pour un site Phoenix LiveView (sans 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.

Nous utilisons 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. Déclencheurs

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

La pipeline CI s'exécute automatiquement :

  • Lors de tout push vers le main branche
  • Quand un nouveau tag est créé (utilisé pour la création de la Docker Image)
  • Sur les pull requests vers main

2. Qu'est-ce que 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. Plus de surprises entre votre machine et GitHub Actions !

2.1 Version d'outils définit

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

Cette configuration garantit des environnements d'exécution cohérents :

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

2.2 Tâches réutilisables

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. Ces tâches sont excellentes pour l'intégration et l'automatisation.

  [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' dans la CI

Dans la CI, nous utilisons l'officiel `jdx/mise-action` pour installer et mettre en cache ces outils rapidement. 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. Apreçu des jobs

Le workflow définit quatre jobs. Examinons-les !

3.1 Outils

Ce job initialise l'environnement CI en installant tous les outils requis en utilisant 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. Des outils comme credo, sobelow, hex.audit, et mix deps.unlock --check-unused sont utilisés pour garantir que le code reste propre et sécurisé.

    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

Ce job exécute la suite de tests avec 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. Publication d'une image Docker

Sur chaque commit tagué, le flux de travail:

  • Construit la version Phoenix avec des actifs statiques
  • Generates Docker metadata and tags
  • Se connecte à Docker Hub en utilisant des secrets
  • Construit et pousse l'image en utilisant 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. Lorsque vous êtes prêt à activer Postgres, les fondations sont déjà là.