@catapultjs/deploy 0.8.0 → 0.10.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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -6
  3. package/build/commands/list_releases.d.ts +1 -0
  4. package/build/commands/list_releases.js +23 -1
  5. package/build/commands/list_releases.js.map +1 -1
  6. package/build/commands/list_revisions.d.ts +1 -0
  7. package/build/commands/list_revisions.js +37 -20
  8. package/build/commands/list_revisions.js.map +1 -1
  9. package/build/commands/list_tasks.d.ts +1 -0
  10. package/build/commands/list_tasks.js +19 -1
  11. package/build/commands/list_tasks.js.map +1 -1
  12. package/build/commands/pipeline.d.ts +1 -0
  13. package/build/commands/pipeline.js +18 -1
  14. package/build/commands/pipeline.js.map +1 -1
  15. package/build/commands/status.d.ts +2 -0
  16. package/build/commands/status.js +60 -50
  17. package/build/commands/status.js.map +1 -1
  18. package/build/commands/version.d.ts +1 -0
  19. package/build/commands/version.js +21 -4
  20. package/build/commands/version.js.map +1 -1
  21. package/build/index.d.ts +3 -0
  22. package/build/index.js +3 -0
  23. package/build/index.js.map +1 -1
  24. package/build/recipes/pm2.js +2 -2
  25. package/build/recipes/pm2.js.map +1 -1
  26. package/build/src/api.d.ts +60 -0
  27. package/build/src/api.js +159 -0
  28. package/build/src/api.js.map +1 -0
  29. package/build/src/base_command.d.ts +3 -1
  30. package/build/src/base_command.js +3 -1
  31. package/build/src/base_command.js.map +1 -1
  32. package/build/src/deployer.d.ts +7 -1
  33. package/build/src/deployer.js +19 -1
  34. package/build/src/deployer.js.map +1 -1
  35. package/build/src/pipeline/hooks.d.ts +4 -2
  36. package/build/src/pipeline/hooks.js.map +1 -1
  37. package/build/src/pipeline.d.ts +3 -3
  38. package/build/src/pipeline.js.map +1 -1
  39. package/build/src/status.d.ts +18 -0
  40. package/build/src/status.js +59 -0
  41. package/build/src/status.js.map +1 -0
  42. package/build/src/task/store.d.ts +4 -1
  43. package/build/src/task/store.js +8 -2
  44. package/build/src/task/store.js.map +1 -1
  45. package/build/src/task.d.ts +4 -1
  46. package/build/src/task.js +2 -2
  47. package/build/src/task.js.map +1 -1
  48. package/package.json +12 -7
  49. package/skills/catapultjs/SKILL.md +27 -0
  50. package/skills/catapultjs/references/api.md +70 -0
  51. package/skills/catapultjs/references/cli.md +160 -0
  52. package/skills/catapultjs/references/config.md +166 -0
  53. package/skills/catapultjs/references/github-actions.md +99 -0
  54. package/skills/catapultjs/references/recipe.md +115 -0
  55. package/skills/catapultjs/references/recipes.md +316 -0
