Skip to content

feat: provision DigitalOcean droplets - #59

Merged
loadinglucian merged 4 commits into
mainfrom
feat/provision-digitalocean-droplet
Oct 30, 2025
Merged

feat: provision DigitalOcean droplets#59
loadinglucian merged 4 commits into
mainfrom
feat/provision-digitalocean-droplet

Conversation

@loadinglucian

@loadinglucian loadinglucian commented Oct 30, 2025

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added DigitalOcean droplet provisioning and integration into inventory
    • ServerDTO now records provider and droplet ID (shown in server details)
  • Improvements

    • Added --force option to skip name-typing confirmation on delete
    • Stronger prompts, clearer private-key path handling, and improved validation for provisioning inputs
    • Server deletion now attempts cloud resource cleanup before removing inventory
    • Improved server list and detail display with a clearer "List Servers" header
  • Bug Fixes

    • Better error handling and rollback during provisioning and save failures

@coderabbitai

coderabbitai Bot commented Oct 30, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds DigitalOcean provisioning and related validation/helpers, extends ServerDTO with provider and dropletId, updates repository serialization, and refactors server console commands (add/list/delete) and helper traits for cloud-aware flows and improved key handling.

Changes

Cohort / File(s) Summary
DigitalOcean provisioning command
app/Console/Server/ServerProvisionDigitalOceanCommand.php
New Symfony console command to provision DigitalOcean droplets: API init, gather/validate params (name, region, size, image, SSH keys, private key path, VPC, backups, ipv6, monitoring), create droplet, wait for activation, retrieve IP, add ServerDTO with provider/dropletId, and rollback on failures.
Server commands (add/list/delete) updates
app/Console/Server/ServerAddCommand.php, app/Console/Server/ServerDeleteCommand.php, app/Console/Server/ServerListCommand.php
ServerAddCommand: adds KeyHelpersTrait/KeyValidationTrait, resolves private-key-path via helper, adds error branches. ServerDeleteCommand: adds DigitalOceanCommandTrait, --force option, DigitalOcean droplet handling, forced-name typing safety, cloud destroy with spinner and conditional inventory removal. ServerListCommand: UI heading, uses ensureServersAvailable() and displayServerDeets().
DTO & repository changes
app/DTOs/ServerDTO.php, app/Repositories/ServerRepository.php
ServerDTO gains public ?string $provider = null and public ?int $dropletId = null. ServerRepository updated to dehydrate/hydrate those fields (normalize types).
DigitalOcean validation & traits
app/Traits/DigitalOceanValidationTrait.php, app/Traits/DigitalOceanCommandTrait.php
New DigitalOceanValidationTrait with validators for region, size, image, VPC UUID, SSH key(s). DigitalOceanCommandTrait docblock reordered and adds @property DigitalOceanService $digitalOcean annotation; no logic changes.
Server helpers & validation trait changes
app/Traits/ServerHelpersTrait.php, app/Traits/ServerValidationTrait.php
ServerHelpersTrait: new ensureServersAvailable(), isDigitalOceanServer(), updated selectServer() return type (ServerDTO
Key helpers & site helpers tweaks
app/Traits/KeyHelpersTrait.php, app/Traits/SiteHelpersTrait.php
KeyHelpersTrait docblock reorder only. SiteHelpersTrait updates array_map callables to callable-unpack style (e.g., trim(...), strval(...)) — callable form changes without signature changes.
App wiring
app/SymfonyApp.php
Registers ServerProvisionDigitalOceanCommand::class in the commands list.
Site repository small change
app/Repositories/SiteRepository.php
Adjusted array_filter callback usage in hydrateSiteDTO from 'is_string' to is_string(...).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Cmd as ServerProvisionDOCmd
    participant DO as DigitalOceanService
    participant Repo as ServerRepository
    participant IO as IOService

    User->>Cmd: run command
    Cmd->>DO: init API (credentials)
    DO-->>Cmd: account data (keys, regions, sizes, images)
    Cmd->>User: prompt for name/region/size/image/ssh-key/private-key-path/options
    User-->>Cmd: provide inputs
    Cmd->>Cmd: validate inputs (DigitalOceanValidationTrait)
    Cmd->>IO: resolve private key path (KeyHelpersTrait)
    alt resolution failure
        IO-->>Cmd: null -> Cmd returns FAILURE
    end
    Cmd->>DO: create droplet (with ssh keys)
    DO-->>Cmd: droplet id
    Cmd->>DO: poll until active
    alt droplet fails
        Cmd->>DO: rollback destroy droplet
        Cmd->>User: error + exit FAILURE
    else droplet ready
        DO-->>Cmd: droplet IP
        Cmd->>Repo: persist ServerDTO(provider='digitalocean', dropletId)
        Repo-->>Cmd: success
        Cmd->>User: displayServerDeets + usage hint
    end
Loading
sequenceDiagram
    participant User
    participant Cmd as ServerDeleteCmd
    participant IO as IOService
    participant DO as DigitalOceanService
    participant Repo as ServerRepository

    User->>Cmd: run delete (maybe --force)
    Cmd->>Cmd: ensureServersAvailable()
    Cmd->>User: selectServer prompt
    User-->>Cmd: choose server
    Cmd->>IO: displayServerDeets (includes droplet info if DO)
    alt server is DigitalOcean (isDigitalOceanServer)
        Cmd->>DO: init API
        alt auth failure
            DO-->>Cmd: error -> Cmd returns FAILURE
        end
        alt droplet exists
            Cmd->>User: show droplet warning & require typed name (unless --force)
            User-->>Cmd: confirm (typed match)
            Cmd->>DO: destroy droplet (spinner)
            alt destroy fails
                Cmd->>User: offer inventory-only removal
            end
        end
    end
    Cmd->>Repo: remove server from inventory
    Repo-->>Cmd: success -> Cmd displays success hint (force flag in redir data if used)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Focus review on:
    • ServerProvisionDigitalOceanCommand: API interactions, rollback paths, input validation callbacks.
    • ServerDeleteCommand: force/typed-name safety logic and DigitalOcean destroy/exception handling.
    • ServerHelpersTrait: changed return types and new methods that affect all callers.
    • ServerDTO/ServerRepository: ensure hydration/dehydration correctness and backward compatibility.

Possibly related PRs

  • bigpixelrocket/deployer-php#25 — Related: foundational ServerDTO/ServerRepository work; this PR extends ServerDTO with provider/dropletId and updates repository serialization.
  • bigpixelrocket/deployer-php#53 — Related: DigitalOcean key/command functionality and shared traits (DigitalOceanCommandTrait, KeyHelpersTrait) touched in this change.
  • bigpixelrocket/deployer-php#41 — Related: overlaps on console command and trait refactors affecting ServerAdd/ServerDelete/ServerList and validation flows.

Poem

🐇 I hopped into code at dawn’s first light,

keys and droplets brought to sight,
Traits tidy up the routing trail,
Servers spin up on a DigitalOcean gale,
I nibble bugs and dance with delight.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "feat: provision DigitalOcean droplets" is fully aligned with the primary purpose of the changeset. The main feature introduced is a new ServerProvisionDigitalOceanCommand that enables users to provision DigitalOcean droplets and integrate them into the inventory system. The title uses the conventional commit format (feat:) and clearly identifies both the action (provision) and the resource (DigitalOcean droplets). The title is concise, specific, and avoids vague terminology, making it immediately clear to anyone reviewing commit history what the PR accomplishes.
Docstring Coverage ✅ Passed Docstring coverage is 95.83% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/provision-digitalocean-droplet

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d2e193 and 01ffd4b.

📒 Files selected for processing (12)
  • app/Console/Server/ServerAddCommand.php (4 hunks)
  • app/Console/Server/ServerDeleteCommand.php (5 hunks)
  • app/Console/Server/ServerListCommand.php (2 hunks)
  • app/Console/Server/ServerProvisionDigitalOceanCommand.php (1 hunks)
  • app/DTOs/ServerDTO.php (1 hunks)
  • app/Repositories/ServerRepository.php (2 hunks)
  • app/SymfonyApp.php (2 hunks)
  • app/Traits/DigitalOceanCommandTrait.php (1 hunks)
  • app/Traits/DigitalOceanValidationTrait.php (1 hunks)
  • app/Traits/KeyHelpersTrait.php (1 hunks)
  • app/Traits/ServerHelpersTrait.php (2 hunks)
  • app/Traits/ServerValidationTrait.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/00-main.mdc)

