Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions app/Console/Site/SiteAddCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,21 +91,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return $validationResult;
}

//
// Select PHP version
// ----

$phpVersion = $this->selectPhpVersion($info);

if (is_int($phpVersion)) {
return $phpVersion;
}

//
// Gather site details
// ----

$siteInfo = $this->gatherSiteInfo();
$siteInfo = $this->gatherSiteInfo($info);

if ($siteInfo === null) {
return Command::FAILURE;
Expand All @@ -115,6 +105,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
'domain' => $domain,
'repo' => $repo,
'branch' => $branch,
'phpVersion' => $phpVersion,
] = $siteInfo;

//
Expand Down Expand Up @@ -172,12 +163,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
// ----

$this->io->writeln([
'',
'Next steps:',
' • Site is accessible at <fg=cyan>https://' . $domain . '</>',
' • Update <fg=cyan>DNS records</> to point ' . $domain . ' to <fg=cyan>' . $server->host . '</>',
' • Deploy your application with <fg=cyan>site:deploy</>',
'',
]);

//
Expand Down Expand Up @@ -303,9 +292,10 @@ private function selectPhpVersion(array $info): string|int
/**
* Gather site details from user input or CLI options.
*
* @return array{domain: string, repo: string, branch: string}|null
* @param array<string, mixed> $info Server information from serverInfo()
* @return array{domain: string, repo: string, branch: string, phpVersion: string}|null
*/
protected function gatherSiteInfo(): ?array
protected function gatherSiteInfo(array $info): ?array
{
/** @var string|null $domain */
$domain = $this->io->getValidatedOptionOrPrompt(
Expand Down Expand Up @@ -365,10 +355,21 @@ protected function gatherSiteInfo(): ?array
return null;
}

//
// Select PHP version
// ----

$phpVersion = $this->selectPhpVersion($info);

if (is_int($phpVersion)) {
return null;
}

return [
'domain' => $domain,
'repo' => $repo,
'branch' => $branch,
'phpVersion' => $phpVersion,
];
}
}
10 changes: 10 additions & 0 deletions app/Console/Site/SiteSharedPullCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return $info;
}

//
// Validate site is provisioned on server
// ----

$validationResult = $this->validateSiteProvisioned($server, $site);

if (is_int($validationResult)) {
return $validationResult;
}

//
// Resolve paths
// ----
Expand Down
10 changes: 10 additions & 0 deletions app/Console/Site/SiteSharedPushCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return $info;
}

//
// Validate site is provisioned on server
// ----

$validationResult = $this->validateSiteProvisioned($server, $site);

if (is_int($validationResult)) {
return $validationResult;
}

//
// Resolve paths
// ----
Expand Down
11 changes: 11 additions & 0 deletions app/Services/FilesystemService.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ public function dumpFile(string $path, string $content): void
$this->fs->dumpFile($path, $content);
}

/**
* Remove files or directories.
*
* @param string|iterable<string> $files A filename, an array of files, or a \Traversable instance to remove
* @throws \RuntimeException If removal fails
*/
public function remove(string|iterable $files): void
{
$this->fs->remove($files);
}

//
// Gap-Filling Methods (Native PHP Functions)
// ----
Expand Down
55 changes: 53 additions & 2 deletions app/Services/GitService.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
*/
final readonly class GitService
{
public function __construct(private ProcessService $proc)
{
public function __construct(
private ProcessService $proc,
private FilesystemService $fs,
) {
}

//
Expand Down Expand Up @@ -47,6 +49,55 @@ public function detectCurrentBranch(?string $workingDir = null): ?string
);
}

//
// Remote Repository
// ----

/**
* Check if files exist in a remote git repository without full clone.
*
* Uses shallow clone with depth=1 to minimize data transfer.
*
* @param string $repo Git repository URL
* @param string $branch Branch to check
* @param list<string> $paths File paths to check (relative to repo root)
* @return array<string, bool> Map of path => exists
* @throws \RuntimeException If git operations fail
*/
public function checkRemoteFilesExist(string $repo, string $branch, array $paths): array
{
$tempDir = sys_get_temp_dir().'/deployer-git-check-'.bin2hex(random_bytes(8));

try {
// Shallow clone with depth=1 to minimize data transfer
$process = $this->proc->run(
['git', 'clone', '--depth', '1', '--branch', $branch, '--single-branch', $repo, $tempDir],
sys_get_temp_dir(),
30.0
);

if (! $process->isSuccessful()) {
throw new \RuntimeException(
"Failed to access git repository '{$repo}' branch '{$branch}': ".trim($process->getErrorOutput())
);
}

// Check each path
$results = [];
foreach ($paths as $path) {
$fullPath = $tempDir.'/'.ltrim($path, '/');
$results[$path] = $this->fs->exists($fullPath);
}

return $results;
} finally {
// Clean up temp directory
if ($this->fs->isDirectory($tempDir)) {
$this->fs->remove($tempDir);
}
}
}

