diff --git a/bin/restore b/bin/restore index 7de81cfcf..b63d2d88d 100755 --- a/bin/restore +++ b/bin/restore @@ -66,11 +66,6 @@ function parseArguments(array $args): array $options = []; foreach (array_slice($args, 1) as $arg) { - if (strncmp($arg, '--staging-root=', 15) === 0) { - $options['staging_root'] = substr($arg, 15); - continue; - } - if (substr($arg, 0, 2) === '--') { echo "Unknown option: {$arg}\n"; exit(1); @@ -89,50 +84,12 @@ function parseArguments(array $args): array /** * @return string|null */ -function readConfiguredStagingRoot(): ?string -{ - $configFiles = [ - GRAV_ROOT . '/user/config/system.yaml', - GRAV_ROOT . '/system/config/system.yaml' - ]; - - foreach ($configFiles as $file) { - if (!is_file($file)) { - continue; - } - - try { - $data = Yaml::parseFile($file); - } catch (\Throwable $e) { - continue; - } - - if (!is_array($data)) { - continue; - } - - $current = $data['system']['updates']['staging_root'] ?? null; - if (null !== $current && $current !== '') { - return $current; - } - } - - return null; -} - /** * @param array $options * @return SafeUpgradeService */ function createUpgradeService(array $options): SafeUpgradeService { - $config = readConfiguredStagingRoot(); - if ($config !== null && empty($options['staging_root'])) { - $options['staging_root'] = $config; - } elseif (isset($options['staging_root']) && $options['staging_root'] === '') { - unset($options['staging_root']); - } - $options['root'] = GRAV_ROOT; return new SafeUpgradeService($options); diff --git a/system/UPGRADE_PROTOTYPE.md b/system/UPGRADE_PROTOTYPE.md index 2047ee284..6444a4f26 100644 --- a/system/UPGRADE_PROTOTYPE.md +++ b/system/UPGRADE_PROTOTYPE.md @@ -16,13 +16,13 @@ This document tracks the design decisions behind the new self-upgrade prototype - Refresh GPM metadata and require all plugins/themes to be on their latest compatible release. - Scan plugin `composer.json` files for dependencies that are known to break under Grav 1.8 (eg. `psr/log` < 3) and surface actionable warnings. 2. **Stage** - - Download the Grav update archive into a staging area outside the live tree (`{parent}/grav-upgrades/{timestamp}`). + - Download the Grav update archive into a staging area (`tmp://grav-snapshots/{timestamp}`). - Extract the package, then write a manifest describing the target version, PHP info, and enabled packages. - Snapshot the live `user/` directory and relevant metadata into the same stage folder. 3. **Promote** - - Switch the installation by renaming the live tree to a rollback folder and promoting the staged tree into place via atomic renames. + - Copy the staged package into place, overwriting Grav core files while leaving hydrated user content intact. - Clear caches in the staged tree before promotion. - - Run Grav CLI smoke checks (`bin/grav check`) while still holding maintenance state; swap back automatically on failure. + - Run Grav CLI smoke checks (`bin/grav check`) while still holding maintenance state; restore from the snapshot automatically on failure. 4. **Finalize** - Record the manifest under `user/data/upgrades`. - Resume normal traffic by removing the maintenance flag. @@ -46,4 +46,3 @@ This document tracks the design decisions behind the new self-upgrade prototype - Finalize compatibility heuristics (initial pass focuses on `psr/log` and removed logging APIs). - UX polish for the Recovery UI (initial prototype will expose basic actions only). - Decide retention policy for old manifests and snapshots (prototype keeps the most recent three). - diff --git a/system/blueprints/config/system.yaml b/system/blueprints/config/system.yaml index 4684d5da5..d1c760e72 100644 --- a/system/blueprints/config/system.yaml +++ b/system/blueprints/config/system.yaml @@ -1596,14 +1596,6 @@ form: validate: type: bool - updates.staging_root: - type: text - label: PLUGIN_ADMIN.SAFE_UPGRADE_STAGING - help: PLUGIN_ADMIN.SAFE_UPGRADE_STAGING_HELP - placeholder: '/absolute/path/to/grav-upgrades' - validate: - type: string - http_section: type: section title: PLUGIN_ADMIN.HTTP_SECTION @@ -1930,4 +1922,3 @@ form: # # pages.type: # type: hidden - diff --git a/system/config/system.yaml b/system/config/system.yaml index 1fcb1cf3b..3f9e32b52 100644 --- a/system/config/system.yaml +++ b/system/config/system.yaml @@ -205,7 +205,6 @@ gpm: updates: safe_upgrade: true # Enable guarded staging+rollback pipeline for Grav self-updates - staging_root: '' # Optional absolute path for staging backups (default: /grav-upgrades) http: method: auto # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL diff --git a/system/languages/en.yaml b/system/languages/en.yaml index 973199ff3..3b34a93db 100644 --- a/system/languages/en.yaml +++ b/system/languages/en.yaml @@ -124,5 +124,3 @@ PLUGIN_ADMIN: UPDATES_SECTION: Updates SAFE_UPGRADE: Safe self-upgrade SAFE_UPGRADE_HELP: When enabled, Grav core updates use staged installation with automatic rollback support. - SAFE_UPGRADE_STAGING: Staging directory - SAFE_UPGRADE_STAGING_HELP: Optional absolute path for storing upgrade backups. Leave empty to use the default inside the parent directory. diff --git a/system/src/Grav/Common/Upgrade/SafeUpgradeService.php b/system/src/Grav/Common/Upgrade/SafeUpgradeService.php index c334c5567..7a43f4c20 100644 --- a/system/src/Grav/Common/Upgrade/SafeUpgradeService.php +++ b/system/src/Grav/Common/Upgrade/SafeUpgradeService.php @@ -12,6 +12,7 @@ namespace Grav\Common\Upgrade; use DirectoryIterator; use Grav\Common\Filesystem\Folder; use Grav\Common\GPM\GPM; +use Grav\Common\Grav; use Grav\Common\Yaml; use InvalidArgumentException; use RuntimeException; @@ -55,11 +56,11 @@ class SafeUpgradeService /** @var string */ private $rootPath; /** @var string */ - private $parentDir; - /** @var string */ private $stagingRoot; /** @var string */ private $manifestStore; + /** @var \Grav\Common\Config\ConfigInterface|null */ + private $config; /** @var array */ private $ignoredDirs = [ @@ -70,6 +71,8 @@ class SafeUpgradeService 'cache', 'user', ]; + /** @var callable|null */ + private $progressCallback = null; /** * @param array $options @@ -78,29 +81,32 @@ class SafeUpgradeService { $root = $options['root'] ?? GRAV_ROOT; $this->rootPath = rtrim($root, DIRECTORY_SEPARATOR); - $this->parentDir = $options['parent_dir'] ?? dirname($this->rootPath); + $this->config = $options['config'] ?? null; - $candidates = []; - if (!empty($options['staging_root'])) { - $candidates[] = $options['staging_root']; + $locator = null; + try { + $locator = Grav::instance()['locator'] ?? null; + } catch (Throwable $e) { + $locator = null; } - $candidates[] = $this->parentDir . DIRECTORY_SEPARATOR . 'grav-upgrades'; - if (getenv('HOME')) { - $candidates[] = getenv('HOME') . DIRECTORY_SEPARATOR . 'grav-upgrades'; - } - $candidates[] = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'grav-upgrades'; - $this->stagingRoot = null; - foreach ($candidates as $candidate) { - $resolved = $this->resolveStagingPath($candidate); - if ($resolved) { - $this->stagingRoot = $resolved; - break; + $primary = null; + if ($locator && method_exists($locator, 'findResource')) { + try { + $primary = $locator->findResource('tmp://grav-snapshots', true, true); + } catch (Throwable $e) { + $primary = null; } } + if (!$primary) { + $primary = $this->rootPath . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR . 'grav-snapshots'; + } + + $this->stagingRoot = $this->resolveStagingPath($primary); + if (null === $this->stagingRoot) { - throw new RuntimeException('Unable to locate writable staging directory. Configure system.updates.staging_root or adjust permissions.'); + throw new RuntimeException('Unable to locate writable staging directory. Ensure tmp://grav-snapshots is writable.'); } $this->manifestStore = $options['manifest_store'] ?? ($this->rootPath . DIRECTORY_SEPARATOR . 'user' . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'upgrades'); if (isset($options['ignored_dirs']) && is_array($options['ignored_dirs'])) { @@ -160,12 +166,13 @@ class SafeUpgradeService $stageId = uniqid('stage-', false); $stagePath = $this->stagingRoot . DIRECTORY_SEPARATOR . $stageId; $packagePath = $stagePath . DIRECTORY_SEPARATOR . 'package'; - $backupPath = $this->stagingRoot . DIRECTORY_SEPARATOR . 'rollback-' . $stageId; + $backupPath = $this->stagingRoot . DIRECTORY_SEPARATOR . 'snapshot-' . $stageId; Folder::create($packagePath); // Copy extracted package into staging area. Folder::rcopy($extractedPath, $packagePath, true); + $this->reportProgress('installing', 'Preparing staged package...', null); $this->carryOverRootDotfiles($packagePath); @@ -173,13 +180,32 @@ class SafeUpgradeService $this->hydrateIgnoredDirectories($packagePath, $ignores); $this->carryOverRootFiles($packagePath, $ignores); - $manifest = $this->buildManifest($stageId, $targetVersion, $packagePath, $backupPath); + $entries = $this->collectPackageEntries($packagePath); + if (!$entries) { + 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); + + $manifest = $this->buildManifest($stageId, $targetVersion, $packagePath, $backupPath, $entries); $manifestPath = $stagePath . DIRECTORY_SEPARATOR . 'manifest.json'; Folder::create(dirname($manifestPath)); file_put_contents($manifestPath, json_encode($manifest, JSON_PRETTY_PRINT)); - // Promote staged package into place. - $this->promoteStagedTree($packagePath, $backupPath); + $this->reportProgress('installing', 'Copying update files...', null); + + try { + $this->copyEntries($entries, $packagePath, $this->rootPath); + } catch (Throwable $e) { + $this->copyEntries($entries, $backupPath, $this->rootPath); + $this->syncGitDirectory($backupPath, $this->rootPath); + 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(); Folder::delete($stagePath); @@ -187,6 +213,88 @@ class SafeUpgradeService return $manifest; } + private function collectPackageEntries(string $packagePath): array + { + $entries = []; + $iterator = new DirectoryIterator($packagePath); + foreach ($iterator as $fileinfo) { + if ($fileinfo->isDot()) { + continue; + } + + $entries[] = $fileinfo->getFilename(); + } + + sort($entries); + + return $entries; + } + + private function createBackupSnapshot(array $entries, string $backupPath): void + { + Folder::create($backupPath); + $this->copyEntries($entries, $this->rootPath, $backupPath); + } + + private function copyEntries(array $entries, string $sourceBase, string $targetBase): void + { + foreach ($entries as $entry) { + $source = $sourceBase . DIRECTORY_SEPARATOR . $entry; + if (!is_file($source) && !is_dir($source) && !is_link($source)) { + continue; + } + + $destination = $targetBase . DIRECTORY_SEPARATOR . $entry; + $this->removeEntry($destination); + + if (is_link($source)) { + Folder::create(dirname($destination)); + if (!@symlink(readlink($source), $destination)) { + throw new RuntimeException(sprintf('Failed to replicate symlink "%s".', $source)); + } + } elseif (is_dir($source)) { + Folder::create(dirname($destination)); + Folder::rcopy($source, $destination, true); + } else { + Folder::create(dirname($destination)); + if (!@copy($source, $destination)) { + throw new RuntimeException(sprintf('Failed to copy file "%s" to "%s".', $source, $destination)); + } + $perm = @fileperms($source); + if ($perm !== false) { + @chmod($destination, $perm & 0777); + } + $mtime = @filemtime($source); + if ($mtime !== false) { + @touch($destination, $mtime); + } + } + } + } + + private function removeEntry(string $path): void + { + if (is_link($path) || is_file($path)) { + @unlink($path); + } elseif (is_dir($path)) { + Folder::delete($path); + } + } + + 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. * @@ -205,15 +313,18 @@ class SafeUpgradeService throw new RuntimeException('Rollback snapshot is no longer available.'); } - // Put the current tree aside before flip. - $rotated = $this->rotateCurrentTree(); - - $this->promoteBackup($backupPath); - $this->syncGitDirectory($rotated, $this->rootPath); - $this->markRollback($manifest['id']); - if ($rotated && is_dir($rotated)) { - Folder::delete($rotated); + $entries = $manifest['entries'] ?? []; + if (!$entries) { + $entries = $this->collectPackageEntries($backupPath); } + if (!$entries) { + 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']); return $manifest; } @@ -278,6 +389,9 @@ class SafeUpgradeService } $slug = basename($path); + if (!$this->isPluginEnabled($slug)) { + continue; + } $rawConstraint = $json['require']['psr/log'] ?? ($json['require-dev']['psr/log'] ?? null); if (!$rawConstraint) { continue; @@ -302,6 +416,34 @@ class SafeUpgradeService return $conflicts; } + protected function isPluginEnabled(string $slug): bool + { + if ($this->config) { + try { + $value = $this->config->get("plugins.{$slug}.enabled"); + if ($value !== null) { + return (bool)$value; + } + } catch (Throwable $e) { + // ignore and fall back to file checks + } + } + + $configPath = $this->rootPath . '/user/config/plugins/' . $slug . '.yaml'; + if (is_file($configPath)) { + try { + $data = Yaml::parseFile($configPath); + if (is_array($data) && array_key_exists('enabled', $data)) { + return (bool)$data['enabled']; + } + } catch (Throwable $e) { + // ignore parse errors and treat as enabled + } + } + + return true; + } + /** * Detect usage of deprecated Monolog `add*` methods removed in newer releases. * @@ -314,6 +456,11 @@ class SafeUpgradeService $pattern = '/->add(?:Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)\s*\(/i'; foreach ($pluginRoots as $path) { + $slug = basename($path); + if (!$this->isPluginEnabled($slug)) { + continue; + } + $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS) ); @@ -330,7 +477,6 @@ class SafeUpgradeService } if (preg_match($pattern, $contents, $match)) { - $slug = basename($path); $relative = str_replace($this->rootPath . '/', '', $file->getPathname()); $conflicts[$slug][] = [ 'file' => $relative, @@ -481,7 +627,7 @@ class SafeUpgradeService * @param string $backupPath * @return array */ - private function buildManifest(string $stageId, string $targetVersion, string $packagePath, string $backupPath): array + private function buildManifest(string $stageId, string $targetVersion, string $packagePath, string $backupPath, array $entries): array { $plugins = []; $pluginRoots = glob($this->rootPath . '/user/plugins/*', GLOB_ONLYDIR) ?: []; @@ -518,65 +664,11 @@ class SafeUpgradeService 'php_version' => PHP_VERSION, 'package_path' => $packagePath, 'backup_path' => $backupPath, + 'entries' => array_values($entries), 'plugins' => $plugins, ]; } - /** - * Promote staged package by swapping directory names. - * - * @param string $packagePath - * @param string $backupPath - * @return void - */ - private function promoteStagedTree(string $packagePath, string $backupPath): void - { - $liveRoot = $this->rootPath; - Folder::create(dirname($backupPath)); - - if (!rename($liveRoot, $backupPath)) { - throw new RuntimeException('Failed to move current Grav directory into backup.'); - } - - if (!rename($packagePath, $liveRoot)) { - // Attempt to restore live tree. - rename($backupPath, $liveRoot); - throw new RuntimeException('Failed to promote staged Grav release.'); - } - - $this->syncGitDirectory($backupPath, $liveRoot); - } - - /** - * Move existing tree aside to allow rollback swap. - * - * @return void - */ - private function rotateCurrentTree(): string - { - $liveRoot = $this->rootPath; - $target = $this->stagingRoot . DIRECTORY_SEPARATOR . 'rotated-' . time(); - Folder::create($this->stagingRoot); - if (!rename($liveRoot, $target)) { - throw new RuntimeException('Unable to rotate live tree during rollback.'); - } - - return $target; - } - - /** - * Promote a backup tree into the live position. - * - * @param string $backupPath - * @return void - */ - private function promoteBackup(string $backupPath): void - { - if (!rename($backupPath, $this->rootPath)) { - throw new RuntimeException('Rollback failed: unable to move backup into live position.'); - } - } - /** * Ensure Git metadata is retained after stage promotion. * @@ -633,6 +725,8 @@ class SafeUpgradeService $home = getenv('HOME'); if ($home) { $expanded = rtrim($home, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($expanded, '~\/'); + } else { + return null; } } if (!$this->isAbsolutePath($expanded)) { diff --git a/system/src/Grav/Console/Gpm/SelfupgradeCommand.php b/system/src/Grav/Console/Gpm/SelfupgradeCommand.php index 2ca307d49..5d5023176 100644 --- a/system/src/Grav/Console/Gpm/SelfupgradeCommand.php +++ b/system/src/Grav/Console/Gpm/SelfupgradeCommand.php @@ -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; @@ -290,10 +292,8 @@ class SelfupgradeCommand extends GpmCommand $config = null; } - $stagingRoot = $config ? $config->get('system.updates.staging_root') : null; - return new SafeUpgradeService([ - 'staging_root' => $stagingRoot, + 'config' => $config, ]); } @@ -438,6 +438,7 @@ class SelfupgradeCommand extends GpmCommand private function upgrade(): bool { $io = $this->getIO(); + $this->lastProgressMessage = null; $this->upgradeGrav($this->file); @@ -496,14 +497,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... ok '); $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 +524,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 = [ diff --git a/system/src/Grav/Framework/Compat/Monolog/Utils.php b/system/src/Grav/Framework/Compat/Monolog/Utils.php new file mode 100644 index 000000000..529ab06a7 --- /dev/null +++ b/system/src/Grav/Framework/Compat/Monolog/Utils.php @@ -0,0 +1,183 @@ +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(). * @@ -266,13 +282,18 @@ ERR; try { $grav = Grav::instance(); if ($grav && isset($grav['config'])) { - $options['staging_root'] = $grav['config']->get('system.updates.staging_root'); + $options['config'] = $grav['config']; } } catch (\Throwable $e) { // ignore } $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 { diff --git a/tests/unit/Grav/Common/Upgrade/SafeUpgradeServiceTest.php b/tests/unit/Grav/Common/Upgrade/SafeUpgradeServiceTest.php index 43bec9258..9b38e713d 100644 --- a/tests/unit/Grav/Common/Upgrade/SafeUpgradeServiceTest.php +++ b/tests/unit/Grav/Common/Upgrade/SafeUpgradeServiceTest.php @@ -95,10 +95,9 @@ class SafeUpgradeServiceTest extends \PHPUnit\Framework\TestCase public function testPromoteAndRollback(): void { - [$root, $staging, $manifestStore] = $this->prepareLiveEnvironment(); + [$root, $manifestStore] = $this->prepareLiveEnvironment(); $service = new SafeUpgradeService([ 'root' => $root, - 'staging_root' => $staging, 'manifest_store' => $manifestStore, ]); @@ -106,7 +105,7 @@ class SafeUpgradeServiceTest extends \PHPUnit\Framework\TestCase $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); @@ -116,16 +115,14 @@ class SafeUpgradeServiceTest extends \PHPUnit\Framework\TestCase self::assertFileExists($root . '/ORIGINAL'); self::assertFileDoesNotExist($root . '/system/new.txt'); - $rotated = glob($staging . '/rotated-*'); - self::assertEmpty($rotated); + 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, ]); @@ -151,7 +148,6 @@ class SafeUpgradeServiceTest extends \PHPUnit\Framework\TestCase $service = new SafeUpgradeService([ 'root' => $root, - 'staging_root' => $this->tmpDir . '/staging', ]); $method = new ReflectionMethod(SafeUpgradeService::class, 'detectPsrLogConflicts'); @@ -180,7 +176,6 @@ PHP; $service = new SafeUpgradeService([ 'root' => $root, - 'staging_root' => $this->tmpDir . '/staging', ]); $method = new ReflectionMethod(SafeUpgradeService::class, 'detectMonologConflicts'); @@ -201,7 +196,6 @@ PHP; $service = new SafeUpgradeService([ 'root' => $root, - 'staging_root' => $this->tmpDir . '/staging', ]); $service->clearRecoveryFlag(); @@ -209,12 +203,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'); @@ -224,7 +217,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]; } /**