The Scotty.sh format is plain bash with annotation comments. Every line is real bash, so your editor highlights it correctly and all your existing shell tooling works.
##Servers
At the top of your file, define which servers you want to connect to:
You can define as many as you need:
If your server listens on a non-default SSH port, append it to the host with a colon:
For more complex SSH options (identity files, jump hosts, ProxyCommand, etc.), put them in ~/.ssh/config under a Host block and reference that host name from @servers.
##Tasks
A task is just a bash function with a # @task annotation above it. The on: parameter tells Scotty which server to run it on:
deploy() {
cd /var/www/my-app
git pull origin main
php artisan migrate --force
}
That's the core concept. Everything else builds on this.
##Running on multiple servers
You can target multiple servers by separating their names with commas:
deploy() {
cd /var/www/my-app
git pull origin main
}
By default, the task runs on each server one after the other.
##Parallel execution
If you want to run on all servers at the same time, add parallel:
restartWorkers() {
sudo supervisorctl restart all
}
This is handy for things like restarting workers across a cluster, where you don't need to wait for one to finish before starting the next.
##Confirmation
For dangerous tasks (like deploying to production), you can require confirmation:
deploy() {
cd /var/www/my-app
git pull origin main
}
Scotty will ask before running the task. If you say no, it stops.
##Macros
A macro groups multiple tasks together so you can run them with a single command:
If the list gets long, you can use the multi-line format:
Run it with scotty run deploy. The tasks execute in the order you listed them. If any task fails, execution stops immediately.
##Variables
You can define variables at the top of your file, right after the server and macro lines:
BRANCH="main"
REPOSITORY="your/repo"
APP_DIR="/var/www/my-app"
RELEASES_DIR="$APP_DIR/releases"
NEW_RELEASE_NAME=$(date +%Y%m%d-%H%M%S)
These are plain bash variables, so computed values like $(date) work naturally. All variables are available in all tasks.
Top-level variable assignments are evaluated once locally before the first task runs. The captured values are then injected into every task's script. This means a value like NEW_RELEASE_NAME=$(date +%Y%m%d-%H%M%S) produces the same timestamp in every task of the same run, which is what you want for zero-downtime deploys where multiple tasks need to agree on a release directory.
Helper functions defined at the top of the file still run inside each task on the remote server. Side effects in top-level assignments (mkdir, rm, etc.) happen on your local machine, not on the remote. If you need work to happen remotely, put it in a task.
You can also accept variables from the command line by declaring them with # @option. Three forms are supported:
scotty run deploy --branch=develop --release-name=v42 --staging
The key gets uppercased and dashes become underscores, so --release-name=v42 sets $RELEASE_NAME. Value options also fall back to an environment variable of the same (uppercased) name before using the declared default. See Dynamic options for the full precedence rules.
##Helper functions
Any function without a # @task annotation is treated as a helper. Helpers are available in all tasks:
log() {
echo -e "\033[32m$1\033[0m"
}
deploy() {
log "Deploying..."
cd /var/www/my-app
git pull origin main
}
##Hooks
You can run code at different points during execution. This is useful for things like sending Slack notifications:
beforeEachTask() {
echo "Starting task..."
}
afterEachTask() {
echo "Task done."
}
onSuccess() {
curl -X POST https://hooks.slack.com/... \
-d '{"text": "Deploy succeeded!"}'
}
onError() {
curl -X POST https://hooks.slack.com/... \
-d '{"text": "Deploy failed!"}'
}
onFinished() {
echo "Deploy process complete."
}
@before and @after run around each task. @success and @error run once at the end depending on whether everything passed. @finished always runs, regardless of the outcome.
##Complete example
Here's a full deploy script using all the concepts above:
BRANCH="main"
REPOSITORY="your/repo"
APP_DIR="/var/www/my-app"
RELEASES_DIR="$APP_DIR/releases"
CURRENT_DIR="$APP_DIR/current"
NEW_RELEASE_NAME=$(date +%Y%m%d-%H%M%S)
NEW_RELEASE_DIR="$RELEASES_DIR/$NEW_RELEASE_NAME"
startDeployment() {
git checkout $BRANCH
git pull origin $BRANCH
}
cloneRepository() {
cd $RELEASES_DIR
git clone --depth 1 git@github.com:$REPOSITORY --branch $BRANCH $NEW_RELEASE_NAME
}
runComposer() {
cd $NEW_RELEASE_DIR
ln -nfs $APP_DIR/.env .env
composer install --prefer-dist --no-dev -o
}
blessNewRelease() {
ln -nfs $NEW_RELEASE_DIR $CURRENT_DIR
sudo service php8.4-fpm restart
}
cleanOldReleases() {
cd $RELEASES_DIR
ls -dt $RELEASES_DIR/* | tail -n +4 | xargs rm -rf
}
##Migrating from Envoy
If you're coming from Laravel Envoy, here's a quick reference. For the full Blade format documentation, see the Envoy compatibility page.
| Blade format |
Scotty.sh format |
@servers(['remote' => '1.1.1.1']) |
# @servers remote=1.1.1.1 |
@task('deploy', ['on' => 'remote']) |
# @task on:remote |
@endtask |
} (end of function) |
@story('deploy') ... @endstory |
# @macro deploy task1 task2 |
{{ $variable }} |
$VARIABLE |
@setup PHP block |
Shell $(command) substitution |
@if($condition) |
Bash if [ condition ] |