mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-09-03 05:41:07 +00:00
feat(operations): fresh platform-operations implementation
Full rewrite of the installer (incl. the finish step), self-update pipeline, settings store, cron webhook, dashboard/bootstrap surface, environment manager, wizard login, config endpoint and shared helpers. Byte-compatible public interfaces and observable behavior, including documented legacy quirks; verified by the pilot behavioral suite and the full test suite.
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
namespace App\Platform\Operations\Console;
|
||||
|
||||
use App\Platform\Operations\Update\Updater;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
/**
|
||||
* Applies a release from the command line.
|
||||
*
|
||||
* Walks the same pipeline the admin UI drives, announcing every stage and
|
||||
* bailing out on the first one that fails. Containerized installs are told to
|
||||
* pull a new image instead.
|
||||
*/
|
||||
class UpdateCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Version this instance runs right now.
|
||||
*/
|
||||
public $installed;
|
||||
|
||||
/**
|
||||
* Version being installed, or false when there is nothing to do.
|
||||
*/
|
||||
public $version;
|
||||
|
||||
/**
|
||||
* Whatever the release-server check handed back.
|
||||
*/
|
||||
public $response;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'core:update';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Automatically update your InvoiceShelf Core App';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
set_time_limit(3600); // 1 hour
|
||||
|
||||
if (config('invoiceshelf.containerized')) {
|
||||
$this->error('The in-app updater is disabled in containerized installs. Upgrade with `docker compose pull`.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->installed = $this->getInstalledVersion();
|
||||
$this->response = $this->getLatestVersionResponse();
|
||||
$this->version = ($this->response) ? $this->response->version : false;
|
||||
|
||||
if ($this->response == 'extension_required') {
|
||||
$this->info('Sorry! Your system does not meet the minimum requirements for this update.');
|
||||
$this->info('Please retry after installing the required version/extensions.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->version) {
|
||||
$this->info('No Update Available! You are already on the latest version.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->confirm("Do you wish to update to {$this->version}?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
$archive = $this->download();
|
||||
|
||||
if ($archive === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$extracted = $this->unzip($archive);
|
||||
|
||||
if ($extracted === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->copyFiles($extracted)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$removals = $this->response->deleted_files ?? null;
|
||||
|
||||
if (! empty($removals) && ! $this->deleteFiles($removals)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->migrateUpdate() || ! $this->finish()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info('Successfully updated to '.$this->version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the running version off the file shipped with the release.
|
||||
*/
|
||||
public function getInstalledVersion()
|
||||
{
|
||||
return preg_replace('~[\r\n]+~', '', File::get(base_path('version.md')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the release server what is available and grade the requirements.
|
||||
*
|
||||
* @return object|string|false the release, 'extension_required' when this
|
||||
* machine falls short, false when there is
|
||||
* nothing newer or the check failed
|
||||
*/
|
||||
public function getLatestVersionResponse()
|
||||
{
|
||||
$this->info('Your currently installed version is '.$this->installed);
|
||||
$this->line('');
|
||||
$this->info('Checking for update...');
|
||||
|
||||
try {
|
||||
$response = Updater::checkForUpdate($this->installed);
|
||||
|
||||
if ($response->success) {
|
||||
$extensions = $response->version->extensions;
|
||||
|
||||
$is_required = false;
|
||||
|
||||
foreach ($extensions as $key => $extension) {
|
||||
if (! $extension) {
|
||||
$is_required = true;
|
||||
$this->info('❌ '.$key);
|
||||
}
|
||||
|
||||
$this->info('✅ '.$key);
|
||||
}
|
||||
|
||||
if ($is_required) {
|
||||
return 'extension_required';
|
||||
}
|
||||
|
||||
return $response->version;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the release archive.
|
||||
*/
|
||||
public function download()
|
||||
{
|
||||
return $this->runStep(
|
||||
'Downloading update...',
|
||||
fn () => Updater::download($this->version, 1),
|
||||
'Download exception'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the archive that was just fetched.
|
||||
*/
|
||||
public function unzip($path)
|
||||
{
|
||||
return $this->runStep(
|
||||
'Unzipping update package...',
|
||||
fn () => Updater::unzip($path),
|
||||
'Unzipping exception'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lay the extracted release over the installation.
|
||||
*/
|
||||
public function copyFiles($path)
|
||||
{
|
||||
return $this->runStep('Copying update files...', fn () => Updater::copyFiles($path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the files the release server flagged as gone.
|
||||
*/
|
||||
public function deleteFiles($files)
|
||||
{
|
||||
return $this->runStep('Deleting unused old files...', fn () => Updater::deleteFiles($files));
|
||||
}
|
||||
|
||||
/**
|
||||
* Bring the database schema in line with the new code.
|
||||
*/
|
||||
public function migrateUpdate()
|
||||
{
|
||||
return $this->runStep('Running Migrations...', fn () => Updater::migrateUpdate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Stamp the new version and fire the completion event.
|
||||
*/
|
||||
public function finish()
|
||||
{
|
||||
return $this->runStep('Finishing update...', fn () => Updater::finishUpdate($this->installed, $this->version));
|
||||
}
|
||||
|
||||
/**
|
||||
* Announce a stage, run it, and report what came back.
|
||||
*
|
||||
* Without $pathExpected the stage is judged on whether it threw; with it,
|
||||
* the stage must hand back a path and that path becomes the return value.
|
||||
*
|
||||
* @return string|bool
|
||||
*/
|
||||
private function runStep(string $announcement, callable $stage, ?string $pathExpected = null)
|
||||
{
|
||||
$this->info($announcement);
|
||||
|
||||
try {
|
||||
$outcome = $stage();
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($pathExpected === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! is_string($outcome)) {
|
||||
$this->error($pathExpected);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $outcome;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user