diff --git a/.cursor/rules/00-main.mdc b/.cursor/rules/00-main.mdc index 5331ec5b..dcc146d4 100644 --- a/.cursor/rules/00-main.mdc +++ b/.cursor/rules/00-main.mdc @@ -41,6 +41,16 @@ Build Deployer PHP: Composer package and CLI tool simplifying server provisionin - Review surrounding code for reusable patterns - Code should appear written by single person: naming, parameter precedence, logic flow, organization +### File Operations + +Use terminal commands for file management—never read+write entire contents: + +```bash +mv old.php new.php # Rename/move +cp source.php dest.php # Copy +mkdir -p path/to/dir # Create directories +``` + ### Execution Protocol 1. ULTRATHINK - analyze problem deeply diff --git a/app/Console/HelloCommand.php b/app/Console/HelloCommand.php deleted file mode 100644 index 204cbd07..00000000 --- a/app/Console/HelloCommand.php +++ /dev/null @@ -1,32 +0,0 @@ -env->get(['USER', 'USERNAME'], false) ?? 'there'; - - $this->yay('Hello ' . $user . '!'); - - return Command::SUCCESS; - } -} diff --git a/app/Console/Server/ServerAddCommand.php b/app/Console/Server/ServerAddCommand.php index 7b0fe828..7f964a5d 100644 --- a/app/Console/Server/ServerAddCommand.php +++ b/app/Console/Server/ServerAddCommand.php @@ -17,7 +17,7 @@ #[AsCommand( name: 'server:add', - description: 'Add a new server to the inventory' + description: 'Add a new server to inventory' )] class ServerAddCommand extends BaseCommand { @@ -96,6 +96,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->yay('Server added to inventory'); + $this->ul([ + 'Run <|cyan>server:info to view server information', + 'Or run <|cyan>server:install to install your new server', + ]); + // // Show command replay // ---- diff --git a/app/Console/Server/ServerDeleteCommand.php b/app/Console/Server/ServerDeleteCommand.php index cfabea36..274a27b1 100644 --- a/app/Console/Server/ServerDeleteCommand.php +++ b/app/Console/Server/ServerDeleteCommand.php @@ -180,7 +180,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->servers->delete($server->name); - $this->yay("Server '{$server->name}' deleted from inventory"); + $this->yay("Server '{$server->name}' removed from inventory"); // // Delete associated sites diff --git a/app/Console/Server/ServerInstallCommand.php b/app/Console/Server/ServerInstallCommand.php index 79d3b505..7d198534 100644 --- a/app/Console/Server/ServerInstallCommand.php +++ b/app/Console/Server/ServerInstallCommand.php @@ -156,7 +156,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->yay('Server installation completed successfully'); $this->ul([ - 'Run <|cyan>site:add to add a new site', + 'Run <|cyan>site:create to create a new site', 'Add the following <|yellow>public key to your Git provider (GitHub, GitLab, etc.) to enable deployments:', ]); diff --git a/app/Console/Server/ServerListCommand.php b/app/Console/Server/ServerListCommand.php index 0d0bdbc0..f2d17262 100644 --- a/app/Console/Server/ServerListCommand.php +++ b/app/Console/Server/ServerListCommand.php @@ -14,7 +14,7 @@ #[AsCommand( name: 'server:list', - description: 'List servers in the inventory' + description: 'List servers in inventory' )] class ServerListCommand extends BaseCommand { diff --git a/app/Console/Server/ServerProvisionDigitalOceanCommand.php b/app/Console/Server/ServerProvisionDigitalOceanCommand.php index bd443e0c..81b70f82 100644 --- a/app/Console/Server/ServerProvisionDigitalOceanCommand.php +++ b/app/Console/Server/ServerProvisionDigitalOceanCommand.php @@ -171,6 +171,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->yay('Server added to inventory'); + $this->ul([ + 'Run <|cyan>server:info to view server information', + 'Or run <|cyan>server:install to install your new server', + ]); + $shouldKeepDroplet = true; } catch (\RuntimeException $e) { $this->nay($e->getMessage()); diff --git a/app/Console/Site/SiteAddCommand.php b/app/Console/Site/SiteCreateCommand.php similarity index 72% rename from app/Console/Site/SiteAddCommand.php rename to app/Console/Site/SiteCreateCommand.php index 08f48f4a..14851fa8 100644 --- a/app/Console/Site/SiteAddCommand.php +++ b/app/Console/Site/SiteCreateCommand.php @@ -16,10 +16,10 @@ use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( - name: 'site:add', - description: 'Set up a new site on the server and add it to the inventory' + name: 'site:create', + description: 'Create a new site on a server and add it to inventory' )] -class SiteAddCommand extends BaseCommand +class SiteCreateCommand extends BaseCommand { use PlaybooksTrait; use ServersTrait; @@ -35,8 +35,6 @@ protected function configure(): void $this ->addOption('domain', null, InputOption::VALUE_REQUIRED, 'Domain name') - ->addOption('repo', null, InputOption::VALUE_REQUIRED, 'Git repository URL') - ->addOption('branch', null, InputOption::VALUE_REQUIRED, 'Git branch name') ->addOption('server', null, InputOption::VALUE_REQUIRED, 'Server name') ->addOption('php-version', null, InputOption::VALUE_REQUIRED, 'PHP version to use') ->addOption('www-mode', null, InputOption::VALUE_REQUIRED, 'WWW handling mode (redirect-to-root, redirect-to-www)'); @@ -50,7 +48,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { parent::execute($input, $output); - $this->h1('Add New Site'); + $this->h1('Create New Site'); // // Select server @@ -58,7 +56,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $server = $this->selectServer(); - if (is_int($server) || $server->info === null) { + if (is_int($server) || null === $server->info) { return Command::FAILURE; } @@ -71,7 +69,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** @var string $permissions */ // - // Validate server is ready to add site + // Validate server is ready to create site // ---- $validationResult = $this->validateServerReady($server->info); @@ -86,14 +84,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $siteInfo = $this->gatherSiteInfo($server->info); - if ($siteInfo === null) { + if (null === $siteInfo) { return Command::FAILURE; } [ 'domain' => $domain, - 'repo' => $repo, - 'branch' => $branch, 'phpVersion' => $phpVersion, 'wwwMode' => $wwwMode, ] = $siteInfo; @@ -104,21 +100,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int $site = new SiteDTO( domain: $domain, - repo: $repo, - branch: $branch, + repo: null, + branch: null, server: $server->name ); $this->displaySiteDeets($site); // - // Add site on server + // Create site on server // ---- $result = $this->executePlaybookSilently( $server, - 'site-add', - 'Adding site...', + 'site-create', + 'Creating site on server...', [ 'DEPLOYER_DISTRO' => $distro, 'DEPLOYER_PERMS' => $permissions, @@ -132,8 +128,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $result; } - $this->yay('Site added successfully'); - // // Save to inventory // ---- @@ -146,35 +140,27 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - $this->yay('Site added to inventory'); + $this->yay("Site '{$domain}' added to inventory"); // // Display next steps // ---- - $displayUrl = ($wwwMode === 'redirect-to-www') - ? 'http://www.' . $domain - : 'http://' . $domain; - - $this->out([ - 'Next steps:', - ' • Site is accessible at ' . $displayUrl . '', - ' • Update DNS records:', - ' - Point @ (root) to ' . $server->host . '', - ' - Point www to ' . $server->host . '', - ' • Run site:https to enable HTTPS once you have your DNS records set up', - ' • Deploy your application with site:deploy', - '', + $this->info('Please update your DNS records:'); + + $this->ul([ + 'Point @ (root) to ' . $server->host . '', + 'Point www to ' . $server->host . '', + 'Run site:https to enable HTTPS once you have your DNS records set up', + 'Deploy your new site with site:deploy' ]); // // Show command replay // ---- - $this->commandReplay('site:add', [ + $this->commandReplay('site:create', [ 'domain' => $domain, - 'repo' => $repo, - 'branch' => $branch, 'server' => $server->name, 'php-version' => $phpVersion, 'www-mode' => $wwwMode, @@ -200,12 +186,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function validateServerReady(array $info): ?int { // Check if Caddy is installed - $caddyInstalled = isset($info['caddy']) && is_array($info['caddy']) && ($info['caddy']['available'] ?? false) === true; + $caddyInstalled = isset($info['caddy']) && is_array($info['caddy']) && true === ($info['caddy']['available'] ?? false); // Check if PHP is installed $phpInstalled = isset($info['php']) && is_array($info['php']) && isset($info['php']['versions']) && is_array($info['php']['versions']) && count($info['php']['versions']) > 0; - if (!$caddyInstalled || !$phpInstalled) { + if (! $caddyInstalled || ! $phpInstalled) { $this->nay('Looks like the server was not installed as expected'); $this->out([ 'Run server:install to install required software.', @@ -251,7 +237,7 @@ private function selectPhpVersion(array $info): string|int } // If only one version, use it automatically - if (count($installedPhpVersions) === 1) { + if (1 === count($installedPhpVersions)) { return $installedPhpVersions[0]; } @@ -261,7 +247,7 @@ private function selectPhpVersion(array $info): string|int /** @var array{default?: string|int|float}|null $phpInfo */ $phpInfo = $info['php'] ?? null; $defaultVersion = is_array($phpInfo) ? ($phpInfo['default'] ?? null) : null; - $defaultVersionStr = $defaultVersion !== null ? (string) $defaultVersion : $installedPhpVersions[0]; + $defaultVersionStr = null !== $defaultVersion ? (string) $defaultVersion : $installedPhpVersions[0]; $phpVersion = (string) $this->io->getOptionOrPrompt( 'php-version', @@ -273,7 +259,7 @@ private function selectPhpVersion(array $info): string|int ); // Validate CLI-provided version exists in available versions - if (!in_array($phpVersion, $installedPhpVersions, true)) { + if (! in_array($phpVersion, $installedPhpVersions, true)) { $this->nay( "PHP version {$phpVersion} is not installed on this server. Available: " . implode(', ', $installedPhpVersions) ); @@ -292,7 +278,7 @@ private function selectPhpVersion(array $info): string|int * Gather site details from user input or CLI options. * * @param array $info Server information from serverInfo() - * @return array{domain: string, repo: string, branch: string, phpVersion: string, wwwMode: string}|null + * @return array{domain: string, phpVersion: string, wwwMode: string}|null */ protected function gatherSiteInfo(array $info): ?array { @@ -308,7 +294,7 @@ protected function gatherSiteInfo(array $info): ?array fn ($value) => $this->validateSiteDomain($value) ); - if ($domain === null) { + if (null === $domain) { return null; } @@ -338,49 +324,7 @@ protected function gatherSiteInfo(array $info): ?array : sprintf("Invalid WWW mode '%s'. Allowed: %s", is_scalar($value) ? $value : gettype($value), implode(', ', array_keys($wwwModes))) ); - if ($wwwMode === null) { - return null; - } - - // - // Gather git details - // ---- - - $defaultRepo = $this->git->detectRemoteUrl() ?? ''; - - /** @var string|null $repo */ - $repo = $this->io->getValidatedOptionOrPrompt( - 'repo', - fn ($validate) => $this->io->promptText( - label: 'Git repository URL:', - placeholder: 'git@github.com:user/repo.git', - default: $defaultRepo, - required: true, - validate: $validate - ), - fn ($value) => $this->validateSiteRepo($value) - ); - - if ($repo === null) { - return null; - } - - $defaultBranch = $this->git->detectCurrentBranch() ?? 'main'; - - /** @var string|null $branch */ - $branch = $this->io->getValidatedOptionOrPrompt( - 'branch', - fn ($validate) => $this->io->promptText( - label: 'Git branch:', - placeholder: $defaultBranch, - default: $defaultBranch, - required: true, - validate: $validate - ), - fn ($value) => $this->validateSiteBranch($value) - ); - - if ($branch === null) { + if (null === $wwwMode) { return null; } @@ -396,8 +340,6 @@ protected function gatherSiteInfo(array $info): ?array return [ 'domain' => $domain, - 'repo' => $repo, - 'branch' => $branch, 'phpVersion' => $phpVersion, 'wwwMode' => $wwwMode, ]; diff --git a/app/Console/Site/SiteDeleteCommand.php b/app/Console/Site/SiteDeleteCommand.php index 5d4dfb26..8320318d 100644 --- a/app/Console/Site/SiteDeleteCommand.php +++ b/app/Console/Site/SiteDeleteCommand.php @@ -16,7 +16,7 @@ #[AsCommand( name: 'site:delete', - description: 'Remove a site from the server and delete it from inventory' + description: 'Delete a site from a server and remove it from inventory' )] class SiteDeleteCommand extends BaseCommand { @@ -99,10 +99,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int } // - // Attempt to remove site from server + // Attempt to delete site from server // ---- - $removedFromServer = false; + $deletedFromServer = false; $server = $this->servers->findByName($site->server); if ($server === null) { @@ -131,7 +131,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $result = $this->executePlaybookSilently( $server, 'site-delete', - 'Removing site from server...', + 'Deleting site from server...', [ 'DEPLOYER_DISTRO' => $distro, 'DEPLOYER_PERMS' => $permissions, @@ -140,18 +140,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int ); if (is_int($result)) { - $this->warn('Failed to remove site from server'); + $this->warn('Failed to delete site from server'); } else { - $removedFromServer = true; + $deletedFromServer = true; } } } // - // Confirm inventory deletion if server removal failed + // Confirm inventory removal if server deletion failed // ---- - if (!$removedFromServer) { + if (!$deletedFromServer) { /** @var bool $proceedAnyway */ $proceedAnyway = $this->io->getOptionOrPrompt( 'yes', @@ -172,11 +172,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->sites->delete($site->domain); - if ($removedFromServer) { - $this->yay("Site '{$site->domain}' deleted successfully"); - } else { - $this->yay("Site '{$site->domain}' deleted from inventory"); - } + $this->yay("Site '{$site->domain}' removed from inventory"); // // Show command replay diff --git a/app/Console/Site/SiteDeployCommand.php b/app/Console/Site/SiteDeployCommand.php index efad0829..108442c7 100644 --- a/app/Console/Site/SiteDeployCommand.php +++ b/app/Console/Site/SiteDeployCommand.php @@ -44,6 +44,8 @@ protected function configure(): void $this ->addOption('domain', null, InputOption::VALUE_REQUIRED, 'Site domain') + ->addOption('repo', null, InputOption::VALUE_REQUIRED, 'Git repository URL') + ->addOption('branch', null, InputOption::VALUE_REQUIRED, 'Git branch name') ->addOption('keep-releases', null, InputOption::VALUE_REQUIRED, 'Number of releases to keep (default: 5)') ->addOption('yes', 'y', InputOption::VALUE_NONE, 'Deploy without confirmation prompt'); } @@ -59,7 +61,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->h1('Deploy Site'); // - // Select site & display details + // Select site // ---- $site = $this->selectSite(); @@ -68,6 +70,30 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $site; } + // + // Resolve repo and branch (prompt if not stored) + // ---- + + $resolvedGit = $this->resolveRepoAndBranch($input, $site); + + if (null === $resolvedGit) { + return Command::FAILURE; + } + + [$repo, $branch, $needsUpdate] = $resolvedGit; + + // Create updated site DTO with resolved repo/branch + $site = new SiteDTO( + domain: $site->domain, + repo: $repo, + branch: $branch, + server: $site->server + ); + + // + // Display site details + // ---- + $this->displaySiteDeets($site); // @@ -82,7 +108,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - if ($missingHooks !== []) { + if ([] !== $missingHooks) { $this->warn('Missing deployment hooks in repository:'); foreach ($missingHooks as $hook) { $this->out(' • ' . $hook); @@ -121,7 +147,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $server = $this->serverInfo($server); - if (is_int($server) || $server->info === null) { + if (is_int($server) || null === $server->info) { return Command::FAILURE; } @@ -147,15 +173,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Resolve deployment parameters // ---- - $branch = $site->branch; $keepReleases = $this->resolveKeepReleases($input); - if ($keepReleases === null) { + if (null === $keepReleases) { return Command::FAILURE; } $phpVersion = $this->resolvePhpVersion($server->info); - if ($phpVersion === null) { + if (null === $phpVersion) { return Command::FAILURE; } @@ -191,7 +216,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'DEPLOYER_DISTRO' => $distro, 'DEPLOYER_PERMS' => $permissions, 'DEPLOYER_SITE_DOMAIN' => $site->domain, - 'DEPLOYER_SITE_REPO' => $site->repo, + 'DEPLOYER_SITE_REPO' => $repo, 'DEPLOYER_SITE_BRANCH' => $branch, 'DEPLOYER_PHP_VERSION' => (string) $phpVersion, 'DEPLOYER_KEEP_RELEASES' => (string) $keepReleases, @@ -202,6 +227,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $result; } + // + // Save repo/branch to inventory if newly set + // ---- + + if ($needsUpdate) { + try { + $this->sites->update($site); + } catch (\RuntimeException $e) { + $this->warn('Could not update inventory: ' . $e->getMessage()); + } + } + // // Display results // ---- @@ -222,6 +259,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->commandReplay('site:deploy', [ 'domain' => $site->domain, + 'repo' => $repo, + 'branch' => $branch, 'keep-releases' => $keepReleases, 'yes' => true, ]); @@ -233,10 +272,84 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Helpers // ---- + /** + * Resolve repo and branch from site, CLI options, or prompts. + * + * @return array{0: string, 1: string, 2: bool}|null [repo, branch, needsUpdate] or null on failure + */ + private function resolveRepoAndBranch(InputInterface $input, SiteDTO $site): ?array + { + $storedRepo = $site->repo; + $storedBranch = $site->branch; + $needsUpdate = false; + + // Resolve repo + if (null !== $storedRepo && '' !== $storedRepo) { + // Use stored value, but allow CLI override + /** @var string|null $cliRepo */ + $cliRepo = $input->getOption('repo'); + $repo = (null !== $cliRepo && '' !== $cliRepo) ? $cliRepo : $storedRepo; + } else { + // Not stored - prompt for it + $defaultRepo = $this->git->detectRemoteUrl() ?? ''; + + /** @var string|null $repo */ + $repo = $this->io->getValidatedOptionOrPrompt( + 'repo', + fn ($validate) => $this->io->promptText( + label: 'Git repository URL:', + placeholder: 'git@github.com:user/repo.git', + default: $defaultRepo, + required: true, + validate: $validate + ), + fn ($value) => $this->validateSiteRepo($value) + ); + + if (null === $repo) { + return null; + } + + $needsUpdate = true; + } + + // Resolve branch + if (null !== $storedBranch && '' !== $storedBranch) { + // Use stored value, but allow CLI override + /** @var string|null $cliBranch */ + $cliBranch = $input->getOption('branch'); + $branch = (null !== $cliBranch && '' !== $cliBranch) ? $cliBranch : $storedBranch; + } else { + // Not stored - prompt for it + $defaultBranch = $this->git->detectCurrentBranch() ?? 'main'; + + /** @var string|null $branch */ + $branch = $this->io->getValidatedOptionOrPrompt( + 'branch', + fn ($validate) => $this->io->promptText( + label: 'Git branch:', + placeholder: $defaultBranch, + default: $defaultBranch, + required: true, + validate: $validate + ), + fn ($value) => $this->validateSiteBranch($value) + ); + + if (null === $branch) { + return null; + } + + $needsUpdate = true; + } + + return [$repo, $branch, $needsUpdate]; + } + /** * Display deployment summary details. * - * @param array $result + * @param array $result */ private function displayDeploymentSummary(array $result, string $branch, string $phpVersion): void { @@ -265,7 +378,7 @@ private function resolveKeepReleases(InputInterface $input): ?int { /** @var string|null $value */ $value = $input->getOption('keep-releases'); - if ($value === null || trim($value) === '') { + if (null === $value || '' === trim($value)) { return self::DEFAULT_KEEP_RELEASES; } @@ -288,7 +401,7 @@ private function resolveKeepReleases(InputInterface $input): ?int /** * Resolve PHP version from server info, prompting user if multiple exist. * - * @param array $info + * @param array $info */ private function resolvePhpVersion(array $info): ?string { @@ -304,7 +417,7 @@ private function resolvePhpVersion(array $info): ?string } } - if ($versions === []) { + if ([] === $versions) { $this->nay('No PHP versions found on the server. Run server:install first.'); return null; @@ -315,7 +428,7 @@ private function resolvePhpVersion(array $info): ?string $default = (string) $phpInfo['default']; } - if (count($versions) === 1) { + if (1 === count($versions)) { /** @var string $only */ $only = $versions[0]; @@ -343,6 +456,10 @@ private function resolvePhpVersion(array $info): ?string */ private function checkRemoteHooksExist(SiteDTO $site): array { + if (null === $site->repo || null === $site->branch) { + return []; + } + $hookPaths = array_map( fn ($hook) => ".deployer/hooks/{$hook}", self::REQUIRED_HOOKS diff --git a/app/Console/Site/SiteHttpsCommand.php b/app/Console/Site/SiteHttpsCommand.php index 100825d6..e4a267c6 100644 --- a/app/Console/Site/SiteHttpsCommand.php +++ b/app/Console/Site/SiteHttpsCommand.php @@ -93,8 +93,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->warn("Site '{$site->domain}' configuration not found on server"); $this->out([ '', - 'It looks like this site has not been added yet.', - 'Run site:add to add the site first.', + 'It looks like this site has not been created yet.', + 'Run site:create to create the site first.', '', ]); diff --git a/app/Console/Site/SiteListCommand.php b/app/Console/Site/SiteListCommand.php index acb76722..a2f7c62a 100644 --- a/app/Console/Site/SiteListCommand.php +++ b/app/Console/Site/SiteListCommand.php @@ -13,7 +13,7 @@ #[AsCommand( name: 'site:list', - description: 'List sites in the inventory' + description: 'List sites in inventory' )] class SiteListCommand extends BaseCommand { diff --git a/app/DTOs/SiteDTO.php b/app/DTOs/SiteDTO.php index 5468b146..69fe43df 100644 --- a/app/DTOs/SiteDTO.php +++ b/app/DTOs/SiteDTO.php @@ -10,14 +10,14 @@ * Create a SiteDTO containing the site's domain, repository, branch, and associated server. * * @param string $domain The site's domain name (e.g. example.com). - * @param string $repo The repository URL for git sites. - * @param string $branch The repository branch for git sites (e.g. main). + * @param ?string $repo The repository URL for git sites (null if not yet configured). + * @param ?string $branch The repository branch for git sites (null if not yet configured). * @param string $server Server name associated with the site. */ public function __construct( public string $domain, - public string $repo, - public string $branch, + public ?string $repo, + public ?string $branch, public string $server, ) { } diff --git a/app/Repositories/SiteRepository.php b/app/Repositories/SiteRepository.php index b1b972cb..e8243779 100644 --- a/app/Repositories/SiteRepository.php +++ b/app/Repositories/SiteRepository.php @@ -37,7 +37,7 @@ public function loadInventory(InventoryService $inventory): void $this->inventory = $inventory; $sites = $inventory->get(self::PREFIX); - if (!is_array($sites)) { + if (! is_array($sites)) { $sites = []; $inventory->set(self::PREFIX, $sites); } @@ -67,11 +67,37 @@ public function create(SiteDTO $site): void } /** - * Retrieve the site matching the given domain. - * - * @throws \RuntimeException If the inventory has not been loaded via loadInventory(). - * @return SiteDTO|null The SiteDTO for the matching domain, or `null` if no match is found. - */ + * Update an existing site in inventory storage. + * + * @param SiteDTO $site The site to update; must already exist by domain. + * @throws \RuntimeException If the inventory has not been loaded or site does not exist. + */ + public function update(SiteDTO $site): void + { + $this->assertInventoryLoaded(); + + $found = false; + foreach ($this->sites as $index => $siteData) { + if (isset($siteData['domain']) && $siteData['domain'] === $site->domain) { + $this->sites[$index] = $this->dehydrateSiteDTO($site); + $found = true; + break; + } + } + + if (! $found) { + throw new \RuntimeException("Site '{$site->domain}' not found"); + } + + $this->inventory->set(self::PREFIX, $this->sites); + } + + /** + * Retrieve the site matching the given domain. + * + * @throws \RuntimeException If the inventory has not been loaded via loadInventory(). + * @return SiteDTO|null The SiteDTO for the matching domain, or `null` if no match is found. + */ public function findByDomain(string $domain): ?SiteDTO { $this->assertInventoryLoaded(); @@ -158,7 +184,7 @@ public function delete(string $domain): void */ private function assertInventoryLoaded(): void { - if ($this->inventory === null) { + if (null === $this->inventory) { throw new \RuntimeException('Inventory not set. Call loadInventory() first.'); } } @@ -166,36 +192,46 @@ private function assertInventoryLoaded(): void /** * Serialize a SiteDTO into an associative array suitable for inventory storage. * + * Only includes repo and branch if they are set. + * * @param SiteDTO $site The site DTO to serialize. - * @return array Associative array with keys `domain`, `repo`, `branch`, and `server`. + * @return array Associative array with keys `domain`, `server`, and optionally `repo`, `branch`. */ private function dehydrateSiteDTO(SiteDTO $site): array { - return [ + $data = [ 'domain' => $site->domain, - 'repo' => $site->repo, - 'branch' => $site->branch, 'server' => $site->server, ]; + + if (null !== $site->repo) { + $data['repo'] = $site->repo; + } + + if (null !== $site->branch) { + $data['branch'] = $site->branch; + } + + return $data; } /** - * Create a SiteDTO from raw inventory data. - * - * @param array $data Raw associative array from inventory. - * @return SiteDTO A SiteDTO where `domain`, `repo`, `branch`, and `server` are strings (empty if missing). - */ + * Create a SiteDTO from raw inventory data. + * + * @param array $data Raw associative array from inventory. + * @return SiteDTO A SiteDTO where `domain` and `server` are strings, `repo` and `branch` are nullable. + */ private function hydrateSiteDTO(array $data): SiteDTO { $domain = $data['domain'] ?? ''; - $repo = $data['repo'] ?? ''; - $branch = $data['branch'] ?? ''; + $repo = $data['repo'] ?? null; + $branch = $data['branch'] ?? null; $server = $data['server'] ?? ''; return new SiteDTO( domain: is_string($domain) ? $domain : '', - repo: is_string($repo) ? $repo : '', - branch: is_string($branch) ? $branch : '', + repo: is_string($repo) ? $repo : null, + branch: is_string($branch) ? $branch : null, server: is_string($server) ? $server : '', ); } diff --git a/app/SymfonyApp.php b/app/SymfonyApp.php index 82f4ba57..32e58452 100644 --- a/app/SymfonyApp.php +++ b/app/SymfonyApp.php @@ -4,7 +4,6 @@ namespace Deployer; -use Deployer\Console\HelloCommand; use Deployer\Console\Key\KeyAddDigitalOceanCommand; use Deployer\Console\Key\KeyDeleteDigitalOceanCommand; use Deployer\Console\Key\KeyListDigitalOceanCommand; @@ -17,7 +16,7 @@ use Deployer\Console\Server\ServerLogsCommand; use Deployer\Console\Server\ServerProvisionDigitalOceanCommand; use Deployer\Console\Server\ServerRunCommand; -use Deployer\Console\Site\SiteAddCommand; +use Deployer\Console\Site\SiteCreateCommand; use Deployer\Console\Site\SiteDeleteCommand; use Deployer\Console\Site\SiteDeployCommand; use Deployer\Console\Site\SiteHttpsCommand; @@ -120,8 +119,6 @@ private function displayBanner(): void private function registerCommands(): void { $commands = [ - HelloCommand::class, - // // Scaffolding @@ -151,7 +148,7 @@ private function registerCommands(): void // // Site management - SiteAddCommand::class, + SiteCreateCommand::class, SiteDeleteCommand::class, SiteListCommand::class, SiteSharedPushCommand::class, diff --git a/app/Traits/SitesTrait.php b/app/Traits/SitesTrait.php index 17e84ff3..3fd0466a 100644 --- a/app/Traits/SitesTrait.php +++ b/app/Traits/SitesTrait.php @@ -37,7 +37,7 @@ trait SitesTrait /** * Display a warning to add a site if no sites are available. Otherwise, return all sites. * - * @param array|null $sites Optional pre-fetched sites; if null, fetches from repository + * @param array|null $sites Optional pre-fetched sites; if null, fetches from repository * @return array|int Returns array of sites or Command::SUCCESS if no sites available */ protected function ensureSitesAvailable(?array $sites = null): array|int @@ -50,9 +50,9 @@ protected function ensureSitesAvailable(?array $sites = null): array|int // // Check if no sites are available - if (count($allSites) === 0) { + if (0 === count($allSites)) { $this->info('This command requires at least one site in inventory:'); - $this->ul('Run site:add to add a site'); + $this->ul('Run site:create to create a site'); return Command::SUCCESS; } @@ -63,7 +63,7 @@ protected function ensureSitesAvailable(?array $sites = null): array|int /** * Select a site from inventory by domain option or interactive prompt. * - * @param array|null $sites Optional pre-fetched sites; if null, fetches from repository + * @param array|null $sites Optional pre-fetched sites; if null, fetches from repository * @return SiteDTO|int Returns SiteDTO on success, or Command::SUCCESS if empty inventory, or Command::FAILURE if not found */ protected function selectSite(?array $sites = null): SiteDTO|int @@ -95,7 +95,7 @@ protected function selectSite(?array $sites = null): SiteDTO|int $site = $this->sites->findByDomain($domain); - if ($site === null) { + if (null === $site) { $this->nay("Site '{$domain}' not found in inventory"); return Command::FAILURE; @@ -111,12 +111,17 @@ protected function displaySiteDeets(SiteDTO $site): void { $details = [ 'Domain' => $site->domain, - 'Source' => 'Git', - 'Repo' => $site->repo, - 'Branch' => $site->branch, 'Server' => $site->server, ]; + if (null !== $site->repo) { + $details['Repo'] = $site->repo; + } + + if (null !== $site->branch) { + $details['Branch'] = $site->branch; + } + $this->displayDeets($details); $this->out('───'); } @@ -139,14 +144,14 @@ protected function validateSiteDomain(mixed $domain): ?string $domain = $this->normalizeDomain($domain); // Check format - $isValid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false; + $isValid = false !== filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME); if (! $isValid) { return 'Must be a valid domain name (e.g., example.com, subdomain.example.com)'; } // Check uniqueness $existing = $this->sites->findByDomain($domain); - if ($existing !== null) { + if (null !== $existing) { return "Domain '{$domain}' already exists in inventory"; } @@ -178,7 +183,7 @@ protected function validateSiteBranch(mixed $branch): ?string return 'Branch name must be a string'; } - if (trim($branch) === '') { + if ('' === trim($branch)) { return 'Branch name cannot be empty'; } @@ -196,7 +201,7 @@ protected function validateSiteRepo(mixed $repo): ?string return 'Repository URL must be a string'; } - if (trim($repo) === '') { + if ('' === trim($repo)) { return 'Repository URL cannot be empty'; } @@ -240,10 +245,10 @@ protected function validateSiteAdded(ServerDTO $server, SiteDTO $site): ?int ) ); - if ($result['exit_code'] !== 0) { - $this->nay("Site '{$site->domain}' has not been added on the server"); + if (0 !== $result['exit_code']) { + $this->nay("Site '{$site->domain}' has not been created on the server"); $this->out([ - 'Run site:add to add the site first.', + 'Run site:create to create the site first.', '', ]); @@ -263,7 +268,7 @@ protected function validateSiteAdded(ServerDTO $server, SiteDTO $site): ?int */ protected function getSiteRootPath(SiteDTO $site): string { - return '/home/deployer/sites/'.$site->domain; + return '/home/deployer/sites/' . $site->domain; } /** @@ -271,6 +276,6 @@ protected function getSiteRootPath(SiteDTO $site): string */ protected function getSiteSharedPath(SiteDTO $site): string { - return $this->getSiteRootPath($site).'/shared'; + return $this->getSiteRootPath($site) . '/shared'; } } diff --git a/playbooks/site-add.sh b/playbooks/site-create.sh similarity index 97% rename from playbooks/site-add.sh rename to playbooks/site-create.sh index 41b8c16b..51ffd3a5 100644 --- a/playbooks/site-add.sh +++ b/playbooks/site-create.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash # -# Site Add Playbook - Ubuntu/Debian Only +# Site Create Playbook - Ubuntu/Debian Only # -# Add new site with atomic deployment directory structure +# Create new site with atomic deployment directory structure # ---- # # This playbook only supports Ubuntu and Debian distributions (debian family). @@ -111,7 +111,7 @@ setup_default_page() { '; echo '
  • Run site:https to enable HTTPS
  • '; - echo '
  • Deploy your application with site:deploy
  • '; + echo '
  • Deploy your new site with site:deploy
  • '; echo ''; EOF echo "Error: Failed to create index.php" >&2 diff --git a/playbooks/site-delete.sh b/playbooks/site-delete.sh index 614b0090..4b78381f 100644 --- a/playbooks/site-delete.sh +++ b/playbooks/site-delete.sh @@ -3,11 +3,11 @@ # # Site Delete Playbook - Ubuntu/Debian Only # -# Remove site files and Caddy configuration from server +# Delete site files and Caddy configuration from server # ---- # # This playbook only supports Ubuntu and Debian distributions (debian family). -# Removes site directory and Caddy vhost configuration, then reloads Caddy. +# Deletes site directory and Caddy vhost configuration, then reloads Caddy. # # Required Environment Variables: # DEPLOYER_OUTPUT_FILE - Output file path @@ -36,17 +36,17 @@ export DEPLOYER_PERMS # ---- # -# Remove Caddy vhost configuration +# Delete Caddy vhost configuration # ---- -remove_caddy_vhost() { +delete_caddy_vhost() { local domain=$1 local vhost_file="/etc/caddy/conf.d/sites/${domain}.caddy" if run_cmd test -f "$vhost_file"; then - echo "→ Removing Caddy configuration for ${domain}..." + echo "→ Deleting Caddy configuration for ${domain}..." if ! run_cmd rm -f "$vhost_file"; then - echo "Error: Failed to remove Caddy configuration" >&2 + echo "Error: Failed to delete Caddy configuration" >&2 exit 1 fi fi @@ -67,17 +67,17 @@ reload_caddy() { } # -# Remove site files +# Delete site files # ---- -remove_site_files() { +delete_site_files() { local domain=$1 local site_path="/home/deployer/sites/${domain}" if run_cmd test -d "$site_path"; then - echo "→ Removing files for ${domain}..." + echo "→ Deleting site files for ${domain}..." if ! run_cmd rm -rf "$site_path"; then - echo "Error: Failed to remove files" >&2 + echo "Error: Failed to delete site files" >&2 exit 1 fi fi @@ -91,9 +91,9 @@ main() { local domain=$DEPLOYER_SITE_DOMAIN # Execute cleanup tasks - remove_caddy_vhost "$domain" + delete_caddy_vhost "$domain" reload_caddy - remove_site_files "$domain" + delete_site_files "$domain" # Write output YAML if ! cat > "$DEPLOYER_OUTPUT_FILE" << EOF; then