//
// Helpers
// ----
Expand Down
11 changes: 10 additions & 1 deletion app/SymfonyApp.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
use Bigpixelrocket\DeployerPHP\Console\Site\SiteAddCommand;
use Bigpixelrocket\DeployerPHP\Console\Site\SiteDeleteCommand;
use Bigpixelrocket\DeployerPHP\Console\Site\SiteListCommand;
use Bigpixelrocket\DeployerPHP\Console\Site\SiteSharedPullCommand;
use Bigpixelrocket\DeployerPHP\Console\Site\SiteSharedPushCommand;
use Bigpixelrocket\DeployerPHP\Services\VersionService;
use Symfony\Component\Console\Application as SymfonyApplication;
use Symfony\Component\Console\Command\Command;
Expand Down Expand Up @@ -112,7 +114,7 @@ private function displayBanner(): void
'',
' The Server & Site Deployment Tool for PHP',
'<fg=cyan;options=bold>╰────────</><fg=blue;options=bold>──────────</><fg=bright-blue;options=bold>──────────</><fg=magenta;options=bold>──────────</><fg=gray;options=bold>─────────</>',
''
'',
];

// Display the banner
Expand All @@ -129,6 +131,11 @@ private function registerCommands(): void
$commands = [
HelloCommand::class,

//
// Scaffolding

// ScaffoldHooksCommand::class,

//
// Key management

Expand Down Expand Up @@ -156,6 +163,8 @@ private function registerCommands(): void
SiteAddCommand::class,
SiteDeleteCommand::class,
SiteListCommand::class,
SiteSharedPushCommand::class,
SiteSharedPullCommand::class,
];

foreach ($commands as $command) {
Expand Down
44 changes: 43 additions & 1 deletion app/Traits/SitesTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,25 @@

namespace Bigpixelrocket\DeployerPHP\Traits;

use Bigpixelrocket\DeployerPHP\DTOs\ServerDTO;
use Bigpixelrocket\DeployerPHP\DTOs\SiteDTO;
use Bigpixelrocket\DeployerPHP\Repositories\ServerRepository;
use Bigpixelrocket\DeployerPHP\Repositories\SiteRepository;
use Bigpixelrocket\DeployerPHP\Services\IOService;
use Bigpixelrocket\DeployerPHP\Services\ProcessService;
use Bigpixelrocket\DeployerPHP\Services\SSHService;
use Symfony\Component\Console\Command\Command;

/**
* Reusable site things.
*
* Requires classes using this trait to have IOService, ProcessService, ServerRepository, and SiteRepository properties.
* Requires classes using this trait to have IOService, ProcessService, ServerRepository, SiteRepository, and SSHService properties.
*
* @property IOService $io
* @property ProcessService $proc
* @property ServerRepository $servers
* @property SiteRepository $sites
* @property SSHService $ssh
*/
trait SitesTrait
{
Expand Down Expand Up @@ -204,6 +207,45 @@ protected function validateSiteRepo(mixed $repo): ?string
return null;
}

/**
* Validate that site has been provisioned on the server.
*
* Checks for:
* - Site directory structure exists at /home/deployer/sites/{domain}
* - Caddy configuration file exists
*
* @return int|null Returns Command::FAILURE if validation fails, null if successful
*/
protected function validateSiteProvisioned(ServerDTO $server, SiteDTO $site): ?int
{
try {
$result = $this->ssh->executeCommand(
$server,
sprintf(
'test -d /home/deployer/sites/%s && test -f /etc/caddy/conf.d/sites/%s.caddy',
escapeshellarg($site->domain),
escapeshellarg($site->domain)
)
);

if ($result['exit_code'] !== 0) {
$this->nay("Site '{$site->domain}' has not been provisioned on the server");
$this->io->writeln([
'Run <fg=cyan>site:add</> to provision the site first.',
'',
]);

return Command::FAILURE;
}
} catch (\RuntimeException $e) {
$this->nay($e->getMessage());

return Command::FAILURE;
}

return null;
}

/**
* Get the remote root path for a site.
*/
Expand Down
14 changes: 14 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions playbooks/install-deployer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,37 @@ setup_deployer() {
fi
}

#
# Configure passwordless sudo for deployer user
# ----

configure_deployer_sudo() {
local sudoers_file="/etc/sudoers.d/deployer"

echo "→ Configuring sudo permissions..."

if ! run_cmd tee "$sudoers_file" > /dev/null <<- 'EOF'; then
deployer ALL=(ALL) NOPASSWD: systemctl reload caddy
deployer ALL=(ALL) NOPASSWD: systemctl restart caddy
deployer ALL=(ALL) NOPASSWD: systemctl reload php*-fpm
deployer ALL=(ALL) NOPASSWD: systemctl restart php*-fpm
EOF
echo "Error: Failed to write sudoers configuration" >&2
exit 1
fi

if ! run_cmd chmod 440 "$sudoers_file"; then
echo "Error: Failed to set permissions on sudoers file" >&2
exit 1
fi

if ! run_cmd visudo -c -f "$sudoers_file"; then
echo "Error: sudoers validation failed" >&2
run_cmd rm -f "$sudoers_file"
exit 1
fi
}
Comment thread
loadinglucian marked this conversation as resolved.

#
# Ensure proper permissions on deploy directories
# ----
Expand Down Expand Up @@ -186,6 +217,7 @@ main() {
# Execute deployer setup tasks
setup_deployer "$deployer_home"
setup_deploy_directories "$deployer_home"
configure_deployer_sudo

# Get deploy public key
if ! deploy_public_key=$(run_cmd cat "${deployer_home}/.ssh/id_ed25519.pub" 2>&1); then
Expand Down