PHPackages                             rekuest/laravel-artifact-deployer - 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. [DevOps &amp; Deployment](/categories/devops)
4. /
5. rekuest/laravel-artifact-deployer

ActiveLibrary[DevOps &amp; Deployment](/categories/devops)

rekuest/laravel-artifact-deployer
=================================

Artifact-based post-deploy runner for Laravel (secure unzip/extract + configurable artisan pipeline).

v0.0.2(yesterday)00MITPHPPHP ^8.2

Since Jun 8Pushed yesterdayCompare

[ Source](https://github.com/rekuest/laravel-artifact-deployer)[ Packagist](https://packagist.org/packages/rekuest/laravel-artifact-deployer)[ Docs](https://github.com/rekuest/laravel-artifact-deployer)[ RSS](/packages/rekuest-laravel-artifact-deployer/feed)WikiDiscussions main Synced yesterday

READMEChangelogDependencies (8)Versions (3)Used By (0)

 [ ![Rekuest](docs/logo-rekuest.svg) ](https://www.rekuest.com)

Laravel Artifact Deployer
=========================

[](#laravel-artifact-deployer)

> ⚠️ **Work in progress** — this package is still under active development and may change without notice until the first stable release. Use in production at your own risk.

[![License](https://camo.githubusercontent.com/6c711032aff1ca0eb6b211aa6cb3649ce7fd64a7714e1181d4bb457f9680e7cf/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e2e7376673f7374796c653d666c61742d737175617265)](LICENSE.md)[![PHP](https://camo.githubusercontent.com/4775b6c2f99498ce926c078ae601b675552e47b0c007fdaa45d489f904d5dcb3/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d254532253839254135253230382e322d3737374242342e7376673f7374796c653d666c61742d737175617265)](https://www.php.net)[![Laravel](https://camo.githubusercontent.com/96420f84cbf981d8bc91496c8c847938b27bce6d64408062f72d27fcfd67640f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d3131253230254332254237253230313225323025433225423725323031332d4646324432302e7376673f7374796c653d666c61742d737175617265)](https://laravel.com)

A robust, secure and configurable **artifact-based deploy runner** for Laravel — covering the whole loop: **pack** the build into a single artifact, ship it (FTP/SFTP or your CI), then **lay it down and run a post-deploy pipeline** on the server with **one HTTP call** or an artisan command. No SSH session required to finalize a deploy.

Built around a few principles: **POST/CLI only** with the secret in a **dedicated header** compared via `hash_equals`, **fail-closed** when the key is unset, **anti zip-slip** extraction, optional **SHA-256** integrity, a single-flight **lock**, a **maintenance window** with smart recovery, and structured **JSON / logs / events**.

Why
---

[](#why)

Deploying a Laravel app — large or small — should not require SSH access to the box to "finish" the deploy. This package lets you build the whole app into a **single artifact**, transfer just that one file via **FTP/SFTP**(or your CI's copy step), and finalize everything — extraction, migrations, cache warming — with **one HTTP call**.

It fits wherever you deploy:

- **Shared / managed hosting** where you only have FTP and no shell: upload `build_artifact.zip`, hit the endpoint, done.
- **A Jenkins (or any CI) pipeline**: `rad:pack` on the build agent, ship the file, then a single `curl` POST as the deploy step.
- **No CI/CD at all**: build locally, drag the file up over SFTP, trigger the URL from a browser-free `curl`.

And it does it **safely**. Laying an archive down over a live application and running migrations is easy to get wrong, so the package handles the sharp edges for you:

- **POST/CLI only**, secret in a dedicated header compared with `hash_equals` (never a `GET ?key=` that leaks into logs).
- **Anti zip-slip** extraction: every entry validated before a single byte is written, protected paths (`.env`, `storage/`) never overwritten.
- **Atomic lock** (no corrupt parallel deploys), optional **SHA-256** integrity, **maintenance window** with smart recovery.
- **Structured output** — explicit HTTP codes, JSON result, per-deploy logs and events — instead of `dd()`/`die()` and leaked stack traces.
- A **declarative, configurable** command pipeline: the package runs whatever post-deploy commands your app declares, and knows nothing about it.

Features
--------

[](#features)

- **Full loop** — `rad:pack` builds the artifact on CI; `rad:run` (or the HTTP webhook) deploys it on the server.
- **Two triggers, one pipeline** — `POST /{prefix}/run` for the CI webhook, `php artisan rad:run` for SSH/CI. Identical behaviour.
- **Secure by default** — POST only, key in the `laravel-artifact-deployer-key` header (`hash_equals`), query-string disabled, fail-closed when `RAD_KEY` is unset, environment allow-list, failed-auth audit logging.
- **Safe extraction** — every archive entry is validated (path-traversal, absolute paths, symlinks, protected paths) **before** anything is written; the whole archive is rejected on the first unsafe entry. After validation, native `extractTo()` is used for speed.
- **Pluggable format** — `zip` (ext-zip) or `targz` (PharData / ext-phar), selected via config; a missing extension fails explicitly.
- **Optional integrity** — SHA-256 verified when a `.sha256` sidecar or a body hash is provided; can be made mandatory.
- **Configurable command pipeline** — `package:discover` → `migrate --force` → your `custom[]` commands → `optimize:clear`, each toggleable. The package knows nothing about your app.
- **Single-flight lock** — concurrent deploys get `409 Conflict`.
- **Smart maintenance window** — the site goes `down` only at the moment extraction is about to write (after all checks pass), and `up` on success. A failure **during the checks** (artifact missing, bad archive, checksum mismatch) **never takes the site offline**; a failure **after** mutation started leaves it **down** (no automatic rollback). The deploy route is **excluded from maintenance** (no secret needed) so the CI can always re-trigger.
- **Observable** — `deploy_id` on every log line, a persisted `last.json`, `rad:status`, and `DeployStarted/StepCompleted/Succeeded/Failed` events.

Requirements
------------

[](#requirements)

- PHP `^8.2`
- Laravel `^11.0` · `^12.0` · `^13.0`
- `ext-zip` (for `zip`) and/or `ext-phar` (for `targz`)
- For **packing** `targz` (`rad:pack --format=targz`): `phar.readonly=0` in php.ini. Extraction and zip packing have no such requirement.

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

[](#installation)

```
composer require rekuest/laravel-artifact-deployer
php artisan vendor:publish --tag="artifact-deployer-config"
```

Only two variables are required in `.env`:

```
RAD_ENABLED=true
RAD_KEY=        # 32+ byte hex — required for the HTTP endpoint
```

Generate the key:

```
php -r "echo bin2hex(random_bytes(32)).PHP_EOL;"
```

Everything else has sensible defaults — change it in the published `config/artifact-deployer.php`rather than `.env`. A few optional env overrides you may actually want:

VariableDefaultSet it when…`RAD_MAINTENANCE_SECRET`—you want a bypass token to view the site while it's in maintenance`RAD_ARTIFACT_FORMAT``zip`your artifact is a tar.gz (`targz`)`RAD_REQUIRE_CHECKSUM``false`you want to force SHA-256 verification (`true`)`RAD_ARTIFACT_AFTER``rename`you want `delete` or `keep` instead of renaming after a deploy`RAD_ALLOW_QUERY_STRING``false`your CI cannot send custom headers and must pass the key as `?key=`> `RAD_ARTIFACT_PATH` defaults to `base_path('build_artifact.zip')`, so you normally don't set it — only override it if the artifact lives somewhere else. The same goes for the route prefix, migration and `optimize:clear` toggles, etc.: they're all in the config file.

The deploy loop
---------------

[](#the-deploy-loop)

```
┌── CI / build machine ───────────────┐        ┌── target server ──────────────────────┐
│ composer install --no-dev && build  │  scp   │ POST /artifact-deployer/run            │
│ php artisan rad:pack  ───────────────┼──────▶ │   or  ssh … php artisan rad:run        │
│   → build_artifact.zip (+ .sha256)  │        │   → verify → extract → migrate → up    │
└─────────────────────────────────────┘        └────────────────────────────────────────┘

```

### 1. Pack the artifact (CI) — `rad:pack`

[](#1-pack-the-artifact-ci--radpack)

Replaces a raw `zip -r build_artifact.zip .`. It honours `artifact.format`, names the archive from `artifact.path`, applies the configured exclusions — **and always excludes the archive itself and its `.sha256`** (which `zip -r` does not) — and writes the SHA-256 sidecar.

```
php artisan rad:pack
#   [--output=path] [--format=zip|targz] [--source=dir] [--no-checksum]
```

Exclusions live in `config('artifact-deployer.artifact.pack.exclude')` (same matching convention as `protected_paths`):

```
'pack' => [
    'source'  => base_path(),
    'exclude' => ['.git/', '.env', '.env.*', 'node_modules/', 'storage/logs/', 'tests/', '*.sha256', '*.deployed'],
    'checksum' => true, // also write .sha256
],
```

### 2. Deploy on the server

[](#2-deploy-on-the-server)

**HTTP (CI webhook):**

```
curl -fsS -X POST https://app.example.com/artifact-deployer/run \
     -H "laravel-artifact-deployer-key: $RAD_KEY" \
     -H "Content-Type: application/json" \
     -d '{"artifact":"build_artifact.zip","sha256":"'"$(cut -d' ' -f1 build_artifact.zip.sha256)"'"}'
```

**CLI (SSH, preferred — no HTTP port exposed):**

```
php artisan rad:run [--artifact=path] [--sha=hex] [--no-maintenance] [--dry-run]
php artisan rad:status [--json]
```

> `--dry-run` validates the artifact (and integrity) and runs the pipeline without writing files, migrating or entering maintenance. There is no `rad:rollback` — see [Rollback strategy](#rollback-strategy).

### Response

[](#response)

```
{
  "deploy_id": "2026-06-08T10-12-33_a1b2c3",
  "status": "success",
  "duration_ms": 18234,
  "steps": [
    { "name": "verify_artifact", "status": "ok", "output": "format=zip; archive readable; sha256 match" },
    { "name": "extract_artifact", "status": "ok", "output": "412 files extracted" },
    { "name": "run_artisan:migrate", "status": "ok", "output": "..." }
  ]
}
```

CodeMeaning`200`deploy completed`401`missing / wrong token`403`environment not allowed`409`a deploy is already running`422`artifact missing or checksum invalid`500`a step failed (no automatic rollback — re-deploy the previous artifact)`503`package disabledThe pipeline
------------

[](#the-pipeline)

Ordered steps (`DeployPipeline`):

1. `VerifyArtifact` — extension available? artifact present? archive openable? SHA-256 (if provided or required). **Read-only.**
2. `ExtractArtifact` — validates every entry (anti path-traversal, protected paths), then writes. **At the point of no return** — once validation passes and the first byte is about to be written — it enters maintenance (`artisan down`, idempotent and non-fatal).
3. `PreMigrateDump` — optional manual-DR DB dump (off by default), taken while the site is down.
4. `RunArtisanCommands` — `package:discover` → `migrate --force` → `custom[]` → `optimize:clear` (per toggles).
5. `RestartServices` — `queue:restart`, optional `horizon:terminate`.
6. On success: `ExitMaintenanceMode` (`up`) + `DisposeArtifact` (`rename`/`delete`/`keep`).

> **Maintenance is entered only when the deploy is about to mutate the app.** Every check — artifact missing, bad checksum, unopenable/corrupt archive, even a path-traversal entry — happens *before* `down`, so a failed check **never takes the site offline**. The site goes down only at the instant extraction starts writing, and a failure from that point on leaves it down (no automatic rollback).

> **Queue / Horizon and maintenance.** There is no explicit worker "stop" step paired with `down`: it isn't needed when maintenance is enabled, because queue workers and Horizon **auto-pause while the app is in maintenance**(unless a worker runs with `--force`). So `down` suspends them, and `RestartServices` (`queue:restart` / `horizon:terminate`) makes them reload the new code on `up`. With `maintenance.enabled = false` (hot deploy) workers keep running old code during the window — the package can't stop supervisor-managed processes, so only the final restart applies.

The deploy route is registered as **excluded from maintenance mode** (`PreventRequestsDuringMaintenance::except`), so once the site is down the CI can still reach `/artifact-deployer/run` to re-trigger — no secret required.

Customising the pipeline (consumer projects)
--------------------------------------------

[](#customising-the-pipeline-consumer-projects)

The `commands` config is what makes the package generic. Any consumer declares its own commands:

```
// config/artifact-deployer.php
'commands' => [
    'package_discover' => true,
    'migrate'          => true,
    'custom' => [
        ['my-command-1', []],
        ['my-command-2', []],
    ],
    'optimize_clear'   => true,
],
```

Order: `package:discover` → `migrate --force` → `my-command-1` → `my-command-2` → `optimize:clear`.

### Events

[](#events)

Hook integrations (Slack/Teams, audit table, monitor ping) without touching the package:

- `Rekuest\ArtifactDeployer\Events\DeployStarted`
- `Rekuest\ArtifactDeployer\Events\DeployStepCompleted`
- `Rekuest\ArtifactDeployer\Events\DeploySucceeded`
- `Rekuest\ArtifactDeployer\Events\DeployFailed`

Rollback strategy
-----------------

[](#rollback-strategy)

This package uses an **overlay in-place** model and has **no automatic rollback** by design:

- **Files** — rollback = re-deploy the previous artifact kept by the CI.
- **Database** — forward-only; write backward-compatible (expand/contract) migrations. The optional `pre_migrate_dump` is for **manual** disaster recovery only.

A deploy that fails *after* mutation started leaves the site in maintenance; recovery is a re-deploy of the previous artifact. A deploy that fails *before* mutation (bad/missing artifact) leaves the app untouched and brings it back up.

Testing
-------

[](#testing)

```
composer test       # Pest + Orchestra Testbench
composer analyse    # PHPStan / Larastan
composer format     # Pint
```

License
-------

[](#license)

The MIT License (MIT). See [LICENSE.md](LICENSE.md). © Rekuest Srl.

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity37

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

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 ~0 days

Total

2

Last Release

1d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/9ce106565ed81cc09f51caa9ed3c1d8659c5a5fd57284a7b2904bf0a5c6f0d60?d=identicon)[rmorandirekuest](/maintainers/rmorandirekuest)

---

Top Contributors

[![rmorandi-rekuest](https://avatars.githubusercontent.com/u/44582203?v=4)](https://github.com/rmorandi-rekuest "rmorandi-rekuest (2 commits)")

---

Tags

laravelreleasedeploydeploymentci-cdartifactpost-deployrekuestrekuest srl

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/rekuest-laravel-artifact-deployer/health.svg)

```
[![Health](https://phpackages.com/badges/rekuest-laravel-artifact-deployer/health.svg)](https://phpackages.com/packages/rekuest-laravel-artifact-deployer)
```

###  Alternatives

[larastan/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

6.4k51.0M7.4k](/packages/larastan-larastan)[spatie/laravel-responsecache

Speed up a Laravel application by caching the entire response

2.8k8.7M64](/packages/spatie-laravel-responsecache)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

76318.2M110](/packages/laravel-mcp)[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[laravel/ai

The official AI SDK for Laravel.

9782.1M153](/packages/laravel-ai)[zidbih/laravel-deadlock

Make temporary Laravel workarounds expire and fail CI when ignored.

954.0k](/packages/zidbih-laravel-deadlock)

PHPackages © 2026

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