AGENT SKILL

volontariapp-db-migration

Playbook opérationnel pas-à-pas pour concevoir, tester et propager une migration TypeORM sans coupure de service (Zero-Downtime Expand/Contract, partition common/domain, yarn meta sync-migrations).

适用职业律师与法务
使用场景后端与接口
License 许可
MIT
所属仓库
Volontariapp/meta
最近更新
2026-11-10
Stars
★ 0
Forks
0

01 前言 Front Matter

name
volontariapp-db-migration
tags
[ "db-migration", "postgresql", "typeorm", "zero-downtime", "expand-contract", "playbook" ]
type
Playbook
paths
[ "ms-*/src/migrations/**", "npm-packages/packages/database/src/**", "scripts/cli/commands/sync-migrations.mjs" ]
title
Migrations PostgreSQL et Synchronisation Multi-Services
status
stable
sources
[ { "id": "sync-mig", "kind": "local_file", "title": "sync-migrations.mjs", "resource": "scripts/cli/commands/sync-migrations.mjs" }, { "id": "compose", "kind": "local_file", "title": "ci-tools/docker-compose.yml", "resource": "ci-tools/docker-compose.yml" }, { "id": "data-source", "kind": "local_file", "title": "ms-user/src/config/data-source.ts", "resource": "ms-user/src/config/data-source.ts" } ]
verified
[ { "at": "2026-11-06T14:04:00Z", "by": "claude-code/agent", "digest": "a6bfa42cee9dd9ef" }, { "at": "2026-11-07T15:34:34Z", "by": "claude-code/agent", "digest": "65868a898ae28556" }, { "at": "2026-11-07T15:49:20Z", "by": "claude-code/agent", "digest": "65868a898ae28556" }, { "at": "2026-11-07T15:55:37Z", "by": "claude-code/agent", "digest": "65868a898ae28556" }, { "at": "2026-11-08T20:59:18Z", "by": "claude-code/agent", "digest": "65868a898ae28556" } ]
generated
{ "at": "2026-10-12T11:56:30Z", "by": "claude-code/agent" }
mesh_keys
[ "migrations", "sync-migrations", "typeorm" ]
description
Playbook opérationnel pas-à-pas pour concevoir, tester et propager une migration TypeORM sans coupure de service (Zero-Downtime Expand/Contract, partition common/domain, yarn meta sync-migrations).

02 资源文件 4

03 SKILL.md 正文

Migrations PostgreSQL et Synchronisation Multi-Services

Quand utiliser ce playbook

Utiliser ce playbook pour toute évolution de schéma relationnel PostgreSQL dans un microservice (ms-user, ms-post, ms-event, ms-social, ms-storage, ms-ranking) :

  • Ajout, renommage ou modification d'une table, colonne ou index métier.
  • Évolution des tables transactionnelles partagées (jobs_outbox, event_queue, job_audit).
  • Modification des triggers SQL de notification outbox.
  • Synchronisation des migrations vers les packages de test de domaine (npm-packages/packages/domain-*).

Règles d'or de conception

  1. Zero-Downtime obligatoire (Pattern Expand / Contract) :
    • Tout ajout de colonne non-nulle doit d'abord être déployé avec une valeur par défaut (DEFAULT 'value') ou être nullable (NULL).
    • Interdiction formelle de renommer ou supprimer (DROP COLUMN) une colonne active dans la même release : le code existant doit d'abord cesser de la lire/écrire (Phase Contract ultérieure).
  2. Bidirectionnalité stricte :
    • Chaque méthode up() DOIT avoir son pendant inverse exact et idempotente dans down().
  3. Isolation stricte par service :
    • Chaque microservice dispose de sa base PostgreSQL dédiée (ports 5432-5436 en local)^compose. Aucune jointure inter-bases n'est permise.
  4. Partitionnement rigoureux common/ vs domain/ :
    • src/migrations/common/ : Infrastructure technique partagée (Outbox, Audit, Triggers).
    • src/migrations/domain/ : Schémas et tables purement métier propres au microservice.

Procédure pas-à-pas

Étape 1 : Déterminer la portée de la migration

