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 Deploy Operations Laravel Package

dragon-code/laravel-deploy-operations

Laravel Deploy Operations by Dragon Code provides a simple, configurable way to run deployment tasks such as migrations, cache clearing, queue restarts, and custom Artisan commands. Define operations once and execute them reliably during releases, keeping deployments consistent and repeatable.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require dragon-code/laravel-deploy-operations
php artisan vendor:publish --provider="DragonCode\LaravelDeployOperations\ServiceProvider"

This gives you a config file (config/deploy-operations.php) and a migration that creates the bookkeeping table (typically deploy_operations). Run php artisan migrate before the first deploy that uses the package — the command will fail on a missing table otherwise.

Where to look first

  1. config/deploy-operations.php — operation path, table name, and (as of 7.3) a queue block controlling how queued operations are dispatched. Everything you'll want to tweak lives here.
  2. The default operations directory (usually database/operations, overridable in config) — this is your working surface.
  3. php artisan list | grep -i deploy — confirm the exact command namespace and available flags (--force, --isolated) for your installed major version before you wire it into a script.
  4. vendor/dragon-code/laravel-deploy-operations/src — the package is small (243 stars, near-zero dependent footprint) and the README is thin. Reading the source takes ten minutes and beats guessing.

First use case

Anything that must run exactly once per environment but isn't schema: creating a storage symlink on a new node, backfilling a column after a migration, seeding a lookup table, registering a webhook, generating an initial sitemap. Write one operation:

<?php // database/operations/2026_01_15_120000_create_storage_link.php

use DragonCode\LaravelDeployOperations\Operation;

return new class extends Operation
{
    public function run(): void
    {
        $this->artisan('storage:link');
    }
};

Then append it to your deploy script:

php artisan migrate --force && php artisan deploy --force

Implementation Patterns

One operation per concern, timestamp-named. Use the migration naming convention (Y-m-d_His_description.php). Ordering is deterministic by filename, which matters when operation B assumes A already ran.

Use it as the "data/state" half of your migration story. Schema changes stay in migrations; one-shot data shaping, permission seeding, and external side effects go in operations. This keeps migrate fast and reversible while giving you an explicit, tracked home for the messy stuff.

Gate inside the operation, not in the deploy script. The file runs on every environment that deploys, so branch internally:

public function run(): void
{
    if (! app()->isProduction()) {
        return;
    }
    // production-only side effect
}

Make it the last step of a blue/green or multi-node rollout. A typical CI step sequence:

git pull --ff-only
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan deploy --force
php artisan config:cache && php artisan route:cache && php artisan view:cache

Use --isolated instead of hand-rolling a lock. 7.3 aligns the flag's presence semantics with Laravel core, so php artisan deploy --force --isolated behaves the way you'd expect from queue:work --isolated / schedule:run. If your deploy scripts currently wrap the command in their own flock or cache lock, evaluate whether the native flag now covers it.

Let the package dispatch queued operations rather than doing it yourself. Previous advice was "dispatch a job from run()" for anything slow. That's still valid, but if the package dispatches operation work to a queue job, you now have real knobs:

  • queue.job — point this at your own job class to add middleware, tenancy context, timeouts, or $tries/$backoff without patching vendor code. Subclass the package job and override only what you need; keep the parent's completion/marking logic intact.
  • Uniqueness lock duration — the lock that prevents a queued operation from being enqueued/executed twice is now configurable. Set it comfortably above your worst-case job runtime (queue lag + retries included), not equal to it.
// config/deploy-operations.php (shape may vary by version — verify keys in the published file)
'queue' => [
    'job'  => \App\Jobs\DeployOperationJob::class, // your subclass
    // lock/unique duration in seconds
],

Test operations locally before they're history. Deploy once against a scratch database, inspect the tracking table, confirm the row landed, and re-run to confirm it's skipped. Do the same with a queue connection set to sync if you use the queued path — you want to see the job complete, not just enqueue. You cannot "undo" a deploy in production by editing the file.

Extension points worth knowing: custom operation directory and table name via config, the queue.job class override, and the base Operation class — it exposes the container and artisan() for calling other commands, which is usually cleaner than shelling out.

Gotchas and Tips

The tracking table is the contract. If you rename or move an operation file after it has run, the package sees a new operation and runs it again. Treat ran operation files as append-only history — same discipline as migrations.

Concurrency and the "run once" claim. On a multi-server deploy that hits the same database simultaneously, two nodes can both evaluate "not yet run." Use --isolated (now Laravel-consistent in 7.3), deploy operations on a single designated node, or make run() idempotent (guard with exists() / firstOrCreate() / insertOrIgnore). Idempotency is cheap insurance; assume you're not the only process.

Lock duration is a timeout, not a guarantee. If your configured uniqueness lock expires while the queued operation is still running (slow external API, big backfill, job stuck behind a long queue), a second dispatch can get through and the work runs twice. Size the duration to your p99 runtime plus retry budget, and keep the operation itself idempotent anyway.

A custom queue.job is now production code. Overriding the job class is the cleanest extension point in the package, but you own it: if a release changes the parent job's constructor, completion hook, or marking behaviour, your subclass can silently mark an operation as done before the work finished. Re-read the parent job on every minor bump.

--isolated presence semantics. 7.3 makes the flag behave like Laravel core, which means it's evaluated by presence rather than a specific value. Don't pass it conditionally via a variable that could serialise to --isolated=0 or --isolated=false and assume it's "off" — omit the flag entirely when you don't want it.

No transaction around an operation. If run() does three things and the third throws, the first two are committed and the operation is not marked as run — so the next deploy retries the whole thing. Design each operation to be re-runnable, and wrap multi-statement DB work in DB::transaction() yourself.

Config cache ordering bites twice now. If you config:cache before running the deploy command, the package reads a frozen config — including a stale queue.job class name and lock duration. Cache after deploying, and re-clear if you changed the operations path or the queue block.

Rollback is not free. If your version supports a rollback(), implement it as a genuine reverse or an explicit no-op with a comment saying why. A rollback that half-reverts data is worse than none.

Debugging: use the tracking table first, then the queue. Before reading a single line of package code, check the operations table for the row and its timestamp, then php artisan queue:failed. "It ran but nothing happened" is almost always an environment guard returning early, a job that was never processed, or a lock that let a duplicate through.

Watch the Laravel version constraint. The package is maintained actively but has a narrow dependency surface; check composer require output during upgrade season rather than discovering it during a deploy.

Pin the version in composer.json. With a small ecosystem, a minor bump is effectively un-reviewed code in your critical deploy path. "dragon-code/laravel-deploy-operations": "^7.3" — and read the diff on update, especially anything touching the dispatched job.

Don't let it become a junk drawer. If an operation hasn't been relevant in a year, it's documentation, not code. Keep the directory small enough that a new hire can read all of it in one sitting.

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.
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
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
cgv/localized-resource
noith/settings