@catapultjs/deploy 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +34 -13
  2. package/build/bin/run.js +31 -55
  3. package/build/bin/run.js.map +1 -1
  4. package/build/commands/config_validate.d.ts +7 -0
  5. package/build/commands/config_validate.js +45 -0
  6. package/build/commands/config_validate.js.map +1 -0
  7. package/build/commands/init.js +20 -4
  8. package/build/commands/init.js.map +1 -1
  9. package/build/index.d.ts +1 -0
  10. package/build/index.js +1 -0
  11. package/build/index.js.map +1 -1
  12. package/build/recipes/caddy.d.ts +10 -0
  13. package/build/recipes/caddy.js +64 -0
  14. package/build/recipes/caddy.js.map +1 -0
  15. package/build/recipes/systemd.d.ts +9 -0
  16. package/build/recipes/systemd.js +37 -0
  17. package/build/recipes/systemd.js.map +1 -0
  18. package/build/src/config/json/loader.d.ts +4 -0
  19. package/build/src/config/json/loader.js +206 -0
  20. package/build/src/config/json/loader.js.map +1 -0
  21. package/build/src/config/json/recipe_registry.d.ts +3 -0
  22. package/build/src/config/json/recipe_registry.js +44 -0
  23. package/build/src/config/json/recipe_registry.js.map +1 -0
  24. package/build/src/config/json/schema.d.ts +45 -0
  25. package/build/src/config/json/schema.js +198 -0
  26. package/build/src/config/json/schema.js.map +1 -0
  27. package/build/src/config/loader.d.ts +2 -0
  28. package/build/src/config/loader.js +21 -0
  29. package/build/src/config/loader.js.map +1 -0
  30. package/build/src/config/resolve.d.ts +2 -0
  31. package/build/src/config/resolve.js +30 -0
  32. package/build/src/config/resolve.js.map +1 -0
  33. package/build/src/utils.js +8 -1
  34. package/build/src/utils.js.map +1 -1
  35. package/package.json +11 -6
  36. package/schema/deploy.schema.json +287 -0
  37. package/skills/catapultjs/SKILL.md +14 -11
  38. package/skills/catapultjs/references/cli.md +11 -2
  39. package/skills/catapultjs/references/config.md +133 -34
  40. package/skills/catapultjs/references/recipe.md +1 -1
  41. package/skills/catapultjs/references/recipes.md +68 -0
@@ -1,6 +1,17 @@
1
1
  # Writing a Catapult deploy config
2
2
 
3
- The config file is auto-detected as `deploy.ts`, `deploy.js`, `deploy.config.ts` or `deploy.config.js` at the project root (override with `cata --config <path>`). It must default-export `defineConfig()`. Full reference: https://catapultjs.com/guide/api
3
+ Config files are auto-detected at the project root (override with `cata <command> --config <path>`):
4
+
5
+ - TypeScript/JavaScript: `deploy.ts`, `deploy.config.ts`, `deploy.js`, `deploy.config.js`
6
+ - JSON: `deploy.config.json`, `deploy.json`
7
+
8
+ TypeScript and JavaScript must default-export `defineConfig()`. JSON is validated as a strict declarative document. Full references: https://catapultjs.com/guide/api and https://catapultjs.com/guide/json-configuration
9
+
10
+ ## Choosing a format
11
+
12
+ Use JSON for a data-only deployment made from built-in recipes, serializable store values, declarative task steps and direct pipeline controls. Prefer `deploy.config.json`, include the public schema URL, and set `version` to `1`.
13
+
14
+ Use TypeScript or JavaScript when the deployment needs callbacks/hooks, external or project-local recipes, dynamic `bin()` calls inside custom tasks, conditions such as `inPipeline()`, or custom task code with branching, loops, API calls or arbitrary async behavior. Static per-host `bin` overrides are supported in JSON. Do not try to encode executable behavior as JSON strings.
4
15
 
5
16
  ## Before creating a config
6
17
 
@@ -21,7 +32,7 @@ Ask a compact set of questions for missing values:
21
32
  4. Should PM2 be configured? If yes, confirm app name, start entry, port, and whether to create `ecosystem.config.cjs`.
