Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Scotty Laravel Package

spatie/scotty

Scotty is a beautiful SSH task runner for running scripted tasks on remote servers. Define tasks and macros in a simple Scotty.sh (bash + annotations), then run them with clear output. Fully compatible with Laravel Envoy as a drop-in replacement.

View on GitHub
Deep Wiki
Context7

title: The Scotty.sh format weight: 3

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:

# [@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

If your server listens on a non-default SSH port, append it to the host with a colon:

# [@servers](https://github.com/servers) remote=deployer@your-server.com:2222

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](https://github.com/servers).

Tasks

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.

Running on multiple servers

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.

Parallel execution

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.

Confirmation

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.

Macros

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.

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](https://github.com/option). Three forms are supported:

# [@option](https://github.com/option) staging          # boolean flag — $STAGING='1' when --staging is passed
# [@option](https://github.com/option) branch=main      # value with default — $BRANCH='main' unless overridden
# [@option](https://github.com/option) release-name=    # required value — scotty errors if --release-name=... is missing
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](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
}

Hooks

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.

Complete example

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
}

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](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 ]
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
hamzi/corewatch
minionfactory/raw-hydrator
hexters/coinpayment
rjcodes/rjcms
act-training/laravel-permissions-manager
alimarchal/laravel-chart-of-accounts
babenkoivan/elastic-scout-driver
mkwebdesign/filament-watchdog-v5
renatomarinho/laravel-page-speed
zedmagdy/filament-business-hours
renatovdemoura/blade-elements-ui
devgeek/beacon-admin
benjamin-rqt/data-watcher-bundle
atriumphp/atrium
sandermuller/package-boost-laravel
sandermuller/boost-skills
redaxo/core
yusufgenc/filament-api-forge
l3aro/rating-star-for-filament
leek/filament-subtenant-scope