spatie/scotty
Scotty is a beautiful SSH task runner for executing scripted tasks on remote servers. Define tasks in a Scotty.sh file (bash with annotations), run them with clear output, and use it as a drop-in, Envoy-compatible alternative for deploys and ops.
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.
At the top of your file, define which servers you want to connect to:
# [@servers](https://github.com/servers) local=127.0.0.1 remote=deployer@your-server.com
You can define as many as you need:
# [@servers](https://github.com/servers) local=127.0.0.1 web-1=deployer@1.1.1.1 web-2=deployer@2.2.2.2
A task is just a bash function with a # [@task](https://github.com/task) annotation above it. The on: parameter tells Scotty which server to run it on:
# [@task](https://github.com/task) on:remote
deploy() {
cd /var/www/my-app
git pull origin main
php artisan migrate --force
}
That's the core concept. Everything else builds on this.
You can target multiple servers by separating their names with commas:
# [@task](https://github.com/task) on:web-1,web-2
deploy() {
cd /var/www/my-app
git pull origin main
}
By default, the task runs on each server one after the other.
If you want to run on all servers at the same time, add parallel:
# [@task](https://github.com/task) on:web-1,web-2 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.
For dangerous tasks (like deploying to production), you can require confirmation:
# [@task](https://github.com/task) on:remote confirm="Are you sure you want to deploy to production?"
deploy() {
cd /var/www/my-app
git pull origin main
}
Scotty will ask before running the task. If you say no, it stops.
A macro groups multiple tasks together so you can run them with a single command:
# [@macro](https://github.com/macro) deploy pullCode runComposer clearCache restartWorkers
If the list gets long, you can use the multi-line format:
# [@macro](https://github.com/macro) deploy
# pullCode
# runComposer
# generateAssets
# updateSymlinks
# clearCache
# restartWorkers
# [@endmacro](https://github.com/endmacro)
Run it with scotty run deploy. The tasks execute in the order you listed them. If any task fails, execution stops immediately.
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.
You can also pass variables from the command line:
scotty run deploy --branch=develop
The key gets uppercased and dashes become underscores, so --branch=develop sets $BRANCH to develop.
Any function without a # [@task](https://github.com/task) annotation is treated as a helper. Helpers are available in all tasks:
log() {
echo -e "\033[32m$1\033[0m"
}
# [@task](https://github.com/task) on:remote
deploy() {
log "Deploying..."
cd /var/www/my-app
git pull origin main
}
You can run code at different points during execution. This is useful for things like sending Slack notifications:
# [@before](https://github.com/before)
beforeEachTask() {
echo "Starting task..."
}
# [@after](https://github.com/after)
afterEachTask() {
echo "Task done."
}
# [@success](https://github.com/success)
onSuccess() {
curl -X POST https://hooks.slack.com/... \
-d '{"text": "Deploy succeeded!"}'
}
# [@error](https://github.com/error)
onError() {
curl -X POST https://hooks.slack.com/... \
-d '{"text": "Deploy failed!"}'
}
# [@finished](https://github.com/finished)
onFinished() {
echo "Deploy process complete."
}
[@before](https://github.com/before) and [@after](https://github.com/after) run around each task. [@success](https://github.com/success) and [@error](https://github.com/error) run once at the end depending on whether everything passed. [@finished](https://github.com/finished) always runs, regardless of the outcome.
Here's a full deploy script using all the concepts above:
#!/usr/bin/env scotty
# [@servers](https://github.com/servers) local=127.0.0.1 remote=deployer@your-server.com
# [@macro](https://github.com/macro) deploy
# startDeployment
# cloneRepository
# runComposer
# blessNewRelease
# cleanOldReleases
# [@endmacro](https://github.com/endmacro)
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"
# [@task](https://github.com/task) on:local
startDeployment() {
git checkout $BRANCH
git pull origin $BRANCH
}
# [@task](https://github.com/task) on:remote
cloneRepository() {
cd $RELEASES_DIR
git clone --depth 1 git@github.com:$REPOSITORY --branch $BRANCH $NEW_RELEASE_NAME
}
# [@task](https://github.com/task) on:remote
runComposer() {
cd $NEW_RELEASE_DIR
ln -nfs $APP_DIR/.env .env
composer install --prefer-dist --no-dev -o
}
# [@task](https://github.com/task) on:remote
blessNewRelease() {
ln -nfs $NEW_RELEASE_DIR $CURRENT_DIR
sudo service php8.4-fpm restart
}
# [@task](https://github.com/task) on:remote
cleanOldReleases() {
cd $RELEASES_DIR
ls -dt $RELEASES_DIR/* | tail -n +4 | xargs rm -rf
}
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](https://github.com/servers)(['remote' => '1.1.1.1']) |
# [@servers](https://github.com/servers) remote=1.1.1.1 |
[@task](https://github.com/task)('deploy', ['on' => 'remote']) |
# [@task](https://github.com/task) on:remote |
[@endtask](https://github.com/endtask) |
} (end of function) |
[@story](https://github.com/story)('deploy') ... [@endstory](https://github.com/endstory) |
# [@macro](https://github.com/macro) deploy task1 task2 |
{{ $variable }} |
$VARIABLE |
[@setup](https://github.com/setup) PHP block |
Shell $(command) substitution |
[@if](https://github.com/if)($condition) |
Bash if [ condition ] |
How can I help you explore Laravel packages today?