22
33
  5. Is there a healthcheck URL and any extra shared files or directories?
23
34
 
24
- When the user answers, generate `deploy.ts` with the selected recipes and put `set(...)` calls before the recipe imports they configure.
35
+ When the user answers, generate the requested format. For TypeScript/JavaScript, put `set(...)` calls before the recipe imports they configure. For JSON, put those values under `store` and recipe names under `recipes`.
25
36
 
26
37
  ```typescript
27
38
  import { defineConfig } from '@catapultjs/deploy'
@@ -40,18 +51,106 @@ export default defineConfig({
40
51
  })
41
52
  ```
42
53
 
54
+ ## JSON configuration
55
+
56
+ Use strict JSON: double-quoted keys and strings, with no comments or trailing commas. The schema rejects unknown properties and validates the whole document before Catapult connects to a host.
57
+
58
+ ```json
59
+ {
60
+ "$schema": "https://catapultjs.com/schema/deploy.schema.json",
61
+ "version": 1,
62
+ "recipes": ["git"],
63
+ "config": {
64
+ "keepReleases": 2,
65
+ "hosts": [
66
+ {
67
+ "name": "production",
68
+ "ssh": "deploy@example.com",
69
+ "deployPath": "/home/deploy/myapp",
70
+ "branch": "main"
71
+ }
72
+ ]
73
+ },
74
+ "store": {
75
+ "shared_files": [".env"],
76
+ "shared_dirs": ["storage", "logs"]
77
+ },
78
+ "tasks": {
79
+ "app:build": {
80
+ "description": "Install dependencies and build the application",
81
+ "steps": [{ "cd": "{{release_path}}" }, { "run": "npm ci" }, { "run": "npm run build" }]
82
+ }
83
+ },
84
+ "after": {
85
+ "deploy:shared": "app:build"
86
+ }
87
+ }
88
+ ```
89
+
90
+ Top-level contract:
91
+
92
+ | Field | Rule |
93
+ | ---------- | --------------------------------------------------------------------------------------------------- |
94
+ | `$schema` | Recommended: `https://catapultjs.com/schema/deploy.schema.json` |
95
+ | `version` | Required and exactly `1` |
96
+ | `recipes` | Optional closed list of built-in recipe names |
97
+ | `config` | Required serializable `Config` values, excluding function-valued `hooks` |
98
+ | `store` | Optional typed values for documented core and built-in recipe keys, equivalent to `set(key, value)` |
99
+ | `tasks` | Optional object of declarative task definitions |
100
+ | `pipeline` | Optional complete task array, equivalent to `setPipeline()` |
101
+ | `remove` | Optional task array, equivalent to repeated `remove()` calls |
102
+ | `before` | Optional target-to-task mapping, equivalent to `before()` |
103
+ | `after` | Optional target-to-task mapping, equivalent to `after()` |
104
+
105
+ Built-in recipe names accepted by JSON: `adonisjs`, `adonisjs_local`, `astro`, `astro_static`, `caddy`, `directus`, `git`, `nestjs`, `nextjs`, `nextjs_static`, `nuxt`, `nuxt_static`, `pm2`, `redis`, `rsync`, `systemd`, `tanstack`, `vitepress`. Do not use module paths such as `recipes/git`.
106
+
107
+ The schema validates known `store` key types and rejects unknown keys. Use TypeScript or JavaScript when a custom recipe needs its own store contract.
108
+
109
+ `config` accepts `hosts`, `keepReleases`, `repository`, `packageManager` and `verbose`. Host data accepts `name`, `ssh`, `deployPath`, `branch`, `healthcheck` and static `bin` path overrides. Host names must be unique. Built-in recipes can resolve `hosts[].bin`; declarative custom task steps cannot invoke the dynamic `bin()` helper.
110
+
111
+ A task is either an array of steps or `{ "description": "...", "steps": [...] }`. Supported steps:
112
+
113
+ - `{ "cd": "..." }` and `{ "run": "..." }` execute remotely.
114
+ - `{ "local": { "command": "...", "cwd": "..." } }` executes locally; `cwd` is optional.
115
+ - `{ "upload": { "local": "...", "remote": "..." } }` uploads with SCP.
116
+ - `{ "download": { "remote": "...", "local": "..." } }` downloads with SCP.
117
+
118
+ Local, upload and download steps flush the current remote command batch. Repeat `{ "cd": "..." }` before later remote `{ "run": "..." }` steps when a task mixes remote commands with local or transfer steps.
119
+
120
+ Remote commands and paths support Catapult placeholders such as `{{release_path}}`, `{{current_path}}` and `{{shared_path}}`. Declaring a task registers it but does not insert it into the deploy pipeline.
121
+
122
+ Pipeline controls are applied after recipes and tasks are registered, in a fixed order independent of top-level JSON property order:
123
+
124
+ 1. `pipeline` replaces the complete task sequence when present.
125
+ 2. `remove` removes each listed task.
126
+ 3. `before` inserts a task or ordered task array before each target.
127
+ 4. `after` inserts a task or ordered task array after each target.
128
+
129
+ ```json
130
+ {
131
+ "pipeline": ["deploy:lock", "deploy:release", "deploy:publish", "deploy:unlock"],
132
+ "remove": ["deploy:release"],
133
+ "before": { "deploy:publish": "app:verify" },
134
+ "after": { "deploy:lock": ["app:install", "app:build"] }
135
+ }
136
+ ```
137
+
138
+ Every pipeline and placement task must be registered. Each `before`/`after` target must exist when placement is applied, and the final pipeline must contain at least one task. A task can be placed only once across `before` and `after`; it cannot also be a placement target or appear under `remove`. Use an array under one target for an ordered task sequence. When `pipeline` is present it replaces recipe insertions too; recipes still register their tasks, but the array must describe the complete intended sequence.
139
+
140
+ Catapult already unlocks failed deployments automatically; do not invent a `deploy:failed` hook mapping. `ace:migration:run` only exists when `adonisjs` is listed in `recipes`, so import that recipe before trying to remove the migration task.
141
+
43
142
  ## Code delivery