**/*.php: Eliminate single-use methods: inline if a method is called only once
Cache computed values: initialize expensive calculations in the constructor
Avoid method call overhead: prefer direct property access when appropriate

**/*.php: Adhere to PSR-12, enable strict_types, and leverage PHP 8.x features (union types, match, attributes, readonly)
Use explicit return types, including generic-like annotations where applicable (e.g., Collection<int, User>)
Prefer Symfony components (e.g., Filesystem, Process) over native PHP functions for testability
Always import classes via use statements; only use root FQDNs for core exceptions (e.g., \InvalidArgumentException, \RuntimeException). Do not use inline FQDNs for non-root namespaces
Create objects via $container->build(ClassName::class) everywhere except DTOs, value objects, and pure data structures
Use minimalist DocBlocks documenting description, parameters, and return types for classes and functions
Follow the specified comment structure with section headers/subheaders and spacing; remove obsolete comments with removed code

Files:

  • app/DTOs/ServerDTO.php
  • app/Console/Server/ServerAddCommand.php
  • app/Traits/KeyHelpersTrait.php
  • app/Traits/ServerHelpersTrait.php
  • app/Repositories/ServerRepository.php
  • app/Traits/ServerValidationTrait.php
  • app/Console/Server/ServerProvisionDigitalOceanCommand.php
  • app/Traits/DigitalOceanValidationTrait.php
  • app/Traits/DigitalOceanCommandTrait.php
  • app/SymfonyApp.php
  • app/Console/Server/ServerDeleteCommand.php
  • app/Console/Server/ServerListCommand.php
