From e9ccd4dfd0d19c932a16aa5a3d7f6ed27a79d55d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCnch?= Date: Wed, 19 Feb 2025 16:49:09 +0100 Subject: [PATCH] add: new version argument to update to specific version --- .github/workflows/phar_build_and_update.yml | 7 +- src/N98/Magento/Command/SelfUpdateCommand.php | 287 ++++++++++++------ 2 files changed, 201 insertions(+), 93 deletions(-) diff --git a/.github/workflows/phar_build_and_update.yml b/.github/workflows/phar_build_and_update.yml index 6f314178..b5a0a346 100644 --- a/.github/workflows/phar_build_and_update.yml +++ b/.github/workflows/phar_build_and_update.yml @@ -68,7 +68,10 @@ jobs: run: composer install --prefer-dist --no-progress --no-suggest - name: Create phar - run: bash ./build.sh + run: bash ./build.sh && php ./n98-magerun2.phar --version && mv ./n98-magerun2.phar ./n98-magerun2.phar.original - name: Test self-update command - run: php ./n98-magerun2.phar self-update --unstable + run: cp ./n98-magerun2.original ./n98-magerun2.phar && php ./n98-magerun2.phar self-update --unstable && ./n98-magerun2.phar --version + + - name: Rollback phar to a specific version + run: cp ./n98-magerun2.phar.original ./n98-magerun2.phar && ./n98-magerun2.phar self-update 7.5.0 && ./n98-magerun2.phar --version diff --git a/src/N98/Magento/Command/SelfUpdateCommand.php b/src/N98/Magento/Command/SelfUpdateCommand.php index 53d92873..0e373e90 100644 --- a/src/N98/Magento/Command/SelfUpdateCommand.php +++ b/src/N98/Magento/Command/SelfUpdateCommand.php @@ -9,6 +9,7 @@ use RuntimeException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -23,12 +24,12 @@ */ class SelfUpdateCommand extends AbstractMagentoCommand { - public const VERSION_TXT_URL_UNSTABLE = 'https://raw.githubusercontent.com/netz98/n98-magerun2/develop/version.txt'; - public const MAGERUN_DOWNLOAD_URL_UNSTABLE = 'https://files.magerun.net/n98-magerun2-dev.phar'; - public const VERSION_TXT_URL_STABLE = 'https://raw.githubusercontent.com/netz98/n98-magerun2/master/version.txt'; - public const MAGERUN_DOWNLOAD_URL_STABLE = 'https://files.magerun.net/n98-magerun2.phar'; - public const CHANGELOG_DOWNLOAD_URL_UNSTABLE = 'https://raw.github.com/netz98/n98-magerun2/develop/CHANGELOG.md'; - public const CHANGELOG_DOWNLOAD_URL_STABLE = 'https://raw.github.com/netz98/n98-magerun2/master/CHANGELOG.md'; + public const VERSION_TXT_URL_UNSTABLE = 'https://raw.githubusercontent.com/netz98/n98-magerun2/develop/version.txt'; + public const MAGERUN_DOWNLOAD_URL_UNSTABLE = 'https://files.magerun.net/n98-magerun2-dev.phar'; + public const VERSION_TXT_URL_STABLE = 'https://raw.githubusercontent.com/netz98/n98-magerun2/master/version.txt'; + public const MAGERUN_DOWNLOAD_URL_STABLE = 'https://files.magerun.net/n98-magerun2.phar'; + public const CHANGELOG_DOWNLOAD_URL_UNSTABLE = 'https://raw.github.com/netz98/n98-magerun2/develop/CHANGELOG.md'; + public const CHANGELOG_DOWNLOAD_URL_STABLE = 'https://raw.github.com/netz98/n98-magerun2/master/CHANGELOG.md'; protected function configure() { @@ -37,14 +38,24 @@ protected function configure() ->setAliases(['selfupdate']) ->addOption('unstable', null, InputOption::VALUE_NONE, 'Load unstable version from develop branch') ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Tests if there is a new version without any update.') - ->setDescription('Updates n98-magerun2.phar to the latest version.') + ->addArgument( + 'version', + InputArgument::OPTIONAL, + 'Version to update to. Can be used to rollback to a previous version.' + ) + ->setDescription('Updates n98-magerun2.phar to the latest or a specified version.') ->setHelp( <<self-update command checks github for newer +The self-update command checks GitHub for newer versions of n98-magerun2 and if found, installs the latest. -php n98-magerun2.phar self-update +Optionally, you can specify a version to rollback or update to: + php n98-magerun2.phar self-update 7.4.0 + +If you want the unstable (develop) branch, use: + + php n98-magerun2.phar self-update --unstable EOT ); } @@ -54,20 +65,22 @@ protected function configure() */ public function isEnabled() { + // Only enable self-update if running in phar mode return $this->getApplication()->isPharMode(); } /** - * @param \Symfony\Component\Console\Input\InputInterface $input - * @param \Symfony\Component\Console\Output\OutputInterface $output - * @return int - * @throws \Exception + * @throws Exception */ protected function execute(InputInterface $input, OutputInterface $output) { - $isDryRun = $input->getOption('dry-run'); + $isDryRun = $input->getOption('dry-run'); + $loadUnstable = $input->getOption('unstable'); + $requestedVersion = $input->getArgument('version'); + $previousVersion = $this->getApplication()->getVersion(); + $localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0]; - $tempFilename = dirname($localFilename) . '/' . basename($localFilename, '.phar') . '-temp.phar'; + $tempFilename = dirname($localFilename) . '/' . basename($localFilename, '.phar') . '-temp.phar'; // check for permissions in local filesystem before start connection process if (!is_writable($tempDirectory = dirname($tempFilename))) { @@ -83,67 +96,113 @@ protected function execute(InputInterface $input, OutputInterface $output) ); } - $loadUnstable = $input->getOption('unstable'); + // If the user wants the unstable (develop) version, just keep original logic if ($loadUnstable) { - $versionTxtUrl = self::VERSION_TXT_URL_UNSTABLE; + $versionTxtUrl = self::VERSION_TXT_URL_UNSTABLE; $remotePharDownloadUrl = self::MAGERUN_DOWNLOAD_URL_UNSTABLE; - } else { - $versionTxtUrl = self::VERSION_TXT_URL_STABLE; - $remotePharDownloadUrl = self::MAGERUN_DOWNLOAD_URL_STABLE; - } - $response = Requests::get($versionTxtUrl, [], ['verify' => true]); + // We directly fetch the latest “unstable” version from version.txt + $response = Requests::get($versionTxtUrl, [], ['verify' => true]); + if (!$response->success) { + throw new RuntimeException('Cannot get version: ' . $response->status_code); + } - if (!$response->success) { - throw new RuntimeException('Cannot get version: ' . $response->status_code); + $latestVersion = trim($response->body); + + // Proceed with the normal logic, ignoring any specific version + if ($this->isOutdatedVersion($latestVersion, $loadUnstable)) { + $this->updatePhar( + $output, + $remotePharDownloadUrl, + $tempFilename, + $localFilename, + $isDryRun, + true + ); + } else { + $output->writeln("You are using the latest n98-magerun2 unstable version."); + } + return Command::SUCCESS; } - $latestVersion = trim($response->body); - - if ($this->isOutdatedVersion($latestVersion, $loadUnstable)) { - $output->writeln(sprintf("Updating to version %s.", $latestVersion)); + // Otherwise (stable mode): + // 1) If a specific version was provided, skip version.txt and build the download URL for that version + // 2) If no version was provided, use the existing “latest stable version” logic + if ($requestedVersion) { + // Construct a download URL for that version. Example pattern might be: https://files.magerun.net/n98-magerun2-4.0.2.phar + // Adapt the pattern to match how your releases are actually hosted. + $remotePharDownloadUrl = sprintf( + 'https://files.magerun.net/n98-magerun2-%s.phar', + $requestedVersion + ); - try { - $this->downloadNewPhar($output, $remotePharDownloadUrl, $tempFilename); - $this->checkNewPharFile($tempFilename, $localFilename); + // Check if the requested file exists via HEAD + $existsCheck = Requests::head($remotePharDownloadUrl, [], ['verify' => true]); + if (!$existsCheck->success) { + throw new RuntimeException( + sprintf('Requested version "%s" could not be found at %s', $requestedVersion, $remotePharDownloadUrl) + ); + } - $changelog = $this->getChangelog($output, $loadUnstable); + $output->writeln( + sprintf("Rolling back/updating to specific version %s.", $requestedVersion) + ); - if (!$isDryRun) { - $this->replaceExistingPharFile($tempFilename, $localFilename); - } + // We do *not* check if it’s “outdated” – user explicitly wants that version. + $this->updatePhar( + $output, + $remotePharDownloadUrl, + $tempFilename, + $localFilename, + $isDryRun, + false, + $requestedVersion + ); + } else { + // Use the original “latest stable” logic via version.txt + $versionTxtUrl = self::VERSION_TXT_URL_STABLE; + $remotePharDownloadUrl = self::MAGERUN_DOWNLOAD_URL_STABLE; - $output->writeln(''); - $output->writeln(''); - $output->writeln($changelog); - $output->writeln('---------------------------------'); - $output->writeln('Successfully updated n98-magerun2'); - $output->writeln('---------------------------------'); + $response = Requests::get($versionTxtUrl, [], ['verify' => true]); + if (!$response->success) { + throw new RuntimeException('Cannot get version: ' . $response->status_code); + } - $this->_exit(0); - } catch (Exception $e) { - @unlink($tempFilename); - if (!$e instanceof UnexpectedValueException && !$e instanceof PharException) { - throw $e; - } - $output->writeln('The download is corrupted (' . $e->getMessage() . ').'); - $output->writeln('Please re-run the self-update command to try again.'); + $latestVersion = trim($response->body); + + if ($this->isOutdatedVersion($latestVersion, false)) { + $output->writeln( + sprintf("Updating to the latest stable version %s.", $latestVersion) + ); + $this->updatePhar( + $output, + $remotePharDownloadUrl, + $tempFilename, + $localFilename, + $isDryRun, + false + ); + + $output->writeln(sprintf( + 'If you want to rollback to version %s, you can run:', + $previousVersion + )); + $output->writeln(sprintf( + ' php n98-magerun2.phar self-update --version=%s', + $previousVersion + )); + + + } else { + $output->writeln("You are using the latest n98-magerun2 stable version."); } - } else { - $output->writeln("You are using the latest n98-magerun2 version."); } return Command::SUCCESS; } /** - * Stop execution - * - * This is a workaround to prevent warning of dispatcher after replacing - * the phar file. - * - * @param int $statusCode - * @return void + * Stop execution (Workaround to prevent warnings after replacing the phar file). */ protected function _exit($statusCode = 0) { @@ -151,9 +210,47 @@ protected function _exit($statusCode = 0) } /** - * @param \Symfony\Component\Console\Output\OutputInterface $output - * @param string $remoteUrl - * @param string $tempFilename + * Helper method to download, validate, replace, and display changelog. + */ + private function updatePhar( + OutputInterface $output, + string $remotePharDownloadUrl, + string $tempFilename, + string $localFilename, + bool $isDryRun, + bool $loadUnstable, + string $forcedVersion = null + ) { + try { + $this->downloadNewPhar($output, $remotePharDownloadUrl, $tempFilename); + $this->checkNewPharFile($tempFilename, $localFilename); + + $changelog = $this->getChangelog($output, $loadUnstable, $forcedVersion); + + if (!$isDryRun) { + $this->replaceExistingPharFile($tempFilename, $localFilename); + } + + $output->writeln(''); + $output->writeln(''); + $output->writeln($changelog); + $output->writeln('---------------------------------'); + $output->writeln('Successfully updated n98-magerun2'); + $output->writeln('---------------------------------'); + + $this->_exit(0); + } catch (Exception $e) { + @unlink($tempFilename); + if (!$e instanceof UnexpectedValueException && !$e instanceof PharException) { + throw $e; + } + $output->writeln('The download is corrupted (' . $e->getMessage() . ').'); + $output->writeln('Please re-run the self-update command to try again.'); + } + } + + /** + * Download the phar using a progress bar. */ private function downloadNewPhar(OutputInterface $output, string $remoteUrl, string $tempFilename) { @@ -162,8 +259,8 @@ private function downloadNewPhar(OutputInterface $output, string $remoteUrl, str $hooks = new Hooks(); + // Check file size $response = Requests::head($remoteUrl, [], ['verify' => true]); - if (!$response->success) { throw new RuntimeException('Cannot download phar file: ' . $response->status_code); } @@ -183,7 +280,11 @@ function ($data, $responseBytes, $responseByteLimit) use ($progressBar) { } ); - $response = Requests::get($remoteUrl, [], ['blocking' => true, 'hooks' => $hooks, 'verify' => true]); + $response = Requests::get($remoteUrl, [], [ + 'blocking' => true, + 'hooks' => $hooks, + 'verify' => true + ]); if (!$response->success) { throw new RuntimeException('Cannot download phar file: ' . $response->status_code); @@ -192,30 +293,29 @@ function ($data, $responseBytes, $responseByteLimit) use ($progressBar) { file_put_contents($tempFilename, $response->body); if (!file_exists($tempFilename)) { - $output->writeln('The download of the new n98-magerun2 version failed for an unexpected reason'); + $output->writeln( + 'The download of the new n98-magerun2 version failed for an unexpected reason' + ); } } /** - * @param string $tempFilename - * @param string $localFilename + * Validate the downloaded phar file. */ - private function checkNewPharFile($tempFilename, $localFilename) + private function checkNewPharFile(string $tempFilename, string $localFilename) { - error_reporting(E_ALL); // supress notices - + error_reporting(E_ALL); // show all errors @chmod($tempFilename, 0777 & ~umask()); + // test the phar validity $phar = new Phar($tempFilename); - // free the variable to unlock the file unset($phar); } /** - * @param string $tempFilename - * @param string $localFilename + * Replace existing phar file with the newly downloaded one. */ - private function replaceExistingPharFile($tempFilename, $localFilename) + private function replaceExistingPharFile(string $tempFilename, string $localFilename) { if (!@rename($tempFilename, $localFilename)) { throw new RuntimeException( @@ -225,23 +325,25 @@ private function replaceExistingPharFile($tempFilename, $localFilename) } /** - * Download changelog + * Download and format the changelog. * - * @param \Symfony\Component\Console\Output\OutputInterface $output - * @param bool $loadUnstable - * @return string + * @param OutputInterface $output + * @param bool $loadUnstable + * @param string|null $forcedVersion - the version we explicitly requested (if any) */ - private function getChangelog(OutputInterface $output, $loadUnstable) - { + private function getChangelog( + OutputInterface $output, + bool $loadUnstable, + string $forcedVersion = null + ): string { $changelog = ''; - if ($loadUnstable) { - $changeLogUrl = self::CHANGELOG_DOWNLOAD_URL_UNSTABLE; - } else { - $changeLogUrl = self::CHANGELOG_DOWNLOAD_URL_STABLE; - } - $response = Requests::get($changeLogUrl, [], ['verify' => true]); + // Use the normal stable or unstable changelog location + $changeLogUrl = $loadUnstable + ? self::CHANGELOG_DOWNLOAD_URL_UNSTABLE + : self::CHANGELOG_DOWNLOAD_URL_STABLE; + $response = Requests::get($changeLogUrl, [], ['verify' => true]); if (!$response->success) { throw new RuntimeException('Cannot download changelog: ' . $response->status_code); } @@ -249,9 +351,14 @@ private function getChangelog(OutputInterface $output, $loadUnstable) $changeLogContent = $response->body; if ($changeLogContent) { $versionFilePrinter = new VersionFilePrinter($changeLogContent); + + // If we explicitly requested a version, just show all changes from current to that version + // or you could skip the “previousVersion” logic if rolling back is unclear from the changelog. $previousVersion = $this->getApplication()->getVersion(); - $changelog .= $versionFilePrinter->printFromVersion($previousVersion) . "\n"; + $changelog .= $versionFilePrinter->printFromVersion($previousVersion) . "\n"; } + + // If it’s unstable, append dev warning if ($loadUnstable) { $unstableFooterMessage = << @@ -260,7 +367,6 @@ private function getChangelog(OutputInterface $output, $loadUnstable) !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! UNSTABLE_FOOTER; - $changelog .= $unstableFooterMessage . "\n"; } @@ -268,12 +374,11 @@ private function getChangelog(OutputInterface $output, $loadUnstable) } /** - * @param $latest - * @param $loadUnstable - * @return bool + * Determine if we should update to the "latest" version or not. */ - private function isOutdatedVersion($latest, $loadUnstable) + private function isOutdatedVersion(string $latest, bool $loadUnstable): bool { + // The check remains simple: if local != remote or we specifically want an unstable. return $this->getApplication()->getVersion() !== $latest || $loadUnstable; } }