Gérer les migrations de base de données du noyau

Sur cette page

Les migrations du noyau d’EmDash mettent à jour les tables propres à EmDash et les colonnes standard des tables de contenu. Elles ne créent, ne suppriment ni ne renomment vos collections et vos champs ; consultez Faire évoluer le schéma d’un site déployé pour les modifications du modèle de contenu.

Le mode de migration à l’exécution vaut auto par défaut : les déploiements existants continuent donc d’appliquer au démarrage les migrations du noyau en attente. Les migrations gérées par le déploiement permettent à un build de migrer sa base de données avant que le nouveau code de l’application ne reçoive du trafic, puis à l’exécution de vérifier cette étape du déploiement ou de lui faire confiance.

Les migrations du noyau sont à sens unique. Elles sont écrites de façon qu’une commande puisse être relancée après des instructions qui se sont assurément terminées, mais une commande distante interrompue peut laisser un résultat ambigu. La réponse sûre consiste à inspecter la même base de données avec emdash migrate --status, plutôt que de supposer que la migration entière, ou rien du tout, a été exécutée.

Compiler, migrer, déployer, vérifier

Un build ou une synchronisation Astro écrit .emdash/migrations.json. Ce manifeste, exempt de secrets, enregistre la version exacte d’EmDash, l’ensemble ordonné des migrations, la configuration des locales et l’exécuteur de migrations de l’adaptateur utilisés par ce build.

Exécutez ces commandes depuis le projet dont les dépendances ont produit le manifeste. Compilez d’abord, puis inspectez la cible.

pnpm build
pnpm emdash migrate --status

Après avoir confirmé que la cible indiquée est bien la base de données prévue, lancez la migration interactive. Vérifiez à nouveau la cible à l’invite avant de confirmer. Déployez ensuite ce même build et vérifiez le schéma déployé.

pnpm emdash migrate
pnpm wrangler deploy
pnpm emdash migrate --check

emdash migrate --status signale les migrations appliquées, en attente et inconnues sans modifier la base de données. La commande emdash migrate seule affiche la cible et demande une confirmation avant d’appliquer les migrations en attente.

--check n’applique jamais de migrations et se termine avec un code non nul lorsque des migrations connues sont en attente ou que la base de données contient des enregistrements de migration inconnus du build. Utilisez --status lorsque vous voulez inspecter les mêmes ensembles de migrations sans le code de sortie non nul de check, qui signifie « du travail est nécessaire ». La référence de la CLI distingue les codes de sortie pour en attente, inconnu, confirmation, interruption et erreur opérationnelle.

L’application non interactive, ainsi que toute application avec --json, exige --expected-target-fingerprint ; la commande échoue si la cible résolue ne correspond pas. Utilisez ces options dans des tâches de déploiement automatisées, pas dans le flux interactif ci-dessus.

Utilisez --manifest path/to/migrations.json pour un manifeste stocké ailleurs. Pour une investigation locale, --from-config [--config astro.config.mjs] évalue explicitement la configuration d’un projet de confiance, sans exécuter les hooks Astro ni démarrer de serveur. Les pipelines de déploiement doivent consommer le manifeste du build.

Sélectionner la base de données explicitement

L’adaptateur configuré fournit au manifeste des informations de cible sans secrets. Les identifiants restent dans des variables d’environnement et ne sont lus que par la commande de migration.

AdaptateurCible du manifesteVariable d’identifiants par défautRemplacement utile
SQLiteChemin de la base de données ou URL file:—--database <path>
libSQLURL publiqueTURSO_AUTH_TOKENConfigurer migrationAuthTokenEnv
PostgreSQLNom de la variable de connexionDATABASE_URL--database-url-env <name>
Cloudflare D1Nom de la liaison WranglerCLOUDFLARE_API_TOKEN--d1, --account-id, --wrangler-config, --wrangler-env
HyperdriveLiaison principale et nom de la variable d’origineVariable d’origine directe propre à la liaisonConfigurer migrationConnectionStringEnv

