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 Envoyer Sdk Laravel Package

juststeveking/laravel-envoyer-sdk

A Laravel SDK for the Envoyer deployment service, giving you a clean, fluent interface to manage projects, servers, deployments, and hooks from your app or artisan commands. Simplifies Envoyer API calls with typed requests and Laravel-friendly responses.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin deliberately.

composer require juststeveking/laravel-envoyer-sdk:^2.0   # PHP 8.x
# composer require juststeveking/laravel-envoyer-sdk:^1.4  # PHP 7.x estates, until you upgrade runtime

Do not combine this with a PHP runtime upgrade in the same release. v2.0.0 is a PHP 8 floor change with nothing else documented in the changelog — treat it as breaking until you have diffed it.

Where to look first — in this order:

  1. vendor/juststeveking/laravel-envoyer-sdk/config/ — the published config. Confirm the actual key names in the installed version before writing anything; do not assume ENVOYER_API_TOKEN is the env var, and do not assume the base URL key is named what you expect.
  2. vendor/juststeveking/laravel-envoyer-sdk/src/ — the connector, the resource classes, and the request classes. This package is a thin resource wrapper over Envoyer's REST API, so the source is the documentation. Read the resource classes to get the real method names and return types; the README will not stay in sync across three years of no releases.
  3. composer why juststeveking/laravel-envoyer-sdk and composer why on the underlying HTTP client (Saloon/Guzzle) — you need to know what else moves when this moves.

Publish config and set the token.

php artisan vendor:publish --tag=envoyer-sdk-config   # verify the actual tag in the provider
ENVOYER_API_TOKEN=your-account-token

The token is account-scoped — it can trigger deployments on every project the account owns. Generate a dedicated token for this integration (not a personal one), store it as a production secret, and write down who rotates it.

First use case: trigger a deployment for a known project. Not "list projects."

Listing projects is the tempting first call, but it teaches you nothing about the failure modes that matter. Start with the flow you will actually ship:

use JustSteveKing\LaravelEnvoyerSDK\Facades\Envoyer; // verify facade alias in the provider

$response = Envoyer::deployments()->create(
    projectId: (int) config('envoyer.projects.api'),
    from: 'main',
);

Then immediately do the thing everyone skips: poll the deployment until it reaches a terminal state. A 2xx from that call means accepted, not deployed. Prove that to yourself on a throwaway Envoyer project with a throwaway branch before you write a single line of product code.


Implementation Patterns

Wrap it. Always.

The single most important pattern: no controller, command, or job touches the SDK directly. Define your own interface and put the SDK behind it.

interface EnvoyerDeployer
{
    public function trigger(int $projectId, string $branch): DeploymentId;
    public function status(DeploymentId $id): DeploymentStatus;
}

This is what turns "the package is abandoned" from an incident into a two-day swap, and it's what lets you mock the API in tests without a MockClient.

Resolve IDs from config, never by name

Name lookup at deploy time costs an extra round trip and breaks silently on rename.

// config/envoyer-projects.php
return [
    'api'     => ['project' => (int) env('ENVOYER_PROJECT_API'), 'server' => (int) env('ENVOYER_SERVER_API')],
    'website' => ['project' => (int) env('ENVOYER_PROJECT_WEBSITE'), 'server' => (int) env('ENVOYER_SERVER_WEBSITE')],
];

Name resolution happens once, by a human, in the Envoyer dashboard — not at 2am in a queue worker.

Model status as an enum with terminal states, never a boolean

enum DeploymentStatus: string
{
    case Pending  = 'pending';
    case Running  = 'running';
    case Finished = 'finished';
    case Failed   = 'failed';
    case Stuck    = 'stuck';   // your own synthetic state, set by a timeout

    public function isTerminal(): bool
    {
        return in_array($this, [self::Finished, self::Failed, self::Stuck], true);
    }
}

The Stuck case is yours, not Envoyer's. Every poller needs a deadline; a deployment that never reaches a terminal state is a real failure mode and must be representable.

Trigger-then-poll, with the poll on a schedule

final class TriggerDeployment implements ShouldQueue
{
    public int $tries = 1;      // see gotchas — do not retry the trigger
    public int $timeout = 15;

    public function handle(EnvoyerDeployer $envoyer): void
    {
        $id = $envoyer->trigger($this->projectId, $this->branch);

        DeploymentRecord::create([
            'project_id'    => $this->projectId,
            'external_id'   => $id->value,
            'status'        => DeploymentStatus::Pending,
            'requested_by'  => $this->userId,
        ]);
    }
}