Identifier le répertoire cible dans le microservice :

  • Migration Commune (ms-*/src/migrations/common/) :
    • Concerne l'Outbox transactionnelle (jobs_outbox, event_queue, job_audit)^data-source ou un trigger SQL de dispatch.
    • Script de propagation : scripts/push-common-migration.sh <fichier> "<titre du commit>" [--dry-run] copie la migration dans les neuf emplacements ci-dessous (worktrees neufs sur origin/main, md5 contrôlé, commit avec Co-Authored-By, push sur main). Il s'utilise après publication de npm-packages et levée du verrou du STOP (cas de 1794166000000-AddOutboxPollingIndexes, 2026-11-08, neuf dépôts). Pour suivre la CI ensuite, gh run list --commit exige le SHA complet : un SHA court ne renvoie rien.
    • Doit être créée dans un microservice de référence (ex. ms-user), puis copiée à l'identique (même nom, même contenu, vérifier le md5) dans src/migrations/common/ de chaque service qui a sa propre base : ms-user, ms-social, ms-post, ms-event, ms-storage, ms-ranking et ws-service (base ms_ws, oublié lors de la tâche 2.9 : une colonne ajoutée à OutboxModel fait échouer ses écritures tant que la migration manque). post-processors-runner/post-processor-event et workers-runners/worker-event gardent aussi une copie et utilisent en production la base ms_event de ms-event (migration déjà appliquée), mais leur CI reconstruit une base neuve depuis leurs propres migrations (global-setup de worker-event : drop du schéma puis runMigrations) : sans la copie, column EventQueueModel.traceparent does not exist (constaté sur workers-runners#16). Copier aussi la nouvelle migration commune dans ces deux dossiers. Pour reproduire la CI en local sans toucher à sa base : DB_NAME=<base jetable> yarn test:integration. yarn meta sync-migrations ne fait PAS cette propagation inter-services^sync-mig. Pour repérer les copies : chercher le nom d'une migration commune existante (1781000000000-AddCorrelationIdToEventQueue.ts) sur tout le workspace, hors deploy/. Ces trois dossiers ont dérivé (migrations plus anciennes que celles de ms-*) : ne copier que la nouvelle migration. Le md5 peut différer sans que le SQL change : la copie de 1781400000000-AddFinishedAndCancelledToEventStateEnum.ts dans worker-event (workers-runners#50, dont le message de commit annonce « byte-identically ») a un md5 différent de celle de ms-event parce que ses lignes sont recoupées, alors que les requêtes up/down sont les mêmes (cause probable : le formatage du repo cible, non vérifiée). Si le md5 diffère, comparer les requêtes avec diff plutôt que conclure à une dérive. 1792200000000-AddScoringVersionToEvent.ts est, lui, identique au md5.
  • Migration Domaine (ms-*/src/migrations/domain/) :
    • Concerne les tables propres au métier (ex. users, badges dans ms-user ; posts dans ms-post).
    • Reste cantonnée au répertoire domain/ du microservice concerné.

Étape 2 : Écrire la migration TypeORM

Créer un fichier nommé avec le timestamp Unix actuel sous la forme <timestamp>-<NomExplicite>.ts :

import type { MigrationInterface, QueryRunner } from 'typeorm';

export class AddFeatureColumn1792000000000 implements MigrationInterface {
  name = 'AddFeatureColumn1792000000000';

  public async up(queryRunner: QueryRunner): Promise<void> {
    // Pattern Expand : colonne avec DEFAULT pour ne pas bloquer les écritures en prod
    await queryRunner.query(
      `ALTER TABLE "users" ADD COLUMN "feature_flag" boolean NOT NULL DEFAULT false`,
    );
    // Pas de CONCURRENTLY ici : voir le piège juste après ce bloc
    await queryRunner.query(
      `CREATE INDEX "idx_users_feature_flag" ON "users" ("feature_flag")`,
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP INDEX "idx_users_feature_flag"`);
    await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "feature_flag"`);
  }
}

Piège : CREATE INDEX CONCURRENTLY et transaction = false sont refusés. CONCURRENTLY ne s'exécute pas dans une transaction, mais une migration qui déclare transaction = false fait lever ForbiddenTransactionModeOverrideError quand la source de données exécute toutes les migrations en une transaction, ce qui est le mode par défaut de TypeORM (migrationsTransactionMode: 'all', lu dans typeorm/migration/MigrationExecutor.js). Dans ce cas aucun service ne démarre (les migrations tournent dans le conteneur d'init run-migrations). Écrire CREATE INDEX IF NOT EXISTS dans la transaction : le verrou en écriture dure le temps de la construction, quelques millisecondes sur une table petite. Sur une grosse table, purger ou réduire d'abord, ou changer le mode de transaction des sources de données avant la migration (non fait à ce jour).


