more granular install for self upgrade

Signed-off-by: Andy Miller <rhuk@mac.com>
This commit is contained in:
Andy Miller
2025-10-17 18:18:53 -06:00
parent 920642411c
commit 44fd1172b8
4 changed files with 73 additions and 14 deletions

View File

@@ -71,6 +71,8 @@ class SafeUpgradeService
'cache',
'user',
];
/** @var callable|null */
private $progressCallback = null;
/**
* @param array $options
@@ -170,6 +172,7 @@ class SafeUpgradeService
// Copy extracted package into staging area.
Folder::rcopy($extractedPath, $packagePath, true);
$this->reportProgress('installing', 'Preparing staged package...', null);
$this->carryOverRootDotfiles($packagePath);
@@ -182,6 +185,7 @@ class SafeUpgradeService
throw new RuntimeException('Staged package does not contain any files to promote.');
}
$this->reportProgress('snapshot', 'Creating backup snapshot...', null);
$this->createBackupSnapshot($entries, $backupPath);
$this->syncGitDirectory($this->rootPath, $backupPath);
@@ -190,6 +194,8 @@ class SafeUpgradeService
Folder::create(dirname($manifestPath));
file_put_contents($manifestPath, json_encode($manifest, JSON_PRETTY_PRINT));
$this->reportProgress('installing', 'Copying update files...', null);
try {
$this->copyEntries($entries, $packagePath, $this->rootPath);
} catch (Throwable $e) {
@@ -198,6 +204,7 @@ class SafeUpgradeService
throw new RuntimeException('Failed to promote staged Grav release.', 0, $e);
}
$this->reportProgress('finalizing', 'Finalizing upgrade...', null);
$this->syncGitDirectory($backupPath, $this->rootPath);
$this->persistManifest($manifest);
$this->pruneOldSnapshots();
@@ -274,6 +281,20 @@ class SafeUpgradeService
}
}
public function setProgressCallback(?callable $callback): self
{
$this->progressCallback = $callback;
return $this;
}
private function reportProgress(string $stage, string $message, ?int $percent = null, array $extra = []): void
{
if ($this->progressCallback) {
($this->progressCallback)($stage, $message, $percent, $extra);
}
}
/**
* Roll back to the most recent snapshot.
*
@@ -300,6 +321,7 @@ class SafeUpgradeService
throw new RuntimeException('Rollback snapshot entries are missing from the manifest.');
}
$this->reportProgress('rollback', 'Restoring snapshot...', null);
$this->copyEntries($entries, $backupPath, $this->rootPath);
$this->syncGitDirectory($backupPath, $this->rootPath);
$this->markRollback($manifest['id']);

View File

@@ -44,6 +44,8 @@ class SelfupgradeCommand extends GpmCommand
private $tmp;
/** @var Upgrader */
private $upgrader;
/** @var string|null */
private $lastProgressMessage = null;
/** @var string */
protected $all_yes;
@@ -438,6 +440,7 @@ class SelfupgradeCommand extends GpmCommand
private function upgrade(): bool
{
$io = $this->getIO();
$this->lastProgressMessage = null;
$this->upgradeGrav($this->file);
@@ -496,14 +499,24 @@ class SelfupgradeCommand extends GpmCommand
*/
private function upgradeGrav(string $zip): void
{
$io = $this->getIO();
try {
$io->write("\x0D |- Extracting update... ");
$folder = Installer::unZip($zip, $this->tmp . '/zip');
if ($folder === false) {
throw new RuntimeException(Installer::lastErrorMsg());
}
$io->write("\x0D");
$io->writeln(' |- Extracting update... <green>ok</green> ');
$script = $folder . '/system/install.php';
if ((file_exists($script) && $install = include $script) && is_callable($install)) {
if (is_object($install) && method_exists($install, 'setProgressCallback')) {
$install->setProgressCallback(function (string $stage, string $message, ?int $percent = null, array $extra = []) {
$this->handleServiceProgress($stage, $message, $percent);
});
}
$install($zip);
} else {
throw new RuntimeException('Uploaded archive file is not a valid Grav update package');
@@ -513,6 +526,17 @@ class SelfupgradeCommand extends GpmCommand
}
}
private function handleServiceProgress(string $stage, string $message, ?int $percent = null, array $extra = []): void
{
if ($this->lastProgressMessage === $message) {
return;
}
$this->lastProgressMessage = $message;
$io = $this->getIO();
$io->writeln(sprintf(' |- %s', $message));
}
private function ensureExecutablePermissions(): void
{
$executables = [

View File

@@ -122,6 +122,8 @@ final class Install
/** @var static */
private static $instance;
/** @var callable|null */
private $progressCallback = null;
/**
* @return static
@@ -187,6 +189,20 @@ ERR;
$this->finalize();
}
public function setProgressCallback(?callable $callback): self
{
$this->progressCallback = $callback;
return $this;
}
private function relayProgress(string $stage, string $message, ?int $percent = null): void
{
if ($this->progressCallback) {
($this->progressCallback)($stage, $message, $percent);
}
}
/**
* NOTE: This method can only be called after $grav['plugins']->init().
*
@@ -273,6 +289,11 @@ ERR;
}
$service = new SafeUpgradeService($options);
if ($this->progressCallback) {
$service->setProgressCallback(function (string $stage, string $message, ?int $percent = null, array $extra = []) {
$this->relayProgress($stage, $message, $percent);
});
}
$service->promote($this->location, $this->getVersion(), $this->ignores);
Installer::setError(Installer::OK);
} else {

View File

@@ -91,10 +91,9 @@ class SafeUpgradeServiceTest extends \Codeception\TestCase\Test
public function testPromoteAndRollback(): void
{
[$root, $staging, $manifestStore] = $this->prepareLiveEnvironment();
[$root, $manifestStore] = $this->prepareLiveEnvironment();
$service = new SafeUpgradeService([
'root' => $root,
'staging_root' => $staging,
'manifest_store' => $manifestStore,
]);
@@ -102,7 +101,7 @@ class SafeUpgradeServiceTest extends \Codeception\TestCase\Test
$manifest = $service->promote($package, '1.8.0', ['backup', 'cache', 'images', 'logs', 'tmp', 'user']);
self::assertFileExists($root . '/system/new.txt');
self::assertFileDoesNotExist($root . '/ORIGINAL');
self::assertFileExists($root . '/ORIGINAL');
$manifestFile = $manifestStore . '/' . $manifest['id'] . '.json';
self::assertFileExists($manifestFile);
@@ -112,17 +111,14 @@ class SafeUpgradeServiceTest extends \Codeception\TestCase\Test
self::assertFileExists($root . '/ORIGINAL');
self::assertFileDoesNotExist($root . '/system/new.txt');
$snapshots = glob($staging . '/snapshot-*');
self::assertNotEmpty($snapshots);
self::assertEmpty(glob($staging . '/stage-*'));
self::assertDirectoryExists($manifest['backup_path']);
}
public function testPrunesOldSnapshots(): void
{
[$root, $staging, $manifestStore] = $this->prepareLiveEnvironment();
[$root, $manifestStore] = $this->prepareLiveEnvironment();
$service = new SafeUpgradeService([
'root' => $root,
'staging_root' => $staging,
'manifest_store' => $manifestStore,
]);
@@ -148,7 +144,6 @@ class SafeUpgradeServiceTest extends \Codeception\TestCase\Test
$service = new SafeUpgradeService([
'root' => $root,
'staging_root' => $this->tmpDir . '/staging',
]);
$method = new ReflectionMethod(SafeUpgradeService::class, 'detectPsrLogConflicts');
@@ -177,7 +172,6 @@ PHP;
$service = new SafeUpgradeService([
'root' => $root,
'staging_root' => $this->tmpDir . '/staging',
]);
$method = new ReflectionMethod(SafeUpgradeService::class, 'detectMonologConflicts');
@@ -198,7 +192,6 @@ PHP;
$service = new SafeUpgradeService([
'root' => $root,
'staging_root' => $this->tmpDir . '/staging',
]);
$service->clearRecoveryFlag();
@@ -206,12 +199,11 @@ PHP;
}
/**
* @return array{0:string,1:string,2:string}
* @return array{0:string,1:string}
*/
private function prepareLiveEnvironment(): array
{
$root = $this->tmpDir . '/root';
$staging = $this->tmpDir . '/staging';
$manifestStore = $root . '/user/data/upgrades';
Folder::create($root . '/user/plugins/sample');
@@ -221,7 +213,7 @@ PHP;
file_put_contents($root . '/user/plugins/sample/blueprints.yaml', "name: Sample Plugin\nversion: 1.0.0\n");
file_put_contents($root . '/user/plugins/sample/composer.json', json_encode(['require' => ['php' => '^8.0']], JSON_PRETTY_PRINT));
return [$root, $staging, $manifestStore];
return [$root, $manifestStore];
}
/**