new deployer command
This commit is contained in:
@@ -0,0 +1,425 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Process;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class DatabaseStage extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'db:stage
|
||||||
|
{--force : Skip the confirmation prompt}
|
||||||
|
{--keep : Keep the local dump after completion}';
|
||||||
|
|
||||||
|
protected $description = 'Clone the complete local PostgreSQL database to the stage server';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$local = [
|
||||||
|
'host' => config('database.connections.pgsql.host'),
|
||||||
|
'port' => config('database.connections.pgsql.port'),
|
||||||
|
'database' => config('database.connections.pgsql.database'),
|
||||||
|
'username' => config('database.connections.pgsql.username'),
|
||||||
|
'password' => config('database.connections.pgsql.password'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$stage = [
|
||||||
|
'ssh_host' => env('STAGE_SSH_HOST'),
|
||||||
|
'ssh_user' => env('STAGE_SSH_USER'),
|
||||||
|
'ssh_port' => env('STAGE_SSH_PORT', 22),
|
||||||
|
|
||||||
|
'db_host' => env('STAGE_DB_HOST', '127.0.0.1'),
|
||||||
|
'db_port' => env('STAGE_DB_PORT', 5432),
|
||||||
|
'database' => env('STAGE_DB_DATABASE'),
|
||||||
|
'username' => env('STAGE_DB_USERNAME'),
|
||||||
|
'password' => env('STAGE_DB_PASSWORD'),
|
||||||
|
|
||||||
|
// PostgreSQL administrator used to recreate the stage DB.
|
||||||
|
'admin_username' => env('STAGE_DB_ADMIN_USERNAME', 'postgres'),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
'STAGE_SSH_HOST' => $stage['ssh_host'],
|
||||||
|
'STAGE_SSH_USER' => $stage['ssh_user'],
|
||||||
|
'STAGE_DB_DATABASE' => $stage['database'],
|
||||||
|
'STAGE_DB_USERNAME' => $stage['username'],
|
||||||
|
] as $name => $value) {
|
||||||
|
if (! $value) {
|
||||||
|
$this->error("Missing {$name}.");
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$timestamp = now()->format('Ymd-His');
|
||||||
|
$filename = "wms-{$timestamp}.dump";
|
||||||
|
|
||||||
|
$localDump = storage_path("app/{$filename}");
|
||||||
|
$remoteDump = "/tmp/{$filename}";
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->components->info('WMS PostgreSQL FULL CLONE → Stage');
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
$this->table(
|
||||||
|
['Setting', 'Value'],
|
||||||
|
[
|
||||||
|
['Local database', $local['database']],
|
||||||
|
['Stage server', "{$stage['ssh_user']}@{$stage['ssh_host']}"],
|
||||||
|
['Stage database', $stage['database']],
|
||||||
|
['Stage owner', $stage['username']],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
! $this->option('force') &&
|
||||||
|
! $this->confirm(
|
||||||
|
"This will DROP and completely RECREATE '{$stage['database']}'. Continue?",
|
||||||
|
false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
$this->warn('Cancelled.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------
|
||||||
|
* Ensure local dump directory exists
|
||||||
|
* ---------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
$directory = dirname($localDump);
|
||||||
|
|
||||||
|
if (! is_dir($directory)) {
|
||||||
|
mkdir($directory, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------
|
||||||
|
* Dump COMPLETE local database
|
||||||
|
* ---------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
$this->components->task(
|
||||||
|
'Backing up complete local PostgreSQL database',
|
||||||
|
function () use ($local, $localDump) {
|
||||||
|
$result = Process::env([
|
||||||
|
'PGPASSWORD' => $local['password'],
|
||||||
|
])->timeout(3600)->run([
|
||||||
|
'pg_dump',
|
||||||
|
'--host='.$local['host'],
|
||||||
|
'--port='.$local['port'],
|
||||||
|
'--username='.$local['username'],
|
||||||
|
|
||||||
|
// Custom archive format.
|
||||||
|
'--format=custom',
|
||||||
|
|
||||||
|
// Maximum compression.
|
||||||
|
'--compress=9',
|
||||||
|
|
||||||
|
// Stage user becomes owner.
|
||||||
|
'--no-owner',
|
||||||
|
|
||||||
|
// Do not copy local ACLs/grants.
|
||||||
|
'--no-acl',
|
||||||
|
|
||||||
|
// Include everything in the database.
|
||||||
|
'--file='.$localDump,
|
||||||
|
|
||||||
|
$local['database'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result->failed()) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"pg_dump failed:\n".$result->errorOutput()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! file_exists($localDump) || filesize($localDump) === 0) {
|
||||||
|
throw new RuntimeException('Database dump was not created or is empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$dumpSize = $this->formatBytes(filesize($localDump));
|
||||||
|
|
||||||
|
$this->line(" Dump size: {$dumpSize}");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------
|
||||||
|
* Inspect dump
|
||||||
|
* ---------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
$this->components->task(
|
||||||
|
'Verifying PostgreSQL archive',
|
||||||
|
function () use ($localDump) {
|
||||||
|
$result = Process::run([
|
||||||
|
'pg_restore',
|
||||||
|
'--list',
|
||||||
|
$localDump,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result->failed()) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Invalid PostgreSQL archive:\n".$result->errorOutput()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------
|
||||||
|
* Upload
|
||||||
|
* ---------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
$this->components->task(
|
||||||
|
'Uploading database backup',
|
||||||
|
function () use ($stage, $localDump, $remoteDump) {
|
||||||
|
$result = Process::timeout(3600)->run([
|
||||||
|
'scp',
|
||||||
|
'-P',
|
||||||
|
(string) $stage['ssh_port'],
|
||||||
|
$localDump,
|
||||||
|
"{$stage['ssh_user']}@{$stage['ssh_host']}:{$remoteDump}",
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result->failed()) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Upload failed:\n".$result->errorOutput()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------
|
||||||
|
* DROP + recreate stage database
|
||||||
|
*
|
||||||
|
* Pegasus permits the SSH user to run PostgreSQL commands
|
||||||
|
* through sudo -u postgres.
|
||||||
|
* ---------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
$this->components->task(
|
||||||
|
'Recreating stage database',
|
||||||
|
function () use ($stage) {
|
||||||
|
$database = $this->quoteSqlIdentifier($stage['database']);
|
||||||
|
$owner = $this->quoteSqlIdentifier($stage['username']);
|
||||||
|
|
||||||
|
$terminateDatabase = str_replace(
|
||||||
|
"'",
|
||||||
|
"''",
|
||||||
|
$stage['database']
|
||||||
|
);
|
||||||
|
|
||||||
|
$sql = <<<SQL
|
||||||
|
SELECT pg_terminate_backend(pid)
|
||||||
|
FROM pg_stat_activity
|
||||||
|
WHERE datname = '{$terminateDatabase}'
|
||||||
|
AND pid <> pg_backend_pid();
|
||||||
|
|
||||||
|
DROP DATABASE IF EXISTS {$database};
|
||||||
|
CREATE DATABASE {$database}
|
||||||
|
WITH OWNER {$owner}
|
||||||
|
ENCODING 'UTF8'
|
||||||
|
TEMPLATE template0;
|
||||||
|
SQL;
|
||||||
|
|
||||||
|
$remoteCommand = sprintf(
|
||||||
|
'sudo -u %s psql --set ON_ERROR_STOP=on --dbname=postgres',
|
||||||
|
escapeshellarg($stage['admin_username'])
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = Process::input($sql)
|
||||||
|
->timeout(300)
|
||||||
|
->run([
|
||||||
|
'ssh',
|
||||||
|
'-p',
|
||||||
|
(string) $stage['ssh_port'],
|
||||||
|
"{$stage['ssh_user']}@{$stage['ssh_host']}",
|
||||||
|
$remoteCommand,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result->failed()) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Unable to recreate stage database:\n".
|
||||||
|
$result->errorOutput()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------
|
||||||
|
* Restore COMPLETE archive
|
||||||
|
* ---------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
$this->components->task(
|
||||||
|
'Restoring complete stage database',
|
||||||
|
function () use ($stage, $remoteDump) {
|
||||||
|
$restore = sprintf(
|
||||||
|
'PGPASSWORD=%s pg_restore '.
|
||||||
|
'--host=%s '.
|
||||||
|
'--port=%s '.
|
||||||
|
'--username=%s '.
|
||||||
|
'--dbname=%s '.
|
||||||
|
'--no-owner '.
|
||||||
|
'--no-acl '.
|
||||||
|
'--exit-on-error '.
|
||||||
|
'--verbose '.
|
||||||
|
'%s',
|
||||||
|
escapeshellarg($stage['password']),
|
||||||
|
escapeshellarg($stage['db_host']),
|
||||||
|
escapeshellarg((string) $stage['db_port']),
|
||||||
|
escapeshellarg($stage['username']),
|
||||||
|
escapeshellarg($stage['database']),
|
||||||
|
escapeshellarg($remoteDump),
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = Process::timeout(3600)->run([
|
||||||
|
'ssh',
|
||||||
|
'-p',
|
||||||
|
(string) $stage['ssh_port'],
|
||||||
|
"{$stage['ssh_user']}@{$stage['ssh_host']}",
|
||||||
|
$restore,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result->failed()) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Database restore failed:\n".
|
||||||
|
$result->errorOutput()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------
|
||||||
|
* Verify stage tables
|
||||||
|
* ---------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
$this->components->task(
|
||||||
|
'Verifying restored database',
|
||||||
|
function () use ($stage) {
|
||||||
|
$command = sprintf(
|
||||||
|
'PGPASSWORD=%s psql '.
|
||||||
|
'--host=%s '.
|
||||||
|
'--port=%s '.
|
||||||
|
'--username=%s '.
|
||||||
|
'--dbname=%s '.
|
||||||
|
'--tuples-only '.
|
||||||
|
'--command=%s',
|
||||||
|
escapeshellarg($stage['password']),
|
||||||
|
escapeshellarg($stage['db_host']),
|
||||||
|
escapeshellarg((string) $stage['db_port']),
|
||||||
|
escapeshellarg($stage['username']),
|
||||||
|
escapeshellarg($stage['database']),
|
||||||
|
escapeshellarg(
|
||||||
|
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';"
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = Process::run([
|
||||||
|
'ssh',
|
||||||
|
'-p',
|
||||||
|
(string) $stage['ssh_port'],
|
||||||
|
"{$stage['ssh_user']}@{$stage['ssh_host']}",
|
||||||
|
$command,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result->failed()) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Database verification failed:\n".
|
||||||
|
$result->errorOutput()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = trim($result->output());
|
||||||
|
|
||||||
|
$this->line(" Stage public tables: {$count}");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------
|
||||||
|
* Cleanup
|
||||||
|
* ---------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
Process::run([
|
||||||
|
'ssh',
|
||||||
|
'-p',
|
||||||
|
(string) $stage['ssh_port'],
|
||||||
|
"{$stage['ssh_user']}@{$stage['ssh_host']}",
|
||||||
|
'rm',
|
||||||
|
'-f',
|
||||||
|
$remoteDump,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! $this->option('keep')) {
|
||||||
|
@unlink($localDump);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->components->info(
|
||||||
|
'Stage PostgreSQL database cloned successfully.'
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->line(
|
||||||
|
"Local {$local['database']} → Stage {$stage['database']}"
|
||||||
|
);
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->newLine();
|
||||||
|
$this->components->error('Database clone failed.');
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->warn(
|
||||||
|
"The dump may still exist at {$localDump} and/or {$remoteDump}."
|
||||||
|
);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function quoteSqlIdentifier(string $identifier): string
|
||||||
|
{
|
||||||
|
return '"'.str_replace('"', '""', $identifier).'"';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatBytes(int $bytes): string
|
||||||
|
{
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
|
||||||
|
$bytes = max($bytes, 0);
|
||||||
|
$power = $bytes > 0
|
||||||
|
? min((int) floor(log($bytes, 1024)), count($units) - 1)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return number_format(
|
||||||
|
$bytes / (1024 ** $power),
|
||||||
|
$power === 0 ? 0 : 2
|
||||||
|
).' '.$units[$power];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"php": "^8.3",
|
"php": "^8.3",
|
||||||
"ayeo/gs1_barcode": "^1.0",
|
"ayeo/gs1_barcode": "^1.0",
|
||||||
"barryvdh/laravel-dompdf": "^3.1",
|
"barryvdh/laravel-dompdf": "^3.1",
|
||||||
|
"deployer/deployer": "^8.0",
|
||||||
"filament/filament": "^5.6",
|
"filament/filament": "^5.6",
|
||||||
"filament/tables": "^5.6",
|
"filament/tables": "^5.6",
|
||||||
"guzzlehttp/guzzle": "^7.11",
|
"guzzlehttp/guzzle": "^7.11",
|
||||||
|
|||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Deployer;
|
||||||
|
|
||||||
|
require 'recipe/laravel.php';
|
||||||
|
|
||||||
|
set('writable_use_sudo', true);
|
||||||
|
set('clear_use_sudo', true);
|
||||||
|
set('cleanup_use_sudo', true);
|
||||||
|
set('keep_releases', 1);
|
||||||
|
set('repository', 'git@git.absdev.net:mag/WMS.git');
|
||||||
|
set('node_version', '22.12.0');
|
||||||
|
set('allow_anonymous_stats', false);
|
||||||
|
|
||||||
|
add('shared_files', ['.env']);
|
||||||
|
add('shared_dirs', ['storage']);
|
||||||
|
add('writable_dirs', ['storage', 'bootstrap/cache']);
|
||||||
|
|
||||||
|
host('main')
|
||||||
|
->set('hostname', 'helpdesk.absdev.net')
|
||||||
|
->set('remote_user', 'mag')
|
||||||
|
->set('branch', 'main')
|
||||||
|
->set('deploy_path', '/var/www/helpdesk');
|
||||||
|
|
||||||
|
host('develop')
|
||||||
|
->set('hostname', 'helpdesk.mlba.dev')
|
||||||
|
->set('remote_user', 'mag')
|
||||||
|
->set('branch', 'develop')
|
||||||
|
->set('deploy_path', '/var/www/helpdesk_dev');
|
||||||
|
|
||||||
|
host('stage')
|
||||||
|
->set('hostname', 'stage.wms.mpba.dev')
|
||||||
|
->set('remote_user', 'mag')
|
||||||
|
->set('branch', 'develop')
|
||||||
|
->set('deploy_path', '/var/www/wms/core');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Install the root npm tree, then every module that owns its own lock file,
|
||||||
|
* then run ONE root Vite build. Because Vite is compiling a module source file,
|
||||||
|
* normal Node resolution finds that module's own node_modules first.
|
||||||
|
*/
|
||||||
|
task('build', function (): void {
|
||||||
|
$node = 'export NVM_DIR="$HOME/.nvm"'
|
||||||
|
.' && [ -s "$NVM_DIR/nvm.sh" ]'
|
||||||
|
.' && . "$NVM_DIR/nvm.sh"'
|
||||||
|
.' && nvm use {{node_version}}';
|
||||||
|
|
||||||
|
run(
|
||||||
|
$node
|
||||||
|
.' && cd {{release_path}}'
|
||||||
|
.' && npm ci --no-audit --no-fund'
|
||||||
|
);
|
||||||
|
|
||||||
|
run(
|
||||||
|
$node
|
||||||
|
.' && cd {{release_path}}'
|
||||||
|
.' && for lock in Modules/*/package-lock.json; do'
|
||||||
|
.' [ -f "$lock" ] || continue;'
|
||||||
|
.' module_dir="$(dirname "$lock")";'
|
||||||
|
.' echo "[Modules] npm ci: $module_dir";'
|
||||||
|
.' (cd "$module_dir" && npm ci --no-audit --no-fund);'
|
||||||
|
.' done'
|
||||||
|
);
|
||||||
|
|
||||||
|
run(
|
||||||
|
$node
|
||||||
|
.' && cd {{release_path}}'
|
||||||
|
.' && npm run build'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
task('queue:restart', function (): void {
|
||||||
|
run('cd {{release_path}} && php artisan queue:restart');
|
||||||
|
});
|
||||||
|
|
||||||
|
task('deploy:write_revision_file', function (): void {
|
||||||
|
run('cd {{release_path}} && echo "{{branch}}" > BRANCH');
|
||||||
|
run('cd {{release_path}} && date -u +"%Y-%m-%dT%H:%M:%SZ" > DEPLOYED_AT');
|
||||||
|
});
|
||||||
|
|
||||||
|
task('artisan:clear', function (): void {
|
||||||
|
run('cd {{release_path}} && php artisan view:clear');
|
||||||
|
run('cd {{release_path}} && php artisan optimize:clear');
|
||||||
|
run('cd {{release_path}} && php artisan config:clear');
|
||||||
|
run('cd {{release_path}} && php artisan route:clear');
|
||||||
|
});
|
||||||
|
|
||||||
|
task('services:restart', function (): void {
|
||||||
|
run('sudo -n nginx -t');
|
||||||
|
run('sudo -n systemctl reload nginx');
|
||||||
|
run('sudo -n systemctl restart php8.3-fpm');
|
||||||
|
});
|
||||||
|
|
||||||
|
after('deploy:update_code', 'deploy:write_revision_file');
|
||||||
|
after('deploy:vendors', 'build');
|
||||||
|
after('deploy:symlink', 'artisan:clear');
|
||||||
|
after('deploy:symlink', 'queue:restart');
|
||||||
|
after('deploy:failed', 'deploy:unlock');
|
||||||
|
after('deploy:symlink', 'services:restart');
|
||||||
Reference in New Issue
Block a user