44
143
 
45
144
  Catapult registers a default `deploy:update_code` task that uploads `source_path` via SCP. Recipes can override that task when a different delivery mechanism is needed. Pick one delivery mode:
46
145
 
47
- | Recipe | Delivery | Use for |
48
- | --- | --- | --- |
49
- | default `deploy:update_code` | Uploads `source_path` via SCP | Simple local upload, static recipes that set `source_path` |
50
- | `recipes/git` | Clones the repo on the server (bare mirror in `.catapult/repo`) | Server can reach the repo, build on server |
51
- | `recipes/rsync` | Pushes a local directory into the release | Local builds, no repo access from server |
52
- | `recipes/adonisjs_local` | Builds AdonisJS locally, uploads the artifact | AdonisJS without building on the server |
53
- | `recipes/vitepress` | Builds locally, uploads the static output | VitePress static sites |
54
- | custom `task('deploy:update_code', …)` | Whatever you implement | Anything else |
146
+ | Recipe | Delivery | Use for |
147
+ | -------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------- |
148
+ | default `deploy:update_code` | Uploads `source_path` via SCP | Simple local upload, static recipes that set `source_path` |
149
+ | `recipes/git` | Clones the repo on the server (bare mirror in `.catapult/repo`) | Server can reach the repo, build on server |
150
+ | `recipes/rsync` | Pushes a local directory into the release | Local builds, no repo access from server |
151
+ | `recipes/adonisjs_local` | Builds AdonisJS locally, uploads the artifact | AdonisJS without building on the server |
152
+ | `recipes/vitepress` | Builds locally, uploads the static output | VitePress static sites |
153
+ | custom `task('deploy:update_code', …)` | Whatever you implement | Anything else |
55
154
 
56
155
  Avoid combining providers that override `deploy:update_code` (`git`, `rsync`, `adonisjs_local`, `vitepress`, custom) unless intentionally replacing task behavior.
57
156
 
