Skip to content

Repository files navigation

s.Orchestrator

An SSH deployment orchestrator that uploads local assets and runs shell scripts across multiple remote servers in parallel, with a full-featured terminal UI (or plain log output for CI/CD environments).

┌─[web] ───────────────────────────────────────────────────────────────────────┐
│ Checking connection...                                                       │
│ Connection OK                                                                │
│ Creating remote base directory...                                            │
│ Uploading scripts...                                                         │
│ Running: 01-install.sh                                                       │
│ [...]                                                                        │
│ COMPLETED SUCCESSFULLY                                                       │
└──────────────────────────────────────────────────────────────────────────────┘
 Switch: [TAB] | Scroll: [SHIFT]+[↑/↓] [PGUP/PGDN] | Quit: [CTRL+C] | ...

Why s.Orchestrator?

When you need to provision a machine, deploy an app, or apply quick updates across a handful of servers, heavyweight configuration management tools often feel like overkill. s.Orchestrator gets out of your way. There is no complex syntax to learn - just write standard bash scripts, point them at a JSON config, and watch everything execute concurrently.

Compared to Ansible:

s.Orchestrator Ansible
Learning curve Low - just bash + JSON Medium - YAML, modules, playbooks
Target requirements SSH only SSH + Python on target
Idempotency Manual (write your scripts defensively) Built-in via modules
Parallel execution Always Configurable (forks)
Module ecosystem None Large (ansible-galaxy)
Live feedback TUI with per-server log panes stdout only
Config management at scale Not suitable Purpose-built

Use s.Orchestrator when you manage a small cluster of servers, you already know the exact shell commands you want to run, and you want zero tooling overhead.

Use Ansible when you require built-in idempotency, complex configuration templating (Jinja2), or are managing large enterprise infrastructure.

Features

  • Parallel deployment - all servers deploy simultaneously; per-server log panes in the TUI
  • Host key enforcement - on first contact, fetches all key types offered by the server and prompts you to save them; on subsequent runs, verifies each stored key type against known_hosts; all mismatches are reported together in a single run
  • TUI with tab switching - navigate between server log panes, scroll history, forward keystrokes to interactive SSH sessions
  • Plain mode (--ugly) - prefix-based log output suitable for CI/CD pipelines and logging
  • Dry-run mode - validates config and tests connections without executing any remote commands
  • JSON schema export - emit a JSON Schema file for the config so editors can validate it
  • Injected environment variables - every script receives a standard set of server variables automatically; additional per-server variables can be added via env in the config
  • Cross-server script dependencies (needs) - a server can wait before one of its scripts until other servers have finished a given script, receiving that script's output as an environment variable; circular dependencies are detected up front and abort the run
  • Optional key file - specify a per-server SSH private key; falls back to the SSH agent/defaults
  • Per-server log files - each server's output is saved to logs/<name>.log
  • Single-command exec (-e / --exec) - run one ad-hoc command on all servers instead of the scripts directory (assets and scripts are still uploaded)
  • Server filter (-s / --servers) - target a subset of servers by name; all others are skipped. Names may be given as partial (prefix) matches when unambiguous (an exact name always wins)
  • Interactive debug session (--ssh) - upload assets and scripts to a single server (partial name allowed), then drop into an interactive SSH shell in the remote path with the injected environment exported; no scripts are executed and the uploaded files are left in place for debugging

Installation

From npm, installed globally

# Install with npm
npm i -g @scolastico-dev/orchestrator

# Then run it
s-orchestrator [options]

From npm, installed locally

# Install with npm
npm i @scolastico-dev/orchestrator

Then add a script to package.json:

{
  "scripts": {
    "deploy": "s-orchestrator [options]"
  }
}

And run it with:

npm run deploy

From source

# Clone and build
git clone https://github.com/scolastico-dev/orchestrator.git
cd orchestrator
pnpm install
pnpm build

# Then run it
node dist/index.js [options]

Quick Start

1. Create config.json

{
  "web": {
    "ip": "203.0.113.10",
    "user": "deploy",
    "env": {
      "APP_ENV": "production"
    }
  },
  "db": {
    "ip": "203.0.113.20",
    "port": 2222,
    "keyFile": "~/.ssh/db_deploy"
  }
}

2. Add your scripts

It's important to prefix your scripts with numbers to ensure they run in the correct order, as they are executed in lexicographic order.

scripts/
  10-update.sh
  20-restart.sh

Ensure all scripts are written in a "upsert" style, meaning they can be run multiple times without causing issues. s.Orchestrator does not enforce idempotency, so your scripts should be designed to handle repeated executions gracefully.

