|
| 1 | +# shipnode 3.0 — Plan d'exécution (Sprints 2c → 5) |
| 2 | + |
| 3 | +Document de travail temporaire. À supprimer à la release 3.0. |
| 4 | + |
| 5 | +## Contexte de départ |
| 6 | + |
| 7 | +- **Repo**: `~/Works/personal/shipnode` |
| 8 | +- **Branche**: `v3` (synchronisée avec `origin/v3`) |
| 9 | +- **HEAD actuel**: `7c711ff feat(builder): ShipnodeAppBuilder + .apps([]) workspace composition (sprint 2b)` |
| 10 | +- **Tests**: 199 passent (`pnpm test`) |
| 11 | +- **Typecheck**: clean (`pnpm lint` = `tsc --noEmit`) |
| 12 | +- **package.json**: `"version": "2.5.3"` — sera bumpé à `3.0.0` en Sprint 5 |
| 13 | + |
| 14 | +## Lectures obligatoires avant tout code |
| 15 | + |
| 16 | +1. `docs/adr/0004-workspace-multi-app.md` — le design figé pour 3.0 (workspace = N apps partageant ssh/cloudflare/etc., release per-app, CLI `--app` flag, schema-first canonical source, couches internes propres) |
| 17 | +2. `CHANGELOG.md` section `[Unreleased]` — explique le refactor schema-first déjà fait (Sprint 1) et le dual-shape transitionnel (Sprint 2a) |
| 18 | +3. `src/config/schema.ts` — comprendre comment `ShipnodeConfigBaseSchema` + `z.preprocess` synthétise `apps[0]` depuis les legacy top-level fields |
| 19 | +4. `src/config/assembly.ts` — comprendre comment `assembleConfig` mirror `apps[0]` vers les legacy top-level fields après parse |
| 20 | +5. `src/config/builder.ts` — comprendre `ShipnodeBuilder` (legacy) vs `ShipnodeAppBuilder` (nouveau, pour `.apps([])`) |
| 21 | + |
| 22 | +## Invariants à respecter — non-négociables |
| 23 | + |
| 24 | +1. **Chaque commit doit laisser le repo vert**: `pnpm lint` et `pnpm test` passent. Aucun commit "WIP" rouge. |
| 25 | +2. **Pas de régression 2.x**: toute config 2.x existante (sans `.apps([])`) doit continuer à parser, à passer la validation, et à produire le même comportement de déploiement qu'avant. C'est le rôle du dual-shape (legacy top-level mirrors + `apps[]` canonique) — il reste en place jusqu'à 3.0 stable. |
| 26 | +3. **Commits atomiques, un par sous-sprint**, avec message conventionnel `feat|refactor|test|docs(scope): description (sprint 2c)` — pas de co-author, pas de footer "Generated with Claude". |
| 27 | +4. **Push après chaque sous-sprint** (`git push origin v3`). |
| 28 | +5. **Pas d'agents spawnés** — l'agent exécutant utilise ses propres outils (Read/Edit/Write/Bash). |
| 29 | +6. **Pas de breaking change inattendu**: si une refonte casserait un test 2.x existant pour de bonnes raisons, en parler avant de pousser (mettre un commentaire `TODO(sprint-2g)` et continuer). |
| 30 | + |
| 31 | +## Sprint 2c — Migration downstream vers `config.apps[i]` |
| 32 | + |
| 33 | +### Objectif |
| 34 | +Les consumers internes (services, strategies, CLI commands, domain modules) lisent les fields per-app depuis `config.apps[i]` plutôt que depuis les legacy top-level mirrors. Pour la transition: `i = 0` partout — c'est encore single-app sous le capot. La vraie itération multi-app vient en 2d. |
| 35 | + |
| 36 | +### Stratégie recommandée |
| 37 | +Créer un helper unique `src/domain/workspace.ts` qui expose: |
| 38 | + |
| 39 | +```ts |
| 40 | +export function getActiveApp(config: ShipnodeConfig, name?: string): ShipnodeApp { |
| 41 | + if (name) { |
| 42 | + const found = config.apps.find((a) => a.name === name); |
| 43 | + if (!found) throw new Error(`No app named "${name}" in this workspace`); |
| 44 | + return found; |
| 45 | + } |
| 46 | + return config.apps[0]; |
| 47 | +} |
| 48 | +``` |
| 49 | + |
| 50 | +Tous les consumers downstream appellent `getActiveApp(config)` (ou `getActiveApp(config, opts.app)` quand le `--app` flag arrive en 2e). Ça centralise le point de migration pour passer du single-app au multi-app. |
| 51 | + |
| 52 | +### Fichiers à migrer (par couche) |
| 53 | + |
| 54 | +**Domain + services** (impact métier): |
| 55 | +- `src/domain/deploy/backend-strategy.ts` |
| 56 | +- `src/domain/deploy/frontend-strategy.ts` |
| 57 | +- `src/domain/deploy/orchestrator.ts` |
| 58 | +- `src/domain/pm2/apps.ts` |
| 59 | +- `src/services/caddy.service.ts` |
| 60 | +- `src/services/deploy.service.ts` |
| 61 | +- `src/services/health.service.ts` |
| 62 | +- `src/infrastructure/ssh/connection.ts` |
| 63 | + |
| 64 | +**CLI commands** (adapters): |
| 65 | +- `src/cli/commands/deploy.ts` |
| 66 | +- `src/cli/commands/env.ts` |
| 67 | +- `src/cli/commands/restart.ts` |
| 68 | +- `src/cli/commands/stop.ts` |
| 69 | +- `src/cli/commands/logs.ts` |
| 70 | +- `src/cli/commands/status.ts` |
| 71 | +- `src/cli/commands/metrics.ts` |
| 72 | +- `src/cli/commands/run.ts` |
| 73 | +- `src/cli/commands/rollback.ts` |
| 74 | +- `src/cli/commands/migrate.ts` |
| 75 | +- `src/cli/commands/doctor.ts` |
| 76 | +- `src/cli/commands/config.ts` |
| 77 | +- `src/cli/commands/ci.ts` |
| 78 | + |
| 79 | +### Pattern de migration |
| 80 | + |
| 81 | +Pour chaque fichier, remplacer: |
| 82 | + |
| 83 | +| Avant | Après | |
| 84 | +|---|---| |
| 85 | +| `config.app` | `getActiveApp(config).appType` | |
| 86 | +| `config.domain` | `getActiveApp(config).domain` | |
| 87 | +| `config.pm2` | `getActiveApp(config).pm2` | |
| 88 | +| `config.healthCheck` | `getActiveApp(config).healthCheck` | |
| 89 | +| `config.envFile` | `getActiveApp(config).envFile` | |
| 90 | +| `config.keepReleases` | `getActiveApp(config).keepReleases` | |
| 91 | +| `config.sharedDirs` | `getActiveApp(config).sharedDirs` | |
| 92 | +| `config.sharedFiles` | `getActiveApp(config).sharedFiles` | |
| 93 | +| `config.buildDir` | `getActiveApp(config).buildDir` | |
| 94 | +| `config.appRoot` | `getActiveApp(config).appRoot` | |
| 95 | +| `config.hooks` | `getActiveApp(config).hooks` | |
| 96 | + |
| 97 | +**NE PAS toucher** (ces fields restent workspace-level et restent au top-level de `config`): |
| 98 | +- `config.ssh`, `config.remotePath`, `config.nodeVersion`, `config.pkgManager`, `config.installCommand` |
| 99 | +- `config.database`, `config.redis`, `config.backup`, `config.cloudflare`, `config.aliases` |
| 100 | + |
| 101 | +### Critère d'acceptance |
| 102 | +- `pnpm test` passe (199+ tests, parce que le helper aura ses propres tests) |
| 103 | +- `pnpm lint` clean |
| 104 | +- Le grep suivant ne retourne plus aucune occurrence dans `src/` (hors tests): |
| 105 | + ``` |
| 106 | + grep -rnE 'config\.(app|domain|pm2|healthCheck|envFile|keepReleases|sharedDirs|sharedFiles|buildDir|appRoot|hooks)\b' src/ | grep -v '\.test\.' |
| 107 | + ``` |
| 108 | +- Tests existants sur les strategies/services/commands continuent à passer sans modification (parce que `getActiveApp(config)` retourne `apps[0]` qui mirror les legacy top-level) |
| 109 | +- Nouveau test `tests/unit/workspace.test.ts` couvre `getActiveApp`: défaut → `apps[0]`, avec name → match exact, name introuvable → throw |
| 110 | + |
| 111 | +### Commit |
| 112 | +`refactor(downstream): consume config.apps via getActiveApp helper (sprint 2c)` |
| 113 | + |
| 114 | +--- |
| 115 | + |
| 116 | +## Sprint 2d — Strategies + orchestrator vraiment multi-app |
| 117 | + |
| 118 | +### Objectif |
| 119 | +Le deploy orchestrator itère sur `config.apps[]` et déploie chaque app séquentiellement. Le layout disque devient per-app: `${remotePath}/${app.name}/releases/<ts>/` et `${remotePath}/${app.name}/current`. |
| 120 | + |
| 121 | +### Changements clés |
| 122 | +- `src/domain/deploy/orchestrator.ts`: boucle sur `config.apps`, appelle la strategy appropriée (backend/frontend) par app. Stop sur première erreur. Affiche un résumé total à la fin. |
| 123 | +- `src/domain/deploy/backend-strategy.ts` et `frontend-strategy.ts`: signature change — prend `(workspace: ShipnodeConfig, app: ShipnodeApp)` au lieu de `(config: ShipnodeConfig)`. `app` est l'app à déployer, `workspace` donne accès à ssh/database/etc. partagés. |
| 124 | +- `src/services/caddy.service.ts`: génère **un site block par app** ayant un `domain`. Le `Caddyfile` agrège tous les sites du workspace. |
| 125 | +- Layout disque per-app: |
| 126 | + - `${remotePath}/${app.name}/releases/<ts>/` |
| 127 | + - `${remotePath}/${app.name}/current` → release active |
| 128 | + - `${remotePath}/${app.name}/shared/${app.envFile}` → .env de cette app |
| 129 | + - Le `${remotePath}/shared/` partagé disparaît (chaque app a son shared) |
| 130 | +- Rollback per-app: `${remotePath}/${app.name}/.releases.json` historique séparé par app |
| 131 | + |
| 132 | +### Compat single-app |
| 133 | +Une config 2.x (qui n'utilise pas `.apps([])`) produit `apps = [{ name: 'app', ... }]` après assemble. Le layout disque devient donc `${remotePath}/app/releases/<ts>/` au lieu de l'ancien `${remotePath}/releases/<ts>/`. |
| 134 | + |
| 135 | +**ATTENTION**: c'est un break disque pour les déploiements existants en place. Deux options: |
| 136 | +- **Option A** (recommandé): au premier deploy 3.0 sur un serveur existant, détecter l'ancien layout (`${remotePath}/releases/` existe), faire une migration automatique (`mv ${remotePath}/releases ${remotePath}/app/releases` + `ln -sf app/current current`). Documenter dans le migration guide. |
| 137 | +- **Option B**: garder l'ancien layout flat quand `config.apps.length === 1` et `apps[0].name === 'app'` (sentinelle "auto-named"). Évite la migration disque mais perpétue 2 code paths. |
| 138 | + |
| 139 | +Choisir Option A — moins de dette technique. Faire la migration disque dans une nouvelle commande `shipnode migrate` ou directement au début du deploy si detected. |
| 140 | + |
| 141 | +### Critère d'acceptance |
| 142 | +- `pnpm test` passe avec les nouveaux tests multi-app |
| 143 | +- Tests existants single-app continuent à passer après ajustements de path |
| 144 | +- `tests/unit/orchestrator.test.ts` couvre: deploy de 2 apps séquentiel, échec sur app[0] empêche app[1], résumé final |
| 145 | +- `tests/unit/backend-strategy.test.ts` et `frontend-strategy.test.ts` adaptés à la nouvelle signature |
| 146 | +- `tests/unit/caddy.service.test.ts` (à créer si absent) couvre: génération du Caddyfile avec N site blocks |
| 147 | +- Le grep `config\.apps\[0\]` ne doit revenir QUE depuis le helper `getActiveApp` (toute itération sur les apps passe par `for (const app of config.apps)`) |
| 148 | + |
| 149 | +### Commits (peut être split) |
| 150 | +- `refactor(deploy): orchestrator iterates over config.apps (sprint 2d)` |
| 151 | +- `feat(deploy): per-app release directories at ${remotePath}/${app.name}/` |
| 152 | +- `feat(caddy): one site block per app having a domain` |
| 153 | + |
| 154 | +--- |
| 155 | + |
| 156 | +## Sprint 2e — CLI `--app <name>` flag |
| 157 | + |
| 158 | +### Objectif |
| 159 | +Les commandes `deploy`, `env`, `restart`, `stop`, `logs`, `status`, `metrics`, `run` acceptent un flag `--app <name>` qui les fait opérer sur une app spécifique. Sans flag, elles opèrent sur **toutes les apps** du workspace (séquentiellement pour deploy/env/restart/stop; agrégé pour logs/status/metrics). |
| 160 | + |
| 161 | +`rollback` **exige** `--app` (rolling back tout le workspace ne fait pas sens — voir ADR). |
| 162 | + |
| 163 | +`cloudflare init`, `cloudflare audit`, `cloudflare status`, `harden`, `doctor`, `setup`, `backup setup` restent **workspace-level** (n'acceptent pas `--app`). |
| 164 | + |
| 165 | +### Implémentation |
| 166 | +- Ajouter `--app <name>` option dans chaque CLI command concernée (via `commander`) |
| 167 | +- Le command appelle `getActiveApp(config, opts.app)` — déjà en place depuis 2c |
| 168 | +- Pour `deploy`/`env`/`restart`/`stop` sans `--app`: itérer `for (const app of config.apps)` et appeler la logique pour chaque |
| 169 | +- Pour `logs`/`status`/`metrics` sans `--app`: tag chaque ligne d'output avec le nom de l'app |
| 170 | +- Pour `rollback` sans `--app`: erreur explicite "`rollback requires --app <name>; available: ${apps.map(a=>a.name).join(', ')}`" |
| 171 | + |
| 172 | +### Critère d'acceptance |
| 173 | +- Tests CLI commands updated pour vérifier le flag |
| 174 | +- Tests pour le mode "sans --app" sur un workspace multi-app: deploy toutes les apps en séquence |
| 175 | +- Test rollback sans --app → throw explicite |
| 176 | + |
| 177 | +### Commits (split possible) |
| 178 | +- `feat(cli): --app <name> flag on deploy/env/restart/stop (sprint 2e)` |
| 179 | +- `feat(cli): --app on logs/status/metrics/run with per-app prefix` |
| 180 | +- `feat(cli): rollback requires --app explicitly` |
| 181 | + |
| 182 | +--- |
| 183 | + |
| 184 | +## Sprint 2f — Cleanup legacy top-level mirrors |
| 185 | + |
| 186 | +### Objectif |
| 187 | +Maintenant que tous les downstream lisent `apps[i]`, les legacy top-level mirrors (`config.app`, `config.domain`, `config.pm2`, etc.) ne sont plus consommés par le code de shipnode lui-même. On peut les retirer du shape canonique pour ne pas perpétuer la dette. |
| 188 | + |
| 189 | +### Changements |
| 190 | +- `src/shared/types.ts`: retirer les legacy fields top-level de `ShipnodeConfig`. Garder uniquement les workspace-level fields + `apps: ShipnodeApp[]`. |
| 191 | +- `src/config/schema.ts`: |
| 192 | + - Retirer les legacy fields top-level de `ShipnodeConfigBaseSchema`. |
| 193 | + - Retirer les refines top-level (`frontend cannot declare pm2` et `domain requires web app`) — ils vivent désormais uniquement sur `ShipnodeAppSchema`. |
| 194 | + - **Garder** la `z.preprocess` qui synthétise `apps[0]` depuis les legacy fields d'INPUT (c'est ce qui permet à une config 2.x de continuer à parser). |
| 195 | +- `src/config/assembly.ts`: retirer le post-parse mirror. `assembleConfig` redevient le 5-liner de Sprint 1 (normalize pm2 + parse). Plus de mirror. |
| 196 | +- `tests/unit/assembly.test.ts`: retirer les tests qui asserentaient `config.domain === config.apps[0].domain` etc. |
| 197 | + |
| 198 | +### Compat externe |
| 199 | +Si un user externe lisait `config.domain` directement (ex: dans un script appelant `loadConfig()`), ça casse. C'est OK — c'est une 3.0 major, et le migration guide (Sprint 5) documente le changement. |
| 200 | + |
| 201 | +### Critère d'acceptance |
| 202 | +- `pnpm test` passe |
| 203 | +- `pnpm lint` clean |
| 204 | +- Le shape `ShipnodeConfig` exporté n'a plus de fields per-app au top level |
| 205 | +- Le z.preprocess fait toujours son job pour les configs 2.x en input |
| 206 | +- Test ajouté: une config 2.x avec `app: 'backend'`, `domain: 'x'`, `pm2: {...}` parse et produit `apps[0]` correct, MAIS `config.domain` (top-level) est `undefined` (et probablement non-typé maintenant) |
| 207 | + |
| 208 | +### Commit |
| 209 | +`refactor(config): remove legacy top-level mirrors from canonical shape (sprint 2f)` |
| 210 | + |
| 211 | +--- |
| 212 | + |
| 213 | +## Sprint 2g — Tests + CHANGELOG récap Sprint 2 |
| 214 | + |
| 215 | +### Objectif |
| 216 | +- Récapituler l'intégralité du Sprint 2 dans une entrée `[Unreleased]` du CHANGELOG (vue d'ensemble du workspace multi-app) |
| 217 | +- Ajouter des tests d'intégration de bout en bout: une config workspace multi-app → assemble → orchestrator dry-run → vérifications du plan de déploiement |
| 218 | +- Ajouter un test "compat 2.x": une config 2.x → assemble → orchestrator dry-run → équivalent du déploiement 2.x |
| 219 | + |
| 220 | +### Commit |
| 221 | +`docs(changelog): consolidate sprint 2 workspace multi-app changes (sprint 2g)` |
| 222 | + |
| 223 | +--- |
| 224 | + |
| 225 | +## Sprint 3 — Couches internes (cli adapters / services / domain) |
| 226 | + |
| 227 | +Voir ADR 0004 section "Internal layers". |
| 228 | + |
| 229 | +### Objectif |
| 230 | +- `cli/commands/*.ts` deviennent des thin adapters (~50 lignes max) |
| 231 | +- `services/*-orchestrator.ts` contiennent la logique d'orchestration |
| 232 | +- `infrastructure/*` contient les I/O (SSH, prompts, terminal UI, cloudflared API client) |
| 233 | +- `domain/*` reste pure logic, pas d'I/O |
| 234 | + |
| 235 | +### Priorité (du plus grand au plus petit gain) |
| 236 | +1. `src/cli/commands/cloudflare.ts` (249 lignes mélangées) → `services/cloudflare.orchestrator.ts` + `cloudflare.ts` (~50 lignes) |
| 237 | +2. `src/cli/commands/setup.ts` — extraire les bash strings dans `infrastructure/provisioning/scripts/*.sh` (fichiers `.sh`, pas embedded strings) |
| 238 | +3. `src/cli/commands/deploy.ts` — déjà partiellement séparé, fignoler |
| 239 | +4. `src/cli/commands/harden.ts` — même pattern que setup |
| 240 | +5. Le reste au fur et à mesure |
| 241 | + |
| 242 | +### Critère d'acceptance |
| 243 | +- Aucun `cli/commands/*.ts` ne dépasse 80 lignes |
| 244 | +- Aucun import depuis `infrastructure/*` dans `domain/*` |
| 245 | +- Aucun appel SSH/exec direct depuis `cli/commands/*.ts` |
| 246 | +- Tests existants passent (les services ont leurs propres tests, les adapters CLI sont triviaux) |
| 247 | + |
| 248 | +### Commits (split par module refactorisé) |
| 249 | +- `refactor(cloudflare): extract orchestrator to services/cloudflare.orchestrator (sprint 3)` |
| 250 | +- `refactor(setup): move provisioning scripts to .sh files` |
| 251 | +- `refactor(deploy): finalize CLI/service split` |
| 252 | +- `refactor(harden): extract orchestrator + .sh scripts` |
| 253 | + |
| 254 | +--- |
| 255 | + |
| 256 | +## Sprint 4 — `domain/cloudflare/` + multi-hostname tunnel |
| 257 | + |
| 258 | +### Objectif |
| 259 | +Cloudflare devient un objet de domaine de premier rang. `cloudflare init` devient idempotent et gère plusieurs hostnames sur un même tunnel naturellement. |
| 260 | + |
| 261 | +### Modèle domain |
| 262 | +Créer `src/domain/cloudflare/`: |
| 263 | +- `tunnel.ts`: `class Tunnel { name, id?, credentialsPath, ingress: Ingress[], catchAll }` avec méthodes `addIngress(hostname, service)`, `toYaml()`, `fromYaml(content)`. |
| 264 | +- `ingress.ts`: `interface Ingress { hostname: string, service: string }` (service est `http://localhost:${port}` typiquement). |
| 265 | + |
| 266 | +### Refactor de `cloudflare init` |
| 267 | +- Lire l'éventuel `/etc/cloudflared/config.yml` existant côté serveur |
| 268 | +- Construire/mettre à jour le `Tunnel` depuis l'état serveur + le workspace local |
| 269 | +- Pour chaque `app` du workspace ayant `domain` ET `pm2.apps` avec port: |
| 270 | + - `tunnel.addIngress(app.domain, 'http://localhost:${webPort}')` |
| 271 | + - `cloudflared tunnel route dns ${tunnelName} ${app.domain}` (idempotent) |
| 272 | +- Émettre `tunnel.toYaml()` → `/etc/cloudflared/config.yml` |
| 273 | +- `systemctl restart cloudflared` |
| 274 | +- Optionnel: lock down firewall (UFW) si `cloudflare.lockdownFirewall === true` |
| 275 | + |
| 276 | +### Breaking |
| 277 | +- `CloudflareConfig.appHostname` **supprimé**. Émettre un warning si présent dans la config + fallback sur `apps[0].domain` pour le run en cours. |
| 278 | +- `CloudflareConfig.sshHostname` reste (cas d'usage SSH-over-tunnel toujours pertinent). |
| 279 | + |
| 280 | +### Critère d'acceptance |
| 281 | +- Test unitaire `tests/unit/cloudflare.tunnel.test.ts`: `Tunnel.fromYaml().addIngress(...).toYaml()` round-trip |
| 282 | +- Test orchestrator: deux apps → deux ingress entries + deux DNS routes + un seul service systemd |
| 283 | +- Le YAML émis est diffable et propre (ingress entries triées par hostname pour stabilité) |
| 284 | + |
| 285 | +### Commit |
| 286 | +`feat(cloudflare): multi-hostname tunnel via domain/cloudflare model (sprint 4)` |
| 287 | + |
| 288 | +--- |
| 289 | + |
| 290 | +## Sprint 5 — Migration guide + 3.0 release |
| 291 | + |
| 292 | +### Objectif |
| 293 | +- Écrire `docs/migrations/2.x-to-3.0.md` avec exemples concrets avant/après |
| 294 | +- Mettre à jour `README.md` pour le nouveau workspace |
| 295 | +- Compléter `CHANGELOG.md` section `[3.0.0]` avec **toutes** les sorties Sprint 1-4 fusionnées |
| 296 | +- Bumper `package.json` à `3.0.0` |
| 297 | +- Tag annoté `v3.0.0` |
| 298 | +- Merge `v3` → `v2` (ou rename `v2` → `v2-lts` selon la stratégie de maintenance long terme — à clarifier avec le mainteneur) |
| 299 | +- `npm publish` (le mainteneur le fait à la main; l'agent ne publish PAS) |
| 300 | +- Mettre à jour `~/Works/personal/biormin/` pour consommer `@devalade/shipnode@3.0.0`: |
| 301 | + - Fusionner `shipnode.backend.config.ts` + `shipnode.frontend.config.ts` en un seul `shipnode.config.ts` avec `.apps([api, web])` + workspace `.cloudflare()` |
| 302 | + - Adapter les scripts `package.json` `deploy:backend`/`deploy:frontend` → `deploy --app api` / `deploy --app web` (ou juste `deploy` pour tout) |
| 303 | + - Tester un deploy de bout en bout sur `server.biormin.com` |
| 304 | + |
| 305 | +### Critère d'acceptance |
| 306 | +- `docs/migrations/2.x-to-3.0.md` couvre: ajout de `apps[]`, déplacement des fields per-app, suppression de `CloudflareConfig.appHostname`, layout disque per-app, `--app` flag, rollback flow |
| 307 | +- `README.md` montre un workspace multi-app comme exemple principal |
| 308 | +- Test e2e biormin (au moins en dry-run) passe |
| 309 | + |
| 310 | +### Commits |
| 311 | +- `docs: migration guide 2.x → 3.0 (sprint 5)` |
| 312 | +- `chore: release v3.0.0` |
| 313 | +- Sur le repo biormin: `chore(shipnode): migrate to 3.0 workspace config` |
| 314 | + |
| 315 | +--- |
| 316 | + |
| 317 | +## Notes pour le reviewer (l'humain qui valide) |
| 318 | + |
| 319 | +- Chaque sous-sprint produit un commit (ou plusieurs si le split est naturel) sur `v3` avec un message explicite |
| 320 | +- Le reviewer peut lire le diff par commit (`git log v3 --oneline` puis `git show <hash>`) et challenger chaque décision |
| 321 | +- En cas de désaccord majeur (genre l'Option A vs B sur le layout disque en 2d, ou l'idempotence cloudflare init en 4), l'agent doit s'arrêter et demander plutôt que pousser quelque chose qui sera reverté |
| 322 | +- Le mainteneur conserve l'autorité finale sur l'API publique (signature des builders, noms de méthodes, etc.) — c'est à confirmer si quelque chose semble border |
| 323 | + |
| 324 | +## Ce qui n'est PAS dans ce plan |
| 325 | + |
| 326 | +- Plugin system / extensibilité framework |
| 327 | +- Provisioning refactor en TypeScript typé (les scripts bash restent des `.sh` extraits, pas plus) |
| 328 | +- Tests d'intégration E2E CLI (besoin d'une cible VPS ou Docker — à évaluer après 3.0) |
| 329 | +- Multi-host workspace |
| 330 | +- Service mesh entre apps |
| 331 | + |
| 332 | +Voir ADR 0004 section "Out of scope" pour justification. |
0 commit comments