|
| 1 | +# 2.x → 3.0 Migration Guide |
| 2 | + |
| 3 | +## What changed |
| 4 | + |
| 5 | +shipnode 3.0 introduces **multi-app workspaces**: a single `shipnode.config.ts` can now deploy |
| 6 | +multiple applications (e.g. an API backend + a frontend SPA) to the same server, each with its own |
| 7 | +domain, PM2 process set, health check, env file, Caddy site block, and release directory. |
| 8 | + |
| 9 | +Under the hood, `ShipnodeConfig` has been split into **workspace-level** and **per-app** fields, |
| 10 | +the config assembly is now schema-driven (zod is the single source of truth), and the CLI accepts |
| 11 | +`--app <name>` to target a specific app. |
| 12 | + |
| 13 | +## Migration |
| 14 | + |
| 15 | +### 1. Config shape: top-level fields moved to `apps[]` |
| 16 | + |
| 17 | +Per-app fields that were previously top-level on `ShipnodeConfig` have moved into `apps[i]`: |
| 18 | + |
| 19 | +| 2.x | 3.0 | |
| 20 | +|---|---| |
| 21 | +| `config.app` | `config.apps[0].appType` | |
| 22 | +| `config.domain` | `config.apps[0].domain` | |
| 23 | +| `config.pm2` | `config.apps[0].pm2` | |
| 24 | +| `config.healthCheck` | `config.apps[0].healthCheck` | |
| 25 | +| `config.envFile` | `config.apps[0].envFile` | |
| 26 | +| `config.keepReleases` | `config.apps[0].keepReleases` | |
| 27 | +| `config.buildDir` | `config.apps[0].buildDir` | |
| 28 | +| `config.appRoot` | `config.apps[0].appRoot` | |
| 29 | +| `config.sharedDirs` | `config.apps[0].sharedDirs` | |
| 30 | +| `config.sharedFiles` | `config.apps[0].sharedFiles` | |
| 31 | +| `config.hooks` | `config.apps[0].hooks` | |
| 32 | + |
| 33 | +**If you only read `ShipnodeConfig` directly** (e.g. in a script calling `loadConfig()`), |
| 34 | +update your accessors to read from `config.apps[i]`. The legacy top-level mirrors are gone. |
| 35 | + |
| 36 | +**If you only use the builder** (`.backend().ssh(...).build()`), **no config changes required** — |
| 37 | +legacy top-level input fields are still accepted by `assembleConfig` and synthesized onto `apps[0]`. |
| 38 | + |
| 39 | +### 2. Multi-app workspace (optional but recommended) |
| 40 | + |
| 41 | +A single-app config continues to work unchanged: |
| 42 | + |
| 43 | +```ts |
| 44 | +// 2.x style — still works in 3.0 |
| 45 | +export default shipnode |
| 46 | + .backend() |
| 47 | + .ssh({ host: '1.2.3.4', user: 'deploy' }) |
| 48 | + .deployTo('/var/www/myapp') |
| 49 | + .pm2('myapp') |
| 50 | + .port(3000) |
| 51 | + .domain('api.example.com') |
| 52 | + .build(); |
| 53 | +``` |
| 54 | + |
| 55 | +To deploy multiple apps from one config, use the new `.apps([...])` composition: |
| 56 | + |
| 57 | +```ts |
| 58 | +const api = shipnode |
| 59 | + .app() |
| 60 | + .backend() |
| 61 | + .name('api') |
| 62 | + .appRoot('apps/backend') |
| 63 | + .domain('api.example.com') |
| 64 | + .port(3333) |
| 65 | + .pm2('api'); |
| 66 | + |
| 67 | +const web = shipnode |
| 68 | + .app() |
| 69 | + .frontend() |
| 70 | + .name('web') |
| 71 | + .appRoot('apps/frontend') |
| 72 | + .domain('www.example.com'); |
| 73 | + |
| 74 | +export default shipnode |
| 75 | + .ssh({ host: '1.2.3.4', user: 'deploy' }) |
| 76 | + .deployTo('/var/www/example') |
| 77 | + .nodeVersion('22') |
| 78 | + .pkgManager('pnpm') |
| 79 | + .apps([api, web]) |
| 80 | + .build(); |
| 81 | +``` |
| 82 | + |
| 83 | +Each app in a workspace gets its own release directory (`<remotePath>/<app-name>/releases/<ts>/`), |
| 84 | +its own Caddy site block, and its own PM2 ecosystem. |
| 85 | + |
| 86 | +### 3. CLI `--app` flag |
| 87 | + |
| 88 | +Commands now accept `--app <name>` to target a specific app: |
| 89 | + |
| 90 | +```bash |
| 91 | +shipnode deploy --app api |
| 92 | +shipnode logs --app web |
| 93 | +shipnode restart --app worker |
| 94 | +``` |
| 95 | + |
| 96 | +Without `--app`, commands apply to all apps in the workspace (sequentially for deploy, aggregated |
| 97 | +for logs/status). `rollback` **requires** `--app`. |
| 98 | + |
| 99 | +### 4. `CloudflareConfig.appHostname` removed |
| 100 | + |
| 101 | +`appHostname` on `CloudflareConfig` is removed. Cloudflare tunnel ingress entries are now derived |
| 102 | +from each app's `domain` + web `port` automatically. If you previously set `appHostname`: |
| 103 | + |
| 104 | +```ts |
| 105 | +// 2.x |
| 106 | +cloudflare: { zone: 'example.com', appHostname: 'api.example.com' } |
| 107 | + |
| 108 | +// 3.0 — set domain on the app instead |
| 109 | +apps: [{ name: 'api', domain: 'api.example.com', ... }] |
| 110 | +``` |
| 111 | + |
| 112 | +The `sshHostname` field is unchanged and still produces an SSH ingress entry. |
| 113 | + |
| 114 | +### 5. Disk layout |
| 115 | + |
| 116 | +In 2.x, releases lived at `<remotePath>/releases/<ts>/`. In 3.0, releases live at |
| 117 | +`<remotePath>/<app-name>/releases/<ts>/`. On the first deploy after upgrading, shipnode |
| 118 | +automatically migrates the existing layout. |
| 119 | + |
| 120 | +### 6. Internal API changes |
| 121 | + |
| 122 | +- `assembleConfig()` signature is unchanged |
| 123 | +- `DeployOrchestrator` now accepts `ShipnodeConfig` (with `apps[]`) and iterates all apps |
| 124 | +- `getActiveApp(config, name?)` helper returns `apps[0]` by default or a named app |
| 125 | +- `ShipnodeConfig` TypeScript type no longer has legacy top-level mirrors |
| 126 | + |
| 127 | +## Breaking changes summary |
| 128 | + |
| 129 | +| Change | Impact | |
| 130 | +|---|---| |
| 131 | +| Top-level per-app fields removed from `ShipnodeConfig` | Code reading `config.domain` etc. directly must use `config.apps[i].domain` | |
| 132 | +| `appHostname` removed from `CloudflareConfig` | Set `domain` on the app instead | |
| 133 | +| `--app` required for `rollback` | `shipnode rollback` without `--app` now throws | |
| 134 | +| Per-app release directory layout | Automatic migration on first deploy | |
| 135 | + |
| 136 | +## If something breaks |
| 137 | + |
| 138 | +1. Check your `shipnode.config.ts` parses with `pnpm shipnode config validate` |
| 139 | +2. Run `shipnode config show` to inspect the resolved config |
| 140 | +3. If using the programmatic API, check you're reading per-app fields from `config.apps[i]` |
0 commit comments