3. Run

s-orchestrator

On first run, s.Orchestrator will fetch all SSH host key types for each server and ask you to confirm before saving them to config.json. Subsequent runs verify every stored key type and abort if any of them changes (TOFU - Trust On First Use).

See example/ for a complete working example that provisions two Debian servers: apt upgrade, SSH key setup, and Docker installation.

Configuration Reference

The config file is a JSON object whose keys are server names and whose values are server configs:

Field Type Default Description
ip string (required) - IP address or hostname
port number 22 SSH port
user string "root" SSH username
hostKeys string[] - Stored host public keys for all key types (auto-populated on first run)
keyFile string - Path to SSH private key file
knownHostsFile string - Path to a custom known_hosts file (useful in CI/CD)
env Record<string,string> - Extra environment variables injected into each script (merged on top of the default injected vars)
importedEnv string[] [] Names of local shell environment variables to read at runtime and forward to each remote script
needs Need[] [] Cross-server script dependencies — wait for other servers and receive their script output (see Script Dependencies)

Export JSON Schema

s-orchestrator --schema                      # print schema to stdout
s-orchestrator --schema config.schema.json   # write to file

Add to config.json for editor validation:

{
  "$schema": "./config.schema.json",
  "web": { "ip": "..." }
}

Injected Environment Variables

Every script execution receives the following environment variables automatically:

Variable Value
SERVER_NAME The server's key in config.json
SERVER_IP The server's ip field
SERVER_USER The SSH username (user, default root)
SERVER_SSH_PORT The SSH port (port, default 22)
ASSET_DIR Absolute remote path where assets were uploaded
SCRIPT_DIR Absolute remote path where scripts were uploaded

Variables defined in the server's env block are merged on top, so they can override any of the above if needed.

Importing local environment variables

importedEnv lets you forward specific variables from the shell that runs s.Orchestrator to each remote script — without hard-coding their values in config.json. Variables not present in the local environment are silently skipped. The import happens before the env block is applied, so env entries take precedence over imported values.

{
  "web": {
    "ip": "203.0.113.10",
    "importedEnv": ["DB_PASSWORD", "API_KEY"]
  }
}

This is especially useful with dotenvx, which can decrypt an encrypted .env file and inject the secrets into the local shell before s.Orchestrator runs:

# Decrypt .env.vault, inject secrets locally, then forward listed ones to remote servers
dotenvx run --quiet -- sh -c 's-orchestrator'

The lite version works the same way:

dotenvx run --quiet -- sh -c 'bash lite-version.sh'

dotenvx makes DB_PASSWORD and API_KEY available in the local environment; importedEnv picks them up and forwards only those two to each remote script. Nothing else from the local environment is exposed.

Script Dependencies (needs)

needs lets one server pause before a specific script until other servers have finished a specific script — and receive that script's captured output as an environment variable. The classic use case is a Docker Swarm: workers must wait for the manager to initialize the swarm and need the join token it prints.

Each entry in a server's needs array has these fields:

Field Type Required Description
server string[] no Provider server names (exact config keys). Omitted = every other server in the run
env string no Environment variable that receives the captured output of the depends script. Omitted = wait only, no output is saved or injected
depends string yes Script that must finish on the provider server(s) first
depending string yes Script on this server that waits until the dependency is satisfied
{
  "manager.swarm": { "ip": "203.0.113.10" },
  "worker-1": {
    "ip": "203.0.113.11",
    "needs": [
      {
        "server": ["manager.swarm"],
        "env": "TOKEN",
        "depends": "20-setup-swarm.sh",
        "depending": "25-add-to-swarm.sh"
      }
    ]
  }
}

scripts/20-setup-swarm.sh — runs on every server, but only does work on the manager. Its output on the manager becomes the token:

#!/bin/sh
[ "$SERVER_NAME" = "manager.swarm" ] || exit 0
docker swarm init --advertise-addr "$SERVER_IP" >/dev/null 2>&1 || true
docker swarm join-token -q worker

scripts/25-add-to-swarm.sh — the worker blocks here until the manager finished 20-setup-swarm.sh, then runs with $TOKEN set:

#!/bin/sh
[ "$SERVER_NAME" = "manager.swarm" ] && exit 0
docker swarm join --token "$TOKEN" 203.0.113.10:2377

