Skip to content

Commit b62aec6

Browse files
committed
feat: add fly deploy support
1 parent c189edd commit b62aec6

File tree

21 files changed

+1170
-0
lines changed

21 files changed

+1170
-0
lines changed

bin/leaf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@ $app->register(Leaf\Console\InteractCommand::class);
3030
$app->register(Leaf\Console\RunCommand::class);
3131
$app->register(Leaf\Console\ViewBuildCommand::class);
3232
$app->register(Leaf\Console\ViewInstallCommand::class);
33+
$app->register(Leaf\Console\DeployCommand::class);
3334

3435
$app->run();

src/DeployCommand.php

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Leaf\Console;
6+
7+
use Leaf\Sprout\Command;
8+
9+
class DeployCommand extends Command
10+
{
11+
protected $signature = 'deploy
12+
{--to=fly : The provider to deploy to (has support for Fly.io, more coming soon)}';
13+
protected $description = 'Setup files needed for deployment to a provider of your choice';
14+
15+
protected function handle(): int
16+
{
17+
if (!sprout()->composer()->json()) {
18+
$this->writeln('<error>No composer.json found in the current directory.</error>');
19+
return 1;
20+
}
21+
22+
if (!sprout()->composer()->hasDependencies()) {
23+
$this->writeln('<info>Installing dependencies...</info>');
24+
25+
if (!sprout()->composer()->install()->isSuccessful()) {
26+
$this->writeln('<error>❌ Failed to install dependencies.</error>');
27+
return 1;
28+
}
29+
}
30+
31+
if ($this->isMVCApp()) {
32+
$this->writeln('<info>Building for Leaf MVC...</info>');
33+
}
34+
35+
$provider = $this->option('to');
36+
37+
if ($provider === 'fly' || $provider === 'fly.io') {
38+
$this->writeln('<info>Setting up Fly.io deployment...</info>');
39+
40+
if (!$this->setupFlyDeployment()) {
41+
$this->writeln('<error>❌ Failed to set up Fly.io deployment.</error>');
42+
return 1;
43+
}
44+
} else {
45+
$this->writeln("<info>Deploy does not support $provider, but we are working on it 🤧...</info>");
46+
}
47+
48+
return 0;
49+
}
50+
51+
protected function setupFlyDeployment()
52+
{
53+
$appDir = getcwd();
54+
$appName = strtolower(basename($appDir));
55+
56+
if (!\Leaf\FS\File::exists("$appDir/fly.toml")) {
57+
$this->writeln('<info>Writing fly deploy files...</info>');
58+
59+
if (
60+
\Leaf\FS\Directory::copy(
61+
__DIR__ . '/themes/fly',
62+
$appDir,
63+
['recursive' => true]
64+
)
65+
) {
66+
$this->writeln('<info>Deployment files setup!</info>');
67+
$appName = $this->getEnvValue('APP_NAME', "$appDir/.env");
68+
$appRegion = $this->getEnvValue('APP_PROD_REGION', "$appDir/.env");
69+
70+
\Leaf\FS\File::create(
71+
"$appDir/storage/deployments.yml",
72+
"appName: $appName\nregion: $appRegion\ndeployed: false",
73+
['recursive' => true]
74+
);
75+
76+
\Leaf\FS\File::write(
77+
"$appDir/fly.toml",
78+
function ($content) use ($appRegion, $appName) {
79+
return str_replace(
80+
['LEAF-APP-NAME', 'LEAF-APP-REGION'],
81+
[$appName, $appRegion],
82+
$content
83+
);
84+
}
85+
);
86+
87+
return true;
88+
} else {
89+
$this->writeln('<error>❌ Failed to write deployment files.</error>');
90+
return false;
91+
}
92+
}
93+
94+
if (\Leaf\FS\File::exists("$appDir/storage/deployments.yml")) {
95+
$deployConfig = \Leaf\FS\File::read("$appDir/storage/deployments.yml");
96+
97+
preg_match('/appName:\s*(.+)/', $deployConfig, $appNameMatch);
98+
preg_match('/region:\s*(.+)/', $deployConfig, $regionMatch);
99+
100+
$appName = trim($appNameMatch[1] ?? '');
101+
$appRegion = trim($regionMatch[1] ?? '');
102+
103+
if (strpos(\Leaf\FS\File::read("$appDir/storage/deployments.yml"), 'deployed: false') !== false) {
104+
if (
105+
sprout()
106+
->process("fly launch --now --auto-confirm --copy-config --region $appRegion --name $appName")
107+
->setTimeout(null)
108+
->run() === 0
109+
) {
110+
\Leaf\FS\File::write("$appDir/storage/deployments.yml", function ($content) {
111+
return str_replace('deployed: false', 'deployed: true', $content);
112+
});
113+
114+
return true;
115+
}
116+
117+
return false;
118+
}
119+
120+
return sprout()
121+
->process('fly deploy --yes')
122+
->setTimeout(null)
123+
->run() === 0 ? true : false;
124+
}
125+
126+
return false;
127+
}
128+
129+
protected function getEnvValue($key, $envPath = __DIR__ . '/.env')
130+
{
131+
if (!file_exists($envPath)) {
132+
return null;
133+
}
134+
135+
$pattern = '/^' . preg_quote($key) . '\s*=\s*(.*)$/m';
136+
$envContent = file_get_contents($envPath);
137+
138+
if (preg_match($pattern, $envContent, $matches)) {
139+
$value = trim($matches[1]);
140+
141+
if (str_starts_with($value, '"') && str_ends_with($value, '"')) {
142+
$value = substr($value, 1, -1);
143+
} elseif (str_starts_with($value, "'") && str_ends_with($value, "'")) {
144+
$value = substr($value, 1, -1);
145+
}
146+
147+
return $value;
148+
}
149+
150+
return null;
151+
}
152+
153+
protected function isMVCApp()
154+
{
155+
$directory = getcwd();
156+
157+
return is_dir("$directory/app/views") && file_exists("$directory/leaf") && is_dir("$directory/public");
158+
}
159+
}

src/themes/fly/.dockerignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/.git
2+
/node_modules
3+
.dockerignore
4+
.env
5+
Dockerfile
6+
fly.toml

src/themes/fly/.fly/entrypoint.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env bash
2+
3+
# Run user scripts, if they exist
4+
for f in /var/www/html/.fly/scripts/*.sh; do
5+
# Bail out this loop if any script exits with non-zero status code
6+
bash "$f" || break
7+
done
8+
9+
chown -R www-data:www-data /var/www/html
10+
11+
if [ $# -gt 0 ]; then
12+
# If we passed a command, run it as root
13+
exec "$@"
14+
else
15+
exec supervisord -c /etc/supervisor/supervisord.conf
16+
fi

0 commit comments

Comments
 (0)