@@ -69,35 +168,35 @@ Inspect the project (`package.json`, lock file, config files) before choosing:
69
168
  - **Process manager**: add `pm2` if the app runs under PM2 (`ecosystem.config.cjs` expected in the project). It wires restart after publish and adds `pm2:*` tasks plus a status report.
70
169
  - **Extras**: `directus` (DB migrations and schema snapshots), `redis` (`redis:db:flush*` tasks, configure with `set('redis_db', n)`).
71
170
 
72
- Recipes register their tasks at import time: imports must appear before or inside the config file, never lazily.
171
+ Recipes register their tasks at import time. In TypeScript/JavaScript, imports must appear before or inside the config file, never lazily. In JSON, list built-in names under `recipes`; external recipes require TypeScript or JavaScript.
73
172
 
74
173
  ## Config options
75
174
 
76
- | Option | Type | Notes |
77
- | --- | --- | --- |
78
- | `hosts` | `Host[]` | Required |
79
- | `keepReleases?` | `number` | Default `5` |
80
- | `repository?` | `string` | Auto-detected from git origin |
81
- | `packageManager?` | `PackageManager` | Auto-detected from lock file |
82
- | `hooks?` | `Hooks` | See below |
83
- | `verbose?` | `Verbose` | From `@catapultjs/deploy/enums`: `SILENT`, `NORMAL`, `TRACE` (default), `DEBUG` |
175
+ | Option | Type | Notes |
176
+ | ----------------- | ---------------- | ------------------------------------------------------------------------------- |
177
+ | `hosts` | `Host[]` | Required |
178
+ | `keepReleases?` | `number` | Default `5` |
179
+ | `repository?` | `string` | Auto-detected from git origin |
180
+ | `packageManager?` | `PackageManager` | Auto-detected from lock file |
181
+ | `hooks?` | `Hooks` | TypeScript/JavaScript only; see below |
182
+ | `verbose?` | `Verbose` | From `@catapultjs/deploy/enums`: `SILENT`, `NORMAL`, `TRACE` (default), `DEBUG` |
84
183
 
85
184
  ## Host options
86
185
 
87
- | Option | Type | Notes |
88
- | --- | --- | --- |
89
- | `name` | `string` | Identifier used by `--host` / `hosts:` selectors |
90
- | `ssh` | `string \| SshConfig` | `'user@host'` or `{ user, host, port? }` |
91
- | `deployPath` | `string` | Absolute path on the server |
92
- | `branch?` | `string \| { name, ask }` | `ask: true` prompts at deploy time (CLI only) |
93
- | `healthcheck?` | `{ url?, retries?, delayMs? }` | Curl check after publish; the `deploy:healthcheck` task is removed automatically when no host defines a `url` |
94
- | `bin?` | `Record<string, string>` | Per-host binary paths, e.g. `{ node: '/home/deploy/.nvm/versions/node/v24.0.0/bin/node' }`. Needed when binaries are not on the non-interactive SSH PATH (nvm setups) |
186
+ | Option | Type | Notes |
187
+ | -------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
188
+ | `name` | `string` | Identifier used by `--host` / `hosts:` selectors |
189
+ | `ssh` | `string \| SshConfig` | `'user@host'` or `{ user, host, port? }` |
190
+ | `deployPath` | `string` | Absolute path on the server |
191
+ | `branch?` | `string \| { name, ask }` | `ask: true` prompts at deploy time (CLI only) |
192
+ | `healthcheck?` | `{ url?, retries?, delayMs? }` | Curl check after publish; the `deploy:healthcheck` task is removed automatically when no host defines a `url` |
193
+ | `bin?` | `Record<string, string>` | Supported in all formats. Per-host binary paths, e.g. `{ node: '/home/deploy/.nvm/versions/node/v24.0.0/bin/node' }`. Built-in recipes resolve these paths; calling `bin()` inside a custom task requires TypeScript/JavaScript. |
95
194
 
96
195
  Multiple environments are just multiple hosts (`production`, `staging`); target one with `cata deploy -H staging`.
97
196
 
98
197
  ## Lifecycle hooks
99
198
 
100
- All optional, all `(ctx: { host?, hosts?, error? }) => Promise<void>`:
199
+ TypeScript/JavaScript only. All optional, all `(ctx: { host?, hosts?, error? }) => Promise<void>`:
101
200
 