Semantics:

  • All servers still run all scripts — needs only adds waiting points. Branch on $SERVER_NAME inside scripts.
  • The env value is the entire output of the provider's depends script execution (stderr included, since scripts run on a TTY), with trailing whitespace trimmed — keep the output of a depends script clean and redirect noise to /dev/null.
  • With multiple providers (or an omitted server field), the run waits for all of them; outputs are joined with newlines in provider order.
  • Omit env to use the entry as a pure synchronization barrier — e.g. wait until another server finished restarting a service — without receiving any output.
  • Needs-injected variables are applied last (they override env and importedEnv) and only for the depending script's execution.
  • Validation happens before anything is deployed: unknown scripts or servers, providers excluded by --servers, and circular dependencies (deadlocks) abort the run with an error instead of hanging.
  • --exec ignores needs (no scripts run). Dry-run validates needs without waiting.
  • The lite version handles needs sequentially: a blocked server is skipped, requeued at the end of the queue, and later resumes from the blocked script (no scripts are rerun). If the queue stops making progress (e.g. a provider failed), it exits with an error.

CLI Reference

Usage: s-orchestrator [options]

Options:
  -v, --version             print version and exit
  -c, --config <path>       path to config JSON file            (default: "config.json")
  -y, --skip-confirm        skip confirmation after connection tests
  -n, --dry-run             test connections, do not deploy
  --schema [path]           export JSON schema for config
  -u, --ugly                plain log output, no TUI
  --log-dir <path>          directory for per-server log files  (default: "logs")
  --assets-dir <path>       local assets directory to upload    (default: "assets")
  --scripts-dir <path>      local scripts directory             (default: "scripts")
  --remote-path <path>      remote working directory            (default: "/tmp/s-orchestrator")
  -e, --exec <command>      run one command on each server instead of scripts dir
                            (assets and scripts are still uploaded)
  -s, --servers <names>     comma-separated server names to target; others are skipped
                            (partial/prefix names accepted when unambiguous)
  --ssh <name>              upload to a single server (partial name allowed), then
                            open an interactive SSH session for debugging (no TUI,
                            no scripts executed, files left in place)
  -h, --help                display help

Examples

# Production deploy with a dedicated config
s-orchestrator -c prod.json

# Skip confirmation prompt (for automated pipelines)
s-orchestrator -y

# Dry run - verify config and connections without deploying
s-orchestrator --dry-run --ugly

# CI/CD friendly - ugly mode, skip confirm, custom config
s-orchestrator --ugly -y -c /etc/deploy/config.json

# Export the JSON schema
s-orchestrator --schema > config.schema.json

# Run a single ad-hoc command across all servers
s-orchestrator --exec "systemctl restart nginx" -y

# Deploy only to specific servers (partial names accepted when unambiguous)
s-orchestrator --servers web,db-primary
s-orchestrator -s web            # matches "web" exactly, or the only server starting with "web"

# Combine: one command, specific servers, no prompt
s-orchestrator --exec "docker pull myapp:latest" --servers web1,web2 -y

# Upload to a server and open an interactive SSH shell to debug (partial name ok)
s-orchestrator --ssh web

Deployment Flow

For each run, s.Orchestrator:

  1. Validates the config file
  2. Enforces host keys - fetches all key types on first use (with confirmation), verifies each type on subsequent runs; all mismatches are collected and reported together; any newly confirmed keys are saved to the config even if mismatches are found
  3. Tests connections to all servers in parallel - aborts immediately if any connection fails
  4. Prompts to start (unless --skip-confirm)
  5. Deploys to all servers in parallel:
    • Creates <remote-path>/ on the remote
    • Uploads assets/ (if present)
    • Uploads scripts/ (if present, even when --exec is used)
    • Executes each .sh script in lexicographic order or the single --exec command, with injected environment variables — pausing before any script whose needs dependencies are not yet satisfied
    • Cleans up <remote-path>/ on the remote
  6. Displays final status - Ctrl+C after this point exits cleanly without an abort prompt

Directory Layout

your-project/
├── config.json          # server config (managed by s.Orchestrator)
├── assets/              # files uploaded verbatim (optional)
│   └── nginx.conf
└── scripts/             # executed in alphabetical order on each server
    ├── 01-update.sh
    └── 02-restart.sh

Lite Version

For environments where Node.js is unavailable, a self-contained bash implementation is provided in lite-version.sh. It shares the same config format and feature set but deploys servers sequentially and has no TUI.

Requirements: bash, curl, ssh, scp, ssh-keyscan, ssh-keygen, jq

# Run directly (no build or Node.js required)
curl -sSL https://raw.githubusercontent.com/scolastico-dev/orchestrator/main/lite-version.sh | bash -s -- [options]

The lite version supports the same options as the full version except --ugly, --schema, and --ssh (it also accepts partial/prefix server names for --servers). It also self-updates: on each run, if it is installed as a file on disk and curl/md5sum are available, it checks the latest version from GitHub, compares checksums, and prompts you to update in place.