Les chemins SQLite relatifs sont résolus depuis la racine du projet, et non depuis le paquet EmDash installé ni depuis le sous-répertoire courant du shell. Les libellés de cible PostgreSQL, libSQL et Hyperdrive omettent les identifiants et les paramètres d’URL.

Provisionner D1 avant de le migrer

Créer une base de données D1 et migrer son schéma sont deux opérations distinctes. emdash migrate ne crée jamais de base de données manquante.

  1. Provisionnez la base de données et notez son UUID de production.

    pnpm wrangler d1 create my-site-production
  2. Ajoutez cet UUID à la liaison et à l’environnement prévus dans wrangler.jsonc.

  3. Compilez le site pour que la liaison D1 soit enregistrée dans .emdash/migrations.json.

  4. Définissez l’ID du compte et un jeton d’API à portée limitée disposant de la permission D1 Edit. Inspectez la cible sélectionnée, puis exécutez la migration interactive. Ne confirmez l’invite que si le compte et la base de données correspondent à la base de données de production prévue.

    export CLOUDFLARE_ACCOUNT_ID="..."
    export CLOUDFLARE_API_TOKEN="..."
    pnpm emdash migrate \
      --status \
      --wrangler-config wrangler.jsonc \
      --wrangler-env production
    pnpm emdash migrate \
      --wrangler-config wrangler.jsonc \
      --wrangler-env production

Vous pouvez aussi fournir --account-id avec --d1 <database-uuid-or-name>. La recherche par nom doit aboutir à exactement une base de données. Les ID de prévisualisation, les ID de substitution, les comptes en conflit et les liaisons ambiguës échouent par précaution (fail closed).

Configurer les migrations D1 en CI

EmDash détient un verrou de migration dans la base de données D1 pendant qu’il applique des migrations, aussi bien depuis emdash migrate que depuis les migrations à l’exécution en mode auto. Une seconde exécution qui démarre pendant que le verrou est détenu attend jusqu’à 10 secondes. Si la première exécution se termine dans ce délai, la seconde réussit sans rien appliquer ; sinon, elle échoue sans appliquer de migrations. N’exécutez qu’une seule tâche de migration à la fois par compte et par UUID de base de données, afin qu’une seconde tâche attende dans la file d’attente de la CI au lieu d’échouer.

Définissez le secret et les variables suivants dans l’environnement de CI :

  • Secret CLOUDFLARE_API_TOKEN : un jeton à portée limitée disposant de la permission D1 Edit.
  • Variable CLOUDFLARE_ACCOUNT_ID : l’ID du compte Cloudflare propriétaire de la base de données.
  • Variable D1_DATABASE_ID : l’UUID de la base de données D1 de production.
  • Variable EMDASH_TARGET_FINGERPRINT : l’empreinte affichée par emdash migrate --status après que vous avez examiné localement le compte et la base de données.

Le workflow GitHub Actions suivant utilise ces valeurs et construit la clé du groupe de concurrence à partir des deux identifiants D1 immuables. Son étape d’application n’est pas interactive ; elle fournit donc explicitement l’empreinte de cible examinée.

name: Deploy

on:
  workflow_dispatch:

concurrency:
  group: emdash-migrations-${{ vars.CLOUDFLARE_ACCOUNT_ID }}-${{ vars.D1_DATABASE_ID }}
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build
      - name: Inspect EmDash migration target
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
        run: |
          pnpm emdash migrate --status --json \
            --account-id "${{ vars.CLOUDFLARE_ACCOUNT_ID }}" \
            --d1 "${{ vars.D1_DATABASE_ID }}"
      - name: Apply EmDash migrations
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          EMDASH_TARGET_FINGERPRINT: ${{ vars.EMDASH_TARGET_FINGERPRINT }}
        run: |
          pnpm emdash migrate \
            --account-id "${{ vars.CLOUDFLARE_ACCOUNT_ID }}" \
            --d1 "${{ vars.D1_DATABASE_ID }}" \
            --expected-target-fingerprint "$EMDASH_TARGET_FINGERPRINT"
      - run: pnpm wrangler deploy
      - name: Check EmDash migrations
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
        run: |
          pnpm emdash migrate --check \
            --account-id "${{ vars.CLOUDFLARE_ACCOUNT_ID }}" \
            --d1 "${{ vars.D1_DATABASE_ID }}"

