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
- 最近更新
- 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
- 参考文档index.md799 B
- 参考文档log.md6.3 KB
- 参考文档references/lessons.md4.4 KB
- 脚本scripts/push-common-migration.sh2.4 KB
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
- 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).
- Tout ajout de colonne non-nulle doit d'abord être déployé avec une valeur par défaut (
- Bidirectionnalité stricte :
- Chaque méthode
up()DOIT avoir son pendant inverse exact et idempotente dansdown().
- Chaque méthode
- 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.
- Partitionnement rigoureux
common/vsdomain/: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 surorigin/main, md5 contrôlé, commit avecCo-Authored-By, push surmain). Il s'utilise après publication denpm-packageset levée du verrou du STOP (cas de1794166000000-AddOutboxPollingIndexes, 2026-11-08, neuf dépôts). Pour suivre la CI ensuite,gh run list --commitexige 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) danssrc/migrations/common/de chaque service qui a sa propre base :ms-user,ms-social,ms-post,ms-event,ms-storage,ms-rankingetws-service(basems_ws, oublié lors de la tâche 2.9 : une colonne ajoutée àOutboxModelfait échouer ses écritures tant que la migration manque).post-processors-runner/post-processor-eventetworkers-runners/worker-eventgardent aussi une copie et utilisent en production la basems_eventdems-event(migration déjà appliquée), mais leur CI reconstruit une base neuve depuis leurs propres migrations (global-setupdeworker-event: drop du schéma puisrunMigrations) : 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-migrationsne 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, horsdeploy/. Ces trois dossiers ont dérivé (migrations plus anciennes que celles dems-*) : ne copier que la nouvelle migration. Le md5 peut différer sans que le SQL change : la copie de1781400000000-AddFinishedAndCancelledToEventStateEnum.tsdansworker-event(workers-runners#50, dont le message de commit annonce « byte-identically ») a un md5 différent de celle dems-eventparce que ses lignes sont recoupées, alors que les requêtesup/downsont les mêmes (cause probable : le formatage du repo cible, non vérifiée). Si le md5 diffère, comparer les requêtes avecdiffplutôt que conclure à une dérive.1792200000000-AddScoringVersionToEvent.tsest, lui, identique au md5.
- Concerne l'Outbox transactionnelle (
- Migration Domaine (
ms-*/src/migrations/domain/) :- Concerne les tables propres au métier (ex.
users,badgesdansms-user;postsdansms-post). - Reste cantonnée au répertoire
domain/du microservice concerné.
- Concerne les tables propres au métier (ex.
É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
- 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. - Vérifier que
src/config/data-source.tsréférence bien l'entité dans son tableauentities^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}'),
],
});
- Si un
post-processor-*ou unworker-*lit ou écrit la nouvelle table via leDataSourcede la base dems-<d>(ex.post-processor-usersurms_user), ajouter aussi l'entité à la listeentitiesde soninfrastructure.module.ts: sans cela, TypeORM lève « No metadata for X was found » au premier traitement, et les tests unitaires qui mockent leDataSourcene le voient pas. Exemple :impact_score_credits(ImpactScoreCreditModel, migration1792100000000-CreateImpactScoreCreditsdansms-user). - Monter
domain-<d>dans unworker-*casse sa CI si ses copies de migrations ont dérivé. Seuls les packages qui ont unglobal-setupde test d'intégration (worker-event) rebuild une base neuve depuissrc/migrations/: ledomain-<d>récent y mappe des colonnes que la copie n'a pas. Un package sansglobal-setupn'est pas concerné :post-processor-eventavait pourtant la même dérive (2 migrations de domaine et 4 communes en retard surms-event) mais n'exécute jamais ses migrations (aucunmigrationsRunnirunMigrationsdanssrc, 3 tests unitaires avecDataSourcemocké). 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é surworkers-runners#19:domain-event@3.12.1mappecover_file_id, absente deworker-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), puismd5). Reproduire en local sans toucher aux données :global-setupfaitDROP SCHEMA public CASCADEsurDB_NAME, dont la valeur par défaut estms_event. Créer une base jetable (CREATE DATABASE worker_event_ci_check) et lancerDB_NAME=worker_event_ci_check REDIS_PORT=<port> yarn test:integrationavec un Redis jetable (docker run redis:7-alpine redis-server --requirepass password) :REDIS_DB_INDEXne suffit pas, les jobs BullMQ ne sont alors jamais traités (handlerSpyappelé 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) :
-
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 -
Microservices gérés : tous les
ms-*derepos.jsonavecmigrations: trueet un packagedomain-*présent (user,social,post,event,storage,ranking). Pourranking(état vérifié le 2026-10-28 avant la CLI) : son dossierdomain-ranking/src/test/migrations/domain/contient pour l'instant la migrationCreateRankingTablesécrite à la main, copiée à l'identique dansms-ranking/src/migrations/domain/(vérifié le 2026-10-28 : fichiers identiques, 10 tablesranking_*: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 dems-rankingsont identiques à celles dems-user. -
La commande synchronise par un miroir équivalent à
rsync -ac --delete(comparaison par contenu) les fichiers versnpm-packages/packages/domain-<service>/src/test/migrations(copiems-*versdomain-*, jamais entre services) afin de garantir que les tests du domaine s'exécutent sur un schéma strictement identique. -
Piège : dérive. Ces dossiers
test/migrationsne sont pas à jour (au 2026-10-13 : migrations absentes ou renommées,--deletesupprimerait des fichiers dansdomain-post,domain-event,domain-storage). Vérifier avecyarn meta sync-migrations --dry-runavant. Pour une migration commune isolée, copier uniquement le nouveau fichier plutôt que de resynchroniser le dossier entier. -
Copie
workers:npm-packages/packages/workers/src/test/migrations/contient aussi les migrations communes (outbox,job_audit, triggers), hors deyarn meta sync-migrations. Y ajouter toute nouvelle migration commune, sinon les tests deworkerstournent sur un schéma obsolète (la PR traceparent l'a réalignée le 2026-10-13). -
STOP rule :
domain-*/src/test/migrationsetdatabasesont sousnpm-packages. Faire d'abord toutes les éditionsms-*, puisnpm-packagesen dernier : le hookstop-rule-guard.shrefuse les éditionsms-*après la première éditionnpm-packages. -
Quand
npm-packagesest déjà modifié (le modèle et la migration vont ensemble). Le hook refuse alors l'édition dems-*. Écrire la migration d'abord dansnpm-packages/packages/domain-<d>/src/test/migrations/domain/, dans la PRnpm-packages, puis, une fois les packages publiés et le verrou levé, la copier à l'identique dansms-*/src/migrations/domain/(comparer lemd5). Ne pas lanceryarn meta sync-migrationspour cela : il synchronise tout le dossier et le piège de dérive du point 4 s'applique. Exemple :AddBadgePointsAndProgressDedup1793970000000(npm-packages#299, puisms-user#88). Ajouter aussi les nouvelles entités àentitiesdedata-source.tset dudatabase.module.tsdu microservice.
Étape 5 : Exécution et validation locale
- Démarrer l'infrastructure locale PostgreSQL via Docker Compose^compose :
docker compose -f ci-tools/docker-compose.yml up -d postgres-user - Exécuter la migration en environnement local :
cd ms-user NODE_ENV=local yarn typeorm migration:run -d src/config/data-source.ts - Valider impérativement le rollback (
down) :NODE_ENV=local yarn typeorm migration:revert -d src/config/data-source.ts - Réappliquer la migration :
NODE_ENV=local yarn typeorm migration:run -d src/config/data-source.ts - 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_queueetjobs_outboxportent l'index(status, created_at, id)(migration commune1794166000000-AddOutboxPollingIndexes, déclaré aussi par@IndexsurEventQueueModeletJobsOutboxModel). 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 surms-ranking-dbont fait caler la VM entière plusieurs secondes de suite.OutboxRunnerpurge les lignesCOMPLETEDplus vieilles que la rétention (défaut : 1 jour, une passe par heure, lots de 5 000, sur sa propre boucle), etjob_audità côté dejobs_outbox. Réglage :OutboxRunnerConfig.purge(enabled,retentionMs,intervalMs,batchSize), tous optionnels. Les lignesPENDING,PROCESSINGetFAILEDne sont jamais supprimées. Conséquence : l'historique des lignes terminées ne dépasse pas un jour (voirvolontariapp-trace-async-flow).job_auditn'a pas d'index(status, created_at): sa purge trie les lignesCOMPLETED(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) : unSeq Scansur une table d'outbox est le signal à traiter.
Checklist de validation
- [ ] La migration applique le pattern Expand / Contract (aucun
DROP COLUMNni renommage brutal direct). - [ ] La méthode
down()est strictement l'inverse de la méthodeup(). - [ ] Les nouvelles entités sont exportées et enregistrées dans
entitiesdesrc/config/data-source.ts^data-source. - [ ] Si une migration commune a été touchée,
yarn meta sync-migrationsa été exécuté^sync-mig. - [ ] Le cycle
migration:run$\rightarrow$migration:revert$\rightarrow$migration:runs'exécute sans erreur sur la base locale^compose. - [ ]
yarn testpasse avec succès sur le microservice.
Références
04 同仓库更多 Skills
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.
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.
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.
volontariapp-implement-async-event-flow
Guide pas-à-pas pour concevoir et implémenter un flux asynchrone complet (messaging -> outbox -> post-processors -> ws-service -> client).
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.
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.