Étape 3 : Déclarer les entités et vérifier data-source.ts

  1. Si la migration introduit une nouvelle entité ou un nouveau champ, mettre à jour l'entité TypeORM correspondante dans le package de domaine (@volontariapp/domain-*) ou @volontariapp/database.
  2. Vérifier que src/config/data-source.ts référence bien l'entité dans son tableau entities^data-source :
// ms-user/src/config/data-source.ts
export const AppDataSource = new DataSource({
  type: 'postgres',
  /* ... */
  entities: [
    EventQueueModel,
    JobsOutboxModel,
    JobAuditModel,
    UserModel,
    NewFeatureModel, // <- Entité déclarée
  ],
  synchronize: false,
  migrations: [
    join(dirname(fileURLToPath(import.meta.url)), '..', 'migrations', '**', '*.{ts,js}'),
  ],
});
  1. Si un post-processor-* ou un worker-* lit ou écrit la nouvelle table via le DataSource de la base de ms-<d> (ex. post-processor-user sur ms_user), ajouter aussi l'entité à la liste entities de son infrastructure.module.ts : sans cela, TypeORM lève « No metadata for X was found » au premier traitement, et les tests unitaires qui mockent le DataSource ne le voient pas. Exemple : impact_score_credits (ImpactScoreCreditModel, migration 1792100000000-CreateImpactScoreCredits dans ms-user).
  2. Monter domain-<d> dans un worker-* casse sa CI si ses copies de migrations ont dérivé. Seuls les packages qui ont un global-setup de test d'intégration (worker-event) rebuild une base neuve depuis src/migrations/ : le domain-<d> récent y mappe des colonnes que la copie n'a pas. Un package sans global-setup n'est pas concerné : post-processor-event avait pourtant la même dérive (2 migrations de domaine et 4 communes en retard sur ms-event) mais n'exécute jamais ses migrations (aucun migrationsRun ni runMigrations dans src, 3 tests unitaires avec DataSource mocké). On l'a résorbée par hygiène, sans effet sur la CI. Avant d'affirmer qu'une dérive casse quelque chose, chercher ce qui exécute réellement les migrations du package. Constaté sur workers-runners#19 : domain-event@3.12.1 mappe cover_file_id, absente de worker-event → column "cover_file_id" of relation "events" does not exist, 24 tests rouges. Correctif : copier la migration manquante à l'identique (diff <(ls ms-event/src/migrations/domain) <(ls worker-event/src/migrations/domain), puis md5). Reproduire en local sans toucher aux données : global-setup fait DROP SCHEMA public CASCADE sur DB_NAME, dont la valeur par défaut est ms_event. Créer une base jetable (CREATE DATABASE worker_event_ci_check) et lancer DB_NAME=worker_event_ci_check REDIS_PORT=<port> yarn test:integration avec un Redis jetable (docker run redis:7-alpine redis-server --requirepass password) : REDIS_DB_INDEX ne suffit pas, les jobs BullMQ ne sont alors jamais traités (handlerSpy appelé 0 fois).

Étape 4 : Propager les migrations avec yarn meta sync-migrations (si applicable)