102
201
  ```typescript
103
202
  export default defineConfig({
@@ -124,8 +223,8 @@ Three built-in tasks are registered but NOT inserted by default. Insert them ins
124
223
  import { after } from '@catapultjs/deploy'
125
224
 
126
225
  after('deploy:update_code', 'deploy:install') // package manager install
127
- after('deploy:install', 'deploy:build') // pm run build
128
- after('deploy:build', 'deploy:test') // pm run test
226
+ after('deploy:install', 'deploy:build') // pm run build
227
+ after('deploy:build', 'deploy:test') // pm run test
129
228
  ```
130
229
 
131
230
  Pipeline functions: `after(existing, task)`, `before(existing, task)`, `remove(name)`, `inPipeline(name)` to place a task relative to optional steps, `setPipeline([…])` to replace the whole sequence. Insertion deduplicates: re-adding a task moves it, the last position wins.
@@ -133,9 +232,9 @@ Pipeline functions: `after(existing, task)`, `before(existing, task)`, `remove(n
133
232
  ```typescript
134
233
  import { set, after, inPipeline, remove, task, run, cd } from '@catapultjs/deploy'
135
234
 
136
- set('writable_dirs', ['logs', 'tmp/uploads']) // created under shared/ at setup
137
- set('shared_files', ['.env']) // touched under shared/ at setup
138
- set('rsync_source_path', './dist') // rsync recipe option
235
+ set('writable_dirs', ['logs', 'tmp/uploads']) // created under shared/ at setup
236
+ set('shared_files', ['.env']) // touched under shared/ at setup
237
+ set('rsync_source_path', './dist') // rsync recipe option
139
238
 