@@ -0,0 +1,115 @@
1
+ # Writing a Catapult recipe
2
+
3
+ A recipe is a JavaScript/TypeScript module that registers tasks and inserts them into the deployment pipeline as a side effect of being imported. Users activate it with a bare import in their `deploy.ts`:
4
+
5
+ ```typescript
6
+ import './recipes/my-recipe.ts'
7
+ ```
8
+
9
+ Full docs: https://catapultjs.com/guide/creating-recipes
10
+
11
+ ## Conventions
12
+
13
+ - Namespace every task with a recipe prefix: `my-recipe:build`, not `build`.
14
+ - Call `desc('…')` immediately before each `task()` so the task shows a description in `cata list:tasks`.
15
+ - Tasks that queue remote commands use the DSL (`cd()`, `run()`); tasks that need command output or conditionals use `ssh()` directly with `set -e` and `q()` for quoting.
16
+ - Resolve binaries with `bin('node')` instead of hardcoding, so users can override paths per host via `host.bin`.
17
+ - Expose recipe options through the store (`get()`/`set()`), never through module-level mutable state.
18
+
19
+ ## Basic structure
20
+
21
+ ```typescript
22
+ import { type TaskContext, task, desc, cd, run, after, bin } from '@catapultjs/deploy'
23
+ import { ssh, q } from '@catapultjs/deploy/utils'
24
+
25
+ // TypeScript: register task names for autocompletion
26
+ declare module '@catapultjs/deploy' {
27
+ interface TaskRegistry {
28
+ 'my-recipe:build': true
29
+ }
30
+ }
31
+
32
+ desc('Builds the application in the release directory')
33
+ task('my-recipe:build', () => {
34
+ cd('{{release_path}}')
35
+ run(`${bin('node')} --run build`)
36
+ })
37
+
38
+ // Insert into the pipeline (deduplicated: re-adding moves the task)
39
+ after('deploy:update_code', 'my-recipe:build')
40
+ ```
41
+
42
+ Pipeline functions: `after(existing, newTask)`, `before(existing, newTask)`, `remove(name)`, `inPipeline(name)`, `getPipeline()`, `setPipeline(tasks)`.
43
+
44
+ Default pipeline: `deploy:lock`, `deploy:release`, `deploy:update_code`, `deploy:shared`, `deploy:publish`, `deploy:log_revision`, `deploy:healthcheck`, `deploy:unlock`, `deploy:cleanup`. Recipes may override a built-in task by registering the same name (e.g. `git`/`rsync` recipes provide `deploy:update_code`).
45
+
46
+ Use `inPipeline()` to attach relative to optional steps (`deploy:healthcheck` is removed when no host defines one). Insertion deduplicates: re-adding a task moves it, the last position wins.
47
+
48
+ Three tasks are registered but not inserted: `deploy:install` (package manager install), `deploy:build` (`pm run build`), `deploy:test` (`pm run test`). Prefer inserting or overriding them over inventing new install/build tasks, like the `adonisjs` and `nuxt` recipes do.
49
+
50
+ ## Task context and raw SSH
51
+
52
+ The task function receives a `TaskContext`: `{ host, paths, config, release, logger }`. Use it for output capture or host-specific logic:
53
+
54
+ ```typescript
55
+ desc('Shows service logs')
56
+ task('my-recipe:logs', async ({ host, paths, logger }: TaskContext) => {
57
+ const { stdout } = await ssh(host, `set -e\ncd ${q(paths.current)}\nmy-service logs --tail 50`)
58
+ logger.log(stdout.trim())
59
+ })
60
+ ```
61
+
62
+ Always write display output through `ctx.logger` (not `console.log`): the programmatic API captures it and returns it from `catapult.task()`.
63
+
64
+ `paths` fields: `base`, `releases`, `release`, `current`, `shared`, `cataConfig`, `repo`, `builder`, `lock`.
65
+
66
+ Template variables in `cd()`/`run()` strings: `{{release_path}}`, `{{current_path}}`, `{{shared_path}}`, `{{releases_path}}`, `{{base_path}}`, `{{release}}`.
67
+
68
+ Other DSL helpers: `local(command, { cwd? })` runs on the local machine, `upload(localPath, remotePath)` / `download(remotePath, localPath)` transfer over SCP, `isVerbose(level)` checks verbosity.
69
+
70
+ ## Hooks
71
+
72
+ `onSetup()` runs during `cata deploy:setup`, after base directories exist:
73
+
74
+ ```typescript
75
+ import { onSetup } from '@catapultjs/deploy'
76
+ import { ssh, q, getPaths } from '@catapultjs/deploy/utils'
77
+
78
+ onSetup(async (ctx, host, logger) => {
79
+ const paths = getPaths(host.deployPath, ctx.release)
80
+ await ssh(host, `set -e\nmkdir -p ${q(paths.shared + '/storage')}`)
81
+ logger.step(host.name, 'storage ready')
82
+ })
83
+ ```
84
+
85
+ `onStatus()` runs during `cata status`. Return an object: each key becomes an aligned line in text mode and is merged into the host entry of `status --json` and `catapult.status()`. Output written via the `logger` argument only appears in text mode.
86
+
87
+ ```typescript
88
+ import { onStatus } from '@catapultjs/deploy'
89
+
90
+ onStatus(async (_ctx, host) => {
91
+ const { stdout } = await ssh(host, `set +e\nmy-service --version || true`)
92
+ return { 'my-service': stdout.trim() || 'unavailable' }
93
+ })
94
+ ```
95
+
96
+ ## Configurable options
97
+
98
+ ```typescript
99
+ import { task, run, get } from '@catapultjs/deploy'
100
+
101
+ task('my-recipe:sync', () => {
102
+ const excludes = get<string[]>('my_recipe_excludes', [])
103
+ run(`rsync ${excludes.map((e) => `--exclude=${e}`).join(' ')} …`)
104
+ })
105
+ ```
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/`).
108
+
109
+ ## Checklist before shipping
110
+
111
+ 1. Tasks namespaced and described with `desc()`.
112
+ 2. `TaskRegistry` augmented for TypeScript users.
113
+ 3. Remote commands quoted with `q()` and prefixed with `set -e` (or `set +e` when failure is expected).
114
+ 4. Display tasks write to `ctx.logger`, status hooks return data objects.
115
+ 5. Reusable recipes can be contributed to the `contrib/` directory of https://github.com/catapultjs/deploy via pull request.
@@ -0,0 +1,316 @@
1
+ # Official Catapult recipes
2
+
3
+ Recipes are imported as side effects and register tasks into the pipeline automatically. Import order matters: `set()` calls that configure a recipe must appear **before** the recipe import.
4
+
5
+ Full docs: https://catapultjs.com/guide/recipes
6
+
7
+ ## Choosing a recipe
8
+
9
+ | Recipe | Delivers `deploy:update_code`? | Use when |
10
+ | --- | --- | --- |
11
+ | `recipes/git` | Yes | Server can reach the repo; build runs on the server |
12
+ | `recipes/rsync` | Yes | Local build, push artifacts via rsync |
13
+ | `recipes/astro` | Yes | Astro site, build locally, upload dist |
14
+ | `recipes/vitepress` | Yes | VitePress site, build locally, upload dist |
15
+ | `recipes/adonisjs_local` | Yes | AdonisJS, build locally, upload artifact |
16
+ | `recipes/adonisjs` | No (pair with `git` or `rsync`) | AdonisJS, build on the server |
17
+ | `recipes/nuxt` | No (pair with `git` or `rsync`) | Nuxt, build on the server |
18
+ | `recipes/directus` | No (pair with `git` or `rsync`) | Directus, migrations and schema snapshots |
19
+ | `recipes/pm2` | No | Process management (add to any stack) |
20
+ | `recipes/redis` | No | Redis cache flush (add to any stack) |
21
+
22
+ ---
23
+
24
+ ## `recipes/git`
25
+
26
+ ```typescript
27
+ import '@catapultjs/deploy/recipes/git'
28
+ ```
29
+
30
+ Requires `branch` on each host. `repository` is auto-detected from `git remote get-url origin`.
31
+
32
+ | Task | Inserted | Description |
33
+ | --- | --- | --- |
34
+ | `git:check` | after `deploy:lock` | Verifies the branch exists on the remote |
35
+ | `git:update` | before `deploy:update_code` | Clones a bare mirror into `.catapult/repo`, or fetches it |
36
+ | `deploy:update_code` | — | Clones or resets the branch from the local mirror into the release |
37
+
38
+ No store keys.
39
+
40
+ ---
41
+
42
+ ## `recipes/rsync`
43
+
44
+ ```typescript
45
+ import '@catapultjs/deploy/recipes/rsync'
46
+ ```
47
+
48
+ Always uses `--delete`. Trailing slash on the source is added automatically.
49
+
50
+ | Task | Inserted | Description |
51
+ | --- | --- | --- |
52
+ | `deploy:update_code` | — | Syncs a local directory into `releases/<release>/` via rsync |
53
+
54
+ | Key | Type | Default | Description |
55
+ | --- | --- | --- | --- |
56
+ | `rsync_source_path` | `string` | `./` | Local source directory |
57
+ | `source_path` | `string` | — | Fallback if `rsync_source_path` is not set |
58
+ | `rsync_excludes` | `string[]` | `[]` | Patterns passed to `--exclude` |
59
+
60
+ ```typescript
61
+ set('rsync_source_path', './dist')
62
+ set('rsync_excludes', ['.env'])
63
+ import '@catapultjs/deploy/recipes/rsync'
64
+ ```
65
+
66
+ ---
67
+
68
+ ## `recipes/astro`
69
+
70
+ ```typescript
71
+ import '@catapultjs/deploy/recipes/astro'
72
+ ```
73
+
74
+ Builds locally before the lock step, then uploads the output via SCP. Does not run `astro build` on the server.
75
+
76
+ | Task | Inserted | Description |
77
+ | --- | --- | --- |
78
+ | `deploy:build` | before `deploy:lock` | Runs `astro build --mode <astro_mode>` locally |
79
+ | `deploy:update_code` | — | Uploads the generated directory to `releases/<release>` via SCP |
80
+
81
+ | Key | Type | Default | Description |
82
+ | --- | --- | --- | --- |
83
+ | `astro_mode` | `string \| Record<string, string>` | `'production'` | Build mode. String = same for all hosts; object = per host name |
84
+ | `source_path` | `string` | `'./dist/.'` | Local directory uploaded after the build |
85
+
86
+ ```typescript
87
+ // Per-host modes
88
+ set('astro_mode', { production: 'production', staging: 'staging' })
89
+ import '@catapultjs/deploy/recipes/astro'
90
+ ```
91
+
92
+ To use rsync instead of SCP for the upload, import both `astro` and `rsync` — `astro` provides `deploy:build`, `rsync` overrides `deploy:update_code`. Do not combine with `git`.
93
+
94
+ ---
95
+
96
+ ## `recipes/vitepress`
97
+
98
+ ```typescript
99
+ import '@catapultjs/deploy/recipes/vitepress'
100
+ ```
101
+
102
+ | Task | Inserted | Description |
103
+ | --- | --- | --- |
104
+ | `deploy:build` | before `deploy:lock` | Runs `vitepress build <vitepress_path>` locally |
105
+ | `deploy:update_code` | — | Uploads `<vitepress_path>.vitepress/dist` to `releases/<release>` via SCP |
106
+
107
+ | Key | Type | Default | Description |
108
+ | --- | --- | --- | --- |
109
+ | `vitepress_path` | `string` | `''` | Path passed to `vitepress build`; output read from `<path>.vitepress/dist` |
110
+ | `source_path` | `string` | `''` | Used as default for `vitepress_path` |
111
+
112
+ ```typescript
113
+ set('vitepress_path', 'docs/')
114
+ import '@catapultjs/deploy/recipes/vitepress'
115
+ ```
116
+
117
+ ---
118
+
119
+ ## `recipes/adonisjs`
120
+
121
+ ```typescript
122
+ import '@catapultjs/deploy/recipes/adonisjs'
123
+ ```
124
+
125
+ Remote build. Does not override `deploy:update_code` — pair with `git` or `rsync`.
126
+
127
+ | Task | Inserted | Description |
128
+ | --- | --- | --- |
129
+ | `deploy:install` | after `deploy:update_code` | Installs dependencies in the release |
130
+ | `deploy:build` | after `deploy:shared` | Runs `node ace build`, copies `package.json` + lockfile into `build/`, creates `build/tmp` |
131
+ | `ace:migration:run` | before `deploy:publish` | Runs `node ace migration:run --force` |
132
+ | `ace:migration:rollback` | manual | Runs `node ace migration:rollback` |
133
+ | `ace:migration:status` | manual | Runs `node ace migration:status` |
134
+ | `ace:list:routes` | manual | Runs `node ace list:routes` |
135
+
136
+ | Key | Type | Default | Description |
137
+ | --- | --- | --- | --- |
138
+ | `writable_dirs` | `string[]` | `['storage', 'logs', 'tmp']` | Created in `shared/` at setup |
139
+ | `shared_dirs` | `string[]` | `['storage', 'logs']` | Symlinked into each release |
140
+ | `shared_files` | `string[]` | `['.env']` | Symlinked into each release |
141
+ | `adonisjs_path` | `string` | `''` | Sub-path to the app within the repo (monorepo) |
142
+
143
+ ```typescript
144
+ set('adonisjs_path', 'apps/api')
145
+ set('shared_files', ['.env', '.env.production'])
146
+ import '@catapultjs/deploy/recipes/adonisjs'
147
+ ```
148
+
149
+ ---
150
+
151
+ ## `recipes/adonisjs_local`
152
+
153
+ ```typescript
154
+ import '@catapultjs/deploy/recipes/adonisjs_local'
155
+ ```
156
+
157
+ Local build + artifact upload. Same `ace:*` tasks as `adonisjs`. Copies `package.json`, lockfile, and optionally `pnpm-workspace.yaml`, `.npmrc`, `ecosystem.config.cjs` into the local `build/` before uploading.
158
+
159
+ | Task | Inserted | Description |
160
+ | --- | --- | --- |
161
+ | `deploy:build` | before `deploy:lock` | Runs `node ace build` locally |
162
+ | `deploy:update_code` | — | Uploads local build output to `releases/<release>` via SCP |
163
+ | `deploy:install` | after `deploy:shared` | Installs prod dependencies in the release |
164
+ | `ace:migration:run` | before `deploy:publish` | Runs `node ace migration:run --force` |
165
+ | `ace:migration:rollback` | manual | Runs `node ace migration:rollback` |
166
+ | `ace:migration:status` | manual | Runs `node ace migration:status` |
167
+ | `ace:list:routes` | manual | Runs `node ace list:routes` |
168
+
169
+ | Key | Type | Default | Description |
170
+ | --- | --- | --- | --- |
171
+ | `writable_dirs` | `string[]` | `['storage', 'logs', 'tmp']` | Created in `shared/` at setup |
172
+ | `shared_dirs` | `string[]` | `['storage', 'logs']` | Symlinked into each release |
173
+ | `shared_files` | `string[]` | `['.env']` | Symlinked into each release |
174
+ | `adonisjs_path` | `string` | `''` | Sub-path to the app (monorepo) |
175
+ | `source_path` | `string` | `<adonisjs_path>/build/.` | Local build directory to upload |
176
+
177
+ ---
178
+
179
+ ## `recipes/nuxt`
180
+
181
+ ```typescript
182
+ import '@catapultjs/deploy/recipes/nuxt'
183
+ ```
184
+
185
+ Remote build. Does not override `deploy:update_code` — pair with `git` or `rsync`.
186
+
187
+ | Task | Inserted | Description |
188
+ | --- | --- | --- |
189
+ | `deploy:build` | — | Overrides the built-in build task, runs `nuxt build` |
190
+ | `nuxt:generate` | — | Runs `nuxt generate` (manual) |
191
+
192
+ | Key | Type | Default | Description |
193
+ | --- | --- | --- | --- |
194
+ | `shared_files` | `string[]` | `['.env']` | Symlinked into each release |
195
+ | `nuxt_path` | `string` | `''` | Sub-path to the Nuxt app (monorepo) |
196
+ | `source_path` | `string` | `''` | Used as default for `nuxt_path` |
197
+
198
+ ```typescript
199
+ set('nuxt_path', 'apps/web')
200
+ import '@catapultjs/deploy/recipes/nuxt'
201
+ ```
202
+
203
+ ---
204
+
205
+ ## `recipes/directus`
206
+
207
+ ```typescript
208
+ import '@catapultjs/deploy/recipes/directus'
209
+ ```
210
+
211
+ DB migrations and schema snapshots. Does not override `deploy:update_code` — pair with `git` or `rsync`. All tasks are manual (not inserted automatically).
212
+
213
+ | Task | Inserted | Description |
214
+ | --- | --- | --- |
215
+ | `directus:database:migrate` | manual | Runs `directus database migrate:latest` |
216
+ | `directus:snapshot:create` | manual | Runs `directus schema snapshot <directus_snapshot_path>` |
217
+ | `directus:snapshot:apply` | manual | Runs `directus schema apply -y <directus_snapshot_path>` |
218
+
219
+ | Key | Type | Default | Description |
220
+ | --- | --- | --- | --- |
221
+ | `writable_dirs` | `string[]` | `['uploads']` | Created in `shared/` at setup |
222
+ | `shared_dirs` | `string[]` | `['uploads']` | Symlinked into each release |
223
+ | `shared_files` | `string[]` | `['.env']` | Symlinked into each release |
224
+ | `directus_path` | `string` | `''` | Sub-path to the Directus app (monorepo) |
225
+ | `directus_snapshot_path` | `string` | `'./snapshot.yaml'` | Snapshot file path used by schema tasks |
226
+
227
+ ```typescript
228
+ set('directus_path', 'apps/cms')
229
+ set('directus_snapshot_path', './database/snapshot.yaml')
230
+ import '@catapultjs/deploy/recipes/directus'
231
+ ```
232
+
233
+ ---
234
+
235
+ ## `recipes/pm2`
236
+
237
+ ```typescript
238
+ import '@catapultjs/deploy/recipes/pm2'
239
+ ```
240
+
241
+ Requires an `ecosystem.config.cjs` at the release root. Use absolute paths inside that file resolving through `current/` to avoid stale release references after a new deploy.
242
+
243
+ | Task | Inserted | Description |
244
+ | --- | --- | --- |
245
+ | `pm2:startOrReload` | after `deploy:publish` | Starts or reloads via `startOrReload --update-env` |
246
+ | `pm2:save` | after `pm2:startOrReload` | Persists the PM2 process list |
247
+ | `pm2:start` | manual | Starts PM2 processes |
248
+ | `pm2:reload` | manual | Zero-downtime reload |
249
+ | `pm2:restart` | manual | Hard restart |
250
+ | `pm2:stop` | manual | Stops all processes |
251
+ | `pm2:delete` | manual | Deletes all processes from PM2 |
252
+ | `pm2:logs` | manual | Displays the last 50 lines of logs |
253
+ | `pm2:list` | manual | Lists PM2 processes |
254
+ | `pm2:show` | manual | Shows detailed info for each app in `ecosystem.config.cjs` |
255
+
256
+ No store keys. `onStatus` displays the PM2 version in `cata status`.
257
+
258
+ ```bash
259
+ npx cata task pm2:reload
260
+ npx cata task pm2:logs --host staging
261
+ ```
262
+
263
+ ---
264
+
265
+ ## `recipes/redis`
266
+
267
+ ```typescript
268
+ import '@catapultjs/deploy/recipes/redis'
269
+ ```
270
+
271
+ All tasks are manual (not inserted automatically).
272
+
273
+ | Task | Inserted | Description |
274
+ | --- | --- | --- |
275
+ | `redis:db:flush` | manual | Flushes the configured Redis DB(s) |
276
+ | `redis:db:flush_all` | manual | Runs `redis-cli FLUSHALL` |
277
+
278
+ | Key | Type | Default | Description |
279
+ | --- | --- | --- | --- |
280
+ | `redis_db` | `number \| number[]` | `1` | DB index(es) flushed by `redis:db:flush` |
281
+
282
+ ```typescript
283
+ set('redis_db', [0, 1, 2])
284
+ import '@catapultjs/deploy/recipes/redis'
285
+
286
+ // Insert before publish:
287
+ before('deploy:publish', 'redis:db:flush')
288
+ ```
289
+
290
+ ---
291
+
292
+ ## Monorepo pattern
293
+
294
+ Keep the transfer recipe (`git` or `rsync`) and override `deploy:install`/`deploy:build` paths via `set()`:
295
+
296
+ ```typescript
297
+ import { defineConfig, set, before } from '@catapultjs/deploy'
298
+ import '@catapultjs/deploy/recipes/git'
299
+ import '@catapultjs/deploy/recipes/nuxt'
300
+ import '@catapultjs/deploy/recipes/directus'
301
+ import '@catapultjs/deploy/recipes/pm2'
302
+ import '@catapultjs/deploy/recipes/redis'
303
+
304
+ set('nuxt_path', 'apps/web')
305
+ set('directus_path', 'apps/cms')
306
+ set('directus_snapshot_path', './database/snapshot.yaml')
307
+ set('redis_db', 2)
308
+
309
+ before('deploy:publish', 'redis:db:flush')
310
+
311
+ export default defineConfig({
312
+ hosts: [{ name: 'production', ssh: 'deploy@example.com', deployPath: '/home/deploy/acme', branch: 'main' }],
313
+ })
314
+ ```
315
+
316
+ `deploy:install` (inserted by `nuxt`/`directus`) runs at the workspace root `{{release_path}}` by default — no override needed for standard monorepos.