Pour toute modification dans src/migrations/ (commune ou domaine pour les tests) :

  1. Lancer la commande à la racine de meta^sync-mig (toujours non interactive) :

    yarn meta sync-migrations --dry-run                    # Prévisualiser (+ ajouté, ~ modifié, - supprimé), sans rien écrire
    yarn meta sync-migrations                              # Tout synchroniser
    yarn meta sync-migrations --packages storage,user      # Microservices précis (avec ou sans préfixe ms-)
    yarn meta sync-migrations --pr                         # Branche, changeset, build, test, PR dans npm-packages, puis arrêt (règle STOP) ; refuse si npm-packages a des modifications locales, et incompatible avec --dry-run
    
  2. Microservices gérés : tous les ms-* de repos.json avec migrations: true et un package domain-* présent (user, social, post, event, storage, ranking). Pour ranking (état vérifié le 2026-10-28 avant la CLI) : son dossier domain-ranking/src/test/migrations/domain/ contient pour l'instant la migration CreateRankingTables écrite à la main, copiée à l'identique dans ms-ranking/src/migrations/domain/ (vérifié le 2026-10-28 : fichiers identiques, 10 tables ranking_* : ranking_score, ranking_post_score, ranking_run, ranking_request, ranking_like_log, ranking_event_feedback, ranking_event_wish, ranking_event_mention, ranking_user_location, ranking_rate_bucket). Les 16 migrations communes de ms-ranking sont identiques à celles de ms-user.

  3. La commande synchronise par un miroir équivalent à rsync -ac --delete (comparaison par contenu) les fichiers vers npm-packages/packages/domain-<service>/src/test/migrations (copie ms-* vers domain-*, jamais entre services) afin de garantir que les tests du domaine s'exécutent sur un schéma strictement identique.

  4. Piège : dérive. Ces dossiers test/migrations ne sont pas à jour (au 2026-10-13 : migrations absentes ou renommées, --delete supprimerait des fichiers dans domain-post, domain-event, domain-storage). Vérifier avec yarn meta sync-migrations --dry-run avant. Pour une migration commune isolée, copier uniquement le nouveau fichier plutôt que de resynchroniser le dossier entier.

  5. Copie workers : npm-packages/packages/workers/src/test/migrations/ contient aussi les migrations communes (outbox, job_audit, triggers), hors de yarn meta sync-migrations. Y ajouter toute nouvelle migration commune, sinon les tests de workers tournent sur un schéma obsolète (la PR traceparent l'a réalignée le 2026-10-13).

  6. STOP rule : domain-*/src/test/migrations et database sont sous npm-packages. Faire d'abord toutes les éditions ms-*, puis npm-packages en dernier : le hook stop-rule-guard.sh refuse les éditions ms-* après la première édition npm-packages.

  7. Quand npm-packages est déjà modifié (le modèle et la migration vont ensemble). Le hook refuse alors l'édition de ms-*. Écrire la migration d'abord dans npm-packages/packages/domain-<d>/src/test/migrations/domain/, dans la PR npm-packages, puis, une fois les packages publiés et le verrou levé, la copier à l'identique dans ms-*/src/migrations/domain/ (comparer le md5). Ne pas lancer yarn meta sync-migrations pour cela : il synchronise tout le dossier et le piège de dérive du point 4 s'applique. Exemple : AddBadgePointsAndProgressDedup1793970000000 (npm-packages#299, puis ms-user#88). Ajouter aussi les nouvelles entités à entities de data-source.ts et du database.module.ts du microservice.


Étape 5 : Exécution et validation locale

  1. Démarrer l'infrastructure locale PostgreSQL via Docker Compose^compose :
    docker compose -f ci-tools/docker-compose.yml up -d postgres-user
    
  2. Exécuter la migration en environnement local :
    cd ms-user
    NODE_ENV=local yarn typeorm migration:run -d src/config/data-source.ts
    
  3. Valider impérativement le rollback (down) :
    NODE_ENV=local yarn typeorm migration:revert -d src/config/data-source.ts
    
  4. Réappliquer la migration :
    NODE_ENV=local yarn typeorm migration:run -d src/config/data-source.ts
    
  5. Lancer la suite de tests du microservice :
    yarn test
    

Valider sur une base jetable plutôt que sur le volume du compose. docker compose up postgres-user réutilise le volume persistant postgres_user_data, et le data-source de test de domain-* fait un dropDatabase(). Pour le cycle run, revert, run (et pour les tests d'intégration yarn test:integration de domain-<d>), lancer un Postgres sans volume sur le port 5432, après avoir vérifié qu'il est libre :

lsof -nP -iTCP:5432 -sTCP:LISTEN   # doit ne rien afficher
docker run --rm -d --name pg-migration-check -p 5432:5432 \
  -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=ms_user postgres:16-alpine
# ... migration:run, migration:revert, migration:run ...
docker stop pg-migration-check

Contrôler aussi les données posées par la migration (UPDATE de remplissage) en insérant une ligne de test avant le second run.


Tables d'outbox : index et purge (depuis @volontariapp/database 3.13.0)

  • event_queue et jobs_outbox portent l'index (status, created_at, id) (migration commune 1794166000000-AddOutboxPollingIndexes, déclaré aussi par @Index sur EventQueueModel et JobsOutboxModel). Il sert la requête de sondage des runners (WHERE status = 'PENDING' ORDER BY created_at, id LIMIT n FOR UPDATE SKIP LOCKED) et la purge. Sans lui, chaque sondage lisait toute la table : en novembre 2026, 125 à 245 Mo/s de lectures disque continues sur ms-ranking-db ont fait caler la VM entière plusieurs secondes de suite.
  • OutboxRunner purge les lignes COMPLETED plus vieilles que la rétention (défaut : 1 jour, une passe par heure, lots de 5 000, sur sa propre boucle), et job_audit à côté de jobs_outbox. Réglage : OutboxRunnerConfig.purge (enabled, retentionMs, intervalMs, batchSize), tous optionnels. Les lignes PENDING, PROCESSING et FAILED ne sont jamais supprimées. Conséquence : l'historique des lignes terminées ne dépasse pas un jour (voir volontariapp-trace-async-flow).
  • job_audit n'a pas d'index (status, created_at) : sa purge trie les lignes COMPLETED (environ 200 000 à ce jour) à chaque passe, une lecture ponctuelle de l'ordre de 100 Mo observée à la première passe. À indexer si elle gêne.
  • Pour une nouvelle requête de sondage ou de purge sur une table qui grossit, vérifier le plan (EXPLAIN) : un Seq Scan sur une table d'outbox est le signal à traiter.

Checklist de validation

  • [ ] La migration applique le pattern Expand / Contract (aucun DROP COLUMN ni renommage brutal direct).
  • [ ] La méthode down() est strictement l'inverse de la méthode up().
  • [ ] Les nouvelles entités sont exportées et enregistrées dans entities de src/config/data-source.ts^data-source.
  • [ ] Si une migration commune a été touchée, yarn meta sync-migrations a été exécuté^sync-mig.
  • [ ] Le cycle migration:run $\rightarrow$ migration:revert $\rightarrow$ migration:run s'exécute sans erreur sur la base locale^compose.
  • [ ] yarn test passe avec succès sur le microservice.

Références

04 同仓库更多 Skills

Volontariapp
Volontariapp/meta

volontariapp-user-account-deletion

Suppression de compte en deux temps (PENDING_DELETION): requestDeletionWithUserDeleted, finalizeDeletion, fan-out de user.deleted vers les domaines et gather ws. A utiliser pour tout changement de domain-user touchant la suppression, le statut ou le cycle de vie d'un user.

★ 0
Volontariapp
Volontariapp/meta

volontariapp-proto-contract-evolution

Modifier un contrat Protobuf de proto-registry (champ, message, enum, RPC) sans casser la compatibilité wire, puis suivre la cascade proto-registry, PR automatique dans npm-packages, publication, consommateurs. À utiliser avant toute édition d'un .proto.

律师与法务
★ 0
Volontariapp
Volontariapp/meta

volontariapp-meta-scripts

Utiliser et faire evoluer la CLI Node de meta (setup, doctor, migrate, certs, repos, bump) et le manifeste repos.json, source unique des repos et unites du workspace.

Skill与智能体开发
★ 0
Volontariapp
Volontariapp/meta

volontariapp-implement-async-event-flow

Guide pas-à-pas pour concevoir et implémenter un flux asynchrone complet (messaging -> outbox -> post-processors -> ws-service -> client).

前端开发
★ 0
Volontariapp
Volontariapp/meta

research

Investigate a question against high-trust primary sources and capture the findings as a Markdown file in the repo. Use when the user wants a topic researched, docs or API facts gathered, or reading legwork delegated to a background agent.

资料检索与研究
★ 0
Volontariapp
Volontariapp/meta

prototype

Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.

设计师
★ 0