**/*Command.php

📄 CodeRabbit inference engine (.cursor/rules/01-architecture.mdc)

**/*Command.php: Commands handle user interaction (I/O), orchestration, styling, error formatting, and prompts using SymfonyStyle
Commands must not contain business logic; delegate to Services
Commands must not invoke other commands (no proxy commands)

Files:

  • app/Console/Server/ServerAddCommand.php
  • app/Console/Server/ServerProvisionDigitalOceanCommand.php
  • app/Console/Server/ServerDeleteCommand.php
  • app/Console/Server/ServerListCommand.php
**/*{Command,Service}.php

📄 CodeRabbit inference engine (.cursor/rules/01-architecture.mdc)

All dependencies should be expressed in constructor signatures; avoid circular dependencies

Files:

  • app/Console/Server/ServerAddCommand.php
  • app/Console/Server/ServerProvisionDigitalOceanCommand.php
  • app/Console/Server/ServerDeleteCommand.php
  • app/Console/Server/ServerListCommand.php
**/*ValidationTrait.php

📄 CodeRabbit inference engine (.cursor/rules/03-commands.mdc)

**/*ValidationTrait.php: Validation methods for prompts/options must accept mixed and return ?string error or null (do not throw exceptions).
Naming: use validateInput() for prompt/option validators returning ?string; use validate() to throw exceptions for heavy I/O validations.

Files:

  • app/Traits/ServerValidationTrait.php
  • app/Traits/DigitalOceanValidationTrait.php
**/SymfonyApp.php

📄 CodeRabbit inference engine (.cursor/rules/01-architecture.mdc)

Register console commands in SymfonyApp.php

Files:

  • app/SymfonyApp.php
🧠 Learnings (13)
📚 Learning: 2025-10-24T20:01:06.209Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/03-commands.mdc:0-0
Timestamp: 2025-10-24T20:01:06.209Z
Learning: Applies to src/Command/**/*Command.php : Option naming: --server/--site select existing resources; --name defines a new resource property; follow the provided table for common options.

Applied to files:

  • app/Console/Server/ServerAddCommand.php
  • app/Traits/ServerHelpersTrait.php
  • app/SymfonyApp.php
  • app/Console/Server/ServerDeleteCommand.php
  • app/Console/Server/ServerListCommand.php