Ne mettez à jour EMDASH_TARGET_FINGERPRINT qu’après avoir examiné localement une cible modifiée. L’empreinte ne contient aucun identifiant, mais la modifier sans vérifier le compte et la base de données supprime la protection contre la migration de la mauvaise base de données.

Libérer un verrou de migration bloqué

Une exécution de migration D1 qui s’arrête avant de libérer le verrou de migration laisse ce verrou détenu. Cela se produit lorsqu’une tâche de CI est annulée pendant emdash migrate, lorsqu’un Worker en mode auto s’arrête pendant une migration à l’exécution, ou lorsqu’un serveur de développement est arrêté pendant qu’il applique des migrations. Une exécution qui échoue avec une erreur de migration libère le verrou. EmDash ne libère pas un verrou qu’il ne détient pas, car le détenteur peut encore être en train d’appliquer des migrations ou s’être arrêté en plein milieu de l’une d’elles. Tant que le verrou n’est pas libéré, le site ne peut pas appliquer ses migrations en attente et, en mode auto, EmDash ne parvient pas à s’initialiser.

Lorsque le verrou est détenu depuis plus d’une minute, emdash migrate et les migrations à l’exécution cessent de l’attendre et signalent l’erreur suivante :

The migration lock has been held since 2026-09-01T12:00:00.000Z (lock 1788264000000). A migration may still be running; if none is, check the database and release the lock: https://docs.emdashcms.com/deployment/core-migrations/#release-a-stuck-migration-lock

Libérer le verrou d’une base de données D1 distante passe par emdash migrate ; il faut donc un build qui a écrit .emdash/migrations.json et un jeton d’API disposant de la permission D1 Edit, comme décrit dans Provisionner D1 avant de le migrer.

  1. Confirmez qu’aucune tâche de migration, aucun déploiement ni aucune autre commande emdash migrate ne s’exécute sur la base de données.

  2. Inspectez le verrou et les ensembles de migrations avec les mêmes options de cible que celles utilisées par la migration.

    pnpm emdash migrate --status

    Le rapport commence par le verrou et son id. Si l’exécution arrêtée était en train d’appliquer une migration, il s’agissait de la première migration en attente, et elle peut être partiellement appliquée.

    Migration lock: held since 2026-09-01T12:00:00.000Z (id 1788264000000)

    Un Worker en mode auto peut aussi détenir le verrou pendant qu’il applique des migrations. Relancez la commande une minute ou plus plus tard, et attendez au lieu de libérer le verrou tant que les migrations appliquées connues continuent de changer.

  3. Libérez le verrou avec cet id. La commande vous demande de confirmer la cible et ne libère le verrou que tant qu’il porte encore cet id. Dans un shell non interactif, ajoutez --expected-target-fingerprint avec l’empreinte de cible affichée par --status.

    pnpm emdash migrate --release-lock 1788264000000
  4. Appliquez à nouveau les migrations en attente.

    pnpm emdash migrate

    Si l’application échoue sur la première migration en attente, considérez cette migration comme interrompue en cours de route et suivez l’entrée sur une écriture D1 ambiguë dans Dépannage.

emdash migrate n’atteint que les bases de données D1 distantes. Lorsque le verrou est détenu dans la base de données D1 locale d’un serveur de développement, arrêtez le serveur et effacez le verrou avec Wrangler, en remplaçant DB par le nom de la liaison et le nombre par l’id du verrou indiqué dans l’erreur.

pnpm wrangler d1 execute DB --local --command "UPDATE _emdash_migrations_lock SET is_locked = 0 WHERE is_locked = 1788264000000"

Hyperdrive se connecte à l’origine

L’exécuteur de migrations d’Hyperdrive ouvre une connexion PostgreSQL directe vers l’origine. Il n’envoie pas le trafic de migration via Hyperdrive, n’utilise pas la liaison de cache facultative et n’hérite pas de l’accessibilité au réseau privé du Worker.

