Creating a Recipe
Alpha
@catapultjs/deploy is currently in alpha. Its API may change between minor releases until it reaches a stable version. Pin the package version in your package.json to avoid unexpected breaking changes during updates.
A recipe is a regular JavaScript or TypeScript file that registers tasks and inserts them into the pipeline upon import.
Basic structure
import { task, cd, run, after } from '@catapultjs/deploy'
task('my-recipe:build', () => {
cd('{{release_path}}')
run('npm ci')
run('npm run build')
})
after('deploy:update_code', 'my-recipe:build')The task function receives a ctx argument with the current host, paths, config, release, and logger. Use it when you need to access host information or run raw SSH commands:
import { task, after } from '@catapultjs/deploy'
import { ssh, q } from '@catapultjs/deploy/utils'
task('my-recipe:build', async ({ host, paths, release, logger }) => {
await ssh(host, `set -e\ncd ${q(paths.release)}\nnpm run build`)
logger.step(host.name, `build complete for ${release}`)
})
after('deploy:update_code', 'my-recipe:build')Importing the file is enough to activate it:
import './recipes/my-recipe'Setup hook
Use onSetup() to run server-side initialization during cata deploy:setup:
import { onSetup } from '@catapultjs/deploy'
import { ssh, q, getPaths } from '@catapultjs/deploy/utils'
onSetup(async (ctx, host, logger) => {
const paths = getPaths(host.deployPath, ctx.release)
await ssh(
host,
`
set -e
mkdir -p ${q(paths.shared + '/storage')}
if [ ! -f ${q(paths.shared + '/.env')} ]; then
touch ${q(paths.shared + '/.env')}
fi
`
)
logger.step(host.name, 'shared directories ready')
})Overriding built-in tasks
Recipes can override built-in tasks by registering the same name. This is how official recipes such as git, rsync, adonisjs, and astro customize deploy:update_code or deploy:build.
import { task } from '@catapultjs/deploy'
task('deploy:update_code', async ({ paths }) => {
console.log(`sync to ${paths.release}`)
})Status hook
Use onStatus() to display additional information during cata status:
import { onStatus } from '@catapultjs/deploy'
import { ssh } from '@catapultjs/deploy/utils'
onStatus(async (_ctx, host, logger) => {
const { stdout } = await ssh(host, `set +e\nmy-service --version || true`)
logger.log(`my-service ${stdout.trim() || 'unavailable'}`)
})Configuration: set() / get()
Expose configurable options via set() / get() so users can customize the recipe without modifying it:
import { task, run, get } from '@catapultjs/deploy'
task('my-recipe:deploy', () => {
const excludes = get<string[]>('my_recipe_excludes', [])
run(`rsync --exclude=${excludes.join(' --exclude=')} ...`)
})Users configure it in their deploy.ts:
import { set } from '@catapultjs/deploy'
import './recipes/my-recipe'
set('my_recipe_excludes', ['.git', 'node_modules'])Binaries: bin()
Use bin() to resolve binary paths. It checks the current host's bin config first, then falls back to the binary name:
import { task, cd, run, bin } from '@catapultjs/deploy'
task('my-recipe:build', () => {
cd('{{release_path}}')
run(`${bin('node')} my-script.js`)
})Users configure the path per host in deploy.ts:
hosts: [
{
name: 'production',
ssh: 'deploy@example.com',
deployPath: '/home/deploy/myapp',
bin: {
node: '/home/deploy/.nvm/versions/node/v22.14.0/bin/node',
php: '/usr/bin/php8.2',
},
},
]Contributing a recipe
If your recipe could be useful to others, you're welcome to contribute it to the project.
Open a pull request on GitHub and add your recipe file to the contrib/ directory.
TypeScript: extending TaskRegistry
To get type-safe task names, extend the TaskRegistry interface:
import type {} from '@catapultjs/deploy'
declare module '@catapultjs/deploy' {
interface TaskRegistry {
'my-recipe:build': true
'my-recipe:deploy': true
}
}