140
239
  task('app:warmup', () => {
141
240
  cd('{{current_path}}')
@@ -186,7 +285,7 @@ Rules:
186
285
 
187
286
  ## Checklist
188
287
 
189
- 1. `defineConfig()` is the default export.
288
+ 1. TypeScript/JavaScript default-exports `defineConfig()`; JSON uses strict syntax, the public `$schema` URL and `version: 1`.
190
289
  2. Delivery mode is intentional: default SCP, or one overriding provider such as `git`, `rsync`, `adonisjs_local`, `vitepress`, or a custom `deploy:update_code`.
191
290
  3. Recipe store options (`set(…)`) match the imported recipes.
192
291
  4. Healthcheck URLs point to an endpoint reachable from the server itself.
@@ -104,7 +104,7 @@ task('my-recipe:sync', () => {
104
104
  })
105
105
  ```
106
106
 
107
- Users set values in `deploy.ts` with `set('my_recipe_excludes', […])` before or after the import. Two store keys have built-in meaning during setup: `writable_dirs` (string[], created under `shared/`) and `shared_files` (string[], touched under `shared/`).
107
+ Users set values in `deploy.ts` with `set('my_recipe_excludes', […])`. Values read inside task functions can be set before or after a static import. Values read while the recipe module is loaded, especially options that change pipeline wiring, require `set(...)` followed by `await import(...)` because static ESM imports run before the module body. Two store keys have built-in meaning during setup: `writable_dirs` (string[], created under `shared/`) and `shared_files` (string[], touched under `shared/`).
108
108
 
109
109
  ## Checklist before shipping
110
110
 
@@ -21,8 +21,10 @@ Full docs: https://catapultjs.com/guide/recipes
21
21
  | `recipes/nuxt` | No (pair with `git` or `rsync`) | Nuxt, build on the server |
22
22
  | `recipes/nuxt_static` | Uses default SCP; `rsync` optional | Static Nuxt site, generate locally |
23
23
  | `recipes/directus` | No (pair with `git` or `rsync`) | Directus, migrations and schema snapshots |
24
+ | `recipes/caddy` | No | Caddy validation, reload, and config upload |
24
25
  | `recipes/pm2` | No | Process management (add to any stack) |
25
26
  | `recipes/redis` | No | Redis cache flush (add to any stack) |
27
+ | `recipes/systemd` | No | systemd service management (add when the host uses systemd) |
26
28
  | `recipes/tanstack` | No (pair with `git` or `rsync`) | TanStack Start, build on the server |
27
29
 
28
30
  ---
@@ -512,6 +514,72 @@ import '@catapultjs/deploy/recipes/directus'
512
514
 
513
515
  ---
514
516
 
517
+ ## `recipes/caddy`
518
+
519
+ ```typescript
520
+ import '@catapultjs/deploy/recipes/caddy'
521
+ ```
522
+
523
+ Manages Caddy configuration. Does not deliver application code and does not reload Caddy by default unless `caddy_reload_after_publish` is set before loading the recipe. Service management is intentionally separate; combine with `recipes/systemd` and set `systemd_service` to `caddy` when Caddy is managed by systemd.
524
+
525
+ > [!WARNING]
526
+ > Caddy must be able to traverse every parent directory of the configured web root and read the published files. Deploying under a private home directory such as `/home/deploy/...` may require extra permissions or ACLs. Prefer a web root under `/var/www/<app>` or `/srv/www/<app>` for static sites. On the server, use `namei -l /path/to/current/index.html` to inspect which directory blocks access.
527
+
528
+ | Task | Inserted | Description |
529
+ | --- | --- | --- |
530
+ | `caddy:reload` | after `deploy:publish` when `caddy_reload_after_publish` is true | Validates and reloads Caddy |
531
+ | `caddy:validate` | manual | Runs `caddy validate` |
532
+ | `caddy:fmt` | manual | Formats the configured Caddyfile |
533
+ | `caddy:config:show` | manual | Displays the configured Caddyfile |
534
+ | `caddy:config:upload` | manual | Uploads a local Caddyfile, installs it, and validates it |
535
+
536
+ `onStatus` displays the Caddy version in `cata status`.
537
+
538
+ | Key | Type | Default | Description |
539
+ | --- | --- | --- | --- |
540
+ | `caddy_config_path` | `string` | `'/etc/caddy/Caddyfile'` | Remote Caddyfile path |
541
+ | `caddy_local_config_path` | `string` | `'./Caddyfile'` | Local Caddyfile for upload |
542
+ | `caddy_use_sudo` | `boolean` | `true` | Prefix privileged commands with `sudo` |
543
+ | `caddy_validate_before_reload` | `boolean` | `true` | Validate before reload |
544
+ | `caddy_reload_after_publish` | `boolean` | `false` | Add reload after publish |
545
+
546
+ ```typescript
547
+ set('caddy_reload_after_publish', true)
548
+ await import('@catapultjs/deploy/recipes/caddy')
549
+ ```
550
+
551
+ Use a dynamic import when a recipe option changes pipeline wiring. Static ESM imports run before `set()` calls in the module body.
552
+
553
+ ---
554
+
555
+ ## `recipes/systemd`
556
+
557
+ ```typescript
558
+ import '@catapultjs/deploy/recipes/systemd'
559
+ ```
560
+
561
+ Generic systemd service management. Import only when the target host uses systemd.
562
+
563
+ | Task | Inserted | Description |
564
+ | --- | --- | --- |
565
+ | `systemd:restart` | manual | Restarts the configured service |
566
+ | `systemd:reload` | manual | Reloads the configured service |
567
+ | `systemd:status` | manual | Shows `systemctl status` for the service |
568
+ | `systemd:logs` | manual | Shows recent `journalctl` lines for the service |
569
+
570
+ | Key | Type | Default | Description |
571
+ | --- | --- | --- | --- |
572
+ | `systemd_service` | `string` | `'app'` | systemd service name |
573
+ | `systemd_use_sudo` | `boolean` | `true` | Prefix privileged commands with `sudo` |
574
+ | `systemd_logs_lines` | `number` | `100` | Number of journal lines shown by `systemd:logs` |
575
+
576
+ ```typescript
577
+ set('systemd_service', 'caddy')
578
+ import '@catapultjs/deploy/recipes/systemd'
579
+ ```
580
+
581
+ ---
582
+
515
583
  ## `recipes/pm2`
516
584
 
517
585
  ```typescript