L’exécuteur du déploiement doit pouvoir atteindre l’origine. Définissez migrationConnectionStringEnv sur hyperdrive() lorsque la variable par défaut propre à la liaison ne convient pas, et ne fournissez cette variable qu’à la tâche de migration. Gardez séparés les identifiants Hyperdrive de l’exécution et les identifiants de déploiement vers l’origine directe.

Adopter progressivement le contrôle obligatoire à l’exécution

La configuration d’intégration EmDash suivante active le contrôle obligatoire à l’exécution tout en conservant les migrations automatiques en développement.

emdash({
	database,
	migrations: {
		runtime: "check",
		dev: "auto",
	},
});
  • auto est la valeur par défaut rétrocompatible. Le démarrage à l’exécution vérifie et applique les migrations en attente.
  • check effectue une requête d’état directionnelle et renvoie 503 avant de servir une requête lorsque des migrations connues sont en attente. Il tolère les enregistrements d’un build compatible plus récent pendant un déploiement progressif. Les requêtes vers le point de terminaison d’images qu’EmDash installe continuent d’être servies, car elles contournent le démarrage à l’exécution qui effectue cette vérification.
  • manual n’effectue aucune migration ni requête d’état à l’exécution. Ne l’utilisez qu’une fois que le pipeline de déploiement applique et vérifie chaque build de manière fiable.

EMDASH_MIGRATIONS_MODE peut remplacer le mode d’exécution lorsque le même artefact est promu à travers plusieurs environnements. Les routes de contournement de la configuration et du développement respectent le mode effectif ; elles ne peuvent pas migrer discrètement derrière check ou manual.

Un déploiement prudent consiste à utiliser auto pendant l’introduction de la tâche de déploiement, puis check une fois la tâche fiable, puis manual lorsqu’une vérification externe est imposée à chaque déploiement.

Compatibilité pendant les déploiements progressifs

Les migrations du noyau suivent la séquence expand/deploy/contract (étendre/déployer/contracter). Un déploiement peut temporairement exécuter d’anciens et de nouveaux isolats de l’application sur la base de données étendue, et un remplissage rétroactif (backfill) peut encore être en cours. Ne contractez pas un schéma tant que chaque version déployée n’a pas cessé de l’utiliser.

Les enregistrements de migrations appliquées inconnus sont tolérés par le check à l’exécution pour ce seul sens du déploiement progressif. La vérification exacte de la CLI les signale, et l’application refuse de modifier quoi que ce soit, car la base de données peut être plus récente ou avoir un historique de migrations divergent.

Limite de restauration

Déployer l’artefact précédent de l’application n’annule pas une migration du noyau. Avant d’appliquer les migrations en attente, effectuez une sauvegarde restaurable de la base de données et notez l’artefact de l’application qui lui correspond. Si l’application précédente ne peut pas fonctionner avec le schéma migré, restaurez ensemble la base de données antérieure à la migration et l’application. Ne supprimez pas de lignes de _emdash_migrations et n’exécutez pas la fonction interne down() d’une migration comme restauration opérationnelle.

Réparer la propriété PostgreSQL mixte

Utilisez ce guide lorsqu’un site PostgreSQL existant a créé des objets EmDash avec plus d’un propriétaire et que les migrations suivantes échouent avec des erreurs telles que must be owner of table. Choisissez le rôle canonique que la connexion principale d’EmDash continuera d’utiliser. Effectuez une sauvegarde restaurable de la base de données, puis arrêtez le trafic de l’application et les modifications de schéma avant de changer la propriété.

Inspectez chaque table du schéma actif :

SELECT
  n.nspname AS schema_name,
  c.relname AS table_name,
  pg_get_userbyid(c.relowner) AS owner
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE n.nspname = current_schema()
  AND c.relkind IN ('r', 'p')
ORDER BY c.relname;

Les objets EmDash comprennent les tables système _emdash_* et _plugin_*, les tables de collection ec_* et des tables sans préfixe telles que content_taxonomies, media, options, revisions et taxonomies. Dans un schéma EmDash dédié, chaque table de l’application devrait avoir le propriétaire canonique.

