{"id":50326264,"url":"https://github.com/overflowy/scotty","last_synced_at":"2026-05-29T06:30:19.597Z","repository":{"id":352283858,"uuid":"1214569498","full_name":"overflowy/scotty","owner":"overflowy","description":"Python port of scotty","archived":false,"fork":false,"pushed_at":"2026-04-18T19:36:15.000Z","size":55,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-18T21:08:10.277Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://github.com/spatie/scotty","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/overflowy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-18T18:53:20.000Z","updated_at":"2026-04-18T19:36:19.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/overflowy/scotty","commit_stats":null,"previous_names":["overflowy/scotty"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/overflowy/scotty","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/overflowy%2Fscotty","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/overflowy%2Fscotty/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/overflowy%2Fscotty/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/overflowy%2Fscotty/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/overflowy","download_url":"https://codeload.github.com/overflowy/scotty/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/overflowy%2Fscotty/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33640627,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-29T02:00:06.066Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-29T06:30:18.341Z","updated_at":"2026-05-29T06:30:19.570Z","avatar_url":"https://github.com/overflowy.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Scotty\n\nA beautiful SSH task runner. Write your deploy steps in plain bash with a few annotation comments, and Scotty takes care of connecting to your servers, running each script, and streaming the output back to you.\n\nThis is a Python port of Spatie's Scotty. Only the `Scotty.sh` bash-with-annotations format is supported - Laravel Envoy's Blade format is not.\n\n## Why\n\nThe `Scotty.sh` format is plain bash with annotation comments. Every line is real bash, so your editor highlights it correctly and all your shell tooling (linting, syntax checking, autocompletion) just works.\n\nWhile your tasks run, Scotty shows each one with its name, a step counter, elapsed time, and the command that's currently executing. When everything finishes, you get a summary table so you can see at a glance how long each step took. If you need to interrupt a deploy, you can press `p` to pause after the current task and resume with `Enter`, or `Ctrl+C` to cancel.\n\nThere's also a `scotty doctor` command that checks your setup: it validates your file, tests SSH connectivity to each server, and verifies that tools like `node`, `npm`, and `git` are installed on the remote machines.\n\n## Requirements\n\n- Python 3.13+\n- SSH access to your target servers (key-based authentication recommended)\n- macOS or Linux (Windows via WSL2)\n\n## Installation\n\nUsing [uv](https://docs.astral.sh/uv/) (recommended):\n\n```sh\nuv tool install .\n```\n\nFrom inside a project clone:\n\n```sh\nuv sync\nuv run scotty --help\n```\n\n## Getting started\n\nIn your project root:\n\n```sh\nscotty init\n```\n\nYou'll be asked for your server's SSH connection string (for example `deployer@your-server.com`). Scotty creates a `Scotty.sh` for you.\n\nOr create one by hand:\n\n```sh\n#!/usr/bin/env scotty\n\n# @servers remote=deployer@your-server.com\n\n# @task on:remote\ncheckUptime() {\n    uptime\n    df -h /\n}\n```\n\nRun it:\n\n```sh\nscotty run checkUptime\n```\n\n## The Scotty.sh format\n\n### Servers\n\nDefine which servers you want to connect to:\n\n```sh\n# @servers local=127.0.0.1 remote=deployer@your-server.com\n```\n\nYou can define as many as you need:\n\n```sh\n# @servers local=127.0.0.1 web-1=deployer@1.1.1.1 web-2=deployer@2.2.2.2\n```\n\n`127.0.0.1`, `localhost`, and `local` are all treated as the local machine - Scotty skips SSH entirely for them.\n\n### Tasks\n\nA task is a bash function with a `# @task` annotation above it. The `on:` parameter tells Scotty which server to run it on:\n\n```sh\n# @task on:remote\ndeploy() {\n    cd /var/www/my-app\n    git pull origin main\n}\n```\n\n#### Running on multiple servers\n\nTarget multiple servers by separating their names with commas. By default the task runs on each server sequentially:\n\n```sh\n# @task on:web-1,web-2\ndeploy() {\n    cd /var/www/my-app\n    git pull origin main\n}\n```\n\n#### Parallel execution\n\nAdd `parallel` to run on all servers at the same time:\n\n```sh\n# @task on:web-1,web-2 parallel\nrestartWorkers() {\n    sudo supervisorctl restart all\n}\n```\n\n#### Confirmation\n\nFor dangerous tasks, require confirmation before running:\n\n```sh\n# @task on:remote confirm=\"Are you sure you want to deploy to production?\"\ndeploy() {\n    cd /var/www/my-app\n    git pull origin main\n}\n```\n\n### Macros\n\nA macro groups multiple tasks so you can run them with a single command:\n\n```sh\n# @macro deploy pullCode runComposer clearCache restartWorkers\n```\n\nIf the list gets long, use the multi-line form:\n\n```sh\n# @macro deploy\n#   pullCode\n#   runComposer\n#   generateAssets\n#   updateSymlinks\n#   clearCache\n#   restartWorkers\n# @endmacro\n```\n\nRun it with `scotty run deploy`. Tasks execute in the order you listed them. If any task fails, execution stops immediately.\n\n### Variables\n\nDefine variables at the top of your file, right after the server and macro lines:\n\n```sh\nBRANCH=\"main\"\nREPOSITORY=\"your/repo\"\nAPP_DIR=\"/var/www/my-app\"\nRELEASES_DIR=\"$APP_DIR/releases\"\nNEW_RELEASE_NAME=$(date +%Y%m%d-%H%M%S)\n```\n\nThese are plain bash variables, so computed values like `$(date)` work naturally. All variables are available in all tasks.\n\nYou can also pass variables from the command line:\n\n```sh\nscotty run deploy --branch=develop\n```\n\nThe key gets uppercased and dashes become underscores, so `--branch=develop` sets `$BRANCH` to `develop`. Values are single-quoted on the wire, so special characters (`\"`, `$`, spaces) are passed to the remote script literally.\n\n### Helper functions\n\nAny function without a `# @task` annotation is treated as a helper. Helpers are available in all tasks:\n\n```sh\nlog() {\n    echo -e \"\\033[32m$1\\033[0m\"\n}\n\n# @task on:remote\ndeploy() {\n    log \"Deploying...\"\n    cd /var/www/my-app\n    git pull origin main\n}\n```\n\n### Hooks\n\nYou can run code at different points during execution - useful for notifications, logging, etc:\n\n```sh\n# @before\nbeforeEachTask() {\n    echo \"Starting task...\"\n}\n\n# @after\nafterEachTask() {\n    echo \"Task done.\"\n}\n\n# @success\nonSuccess() {\n    curl -X POST https://hooks.slack.com/... -d '{\"text\": \"Deploy succeeded!\"}'\n}\n\n# @error\nonError() {\n    curl -X POST https://hooks.slack.com/... -d '{\"text\": \"Deploy failed!\"}'\n}\n\n# @finished\nonFinished() {\n    echo \"Deploy process complete.\"\n}\n```\n\n`@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. All hooks execute locally.\n\n## Running tasks\n\n### Basic usage\n\n```sh\nscotty run deploy\nscotty run cloneRepository\n```\n\n### Pretend mode\n\nSee what would happen without connecting anywhere:\n\n```sh\nscotty run deploy --pretend\n```\n\nThis prints the SSH command Scotty would run, including the full heredoc it would pipe to `bash -se` on the remote.\n\n### Continue on failure\n\nBy default, Scotty stops at the first failed task. To keep going:\n\n```sh\nscotty run deploy --continue\n```\n\n### Summary mode\n\nHide task output and only show the results table. Failed tasks always show their output:\n\n```sh\nscotty run deploy --summary\n```\n\n### Dynamic options\n\nPass custom variables from the command line:\n\n```sh\nscotty run deploy --branch=develop\n```\n\n`--branch=develop` becomes `$BRANCH` inside your tasks.\n\n### Pause and resume\n\nPress `p` mid-deploy and Scotty will wait after the current task finishes. Press `Enter` to continue, or `Ctrl+C` to quit.\n\n### Cancelling\n\nPress `Ctrl+C` at any time. Scotty restores the terminal and exits cleanly, leaving the output in your scrollback.\n\n## Other commands\n\n### List tasks\n\n```sh\nscotty tasks\n```\n\nShows all macros and tasks defined in your file, along with the server they target.\n\n### SSH into a server\n\n```sh\nscotty ssh\nscotty ssh remote\n```\n\nWith one remote server defined, Scotty connects directly. With multiple, you'll get a picker. Local servers are skipped.\n\n### Doctor\n\n```sh\nscotty doctor\n```\n\nRuns through a series of checks:\n\n1. A Scotty file exists and is found\n2. The file parses without errors, and reports how many tasks and macros were found\n3. At least one server is defined\n4. At least one task is defined\n5. All tasks referenced by macros actually exist\n6. SSH connectivity to each remote server, with connection timing\n7. Whether `node`, `npm`, and `git` are available on each reachable server\n\nUseful after setting up a new server or before your first deploy to a new environment.\n\n### Init\n\n```sh\nscotty init\n```\n\nPrompts you for a server host and creates a `Scotty.sh` template in the current directory.\n\n## File lookup order\n\nWhen you run a command without `--path` or `--conf`, Scotty looks for the following files in the current directory, in order:\n\n1. `Scotty.sh`\n2. `scotty.sh`\n\nIt uses the first one it finds. Pass `--path=path/to/file.sh` or `--conf=Custom.sh` to point somewhere else.\n\n## Copying files to a remote\n\nScotty doesn't have a built-in file-transfer primitive - every task is just bash. The idiomatic pattern is `rsync` from an `on:local` task, which handles deltas, permissions, and directory trees in one shot:\n\n```sh\nREMOTE_HOST=\"deployer@your-server.com\"\nAPP_DIR=\"/var/www/my-app\"\n\n# @task on:local\nuploadAssets() {\n    rsync -az --delete public/build/ $REMOTE_HOST:$APP_DIR/public/build/\n}\n```\n\n`scp` is fine for one-off single files, but `rsync -az` should be your default - it's faster on re-uploads and `--delete` keeps the remote tree in sync. Tar-over-ssh (`tar c dir/ | ssh $REMOTE_HOST 'tar x -C /path'`) is only worth it when rsync isn't installed on the remote.\n\n## Complete example\n\n```sh\n#!/usr/bin/env scotty\n\n# @servers local=127.0.0.1 remote=deployer@your-server.com\n# @macro deploy\n#   startDeployment\n#   cloneRepository\n#   runComposer\n#   blessNewRelease\n#   cleanOldReleases\n# @endmacro\n\nBRANCH=\"main\"\nREPOSITORY=\"your/repo\"\nAPP_DIR=\"/var/www/my-app\"\nRELEASES_DIR=\"$APP_DIR/releases\"\nCURRENT_DIR=\"$APP_DIR/current\"\nNEW_RELEASE_NAME=$(date +%Y%m%d-%H%M%S)\nNEW_RELEASE_DIR=\"$RELEASES_DIR/$NEW_RELEASE_NAME\"\n\n# @task on:local\nstartDeployment() {\n    git checkout $BRANCH\n    git pull origin $BRANCH\n}\n\n# @task on:remote\ncloneRepository() {\n    cd $RELEASES_DIR\n    git clone --depth 1 git@github.com:$REPOSITORY --branch $BRANCH $NEW_RELEASE_NAME\n}\n\n# @task on:remote\nrunComposer() {\n    cd $NEW_RELEASE_DIR\n    ln -nfs $APP_DIR/.env .env\n    composer install --prefer-dist --no-dev -o\n}\n\n# @task on:remote\nblessNewRelease() {\n    ln -nfs $NEW_RELEASE_DIR $CURRENT_DIR\n    sudo service php8.4-fpm restart\n}\n\n# @task on:remote\ncleanOldReleases() {\n    cd $RELEASES_DIR\n    ls -dt $RELEASES_DIR/* | tail -n +4 | xargs rm -rf\n}\n```\n\n## Development\n\nInstall dev dependencies and run the test suite:\n\n```sh\nuv sync\nuv run pytest\n```\n\nTests live in `tests/`. Unit tests cover the parser, models, and SSH command builder; feature tests drive the `scotty` CLI end-to-end via subprocess.\n\n## Notes on this port\n\n- Laravel Envoy compatibility was dropped. Neither the Blade (`Envoy.blade.php`) format nor auto-discovery of `Envoy.sh` is supported.\n- `scotty doctor` checks `node`, `npm`, and `git` on remotes - no PHP/Composer probes.\n- Environment variables injected into the remote script via `--key=value` are `shlex`-quoted, so values containing `\"`, `$`, spaces, etc. are passed through literally.\n\n## Credits\n\nThis project is a Python port of [Spatie's Scotty](https://github.com/spatie/scotty) by [Spatie](https://spatie.be). The original is licensed under MIT. The `Scotty.sh` format, CLI ergonomics, and output design are all their work - this port reimplements those ideas in Python.\n\n## License\n\nReleased under the MIT License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foverflowy%2Fscotty","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foverflowy%2Fscotty","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foverflowy%2Fscotty/lists"}