📚 Learning: 2025-10-24T19:59:22.863Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/01-architecture.mdc:0-0
Timestamp: 2025-10-24T19:59:22.863Z
Learning: Applies to **/*Command.php : Commands handle user interaction (I/O), orchestration, styling, error formatting, and prompts using SymfonyStyle

Applied to files:

  • app/Console/Server/ServerAddCommand.php
  • app/SymfonyApp.php
📚 Learning: 2025-10-24T20:01:06.209Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/03-commands.mdc:0-0
Timestamp: 2025-10-24T20:01:06.209Z
Learning: Applies to **/BaseCommand.php : BaseCommand handles shared initialization/configuration/orchestration only; do not implement individual I/O operations here.

Applied to files:

  • app/Console/Server/ServerAddCommand.php
📚 Learning: 2025-10-24T19:59:22.863Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/01-architecture.mdc:0-0
Timestamp: 2025-10-24T19:59:22.863Z
Learning: Applies to **/SymfonyApp.php : Register console commands in SymfonyApp.php

Applied to files:

  • app/Console/Server/ServerAddCommand.php
  • app/SymfonyApp.php
📚 Learning: 2025-10-24T20:01:06.209Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/03-commands.mdc:0-0
Timestamp: 2025-10-24T20:01:06.209Z
Learning: Applies to **/ConsoleOutputTrait.php : Keep output/formatting methods in ConsoleOutputTrait and implement them using $this->io (SymfonyStyle).

Applied to files:

  • app/Traits/KeyHelpersTrait.php
📚 Learning: 2025-10-24T20:01:06.209Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/03-commands.mdc:0-0
Timestamp: 2025-10-24T20:01:06.209Z
Learning: Applies to **/BaseCommand.php : If an IO helper is missing, add it to BaseCommand with modern styling instead of using Symfony IO directly.

Applied to files:

  • app/Traits/KeyHelpersTrait.php
📚 Learning: 2025-10-24T20:01:06.209Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/03-commands.mdc:0-0
Timestamp: 2025-10-24T20:01:06.209Z
Learning: Applies to **/*ValidationTrait.php : Naming: use validate*Input() for prompt/option validators returning ?string; use validate*() to throw exceptions for heavy I/O validations.

Applied to files:

  • app/Traits/DigitalOceanValidationTrait.php
📚 Learning: 2025-10-24T20:01:06.210Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/03-commands.mdc:0-0
Timestamp: 2025-10-24T20:01:06.210Z
Learning: Test validation traits via small wrapper methods that expose validate*Input() and assert null or error strings accordingly.

Applied to files:

  • app/Traits/DigitalOceanValidationTrait.php
📚 Learning: 2025-10-24T19:59:22.863Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/01-architecture.mdc:0-0
Timestamp: 2025-10-24T19:59:22.863Z
Learning: Applies to **/*Command.php : Commands must not contain business logic; delegate to Services

Applied to files:

  • app/SymfonyApp.php
📚 Learning: 2025-10-24T20:01:06.209Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/03-commands.mdc:0-0
Timestamp: 2025-10-24T20:01:06.209Z
Learning: Applies to src/Command/**/*Command.php : Use only OPTIONS (no positional arguments) to enable getOptionOrPrompt across the board.

Applied to files:

  • app/Console/Server/ServerDeleteCommand.php
📚 Learning: 2025-10-24T20:01:06.209Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/03-commands.mdc:0-0
Timestamp: 2025-10-24T20:01:06.209Z
Learning: Applies to src/Command/**/*Command.php : Pair all options with getOptionOrPrompt to support both non-interactive and interactive usage.

Applied to files:

  • app/Console/Server/ServerDeleteCommand.php
📚 Learning: 2025-10-24T20:01:06.210Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/03-commands.mdc:0-0
Timestamp: 2025-10-24T20:01:06.210Z
Learning: Applies to src/Command/**/*Command.php : Boolean flags must use VALUE_NONE; data inputs must use VALUE_REQUIRED; only --yes has a short flag (-y).

Applied to files:

  • app/Console/Server/ServerDeleteCommand.php
