Skip to content

Commit 4b69e0e

Browse files
committed
chore: release v3.0.0 (sprint 5)
1 parent 0fc8902 commit 4b69e0e

4 files changed

Lines changed: 188 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
All notable changes to `@devalade/shipnode` will be documented here.
44

5-
## [3.0.0] - unreleased
5+
## [3.0.0] - 2026-07-01
66

77
### Added
88
- **Multi-app workspaces** — a single `shipnode.config.ts` can now declare multiple applications deployed to the same server, each with its own domain, PM2 process set, health check, env file, build steps, and hooks:
@@ -12,22 +12,31 @@ All notable changes to `@devalade/shipnode` will be documented here.
1212
- Orchestrator iterates over all apps, selecting the right strategy per-app (backend/frontend).
1313
- **`--app <name>` CLI flag** — target a single app in a multi-app workspace (`shipnode deploy --app api`, `shipnode logs --app web`). Commands without `--app` apply to all apps. `rollback` requires `--app`.
1414
- **`getActiveApp(config, name?)` workspace helper** — selects the right app by name or returns `apps[0]` when called without a name.
15+
- **`domain/cloudflare/` domain model**`Tunnel` class with typed `Ingress` entries, `toYaml()`/`fromYaml()` serialization, and sorted hostname output for stable, diffable configs.
16+
- **Multi-hostname tunnel**`cloudflare init` now enumerates all workspace apps with `domain` + web port and creates one ingress entry per app, with automatic DNS routing.
1517

1618
### Changed
1719
- **Config shape split: workspace-level vs. per-app fields.** Workspace-level (`remotePath`, `ssh`, `pkgManager`, `aliases`, `nodeVersion`, etc.) stays on the root config. Per-app fields (`domain`, `pm2`, `healthCheck`, `envFile`, `keepReleases`, `buildDir`, `appRoot`, `sharedDirs`, `sharedFiles`, `hooks`) moved into `apps[]`.
1820
- Legacy top-level input fields are still accepted and synthesized onto `apps[0]` via `z.preprocess` — backward compatible.
1921
- **Schema-based assembly**`assembleConfig` no longer enumerates every field manually. The zod schema is the single source of truth; assembly normalizes legacy input and calls `ShipnodeConfigSchema.parse()`. Prevents drift between builder, schema, and assembly (fixes the pattern that dropped `.aliases()` in 2.5.1).
2022
- **`BuilderState` is now a standalone type** with workspace-level and legacy input fields, no longer derives from `ShipnodeConfig` (which no longer has those legacy mirrors).
23+
- **CLI commands are now thin adapters**`cli/commands/` files are under 80 lines (cloudflare went from 249→49). Business logic extracted to `services/*-orchestrator.ts` and I/O to `infrastructure/`.
24+
- **`cloudflare init` rewritten** — uses the `Tunnel` domain model, iterates over `config.apps` for ingress entries instead of a single `appHostname`.
2125

2226
### Removed
2327
- Legacy top-level mirrors from `ShipnodeConfig` TypeScript shape:
2428
`config.app`, `config.pm2`, `config.domain`, `config.healthCheck`,
2529
`config.envFile`, `config.keepReleases`, `config.buildDir`, `config.appRoot`,
2630
`config.sharedDirs`, `config.sharedFiles`, `config.hooks` — read from `config.apps[i]` instead.
31+
- **`CloudflareConfig.appHostname`** — use per-app `domain` instead. Ingress entries are now derived automatically from apps with `domain` + web port.
2732

2833
### Internal
2934
- **`HooksConfigSchema` uses `z.custom<HookFn>` instead of `z.function()`** — preserves reference equality so `config.hooks.postDeploy === userFn` is true after parse.
3035
- **Schema-coverage test** (`tests/unit/builder.test.ts`) — every builder setter is exercised in a round-trip; prevents future drift between builder, schema, and assembly.
36+
- **`src/infrastructure/cloudflare/api.ts`** — Cloudflare API client extracted from CLI command.
37+
- **`src/infrastructure/provisioning/commands.ts`** — database/Redis setup command builders extracted from setup command.
38+
- **`src/infrastructure/provisioning/security.ts`** — SSH/UFW/fail2ban command builders extracted from harden command.
39+
- **`tests/unit/cloudflare.tunnel.test.ts`**`Tunnel.fromYaml().addIngress(...).toYaml()` round-trip tests.
3140

3241
## [2.5.2] - 2026-06-30
3342

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ shipnode deploy
2525

2626
`shipnode init` creates a `shipnode.config.ts` in your project root. You can also write it by hand using the fluent builder:
2727

28+
### Single app (simple)
29+
2830
```ts
2931
import { shipnode } from '@devalade/shipnode';
3032

@@ -41,6 +43,41 @@ export default shipnode
4143
.build();
4244
```
4345

46+
### Multi-app workspace
47+
48+
A monorepo with two apps? Use `.apps([...])` to deploy them together:
49+
50+
```ts
51+
const api = shipnode
52+
.app()
53+
.backend()
54+
.name('api')
55+
.appRoot('apps/backend')
56+
.domain('api.example.com')
57+
.port(3333)
58+
.pm2('api')
59+
.preDeploy(async ({ exec }) => exec('node ace.js migration:run --force'));
60+
61+
const web = shipnode
62+
.app()
63+
.frontend()
64+
.name('web')
65+
.appRoot('apps/frontend')
66+
.domain('www.example.com');
67+
68+
export default shipnode
69+
.ssh({ host: '1.2.3.4', user: 'deploy' })
70+
.deployTo('/var/www/example')
71+
.nodeVersion('22')
72+
.pkgManager('pnpm')
73+
.apps([api, web])
74+
.cloudflare({ zone: 'example.com', tunnelName: 'example' })
75+
.database({ type: 'postgres', host: 'localhost', port: 5432, name: 'example', user: 'example' })
76+
.build();
77+
```
78+
79+
Each app gets its own release directory, Caddy site, PM2 ecosystem, and health check. Deploy everything with `shipnode deploy`, or target one app with `shipnode deploy --app api`.
80+
4481
### Web + workers
4582

4683
A backend can run additional long-running processes alongside the web server. PM2 supervises all of them under one deployment.

docs/migrations/2.x-to-3.0.md

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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]`

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devalade/shipnode",
3-
"version": "2.5.3",
3+
"version": "3.0.0",
44
"description": "Deploy Node.js apps to a single VPS with zero configuration",
55
"type": "module",
66
"main": "dist/index.js",

0 commit comments

Comments
 (0)