EmDash crée aussi des fonctions PostgreSQL utilisées par les déclencheurs d’utilisation des médias. Inspectez la propriété des fonctions et conservez la signature des arguments de chaque fonction pour la commande de réparation :

SELECT
  n.nspname AS schema_name,
  p.proname AS function_name,
  pg_get_function_identity_arguments(p.oid) AS arguments,
  pg_get_userbyid(p.proowner) AS owner
FROM pg_proc AS p
JOIN pg_namespace AS n ON n.oid = p.pronamespace
WHERE n.nspname = current_schema()
ORDER BY p.proname, arguments;

Transférez chaque objet non conforme avec un superutilisateur ou un rôle du fournisseur capable de modifier sa propriété. Utilisez le schéma, l’objet, le rôle et la signature de fonction réels issus de l’inventaire plutôt que de copier tels quels les noms de l’exemple :

ALTER TABLE emdash.content_taxonomies OWNER TO emdash_app;
ALTER TABLE emdash.ec_posts OWNER TO emdash_app;
ALTER FUNCTION emdash.emdash_media_usage_capture_work() OWNER TO emdash_app;

Changer le propriétaire d’une table couvre aussi ses index, contraintes et déclencheurs rattachés, mais pas les fonctions de déclencheur indépendantes. Répétez les deux requêtes d’inventaire jusqu’à ce que chaque table et chaque fonction EmDash indique le propriétaire canonique. Connectez-vous ensuite avec ce rôle et vérifiez current_database(), current_schema() et l’état des migrations avant de relancer le trafic.

Pour qu’un utilisateur qui n’est pas superutilisateur puisse transférer la propriété, il doit posséder l’objet ou en hériter la propriété, pouvoir faire SET ROLE vers le nouveau propriétaire, et le nouveau propriétaire doit avoir CREATE sur le schéma. Les fournisseurs de PostgreSQL géré peuvent exiger que leur rôle d’administration effectue le transfert.

Dépannage

  • Aucun manifeste de migration trouvé. Compilez ou synchronisez d’abord le projet. Utilisez --manifest pour un emplacement d’artefact non standard, ou choisissez explicitement --from-config pour une investigation locale.
  • L’artefact ne correspond pas à l’EmDash du projet. Recompilez et déployez ensemble l’application et le manifeste. Exécutez la CLI du projet plutôt qu’une installation globale.
  • La cible est manquante ou ambiguë. Provisionnez-la d’abord, puis fournissez un chemin de base de données explicite, le nom de la variable de connexion, un sélecteur D1, ou la configuration Wrangler et l’environnement sélectionnés. EmDash ne devine pas à partir de variables d’environnement ou de liaisons sans rapport.
  • L’empreinte de la cible a changé. Arrêtez-vous et examinez le compte, l’environnement, le nom de la base de données, l’UUID ou le chemin affichés. Ne mettez à jour l’empreinte attendue qu’après avoir confirmé la cible prévue.
  • Des enregistrements de migration inconnus sont présents. Ne supprimez pas ces enregistrements et ne relancez pas l’application. Confirmez que l’artefact de l’application correspond à la version prévue et recherchez si un build plus récent ou divergent a migré la base de données.
  • Le résultat d’une écriture D1 est ambigu. Ne rejouez pas la commande de migration. Exécutez emdash migrate --status sur le même compte et le même UUID de base de données, inspectez le résultat et escaladez si la migration s’est arrêtée en cours de route.
  • La normalisation des dates et heures nécessite une revue manuelle. Une ancienne date-heure tombe dans une heure répétée ou sautée lors d’un changement d’heure dans le fuseau horaire configuré du site. L’erreur liste chaque ligne de contenu ou révision concernée. Corrigez ces valeurs avec un décalage UTC explicite, puis relancez la migration. La vérification préalable de la migration n’écrit aucune date-heure tant que chaque valeur stockée ne peut pas être résolue.
  • Hyperdrive ne parvient pas à se connecter. Testez l’accessibilité de l’origine PostgreSQL depuis l’exécuteur du déploiement et vérifiez la variable d’origine directe. La connectivité entre le Worker et Hyperdrive ne prouve pas que l’exécuteur peut atteindre l’origine.