📚 Learning: 2025-10-24T20:01:06.209Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/03-commands.mdc:0-0
Timestamp: 2025-10-24T20:01:06.209Z
Learning: Applies to src/Command/**/*Command.php : Support both interactive prompts and CLI options by using getOptionOrPrompt for each option (define options in configure; resolve via getOptionOrPrompt in execute).

Applied to files:

  • app/Console/Server/ServerDeleteCommand.php
🧬 Code graph analysis (7)
app/Console/Server/ServerAddCommand.php (2)
app/Traits/KeyHelpersTrait.php (1)
  • resolvePrivateKeyPath (46-52)
app/Services/IOService.php (1)
  • error (498-501)
app/Traits/ServerHelpersTrait.php (3)
app/Services/IOService.php (3)
  • IOService (30-583)
  • warning (490-493)
  • writeln (463-469)
app/Repositories/ServerRepository.php (1)
  • all (104-114)
app/DTOs/ServerDTO.php (1)
  • ServerDTO (7-19)
app/Repositories/ServerRepository.php (1)
app/DTOs/ServerDTO.php (1)
  • ServerDTO (7-19)
app/Traits/ServerValidationTrait.php (1)
app/Repositories/ServerRepository.php (1)
  • ServerRepository (15-195)
app/Console/Server/ServerProvisionDigitalOceanCommand.php (11)
app/Contracts/BaseCommand.php (1)
  • BaseCommand (29-158)
app/DTOs/ServerDTO.php (1)
  • ServerDTO (7-19)
app/Services/IOService.php (13)
  • hr (517-523)
  • h1 (506-512)
  • promptSpin (439-452)
  • warning (490-493)
  • writeln (463-469)
  • getValidatedOptionOrPrompt (159-182)
  • promptText (200-218)
  • promptSelect (304-322)
  • getOptionOrPrompt (84-132)
  • error (498-501)
  • promptConfirm (260-276)
  • success (482-485)
  • showCommandHint (530-566)
app/Traits/DigitalOceanCommandTrait.php (1)
  • initializeDigitalOceanAPI (33-66)
app/Services/DigitalOcean/DigitalOceanAccountService.php (5)
  • getUserSshKeys (149-167)
  • getAvailableRegions (27-47)
  • getAvailableSizes (54-79)
  • getAvailableImages (86-116)
  • getUserVpcs (123-142)
app/Traits/ServerValidationTrait.php (1)
  • validateNameInput (23-41)
app/Traits/DigitalOceanValidationTrait.php (5)
  • validateRegionInput (19-35)
  • validateSizeInput (44-60)
  • validateImageInput (69-85)
  • validateSshKeyInput (119-134)
  • validateVpcUuidInput (92-110)
app/Traits/KeyHelpersTrait.php (1)
  • resolvePrivateKeyPath (46-52)
app/Services/DigitalOcean/DigitalOceanDropletService.php (3)
  • createDroplet (33-75)
  • waitForDropletReady (107-129)
  • getDropletIp (136-157)
app/Repositories/ServerRepository.php (1)
  • create (48-65)
app/Traits/ServerHelpersTrait.php (1)
  • displayServerDeets (95-113)
app/Console/Server/ServerDeleteCommand.php (4)
app/Traits/ServerHelpersTrait.php (3)
  • selectServer (53-88)
  • displayServerDeets (95-113)
  • isDigitalOceanServer (118-121)
app/Services/IOService.php (9)
  • hr (517-523)
  • error (498-501)
  • writeln (463-469)
  • warning (490-493)
  • promptText (200-218)
  • getOptionOrPrompt (84-132)
  • promptConfirm (260-276)
  • promptSpin (439-452)
  • success (482-485)
app/Traits/DigitalOceanCommandTrait.php (1)
  • initializeDigitalOceanAPI (33-66)
app/Services/DigitalOcean/DigitalOceanDropletService.php (1)
  • destroyDroplet (168-185)
app/Console/Server/ServerListCommand.php (3)
app/Services/IOService.php (1)
  • h1 (506-512)
app/Traits/ServerHelpersTrait.php (2)
  • ensureServersAvailable (27-46)
  • displayServerDeets (95-113)
