Weave 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

Laravel Task Runner Laravel Package

protonemedia/laravel-task-runner

Laravel Task Runner lets you define reusable tasks as classes and run them from Artisan, code, or a schedule. It offers clean task registration, output handling, and cron-like scheduling for maintenance routines and background work.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish.

composer require protonemedia/laravel-task-runner   # ^1.7 if you're on PHP 8.5
php artisan vendor:publish --tag=task-runner-config   # check the tag in the README

Version 1.7.0 is the first release with PHP 8.5 support; everything else in it is CI/Dependabot housekeeping. If you're still on PHP 8.3/8.4, 1.6.x behaves identically — pin whichever you install and treat 1.7.0 as a compatibility bump, not a feature release.

The package sits on top of symfony/process locally and spatie/ssh for remote hosts, so there is no daemon or agent to install — it shells out the same way Envoy did, just with a Blade-based authoring layer.

Look here first, in this order:

  1. config/task-runner.php — the servers array (host, user, port, key/agent settings) and any default timeout values. Everything remote is driven from this file.
  2. ProtoneMedia\LaravelTaskRunner\Task — the abstract base you extend. Reading its render() signature and any on()/timeout() helpers tells you 80% of the API in one file.
  3. The ProcessOutput/Script value objects — these are what you assert against in tests and what you log in production.
  4. The package's own README section on remote tasks — that's where the SSH semantics and server naming live.

First use case: retire one brittle Envoy/ssh snippet. Pick the smallest real thing you do today — a git pull && composer install && php artisan migrate --force on staging — and model it as one task class plus one Blade view. Run it locally first (TaskRunner::run(new DeployStaging('/var/www/app')) or the equivalent instance call in your pinned version), confirm the output object reports success, then point it at the staging server via config. That loop — local render, inspect output, then remote — is the whole workflow and is worth doing once before you template everything.

Implementation Patterns

One task, one operation, parameters via the constructor. Tasks are cheap and composable; resist the urge to build one giant Deploy task with twelve boolean flags.

namespace App\Tasks;

use ProtoneMedia\LaravelTaskRunner\Task;

class ComposerInstall extends Task
{
    public function __construct(
        public string $path,
        public bool $noDev = false,
    ) {}
}

Scripts live in Blade views, resolved like Blade components (App\Tasks\ComposerInstall → resources/views/tasks/composer-install.blade.php). Keep logic in PHP and the view to literal shell — the view is a template, not a place for conditionals that belong in the class.

set -euo pipefail
cd {!! $path !!}
composer install --no-interaction {!! $noDev ? '--no-dev' : '' !!}

Orchestrate at the application layer, not inside the task. A Deploy action or artisan command that runs GitPull, ComposerInstall, Migrate, and bails on the first failure is far easier to test and reason about than a task that nests other tasks. Aggregate the output objects, log them, and translate to exit codes in one place:

foreach ([new GitPull($path), new ComposerInstall($path), new Migrate($path)] as $task) {
    $output = TaskRunner::run($task);

    Log::info(class($task), ['successful' => $output->isSuccessful()]);

    if (! $output->isSuccessful()) {
        return 1;
    }
}

Remote execution is a config plus a pointer. Name your servers (staging, web-1, web-2) in config/task-runner.php, then select one at call time rather than hardcoding user@host in classes. That keeps the same task runnable locally in tests and remotely in CI, and lets you fan out to multiple web nodes by looping over server names.

Run from jobs for anything slow. A deploy or backup belongs in a queued job with an explicit $timeout slightly larger than the task's own timeout, so the worker doesn't kill the SSH session mid-migration. Emit output to the job's log so you have an audit trail.

Testing. Assert on the rendered script separately from executing it — render the task and assert the string contains the expected commands, which catches Blade escaping and quoting bugs without touching a shell. For the execution path, keep an integration test that runs against a throwaway container or a local server entry; mocking symfony/process is more trouble than it's worth. Wrap user-supplied values in escapeshellarg() inside the task's view data so tests can assert on properly quoted output.

Gotchas and Tips

Blade escapes HTML by default — this will bite you. {{ $path }} turns /var/www into HTML-safe output and mangles quotes, &&, > and $. Use the package's unescaped echo ({!! !!}, or whatever the README prescribes for your version) for every shell interpolation, and never pass raw user input through it — build the string with escapeshellarg() in PHP and interpolate the already-quoted value.

Whitespace and line endings. Blade rendering preserves indentation from your view file, and a stray leading space before a shebang or a \r from a Windows checkout will produce confusing command not found errors. Keep scripts left-aligned, prefer #!/usr/bin/env bash plus an explicit set -euo pipefail header, and add a .gitattributes rule if your team mixes platforms. pipefail in particular matters: without it, cmd | tee log masks a non-zero exit and your task reports success after a failed migration.

Non-interactive SSH is not your login shell. Remote tasks get no TTY, so sudo password prompts hang until the idle timeout, aliases and .bashrc PATH additions don't apply, and ~ may resolve differently. Use absolute paths, invoke bash -lc '...' if you genuinely need the profile, and configure passwordless sudo or a deploy user rather than trying to feed passwords through the process.

Timeouts are two different knobs. The overall timeout catches a long-but-progressing task; the idle/no-output timeout catches a hung one (a sudo prompt, a read in a script, a stuck composer network call). Set both explicitly on long tasks — the defaults are usually tuned for short commands, and a default timeout on a 20-minute migration is a self-inflicted incident.

Environment drift between local and remote. .env on the server is not the .env your task was tested against. Anything that reads app config (php artisan migrate, queue:restart) must run from the correct working directory — always cd first — and you should not assume the remote PHP binary is the same version. Consider rendering php -v into a preflight task. With 1.7.0 claiming PHP 8.5 support, "remote PHP version" is now more likely to differ from local than it was a year ago — verify rather than assume.

Output buffering on chatty commands. A composer install or rsync --progress can produce megabytes of output. If you're capturing the full buffer to log it, truncate or stream it; storing whole deploy logs in a DB column gets expensive fast. Be equally careful that secrets (--password=, tokens in URLs, .env dumps) never land in that buffer.

Zero dependents and ~131 stars is the real risk. This is a single-maintainer package with a small blast radius, and 1.7.0's changelog is almost entirely bot-driven dependency bumps plus one first-time contributor's PHP 8.5 patch — a reminder that the bus factor hasn't changed. Pin the version in composer.json, keep all task definitions thin, and put the execution behind your own small interface (TaskExecutor::run(Task $task, ?string $server)) so that swapping to raw symfony/process plus spatie/ssh is a one-file change rather than a rewrite. That wrapper also gives you a natural place for logging, metrics, and a per-task lock.

Add a concurrency lock before you need it. Two overlapping deploys of the same task is the classic way to corrupt a release. Cache::lock("task:{$name}", 600) around the run costs three lines and prevents the worst outcome. Similarly, treat the package as a runner, not a scheduler — pair it with your existing queue/cron rather than inventing a new trigger mechanism.

Extension points worth knowing: a shared App\Tasks\Task base class that injects common env vars and defaults (timeouts, working directory, retry-once-on-SSH-drop), custom Blade directives for frequently repeated shell idioms, and an output handler that redacts known secret values before anything hits the log. Verify exact method names (on(), timeout(), run() vs. facade) against the README for the version you pin — the API has moved between releases, and 1.7.0 shipped with no API-affecting changes, so the README you last read for 1.6.x still applies.

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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect