Mise en place : un seul outil pour les gouverner tous

De six outils à un seul. Retour sur la refonte du template Hugo + Bunny CDN avec mise.

Ce qui n’allait pas

Dans l’article précédent, le template reposait sur une poignée d’outils indépendants :

  • Just pour les commandes (just dev, just build, just deploy)
  • direnv pour charger les variables d’environnement au cd
  • 1Password CLI appelé directement dans .envrc pour les secrets
  • Hugo, OpenTofu téléchargés manuellement en CI via curl + tar
  • bunny-transfer lancé via npx @latest — sans version fixée

Ça fonctionnait, mais chaque brique ajoutait sa propre complexité :

Les versions divergeaient entre local et CI. Hugo 0.153 sur le poste de dev, 0.155 en CI parce que quelqu’un a oublié de mettre à jour le HUGO_VERSION dans .gitlab-ci.yml. Même problème pour Just, OpenTofu. Deux sources de vérité, aucune fiable.

La CI téléchargeait des binaires sans vérification. Un curl | tar pour Hugo, un autre pour Just. Pas de checksum, pas de signature. Le template le reconnaissait d’ailleurs dans sa section “Limites de sécurité”.

npx bunny-transfer@latest était un pari. Chaque déploiement tirait la dernière version — si elle introduisait un breaking change, le site ne se déployait plus. Supply chain non maîtrisée.

direnv ne fonctionnait qu’en local. Les variables d’environnement étaient dupliquées entre .envrc (dev) et .gitlab-ci.yml (CI). Modifier un nom de variable obligeait à toucher les deux fichiers.

Mise entre en scène

mise (prononcé “mise en place”) est un gestionnaire d’outils et d’environnement. Un seul fichier mise.toml remplace Just, direnv, et toutes les installations manuelles :

[tools]
hugo-extended = "0.159.0"
node = "24.14.1"
opentofu = "1.11.5"
jq = "1.8.1"
"npm:bunny-transfer" = "0.0.5"
"npm:lighthouse" = "13.0.3"

mise install et tout est prêt — mêmes versions en local et en CI.

Des CLI npm sans package.json

Le préfixe npm: dans la section [tools] permet d’installer des packages npm comme de simples binaires CLI. Mise les télécharge, les installe dans son propre répertoire et les rend disponibles dans le PATH — exactement comme Hugo ou OpenTofu.

C’est un changement important. Avant, on avait deux options pour utiliser un outil npm comme bunny-transfer :

  • npx bunny-transfer@latest : télécharge à chaque appel, pas de version fixée, lent
  • package.json + npm ci : version fixée, mais ça vient avec node_modules/, package-lock.json, des jobs CI pour l’audit npm et la vérification du lockfile

Avec mise, on obtient le meilleur des deux mondes : une version fixée, pas de package.json, pas de node_modules, pas de npm ci en CI. bunny-transfer et lighthouse sont des outils au même titre que Hugo — installés une fois, mis en cache, mis à jour par Renovate.

Résultat : cinq fichiers en moins (package.json, package-lock.json, scripts/update-npm-lockfile.sh) et deux jobs CI supprimés (npm:audit, npm:lockfile:verify).

Variables d’environnement centralisées

Fini la duplication entre .envrc et .gitlab-ci.yml. Tout est dans mise.toml :

[env]
BUNNYNET_STORAGE_ZONE = "my-storage-zone"
BUNNYNET_PULL_ZONE = "my-pull-zone"
BUNNYNET_DOMAIN = "example.com"
HUGO_BASEURL = "https://{{env.BUNNYNET_DOMAIN}}/"

# OpenTofu — les variables TF_VAR_* sont dérivées des variables Bunny
TF_VAR_storage_zone_name = "{{env.BUNNYNET_STORAGE_ZONE}}"
TF_VAR_pull_zone_name = "{{env.BUNNYNET_PULL_ZONE}}"
TF_VAR_domain = "{{env.BUNNYNET_DOMAIN}}"

# Backend HTTP GitLab — même config en local et en CI
TF_HTTP_ADDRESS = "https://gitlab.com/api/v4/projects/{{env.GITLAB_PROJECT_ID}}/terraform/state/bunny-cdn"
TF_HTTP_USERNAME = "gitlab-ci-token"
TF_HTTP_PASSWORD = '{{env.CI_JOB_TOKEN | default(value="")}}'

En CI, CI_JOB_TOKEN est injecté automatiquement par GitLab. En local, un fichier mise.development.toml prend le relais avec les credentials personnels via 1Password :

# mise.development.toml — activé avec MISE_ENV=development
[env]
_.source = { path = "{{ config_root }}/scripts/op-inject.sh", redact = true }

Le redact = true masque les valeurs dans les logs mise — un détail qui évite les fuites accidentelles.

Le script op-inject.sh injecte les secrets depuis un fichier 1pass.env contenant les références 1Password :