app/Repositories/SiteRepository.php (1)
  • findByServer (111-124)
🪛 GitHub Actions: Rector
app/Traits/DigitalOceanValidationTrait.php

[error] 150-150: Rector dry-run would modify code: use 'trim' as a function (FunctionFirstClassCallableRector) in line 150. Suggested change present in diff.

Comment thread app/Console/Server/ServerProvisionDigitalOceanCommand.php
Comment thread app/Traits/DigitalOceanValidationTrait.php
Automatically destroy droplets when provisioning fails after creation.
Handles failures during IP retrieval and inventory addition by
cleaning up the created droplet to prevent orphaned resources.
Replace string callables with first-class callable syntax for
PHP 8.1+ compatibility and improved type safety.
Add blank lines after error messages for better visual
separation and remove decorative comment separators.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
app/Console/Server/ServerAddCommand.php (2)

131-136: Enhance error message with actionable guidance.

The error message doesn't inform users which paths were checked or how to resolve the issue. Consider providing more context about the attempted paths and next steps.

Apply this diff to provide a more helpful error message:

     if ($privateKeyPath === null) {
-        $this->io->error('SSH private key not found.');
+        $this->io->error('SSH private key not found. Checked: ' . ($privateKeyPathRaw ?: '~/.ssh/id_ed25519, ~/.ssh/id_rsa'));
         $this->io->writeln('');

Alternatively, for a more detailed message:

     if ($privateKeyPath === null) {
-        $this->io->error('SSH private key not found.');
+        if ($privateKeyPathRaw !== '') {
+            $this->io->error("SSH private key not found at: {$privateKeyPathRaw}");
+        } else {
+            $this->io->error('SSH private key not found. Please ensure ~/.ssh/id_ed25519 or ~/.ssh/id_rsa exists, or specify a custom path.');
+        }
         $this->io->writeln('');

117-126: Consider simplifying the prompt label.

The label on line 121 is quite long. Consider moving the default path information to the hint for better readability.

Apply this diff to improve prompt clarity:

     $privateKeyPathRaw = $this->io->getOptionOrPrompt(
         'private-key-path',
         fn (): string => $this->io->promptText(
-                label: 'Path to SSH private key (leave empty for default ~/.ssh/id_ed25519 or ~/.ssh/id_rsa):',
+                label: 'Path to SSH private key:',
             default: '',
             required: false,
-            hint: 'Used to connect to the server'
+            hint: 'Leave empty to use ~/.ssh/id_ed25519 or ~/.ssh/id_rsa'
         )
     );
app/Console/Server/ServerProvisionDigitalOceanCommand.php (3)

73-93: Wrap account data retrieval in try-catch for better error handling.

If any of the account service methods (getUserSshKeys, getAvailableRegions, etc.) throw a \RuntimeException, it will bubble up as an uncaught exception, resulting in poor UX. Consider wrapping the retrieval and the empty keys check in a try-catch block to provide formatted error messages, similar to how API initialization is handled at lines 66-68.

Apply this diff to add error handling:

+    try {
         $accountData = $this->io->promptSpin(
             fn () => [
                 'keys' => $this->digitalOcean->account->getUserSshKeys(),
                 'regions' => $this->digitalOcean->account->getAvailableRegions(),
                 'sizes' => $this->digitalOcean->account->getAvailableSizes(),
                 'images' => $this->digitalOcean->account->getAvailableImages(),
             ],
             'Retrieving account information...'
         );
 
         if (count($accountData['keys']) === 0) {
             $this->io->warning('No SSH keys found in your DigitalOcean account');
             $this->io->writeln([
                 '',
                 'You must add at least one SSH key before provisioning a server.',
                 'Run <fg=cyan>key:add:digitalocean</> to add an SSH key.',
                 '',
             ]);
 
             return Command::FAILURE;
         }
+    } catch (\RuntimeException $e) {
+        $this->io->error('Failed to retrieve account information: ' . $e->getMessage());
+        $this->io->writeln('');
+
+        return Command::FAILURE;
+    }

309-309: Optional: Consider if blank line is necessary.

This blank line adds spacing before the "Waiting for droplet..." message. While it improves readability, it's optional and could be removed if you prefer more compact output.


409-411: Provide more context in rollback error message.

The catch block outputs just the error message without context. Users would benefit from knowing that this error occurred specifically during cleanup/rollback of the droplet.

Apply this diff to add context:

         } catch (\Throwable $cleanupError) {
-            $this->io->warning($cleanupError->getMessage());
+            $this->io->warning('Could not destroy droplet during rollback: ' . $cleanupError->getMessage());
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 01ffd4b and 5544e34.

📒 Files selected for processing (5)
  • app/Console/Server/ServerAddCommand.php (4 hunks)
  • app/Console/Server/ServerProvisionDigitalOceanCommand.php (1 hunks)
  • app/Repositories/SiteRepository.php (1 hunks)
  • app/Traits/DigitalOceanValidationTrait.php (1 hunks)
  • app/Traits/SiteHelpersTrait.php (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/Traits/DigitalOceanValidationTrait.php
🧰 Additional context used
📓 Path-based instructions (3)
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/00-main.mdc)

**/*.php: Eliminate single-use methods: inline if a method is called only once
Cache computed values: initialize expensive calculations in the constructor
Avoid method call overhead: prefer direct property access when appropriate

**/*.php: Adhere to PSR-12, enable strict_types, and leverage PHP 8.x features (union types, match, attributes, readonly)
Use explicit return types, including generic-like annotations where applicable (e.g., Collection<int, User>)
Prefer Symfony components (e.g., Filesystem, Process) over native PHP functions for testability
Always import classes via use statements; only use root FQDNs for core exceptions (e.g., \InvalidArgumentException, \RuntimeException). Do not use inline FQDNs for non-root namespaces
Create objects via $container->build(ClassName::class) everywhere except DTOs, value objects, and pure data structures
Use minimalist DocBlocks documenting description, parameters, and return types for classes and functions
Follow the specified comment structure with section headers/subheaders and spacing; remove obsolete comments with removed code

Files:

  • app/Console/Server/ServerAddCommand.php
  • app/Console/Server/ServerProvisionDigitalOceanCommand.php
  • app/Repositories/SiteRepository.php
  • app/Traits/SiteHelpersTrait.php
**/*Command.php

📄 CodeRabbit inference engine (.cursor/rules/01-architecture.mdc)

**/*Command.php: Commands handle user interaction (I/O), orchestration, styling, error formatting, and prompts using SymfonyStyle
Commands must not contain business logic; delegate to Services
Commands must not invoke other commands (no proxy commands)

Files:

  • app/Console/Server/ServerAddCommand.php
  • app/Console/Server/ServerProvisionDigitalOceanCommand.php
**/*{Command,Service}.php

📄 CodeRabbit inference engine (.cursor/rules/01-architecture.mdc)

All dependencies should be expressed in constructor signatures; avoid circular dependencies

Files:

  • app/Console/Server/ServerAddCommand.php
  • app/Console/Server/ServerProvisionDigitalOceanCommand.php
🧠 Learnings (4)
📚 Learning: 2025-10-24T20:01:06.209Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/03-commands.mdc:0-0
Timestamp: 2025-10-24T20:01:06.209Z
Learning: Applies to src/Command/**/*Command.php : Option naming: --server/--site select existing resources; --name defines a new resource property; follow the provided table for common options.

Applied to files:

  • app/Console/Server/ServerAddCommand.php
📚 Learning: 2025-10-24T19:59:22.863Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/01-architecture.mdc:0-0
Timestamp: 2025-10-24T19:59:22.863Z
Learning: Applies to **/*Command.php : Commands handle user interaction (I/O), orchestration, styling, error formatting, and prompts using SymfonyStyle

Applied to files:

  • app/Console/Server/ServerAddCommand.php
📚 Learning: 2025-10-24T20:01:06.209Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/03-commands.mdc:0-0
Timestamp: 2025-10-24T20:01:06.209Z
Learning: Applies to **/BaseCommand.php : BaseCommand handles shared initialization/configuration/orchestration only; do not implement individual I/O operations here.

Applied to files:

  • app/Console/Server/ServerAddCommand.php
📚 Learning: 2025-10-24T19:59:22.863Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/01-architecture.mdc:0-0
Timestamp: 2025-10-24T19:59:22.863Z
Learning: Applies to **/SymfonyApp.php : Register console commands in SymfonyApp.php

Applied to files:

  • app/Console/Server/ServerAddCommand.php
🧬 Code graph analysis (2)
app/Console/Server/ServerAddCommand.php (3)
app/Traits/KeyHelpersTrait.php (2)
  • resolvePrivateKeyPath (46-52)
  • KeyHelpersTrait (19-137)
app/Services/IOService.php (2)
  • error (498-501)
  • writeln (463-469)
app/Traits/KeyValidationTrait.php (1)
  • KeyValidationTrait (16-116)
app/Console/Server/ServerProvisionDigitalOceanCommand.php (10)
app/Contracts/BaseCommand.php (1)
  • BaseCommand (29-158)
app/DTOs/ServerDTO.php (1)
  • ServerDTO (7-19)
app/Services/IOService.php (13)
  • hr (517-523)
  • h1 (506-512)
  • promptSpin (439-452)
  • warning (490-493)
  • writeln (463-469)
  • getValidatedOptionOrPrompt (159-182)
  • promptText (200-218)
  • promptSelect (304-322)
  • getOptionOrPrompt (84-132)
  • error (498-501)
  • promptConfirm (260-276)
  • success (482-485)
  • showCommandHint (530-566)
app/Traits/DigitalOceanCommandTrait.php (1)
  • initializeDigitalOceanAPI (33-66)
app/Services/DigitalOcean/DigitalOceanAccountService.php (5)
  • getUserSshKeys (149-167)
  • getAvailableRegions (27-47)
  • getAvailableSizes (54-79)
  • getAvailableImages (86-116)
  • getUserVpcs (123-142)
app/Traits/ServerValidationTrait.php (1)
  • validateNameInput (23-41)
app/Traits/DigitalOceanValidationTrait.php (5)
  • validateRegionInput (19-35)
  • validateSizeInput (44-60)
  • validateImageInput (69-85)
  • validateSshKeyInput (119-134)
  • validateVpcUuidInput (92-110)
app/Traits/KeyHelpersTrait.php (1)
  • resolvePrivateKeyPath (46-52)
app/Services/DigitalOcean/DigitalOceanDropletService.php (4)
  • createDroplet (33-75)
  • waitForDropletReady (107-129)
  • getDropletIp (136-157)
  • destroyDroplet (168-185)
app/Traits/ServerHelpersTrait.php (1)
  • displayServerDeets (95-113)
🔇 Additional comments (4)
app/Repositories/SiteRepository.php (1)

205-205: LGTM! Good modernization using first-class callable syntax.

The change from 'is_string' to is_string(...) adopts PHP 8.1's first-class callable syntax, which aligns with the coding guideline to leverage PHP 8.x features. This is a syntax modernization with no behavioral change—the filtering logic remains identical.

As per coding guidelines.

app/Traits/SiteHelpersTrait.php (1)

107-107: LGTM! Excellent modernization to first-class callables.

The changes from string-based callable references ('trim', 'strval') to first-class callable syntax (trim(...), strval(...)) are functionally equivalent and leverage PHP 8.1+ features as required by the coding guidelines. This approach is more explicit, type-safe, and provides better IDE support.

As per coding guidelines.

Also applies to: 121-121

app/Console/Server/ServerAddCommand.php (2)

9-10: LGTM: Trait integration follows best practices.

The KeyHelpersTrait and KeyValidationTrait are properly imported and integrated, providing key resolution and validation capabilities that align with the broader DigitalOcean provisioning feature introduced in this PR.

Also applies to: 22-23


39-40: LGTM: Logical option ordering.

Placing private-key-path before username is reasonable since the key path requires resolution logic while username has a simple default value.

@loadinglucian
loadinglucian merged commit f991a56 into main Oct 30, 2025
5 checks passed
@loadinglucian
loadinglucian deleted the feat/provision-digitalocean-droplet branch October 30, 2025 16:46
@coderabbitai coderabbitai Bot mentioned this pull request Dec 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant