Compare commits
1 Commits
f7fdf3930c
...
fix/stagin
| Author | SHA1 | Date | |
|---|---|---|---|
| f36ee534af |
@@ -14,7 +14,7 @@ services:
|
||||
restart: "no"
|
||||
env_file:
|
||||
- .env
|
||||
command: ["sh", "-c", "php artisan migrate --force --no-interaction && php artisan db:seed --class=ContentSeeder --force; php artisan media:generate-variants --force; true"]
|
||||
command: ["/app/scripts/deploy/initialize-application.sh"]
|
||||
networks:
|
||||
- dokploy-network
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ Promote (manual) → retag same digest as :production (no rebuild)
|
||||
| Piece | Detail |
|
||||
|---|---|
|
||||
| Compose file | [`docker-compose.deploy.yml`](../../docker-compose.deploy.yml) |
|
||||
| Processes | `migrate` (one-shot) → `web` / `queue` / `scheduler` |
|
||||
| Processes | `migrate` initialization (one-shot) → `web` / `queue` / `scheduler` |
|
||||
| Image | `ghcr.io/<owner>/<repo>:<sha>` (+ aliases `:staging`, `:production`) |
|
||||
| Database | Dokploy PostgreSQL **per environment** (not in the app image) |
|
||||
| Media | Cloudflare R2 (`FILESYSTEM_DISK=r2`), separate buckets per environment |
|
||||
@@ -145,7 +145,7 @@ HTTP 404 from `compose.deploy` usually means the compose id is wrong (Applicatio
|
||||
|
||||
1. Waits for workflow `CI` success on push to `main`.
|
||||
2. Builds once; pushes `:<full-sha>` and `:staging`.
|
||||
3. Calls Dokploy `compose.deploy` and polls until done.
|
||||
3. Calls Dokploy `compose.deploy`; initialization migrates, runs `ContentSeeder`, and generates media variants before the app services start.
|
||||
4. Runs [`scripts/deploy/smoke.sh`](../../scripts/deploy/smoke.sh) against `STAGING_URL`.
|
||||
|
||||
### Production (manual)
|
||||
@@ -155,7 +155,11 @@ HTTP 404 from `compose.deploy` usually means the compose id is wrong (Applicatio
|
||||
1. Operator runs **Actions → Promote production**.
|
||||
2. Inputs: full `sha` already on GHCR; `confirm` must be exactly `PRODUCTION`.
|
||||
3. Retags the **same digest** as `:production` (no rebuild).
|
||||
4. Deploys production compose + smoke.
|
||||
4. Deploys production compose; initialization runs migrations only, then smoke.
|
||||
|
||||
The one-shot initializer is a dependency of `web`, `queue`, and `scheduler`. Migration failure is fatal in every environment. In staging, `ContentSeeder` and `media:generate-variants` also run automatically and are fatal: if any initialization command fails, application services do not start. Production and every non-staging environment remain migrate-only.
|
||||
|
||||
When Dokploy reports a failed deployment, [`scripts/deploy/dokploy-deploy.sh`](../../scripts/deploy/dokploy-deploy.sh) prints the latest deployment metadata and fetches the last 1,000 log lines through `deployment.readLogs`. Log retrieval is best-effort and cannot hide the original deployment failure.
|
||||
|
||||
Private repos on GitHub Free do not get Environment required reviewers; human approval is the explicit `workflow_dispatch` + confirmation string. GitHub Pro Environment reviewers are optional later.
|
||||
|
||||
@@ -192,9 +196,9 @@ XDG_CONFIG_HOME=/tmp php artisan tinker --execute="echo \\App\\Models\\User::que
|
||||
|
||||
Never reuse `admin@amare.local` / `password`.
|
||||
|
||||
### Load authorized testimonials after migrations
|
||||
### Load authorized testimonials in production
|
||||
|
||||
After migrations, manually load the five authorized testimonials in **staging**, then repeat in **production**. From Dokploy, open a terminal on the environment's `web` service (or run an equivalent one-off process):
|
||||
Staging receives the five authorized testimonials through the automatic `ContentSeeder`. Production is migrate-only, so manually load them there after migrations. From Dokploy, open a terminal on the production `web` service (or run an equivalent one-off process):
|
||||
|
||||
```bash
|
||||
php artisan db:seed --class='Database\Seeders\TestimonialsSeeder' --force --no-interaction
|
||||
@@ -219,7 +223,7 @@ echo (\$valid ? 'ok' : 'invalid').PHP_EOL;
|
||||
|
||||
Expected output: `ok`.
|
||||
|
||||
Do not run `DatabaseSeeder` or `ContentSeeder` in staging or production: they include local credentials and/or broad demo-content effects. Deployment workflows intentionally remain migrate-only; loading these testimonials is a deliberate manual operation in each environment.
|
||||
Never run `DatabaseSeeder` in staging or production because it creates local credentials. Do not run `ContentSeeder` in production: its broad content fixtures are intended for staging initialization only. Production testimonial loading remains the deliberate, narrowly scoped manual operation above.
|
||||
|
||||
## Backup and restore
|
||||
|
||||
|
||||
@@ -102,6 +102,17 @@ while (( SECONDS < deadline )); do
|
||||
error|failed|failure)
|
||||
echo "Dokploy deployment failed." >&2
|
||||
echo "$latest" | jq . >&2 || true
|
||||
deployment_id="$(echo "$latest" | jq -r '.deploymentId // .id // empty')"
|
||||
|
||||
if [[ -z "$deployment_id" ]]; then
|
||||
echo "Dokploy deployment logs could not be retrieved: deployment metadata has no id." >&2
|
||||
elif deployment_logs="$(api GET "/deployment.readLogs?deploymentId=${deployment_id}&tail=1000")"; then
|
||||
echo "Dokploy deployment logs:" >&2
|
||||
printf '%s\n' "$deployment_logs" >&2
|
||||
else
|
||||
echo "Dokploy deployment logs could not be retrieved; the original deployment failure remains authoritative." >&2
|
||||
fi
|
||||
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
10
scripts/deploy/initialize-application.sh
Executable file
10
scripts/deploy/initialize-application.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
php artisan migrate --force --no-interaction
|
||||
|
||||
if [ "${APP_ENV:-}" = "staging" ]; then
|
||||
php artisan db:seed --class=ContentSeeder --force --no-interaction
|
||||
php artisan media:generate-variants
|
||||
fi
|
||||
112
tests/Unit/DeployInitializationScriptTest.php
Normal file
112
tests/Unit/DeployInitializationScriptTest.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
function runDeployInitializationScript(string $environment, ?string $failingCommand = null, int $failureCode = 1): array
|
||||
{
|
||||
$temporaryDirectory = sys_get_temp_dir().'/amare-deploy-initialization-'.bin2hex(random_bytes(8));
|
||||
$fakeBinDirectory = $temporaryDirectory.'/bin';
|
||||
$commandLog = $temporaryDirectory.'/commands.log';
|
||||
|
||||
mkdir($fakeBinDirectory, 0777, true);
|
||||
|
||||
$fakePhp = <<<'SH'
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
printf '%s\n' "$*" >> "$FAKE_PHP_LOG"
|
||||
|
||||
if [ -n "${FAKE_PHP_FAIL_ON:-}" ] && [ "$*" = "$FAKE_PHP_FAIL_ON" ]; then
|
||||
exit "${FAKE_PHP_FAIL_CODE:-1}"
|
||||
fi
|
||||
SH;
|
||||
|
||||
file_put_contents($fakeBinDirectory.'/php', $fakePhp);
|
||||
chmod($fakeBinDirectory.'/php', 0755);
|
||||
|
||||
$process = new Process(
|
||||
['/bin/sh', dirname(__DIR__, 2).'/scripts/deploy/initialize-application.sh'],
|
||||
dirname(__DIR__, 2),
|
||||
[
|
||||
'APP_ENV' => $environment,
|
||||
'FAKE_PHP_FAIL_CODE' => (string) $failureCode,
|
||||
'FAKE_PHP_FAIL_ON' => $failingCommand ?? '',
|
||||
'FAKE_PHP_LOG' => $commandLog,
|
||||
'PATH' => $fakeBinDirectory.PATH_SEPARATOR.getenv('PATH'),
|
||||
],
|
||||
);
|
||||
|
||||
$process->run();
|
||||
|
||||
$commands = file_exists($commandLog)
|
||||
? file($commandLog, FILE_IGNORE_NEW_LINES)
|
||||
: [];
|
||||
|
||||
unlink($fakeBinDirectory.'/php');
|
||||
if (file_exists($commandLog)) {
|
||||
unlink($commandLog);
|
||||
}
|
||||
rmdir($fakeBinDirectory);
|
||||
rmdir($temporaryDirectory);
|
||||
|
||||
return [$process->getExitCode(), $commands];
|
||||
}
|
||||
|
||||
it('runs migrations, content seeding, and media generation in staging', function (): void {
|
||||
[$exitCode, $commands] = runDeployInitializationScript('staging');
|
||||
|
||||
expect($exitCode)->toBe(0)
|
||||
->and($commands)->toBe([
|
||||
'artisan migrate --force --no-interaction',
|
||||
'artisan db:seed --class=ContentSeeder --force --no-interaction',
|
||||
'artisan media:generate-variants',
|
||||
]);
|
||||
});
|
||||
|
||||
it('runs only migrations outside staging', function (string $environment): void {
|
||||
[$exitCode, $commands] = runDeployInitializationScript($environment);
|
||||
|
||||
expect($exitCode)->toBe(0)
|
||||
->and($commands)->toBe([
|
||||
'artisan migrate --force --no-interaction',
|
||||
]);
|
||||
})->with([
|
||||
'production' => 'production',
|
||||
'another environment' => 'preview',
|
||||
]);
|
||||
|
||||
it('propagates initialization failures and stops later commands', function (
|
||||
string $failingCommand,
|
||||
int $failureCode,
|
||||
array $expectedCommands,
|
||||
): void {
|
||||
[$exitCode, $commands] = runDeployInitializationScript('staging', $failingCommand, $failureCode);
|
||||
|
||||
expect($exitCode)->toBe($failureCode)
|
||||
->and($commands)->toBe($expectedCommands);
|
||||
})->with([
|
||||
'migration failure' => [
|
||||
'artisan migrate --force --no-interaction',
|
||||
41,
|
||||
['artisan migrate --force --no-interaction'],
|
||||
],
|
||||
'content seeding failure' => [
|
||||
'artisan db:seed --class=ContentSeeder --force --no-interaction',
|
||||
42,
|
||||
[
|
||||
'artisan migrate --force --no-interaction',
|
||||
'artisan db:seed --class=ContentSeeder --force --no-interaction',
|
||||
],
|
||||
],
|
||||
'media generation failure' => [
|
||||
'artisan media:generate-variants',
|
||||
43,
|
||||
[
|
||||
'artisan migrate --force --no-interaction',
|
||||
'artisan db:seed --class=ContentSeeder --force --no-interaction',
|
||||
'artisan media:generate-variants',
|
||||
],
|
||||
],
|
||||
]);
|
||||
Reference in New Issue
Block a user