BUNNYNET_API_KEY={{ op://Private/Bunny/account API key }}
TF_HTTP_USERNAME={{ op://Private/GitLab/username }}
TF_HTTP_PASSWORD={{ op://Private/GitLab/Autres champs/PAT }}

Un seul appel à op inject, un seul fichier de secrets, pas de op read répétés.

Les tâches remplacent Just

Les tâches mise remplacent le Justfile :

[tasks.dev]
description = "Dev server with drafts"
run = "hugo server --buildDrafts --port ${PORT:-1313}"

[tasks.build]
description = "Build for production"
run = """
hugo --minify
echo "  $(find public -name '*.html' | wc -l | tr -d ' ') pages, $(du -sh public | cut -f1)"
"""

[tasks.deploy]
description = "Deploy to Bunny CDN"
run = "./scripts/deploy.sh public"

[tasks.release]
description = "Full release: clean, build, validate, deploy"
depends = ["clean", "build", "validate", "deploy"]

mise run dev remplace just dev. mise run release enchaîne les tâches avec gestion des dépendances. L’avantage sur Just : les tâches héritent automatiquement des variables d’environnement et des outils définis dans mise.toml.

Les tâches OpenTofu sont aussi dans mise :

[tasks.tf-plan]
description = "Plan infrastructure changes"
run = """
mkdir -p ${TF_PLUGIN_CACHE_DIR}
tofu -chdir=infra init
tofu -chdir=infra plan --out=plan.tfplan
if command -v jq >/dev/null 2>&1; then
  tofu -chdir=infra show --json plan.tfplan \
    | jq -r '([.resource_changes[]?.change.actions?]|flatten)|...' \
    > infra/plan.json
fi
"""

Audit Lighthouse intégré

Le template inclut désormais Lighthouse comme outil mise. Un mise run audit lance un audit complet (performance, accessibilité, SEO, bonnes pratiques) via Chrome headless et génère un rapport HTML :

mise run audit              # Audite le site en production
mise run audit -- local     # Audite le serveur local

Lighthouse est un outil de développement local — il ne tourne pas en CI (il a besoin de Chrome). Mais l’avoir dans mise.toml garantit que tous les développeurs utilisent la même version et peuvent vérifier les performances avant de pousser. L’URL du site est construite depuis $BUNNYNET_DOMAIN — pas de valeur codée en dur.

La CI simplifiée

L’ancien pipeline téléchargeait Hugo et Just manuellement avec 20 lignes de curl + tar. Le nouveau :

.mise_common:
  image: cgr.dev/chainguard/wolfi-base:latest
  cache:
    key: mise-tools
    paths:
      - .mise/
  before_script:
    - apk update && apk add --no-cache libstdc++ bash curl
    - |
      if [ ! -f ${MISE_INSTALL_PATH} ]; then
        curl https://mise.run | sh
      fi
    - export PATH="${MISE_DATA_DIR}/bin:${MISE_DATA_DIR}/shims:$PATH"
    - mise install --yes

mise install télécharge tout — Hugo, Node, OpenTofu, jq, bunny-transfer, lighthouse — en une commande. Les versions sont celles de mise.toml, identiques au poste de dev. Le cache GitLab CI conserve les outils entre les pipelines.

Les jobs deviennent triviaux :

hugo:build:
  extends: .mise_common
  script:
    - mise run build

hugo:deploy:
  extends: .mise_common
  script:
    - mise run deploy

tofu:plan:
  extends: .tofu_common
  script:
    - mise run tf-plan

Plus de variables dupliquées dans le CI — elles viennent de mise.toml. Plus de apk add jq — mise l’installe. Plus de npm ci pour bunny-transfer — c’est un outil mise.

Renovate surveille tout

Renovate surveille mise.toml nativement grâce au manager mise. Quand une nouvelle version de Hugo, Node, OpenTofu, bunny-transfer ou jq sort, Renovate ouvre une merge request :

{
  "mise": { "enabled": true },
  "packageRules": [
    {
      "matchManagers": ["mise"],
      "groupName": "mise tools",
      "minimumReleaseAge": "3 days"
    }
  ]
}

Le minimumReleaseAge de 3 jours laisse le temps à la communauté de repérer d’éventuels problèmes avant d’adopter une nouvelle version. C’est un filet de sécurité simple mais efficace.

La structure du template

├── content/              # Contenu Markdown
├── layouts/              # Templates Hugo
├── assets/               # Traités par Hugo (CSS, JS)
├── static/               # Copiés tels quels (favicon, fonts)
├── infra/                # Infrastructure OpenTofu (Bunny CDN)
├── scripts/
│   ├── deploy.sh         # Déploiement Bunny via bunny-transfer
│   ├── indexnow.sh       # Notification moteurs de recherche
│   ├── validate.sh       # Validation du build (alt texts, tailles)
│   ├── stats.sh          # Statistiques du build
│   ├── audit.sh          # Audit Lighthouse (performance, a11y, SEO)
│   └── op-inject.sh      # Injection secrets 1Password
├── mise.toml             # Outils + variables + tâches
├── mise.development.toml # Secrets locaux (1Password)
├── 1pass.env             # Références 1Password
├── renovate.json         # Mises à jour automatiques
├── .gitlab-ci.yml        # Pipeline CI/CD
└── hugo.toml             # Configuration Hugo

Les fichiers qui ont disparu : Justfile, .envrc, .envrc.example, package.json, package-lock.json. Cinq fichiers en moins, zéro fonctionnalité perdue.

Ce qu’on y gagne

AvantAprès
Just + direnv + curl/tar + npxmise
Versions dans 3 fichiersVersions dans mise.toml
Variables dupliquées (.envrc + CI)Variables dans mise.toml
npx bunny-transfer@latestVersion fixée dans mise
20 lignes de curl + tar en CImise install --yes
Pas de vérification de checksumsMise vérifie les checksums
Renovate avec regex customRenovate natif pour mise

Mais le vrai gain est ailleurs : une seule source de vérité. Un développeur qui clone le projet tape mise install et a exactement le même environnement que la CI. Pas de “ça marche sur ma machine”.

Pour commencer

git clone https://gitlab.com/gautier-levert/template-hugo-bunny.git my-site
cd my-site

# Configurez vos valeurs dans mise.toml
# (BUNNYNET_STORAGE_ZONE, BUNNYNET_DOMAIN, GITLAB_PROJECT_ID)

mise install          # Installe tous les outils
mise run dev          # Serveur de développement

# Infrastructure
mise run tf-plan      # Prévisualiser les changements
mise run tf-apply     # Créer les ressources Bunny

# Déployer
git push              # La CI fait le reste