PHPackages                             serversinc/ssh-runner - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. serversinc/ssh-runner

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

serversinc/ssh-runner
=====================

Actions-based SSH Runner

0.3.0(1mo ago)114↓33.3%MITPHP ^8.4

Since May 7Compare

[ Source](https://github.com/serversinc/ssh-runner)[ Packagist](https://packagist.org/packages/serversinc/ssh-runner)[ Docs](https://github.com/serversinc/ssh-runner)[ RSS](/packages/serversinc-ssh-runner/feed)WikiDiscussions Synced 3w ago

READMEChangelogDependencies (12)Versions (7)Used By (0)

SSH Runner for Laravel
======================

[](#ssh-runner-for-laravel)

[![Latest Version on Packagist](https://camo.githubusercontent.com/347080c7bdec54b2d81d6f16d1297466f6c2e83d36f7a9b5a0afd0aa05ff469d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f73657276657273696e632f7373682d72756e6e65722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/serversinc/ssh-runner)[![Total Downloads](https://camo.githubusercontent.com/752ad0fdb9abcc60d38375977bbafc2f24f7c67b7c6418326b550c97ed43f7ac/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f73657276657273696e632f7373682d72756e6e65722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/serversinc/ssh-runner)[![GitHub Actions](https://github.com/serversinc/ssh-runner/actions/workflows/main.yml/badge.svg)](https://github.com/serversinc/ssh-runner/actions/workflows/main.yml/badge.svg)

A pipeline-based SSH runner for Laravel that executes commands on remote servers with support for action composition, failure strategies, automatic rollback, and execution logging.

This package provides a fluent API for building SSH command pipelines using the [Spatie SSH](https://github.com/spatie/ssh) library under the hood.

Installation
------------

[](#installation)

You can install the package via Composer:

```
composer require serversinc/ssh-runner
```

Configuration
-------------

[](#configuration)

Publish the configuration file:

```
php artisan vendor:publish --provider="Serversinc\SshRunner\SshRunnerServiceProvider" --tag="ssh-runner-config"
```

Publish the migrations:

```
php artisan vendor:publish --provider="Serversinc\SshRunner\SshRunnerServiceProvider" --tag="ssh-runner-migrations"
```

Run the migrations to create the logging tables:

```
php artisan migrate
```

### Note: UUID / ULID Primary Keys

[](#note-uuid--ulid-primary-keys)

The package's migration uses `$table->morphs('server')` which creates `server_id` as an `unsignedBigInteger`. If your server model uses UUID or ULID primary keys, create a migration in your application to change the column type:

```
Schema::table('ssh_pipeline_logs', function (Blueprint $table) {
    $table->string('server_id')->change();
});
```

Basic Usage
-----------

[](#basic-usage)

### 1. Implement the SshServer Interface

[](#1-implement-the-sshserver-interface)

Your server model must implement the `SshServer` contract:

```
use Serversinc\SshRunner\Contracts\SshServer;

class Server extends Model implements SshServer
{
    public function getSshHost(): string
    {
        return $this->ip_address;
    }

    public function getSshPort(): int
    {
        return $this->ssh_port ?? 22;
    }

    public function getSshUser(): string
    {
        return $this->ssh_user;
    }

    public function getSshKeyPath(): ?string
    {
        return $this->ssh_key_path;
    }

    public function getSshKeyContents(): ?string
    {
        return $this->ssh_key_contents;
    }

    public function getSshPassword(): ?string
    {
        return $this->ssh_password;
    }

    public function getSshJumpHost(): ?string
    {
        return $this->ssh_jump_host; // e.g. "user@bastion.example.com"
    }
}
```

### 2. Create an Action

[](#2-create-an-action)

Actions are reusable, testable units of work:

```
use Serversinc\SshRunner\Actions\BaseSshAction;
use Serversinc\SshRunner\Contracts\SshServer;
use Serversinc\SshRunner\Results\ActionResult;
use Spatie\Ssh\Ssh;

class InstallPackage extends BaseSshAction
{
    public function __construct(private string $packageName)
    {
    }

    public function handle(SshServer $server, Ssh $ssh): ActionResult
    {
        return $this->run($ssh, ["apt-get install -y {$this->packageName}"]);
    }

    public function undo(SshServer $server, Ssh $ssh): void
    {
        // Called automatically on rollback
        $ssh->execute(["apt-get remove -y {$this->packageName}"]);
    }
}
```

### 3. Execute a Pipeline

[](#3-execute-a-pipeline)

There are several ways to execute pipelines:

#### Using the Facade (Recommended)

[](#using-the-facade-recommended)

```
use SshRunner;
use Serversinc\SshRunner\Enums\FailureStrategy;

$result = SshRunner::pipeline($server)
    ->run(new UpdatePackageList)
    ->run(new InstallPackage('nginx'))
    ->run(new InstallPackage('nginx'))
    ->run(new RestartService('nginx'))
    ->execute();

if ($result->success) {
    echo "Pipeline completed in {$result->duration()} seconds";
} else {
    foreach ($result->failedActions() as $action) {
        echo "Failed: {$action->action}\n";
        echo "Error: {$action->errorOutput}\n";
    }
}
```

#### Using SshConnection

[](#using-sshconnection)

```
use Serversinc\SshRunner\SshConnection;

$connection = SshConnection::for($server);

$result = $connection->pipeline()
    ->run(new UpdatePackageList)
    ->run(new InstallPackage('nginx'))
    ->execute();
```

#### Using the Factory Class

[](#using-the-factory-class)

```
use Serversinc\SshRunner\SshRunner;

$result = SshRunner::pipeline($server)
    ->run(new UpdatePackageList)
    ->run(new InstallPackage('nginx'))
    ->execute();

// Or execute a script directly
$result = SshRunner::script($server, new DeployWordPressSite(
    path: '/var/www/example.com',
    domain: 'example.com',
    dbName: 'wordpress_example',
    dbUser: 'wp_example',
    dbPassword: 'secure-password',
));
```

Failure Strategies
------------------

[](#failure-strategies)

Control what happens when an action fails:

```
use Serversinc\SshRunner\Enums\FailureStrategy;

// STOP (default) - Stop execution on first failure
$pipeline->onFailure(FailureStrategy::STOP);

// CONTINUE - Keep executing remaining actions
$pipeline->onFailure(FailureStrategy::CONTINUE);

// ROLLBACK - Undo completed actions in reverse order
$pipeline->onFailure(FailureStrategy::ROLLBACK)
    ->run(new CreateDatabase)
    ->run(new CreateUser) // If this fails, CreateDatabase->undo() is called
    ->execute();
```

Execution Logging
-----------------

[](#execution-logging)

All pipeline runs are automatically logged to the database:

```
use Serversinc\SshRunner\Models\SshPipelineLog;

// Get all runs for a server
$runs = SshPipelineLog::where('server_id', $server->id)->get();

// Check if a specific run failed
$run = SshPipelineLog::find(1);
if ($run->failed()) {
    foreach ($run->actionLogs as $log) {
        echo "{$log->action}: {$log->exit_code}\n";
    }
}
```

Jump Host / Bastion Support
---------------------------

[](#jump-host--bastion-support)

Route SSH connections through a bastion (jump) host by implementing `getSshJumpHost()` on your server model:

```
public function getSshJumpHost(): ?string
{
    return $this->ssh_jump_host; // e.g. "deploy@bastion.example.com"
}
```

Return `null` to connect directly (no jump host). When a non-null value is returned, `SshConnection` automatically passes it to Spatie SSH's `useJumpHost()` so all pipelines and scripts on that server are routed through the bastion transparently.

Single Action Execution
-----------------------

[](#single-action-execution)

Execute a single action without the pipeline:

```
// Using the Facade
$result = SshRunner::run($server, new UpdatePackageList);

// Or using SshConnection
$connection = SshConnection::for($server);
$result = $connection->execute(new UpdatePackageList);

if ($result->success) {
    echo $result->output;
} else {
    echo $result->errorOutput;
}
```

Script Execution
----------------

[](#script-execution)

Scripts allow you to group multiple related commands into a single action with built-in step-by-step execution, optional rollback per step, and critical/non-critical step handling.

### Creating a Script

[](#creating-a-script)

Extend `BaseScript` and define your steps:

```
use Serversinc\SshRunner\Scripts\BaseScript;
use Serversinc\SshRunner\Scripts\ScriptStep;

class DeployWordPressSite extends BaseScript
{
    public function __construct(
        private string $path,
        private string $domain,
        private string $dbName,
        private string $dbUser,
        private string $dbPassword,
    ) {}

    public function steps(): array
    {
        return [
            new ScriptStep(
                name: 'Create application directory',
                command: "mkdir -p {$this->path}",
                rollback: "rm -rf {$this->path}",
            ),
            new ScriptStep(
                name: 'Download WordPress',
                command: "cd {$this->path} && wget https://wordpress.org/latest.tar.gz",
                rollback: "rm -f {$this->path}/latest.tar.gz",
            ),
            new ScriptStep(
                name: 'Extract archive',
                command: "cd {$this->path} && tar -xzf latest.tar.gz",
            ),
            new ScriptStep(
                name: 'Create database',
                command: "mysql -e \"CREATE DATABASE IF NOT EXISTS {$this->dbName};\"",
                rollback: "mysql -e \"DROP DATABASE IF EXISTS {$this->dbName};\"",
            ),
            new ScriptStep(
                name: 'Create database user',
                command: "mysql -e \"CREATE USER IF NOT EXISTS '{$this->dbUser}'@'localhost' IDENTIFIED BY '{$this->dbPassword}'; GRANT ALL PRIVILEGES ON {$this->dbName}.* TO '{$this->dbUser}'@'localhost'; FLUSH PRIVILEGES;\"",
                rollback: "mysql -e \"DROP USER IF EXISTS '{$this->dbUser}'@'localhost';\"",
            ),
            new ScriptStep(
                name: 'Set permissions',
                command: "chown -R www-data:www-data {$this->path}",
                critical: false, // Non-critical: failure here won't stop the script
            ),
        ];
    }

    public function validate(): void
    {
        if (empty($this->path) || empty($this->domain)) {
            throw new \InvalidArgumentException('Path and domain are required');
        }
    }
}
```

### Using Scripts in Pipelines

[](#using-scripts-in-pipelines)

Scripts work seamlessly inside pipelines:

```
use SshRunner;

$result = SshRunner::pipeline($server)
    ->run(new UpdatePackageList)
    ->script(new DeployWordPressSite(
        path: '/var/www/example.com',
        domain: 'example.com',
        dbName: 'wordpress_example',
        dbUser: 'wp_example',
        dbPassword: 'secure-password',
    ))
    ->run(new RestartService('nginx'))
    ->onFailure(FailureStrategy::ROLLBACK)
    ->execute();
```

### Executing a Script Directly

[](#executing-a-script-directly)

Run a script as a single action without a pipeline:

```
use SshRunner;

$result = SshRunner::script($server, new DeployWordPressSite(
    path: '/var/www/example.com',
    domain: 'example.com',
    dbName: 'wordpress_example',
    dbUser: 'wp_example',
    dbPassword: 'secure-password',
));

if ($result->success) {
    echo $result->output;
} else {
    echo $result->errorOutput;
}
```

### Script Behavior

[](#script-behavior)

- **SSH connection reuse** — Scripts automatically enable SSH multiplexing (`ControlMaster=auto`) so all steps share the same underlying TCP connection, avoiding repeated authentication overhead.
- **Critical steps** (`critical: true`, the default) trigger automatic rollback of all previously completed steps on failure.
- **Non-critical steps** (`critical: false`) log a failure but continue to the next step.
- **Step-level rollback** commands are executed in reverse order when a critical step fails or when the pipeline's `ROLLBACK` failure strategy is triggered.
- **Filesystem state persists** between steps (files created in one step are available in the next).
- Scripts integrate with the existing logging and failure strategy infrastructure.

> **Note:** While the SSH network connection is reused between steps, each `ScriptStep` still runs in its own shell process. This means environment variables set in one step (e.g. `export VAR=value`) are not available in subsequent steps. If you need to share data between steps, use files on the remote filesystem.

Advanced Usage
--------------

[](#advanced-usage)

### Creating a Connection for Reuse

[](#creating-a-connection-for-reuse)

```
use Serversinc\SshRunner\SshConnection;
use Serversinc\SshRunner\SshPipeline;

$connection = SshConnection::for($server);

// Execute multiple pipelines on the same connection
$result1 = $connection->pipeline()
    ->run(new Action1())
    ->execute();

$result2 = $connection->pipeline()
    ->run(new Action2())
    ->execute();
```

### Using SshRunner Factory Methods

[](#using-sshrunner-factory-methods)

```
use Serversinc\SshRunner\SshRunner;

// Create a connection
$connection = SshRunner::connect($server);

// Create a pipeline directly
$pipeline = SshRunner::pipeline($server);

// Execute a single action
$result = SshRunner::run($server, new SomeAction());

// Execute a script directly
$result = SshRunner::script($server, new DeployWordPressSite(
    path: '/var/www/example.com',
    domain: 'example.com',
    dbName: 'wordpress_example',
    dbUser: 'wp_example',
    dbPassword: 'secure-password',
));
```

Testing
-------

[](#testing)

```
composer test
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

### Security

[](#security)

If you discover any security-related issues, please use the [issue tracker](https://github.com/serversinc/ssh-runner/issues) and mark it as a security concern.

Credits
-------

[](#credits)

- [Max Diamond](https://github.com/serversinc)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance91

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~9 days

Total

5

Last Release

41d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/d6051062b9195df0fd8aa25fdf9548ed1981c1aca9cf77b2b0429aba96acf047?d=identicon)[dmdboi](/maintainers/dmdboi)

---

Tags

serversincssh-runner

###  Code Quality

TestsPHPUnit

Static AnalysisRector

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/serversinc-ssh-runner/health.svg)

```
[![Health](https://phpackages.com/badges/serversinc-ssh-runner/health.svg)](https://phpackages.com/packages/serversinc-ssh-runner)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

255.2k](/packages/aedart-athenaeum)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