Then a separate scheduled job (everyMinute) sweeps non-terminal DeploymentRecords and calls status(). This gives you a local audit trail, a queryable history, and — critically — an escape hatch if the package disappears: your records survive it.

Guard against double-deploys with a lock

Queue retries, webhook redelivery, and an impatient developer clicking "Deploy" twice all produce the same result.

$lock = Cache::lock("envoyer:deploy:{$projectId}", 300);

if (! $lock->get()) {
    return; // a deployment is already in flight for this project
}

Hold the lock until the deployment reaches a terminal state, or use a TTL comfortably longer than a normal deploy and let it expire.

Queue everything; nothing synchronous in a request path

A user-facing request that blocks on the Envoyer API inherits Envoyer's latency, its rate limits, and its outages. The controller enqueues; the UI shows the record you just created.

Cache the read-only lists

Projects, servers, collaborators, and environments change rarely and the API is rate-limited. Cache::remember(..., now()->addMinutes(10), ...) on every list call. This also removes the temptation to do name lookups at deploy time.

Artisan command for humans

php artisan envoyer:deploy api --branch=main

Keep it thin — it resolves the project from config, dispatches TriggerDeployment, prints the deployment record ID, and exits. If you need CI-friendly behaviour, add --wait that polls with a bounded timeout and returns a non-zero exit code on Failed or Stuck.

Testing: mock the HTTP layer, never hit the API in CI

A test suite that triggers a real deployment is an incident waiting for a green build. Because the SDK sits on Saloon/Guzzle, mock at the connector boundary (MockClient with recorded fixtures) — or better, bind a fake EnvoyerDeployer in the container and test your orchestration (locking, record creation, enum transitions) rather than the SDK's serialization.

Record one real fixture per endpoint against a throwaway project, commit it, and mock from it forever.


Gotchas and Tips

A 200 from the trigger means "accepted." This is the mistake that ships. Build the UI and the CI exit code around the polled terminal state from day one; retrofitting it later means rewriting every caller.

The trigger must not live in a retried job. $tries = 1 on the trigger job, or split trigger and poll into separate jobs. A retried trigger after a network timeout deploys twice, and there is no idempotency key to save you.

The token's blast radius is the whole account. It can deploy every project the account owns. Treat it as a production secret with a named owner, a rotation runbook, and a revocation procedure. Do not put it in a CI variable that any contributor's branch can read.

v2.0.0 documents nothing but "PHP8 Support." A major bump with that changelog almost certainly moved transitive floors (Saloon/Guzzle), tightened types, and changed return signatures. Those fail at runtime, not at composer update. Diff the installed source against your call sites, run composer why on the HTTP client, and smoke-test against a real throwaway Envoyer project on a branch before merging.

Staleness is real: last release 2022-05-31, 39 stars, 0 dependents. Laravel major-version support is unverified, not merely uncertain. Pin explicitly, calendar a compatibility review at every Laravel/PHP upgrade, and budget for a fork or vendor patch. The wrapper interface is what keeps that cost to days instead of weeks — treat it as a requirement, not a nicety.

Verify config keys and the facade alias in vendor/, not the README. Three years without a release means the README may describe a shape the code no longer has. php artisan about and a dd(config('envoyer')) in tinker take thirty seconds and save an afternoon.

Handle 4xx and 5xx differently. 401/403 means the token is wrong, expired, or revoked — that's an alert, not a retry. 429 means back off and honour Retry-After. 5xx and timeouts are retryable for reads, never for the trigger. Set explicit connect and request timeouts on the HTTP client; the Saloon default will happily hang a worker.

Rollbacks and approval gates are not in the SDK. If the requirement mentions either, the answer is "Envoyer's API doesn't express that" — scope it out rather than building a facade over something the underlying API won't do. Same for audit trails richer than your own DeploymentRecord table.

Extension points, in preference order: (1) your own wrapper interface, (2) a Saloon request class or connector middleware for logging/retry, (3) forking. Reach for (2) before (3) — request middleware is where you add correlation IDs and response logging, and it's the cheapest way to get observability on an SDK that ships none.

Log every request and its response status. With a single-maintainer package and no vendor support, your logs are your support channel. Correlate by deployment record ID so a "stuck deployment" report resolves in one query.

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.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route