Options:
  -c, --config <path>       path to config JSON file            (default: config.json)
  -y, --skip-confirm        skip confirmation after connection tests
  -n, --dry-run             test connections, do not deploy
  --log-dir <path>          directory for per-server log files  (default: logs)
  --assets-dir <path>       local assets directory to upload    (default: assets)
  --scripts-dir <path>      local scripts directory             (default: scripts)
  --remote-path <path>      remote working directory            (default: /tmp/s-orchestrator)
  -e, --exec <command>      run one command instead of the scripts dir
  -s, --servers <names>     comma-separated list of server names to target
                            (partial/prefix names accepted when unambiguous)
Feature Full version Lite version
Parallel deployment yes no - sequential
TUI / plain mode yes plain only
Host key enforcement (TOFU) yes yes
Env injection yes yes
needs script dependencies yes yes - via requeue
Dry-run mode yes yes
Per-server log files yes yes
keyFile / knownHostsFile yes yes
--exec / --servers yes yes
Short aliases (-e/-s/-u) -e -s -u -e -s
Partial/prefix server names yes yes
--ssh interactive session yes no
Self-update check no yes
JSON schema export yes no
Node.js required yes no

Development

pnpm install

# Type check
pnpm typecheck

# Run tests (unit tests run always; Docker integration tests require Docker)
pnpm test
pnpm test:watch

# Build (Parcel compiles TypeScript → dist/index.js)
pnpm build

# Run from source (no build needed)
pnpm dev -- --help

Project Structure

src/
├── index.ts          entry point, main()
├── cli.ts            CLI argument parsing (commander)
├── types.ts          shared TypeScript types
├── config.ts         Zod schema, load / save / export-schema
├── logger.ts         per-server file logger
├── ssh/
│   ├── executor.ts   execRemoteStreaming, execRemoteSimple, scpUpload
│   ├── host-keys.ts  TOFU host key enforcement
│   ├── connection.ts connection health check
│   └── index.ts      barrel export
├── deploy/
│   ├── steps.ts      single-server deploy steps (env injection, script execution)
│   └── index.ts      multi-server orchestration
└── ui/
    ├── types.ts      OrchestratorUI interface
    ├── tui.ts        blessed TUI implementation
    └── plain.ts      plain stdout implementation
tests/
├── fixtures/sshd/        Alpine+OpenSSH Docker image for integration tests
├── helpers/docker.ts     Docker container lifecycle management
├── config.test.ts        config validation unit tests
├── ssh.test.ts           SSH executor integration tests (requires Docker)
├── deploy.test.ts        deployment integration tests (requires Docker)
└── lite-version.test.ts  lite-version.sh integration tests (requires Docker)

Testing

# All tests (unit + Docker integration)
pnpm test

# Unit tests only (no Docker required)
pnpm test tests/config.test.ts

Integration tests spin up Alpine+OpenSSH Docker containers automatically, run the deployment against them, and clean up on completion. They are skipped gracefully when Docker is unavailable.

Security

  • Host key verification uses SSH's StrictHostKeyChecking=yes - connections to unknown hosts are rejected
  • TOFU (Trust On First Use) - the first time a server is seen, the user must explicitly approve all offered key types; they are then stored as an array in config.json
  • Key mismatch = hard abort - if any stored key type doesn't match ~/.ssh/known_hosts, s.Orchestrator exits with an error listing all conflicting entries and the ssh-keygen -R commands needed to resolve them; any newly confirmed keys are saved before aborting so they don't need to be re-confirmed after the conflict is resolved
  • Scripts run with the permissions of the configured SSH user - use a least-privilege deploy user where possible

License

This project is licensed under the MIT License.

About

MIT

A short and simple permissive license with conditions only requiring preservation of copyright and license notices. Licensed works, modifications, and larger works may be distributed under different terms and without source code.

What you can do

Permissions Conditions Limitations
🟢 Commercial useThe licensed material and derivatives may be used for commercial purposes.
🔵 License and copyright noticeA copy of the license and copyright notice must be included with the licensed material.
🔴 LiabilityThis license includes a limitation of liability.
🟢 DistributionThe licensed material may be distributed.
🔴 WarrantyThis license explicitly states that it does NOT provide any warranty.
🟢 ModificationThe licensed material may be modified.
🟢 Private useThe licensed material may be used and modified in private.

Information provided by https://choosealicense.com/licenses/mit/

This information is provided for general understanding and is not legal advice.

About

SSH deployment orchestrator with a blessed TUI - upload assets, run scripts across servers in parallel

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages