Spec: CLI Commands
Issue: MAR-19 Date: 2026-04-23 Status: In Review
Overview
Shipper exposes a declarative deployment workflow through a set of Laravel Zero console commands. These commands form the user-facing interface for the CLI tool, handling configuration validation, deployment planning, deployment execution, site destruction, and orphaned preview site cleanup.
Current Implementation
Key Classes / Files
| File | Class | Responsibility |
|---|---|---|
app/Commands/ValidateCommand.php |
ValidateCommand |
Validates shipper.yml configuration for all projects and profiles |
app/Commands/PlanCommand.php |
PlanCommand |
Produces a human-readable deployment plan (dry-run) |
app/Commands/ApplyCommand.php |
ApplyCommand |
Validates, shows plan, confirms, then executes a deployment |
app/Commands/DeployCommand.php |
DeployCommand |
Stub command (no-op, placeholder) |
app/Commands/DestroyCommand.php |
DestroyCommand |
Plans and executes site destruction with confirmation |
app/Commands/CleanupOrphanedCommand.php |
CleanupOrphanedCommand |
Removes preview sites whose PRs are closed |
app/Commands/Concerns/FormatsDeploymentPlan.php |
FormatsDeploymentPlan |
Trait: safely renders plan values in output |
app/Kernel.php |
Kernel |
Registers all commands via commands() method |
Functional Requirements
FR-001 — Configuration Validation
As a developer, I want to validate my shipper.yml so that I can catch misconfiguration before running a deployment.
- Acceptance: Running
shipper validatereturns exit code 0 for a valid config and exit code 1 with descriptive error output for invalid configs.
FR-002 — Deployment Planning As a developer, I want to preview a deployment plan so that I understand what actions will be taken before committing to them.
- Acceptance: Running
shipper plan <project>outputs provider, project, profile, branch, path, server, domain, and a list of planned actions. Exit code 0 on success.
FR-003 — Deployment Execution As a developer, I want to deploy a project so that my application is provisioned on the target infrastructure.
- Acceptance: Running
shipper apply <project>validates config, displays plan, prompts for confirmation (unless--forceis passed), executes deployment, and returns logs on success or descriptive error on failure. Exit code 0 on success.
FR-004 — Site Destruction As a developer, I want to destroy a deployed site so that I can cleanly remove infrastructure.
- Acceptance: Running
shipper destroy <project>validates config, shows destruction plan, prompts for confirmation (unless--force), executes, and returns success/failure. Exit code 0 on success.
FR-005 — Orphaned Preview Site Cleanup As a developer, I want to automatically clean up preview sites whose PRs are closed so that I don't leave dangling infrastructure.
- Acceptance: Running
shipper cleanup-orphanedrequiresGITHUB_TOKENandGITHUB_REPOSITORYenv vars, performs a dry-run to list orphaned sites, and with--forceor confirmed prompt deletes them.--dry-runflag prevents actual deletion.
Command Interface
validate
shipper validate [--config=<path>]
| Argument/Option | Type | Default | Description |
|---|---|---|---|
--config |
option | shipper.yml |
Path to configuration file |
Exit codes: 0 (valid), 1 (invalid or file not found) Output: Project-by-project and profile-by-profile error listing
plan
shipper plan <project> [--profile=<name>] [--config=<path>]
| Argument/Option | Type | Default | Description |
|---|---|---|---|
<project> |
argument | — | Project name |
--profile |
option | production |
Profile to use |
--config |
option | shipper.yml |
Path to configuration file |
Exit codes: 0 (success), 1 (project or profile not found, or validation error) Output: Formatted deployment plan including provider, project, profile, branch, path, server, domain, repository, web directory, root, and action list.
apply
shipper apply <project> [--profile=<name>] [--config=<path>] [--force]
| Argument/Option | Type | Default | Description |
|---|---|---|---|
<project> |
argument | — | Project name |
--profile |
option | production |
Profile to use |
--config |
option | shipper.yml |
Path to configuration file |
--force |
flag | false | Skip confirmation prompt |
Exit codes: 0 (success), 1 (failure) Behavior:
- Validate configuration
- Display deployment plan
- If not
--force, prompt for confirmation - Execute deployment
- Output deployment logs on success or error details on failure
destroy
shipper destroy <project> [--profile=<name>] [--config=<path>] [--force]
| Argument/Option | Type | Default | Description |
|---|---|---|---|
<project> |
argument | — | Project name |
--profile |
option | production |
Profile to use |
--config |
option | shipper.yml |
Path to configuration file |
--force |
flag | false | Skip confirmation prompt |
Exit codes: 0 (success), 1 (failure)
cleanup-orphaned
shipper cleanup-orphaned [--config=<path>] [--dry-run] [--force]
| Argument/Option | Type | Default | Description |
|---|---|---|---|
--config |
option | shipper.yml |
Path to configuration file |
--dry-run |
flag | false | List orphaned sites without deleting |
--force |
flag | false | Skip confirmation prompt |
Environment variables required: GITHUB_TOKEN, GITHUB_REPOSITORY (format: owner/repo)
Exit codes: 0 (success or no orphans found), 1 (failure)
deploy
shipper deploy
Status: Stub / no-op. Exists as placeholder. Exit code 0.
Configuration Interface
# shipper.yml
providers:
ploi:
api_key: "${PLOI_API_KEY}"
projects:
api:
provider: ploi
profiles:
production:
server_id: "12345"
domain: api.example.com
branch: main
path: ./examples/api
repository: github:test/repo
web_directory: /public
project_root: /
deploy_script: deploy.sh
ssl: true
Data Contracts
ValidateCommand::handle(): int
Returns self::SUCCESS (0) or self::FAILURE (1).
PlanCommand::handle(): int
Returns self::SUCCESS (0) or self::FAILURE (1).
ApplyCommand::handle(): int
public function handle(): int
// Uses ApplyDeploymentFlow::handle() for validation + plan
// Uses ApplyDeploymentFlow::execute() for actual deployment
DestroyCommand::handle(): int
public function handle(): int
// Uses DestroyDeploymentFlow::handle() for validation + plan
// Uses DestroyDeploymentFlow::execute() for actual destruction
CleanupOrphanedCommand::handle(): int
public function handle(): int
// Uses CleanupOrphanedSitesFlow::handle()
// GITHUB_TOKEN and GITHUB_REPOSITORY env vars required
FormatsDeploymentPlan trait
private function getPlanValue(array $plan, string $key, string $default = 'unknown'): string
Edge Cases
- Nonexistent project in
plan/apply/destroy: Returns exit code 1 with messageProject not found: <name> - Nonexistent profile: Returns exit code 1 with message
Profile not found: <name> - Missing config file:
ValidateCommandreturns exit 1. Other commands throwRuntimeException. - Missing
GITHUB_TOKENorGITHUB_REPOSITORYenv vars:CleanupOrphanedCommandreturns exit 1 with descriptive error. - Confirmation declined:
applyanddestroyoutput warning and return exit 0 (user cancelled is not an error). - Provider validation errors: Displayed as
Configuration validation failed:with itemized error list, exit 1. --forcebypasses confirmation but still requires a valid plan to proceed.
Acceptance Criteria
-
shipper validatereturns 0 for valid config and 1 with error output for invalid config -
shipper plan <project>outputs formatted deployment plan and returns 0 on success -
shipper plan nonexistentreturns exit 1 withProject not found: nonexistent -
shipper plan <project> --profile=nonexistentreturns exit 1 withProfile not found: nonexistent -
shipper apply <project> --forcevalidates, displays plan, and executes without prompting -
shipper applyshowsDeployment completed successfully!and returns exit 0 on success -
shipper applyshowsDeployment failed!and returns exit 1 on provider failure -
shipper destroymirrors the apply flow for destruction -
shipper cleanup-orphaned --dry-runlists orphaned sites without deleting them -
shipper cleanup-orphanedrequiresGITHUB_TOKENandGITHUB_REPOSITORYenv vars -
shipper deployis a no-op stub that exits 0
Open Questions / Potential Concerns
- DeployCommand is a stub: It outputs "Starting deployment..." and "Deployment completed successfully!" with no actual deployment logic. This appears to be a placeholder awaiting implementation.
- No atomic rollback on deployment failure — if apply partially completes and then fails, there is no automatic cleanup of what was created.
- CleanupOrphanedCommand depends on a
CleanupOrphanedSitesFlowthat was referenced but the full source file was not provided in this review scope. Verify that flow handles GitHub API pagination for large PR counts. - Confirmation prompt behavior is consistent across
applyanddestroy, but there is no--yesshort alias; only--force. This is fine but worth confirming against user expectations.