Spec: Forge Provider
Issue: MAR-39 Date: 2026-04-23 Status: In Review
Overview
The Forge provider bridges Shipper's deployment pipeline to the Laravel Forge API. It extends AbstractProvider, registers all 17 operation managers, and uses the laravel/forge-sdk to provision sites, databases, SSL, queues, cron, and more on a Forge-managed server.
Current Implementation
Key Classes / Files
| File | Class | Responsibility |
|---|---|---|
ForgeProvider.php |
ForgeProvider |
Entry point; registers all 17 operations; extends AbstractProvider |
ForgeClientFactory.php |
ForgeClientFactory |
Lazily creates Forge SDK client; exposes server ID + timeout |
ForgeSiteManager.php |
ForgeSiteManager |
Find or create site by domain |
ForgeRepositoryManager.php |
ForgeRepositoryManager |
Install git repository on site |
ForgeDatabaseManager.php |
ForgeDatabaseManager |
Create databases with interpolated names |
ForgeDeploymentExecutor.php |
ForgeDeploymentExecutor |
Trigger deployment with blocking wait + log inspection |
ForgeDeploymentLogReader.php |
ForgeDeploymentLogReader |
Fetch deployment log lines |
ForgeSslManager.php |
ForgeSslManager |
Provision SSL certificate |
ForgeEnvironmentManager.php |
ForgeEnvironmentManager |
Write .env file via MergesEnvironment |
ForgeDeployScriptManager.php |
ForgeDeployScriptManager |
Set deployment script |
ForgeQueueManager.php |
ForgeQueueManager |
Configure queue workers |
ForgeCronManager.php |
ForgeCronManager |
Configure scheduled jobs |
ForgeRedirectManager.php |
ForgeRedirectManager |
HTTP redirect rules |
ForgeDaemonManager.php |
ForgeDaemonManager |
Background daemon processes |
ForgeNetworkRuleManager.php |
ForgeNetworkRuleManager |
Firewall rules |
ForgePhpVersionManager.php |
ForgePhpVersionManager |
PHP version selection |
ForgeNginxConfigManager.php |
ForgeNginxConfigManager |
Custom Nginx configuration |
ForgeAliasManager.php |
ForgeAliasManager |
Domain aliases |
ForgeServerSiteList.php |
ForgeServerSiteList |
List all sites on server |
ForgeProvider: Additional Public API
final class ForgeProvider extends AbstractProvider {
use InterpolatesNames;
private ForgeClientFactory $factory;
public function getName(): string // 'forge'
public function getServerId(): string
protected function registerOperations(): void // registers all 17 managers
protected function handleException(\Exception $e): string // maps Forge SDK exceptions
protected function planDatabases(array<DatabaseConfig> $databases, string $projectName, string $profileName): array // InterpolatesNames
}
Validation Rules (in addition to AbstractProvider base validation)
| Field | Rule |
|---|---|
config['api_key'] |
Required, non-empty string |
config['server_id'] |
Required, numeric-only (digits) |
profile['domain'] |
Required, non-empty |
project['repository'] |
Must have provider (github/gitlab/bitbucket/custom) and name (username/repo) |
Exception Mapping
| Exception class | Message prefix |
|---|---|
\Laravel\Forge\Exceptions\NotFoundException |
"Resource not found: Server ID {id} may not exist or you don't have access." |
\Laravel\Forge\Exceptions\ValidationException |
"Validation error: {message}" |
Other Exception |
"Deployment error: {message} (Type: {class})" (via parent) |
SiteManager apply() Behavior
- Fetch all sites on server via
$forge->sites($serverId)— returnsForge\Resources\Site[] - Match by
site->name === domain - If found: return
SiteResult::found(site->id)— does NOT reinstall repository - If not found: create via
$forge->createSite($serverId, [...])withdomain,project_type: 'php',directory - If
idis 0 or null in response: returnSiteResult::fail('Invalid response')
DeploymentExecutor apply() Behavior (Forge)
- Call
$forge->deploySite($serverId, $siteId, wait: true)— blocking, returns when deployment completes - Fetch deployment logs via
ForgeDeploymentLogReader - Scan log lines for keywords (
deployment failed,fatal error,critical error, etc.) - If any keyword found: return
OperationResult::fail('Deployment failed on Forge server (detected in logs)') - Else: return
OperationResult::ok() - On exception: return
OperationResult::fail("Deployment failed: {message}")
Functional Requirements
FR-001 — Provision site by domain As the CLI, I want to find or create a Forge site matching the profile's domain so that subsequent operations have a valid site ID.
- Acceptance:
SiteResult::found(id)when domain already exists;SiteResult::created(id)on new site;SiteResult::fail(...)on API error.
FR-002 — Trigger blocking deployment As the CLI, I want to deploy the site via Forge API with a blocking wait so that deployment status is known before the command returns.
- Acceptance: Returns
OperationResult::ok()on success;OperationResult::fail(msg)if error keywords detected in logs.
FR-003 — Read deployment logs As the CLI, I want to fetch deployment log lines from Forge so that I can surface failures and debug deployment issues.
- Acceptance: Returns
array<int, string>of log lines; empty array if unavailable.
FR-004 — Handle Forge API exceptions gracefully As the CLI, I want Forge-specific exceptions to produce user-friendly messages so that debugging is easier.
- Acceptance: NotFoundException maps to server ID error; ValidationException maps to validation error; all others use generic deployment error format.
Edge Cases
- Site create response missing id: Returns
SiteResult::fail('Failed to create site: Invalid response from Forge API')— does not throw. - Domain already exists: Returns
SiteResult::found()—AbstractProviderskips repository install becauseisNew === false. - Blocking deploy throws: Caught and returned as
OperationResult::fail("Deployment failed: {message}"). - Log scan finds error keyword: Immediately returns
OperationResult::failwith static message — no log excerpt returned (unlike Ploi which appends recent logs). - Delete site not found: Returns
OperationResult::ok()— idempotent destroy. - Server ID type coercion: Accepts both
stringandintforserver_idconfig; casts tostringbeforectype_digitvalidation.
Acceptance Criteria
- ForgeProvider registers all 17 operation managers
-
getName()returns'forge' -
getServerId()returns the configured numeric server ID - Validation fails cleanly when
api_keyorserver_idis missing/wrong - Site find/create works for both existing and new domains
- Deployment blocks until completion and inspects logs for error keywords
- SDK exceptions are caught and mapped to user-friendly messages
-
plan()includes all 15+ sub-actions with human-readable labels
Open Questions / Potential Concerns
- Forge's blocking
deploySite(..., true)vs Ploi's polling approach: Forge waits server-side; Ploi requires client-side polling. Forge's approach is simpler but provides no progress feedback during the wait. Is this intentional? - No deployment timeout configuration in Forge — the blocking call could hang indefinitely if Forge's server-side wait has no timeout.
ForgeClientFactorydoes exposegetDeploymentTimeout()but it is not used byForgeDeploymentExecutor. Should it be? - Log inspection on Forge does not append log content to the error message (unlike Ploi which includes up to 10 log lines). This makes Forge errors harder to debug. Intentional?
- Repository re-install on found site: The current design correctly skips this, but there's no option to force a re-clone. Is this intentional?