diff --git a/src/Updater/Plugin/Command.php b/src/Updater/Plugin/Command.php index 0283b6a0..6690cb38 100644 --- a/src/Updater/Plugin/Command.php +++ b/src/Updater/Plugin/Command.php @@ -4,6 +4,7 @@ use my127\Console\Application\Application; use my127\Console\Application\Plugin\Plugin; +use my127\Console\Usage\Input; use my127\Workspace\Application as BaseApplication; use my127\Workspace\Updater\Exception\NoUpdateAvailableException; use my127\Workspace\Updater\Exception\NoVersionDeterminedException; @@ -23,30 +24,33 @@ public function setup(Application $application): void { $application->section('self-update') ->description('Updates the current version of workspace.') - ->action($this->action()); + ->usage('self-update []') + ->action(fn (Input $input) => $this->action($input)); } - private function action() + private function action(Input $input) { - return function () { - $pharPath = \Phar::running(false); - if (empty($pharPath)) { - echo 'This command can only be executed from within the ws utility.' . PHP_EOL; - exit(1); - } + $pharPath = \Phar::running(false); + if (empty($pharPath)) { + echo 'This command can only be executed from within the ws utility.' . PHP_EOL; + exit(1); + } - try { - $this->updater->update(BaseApplication::getVersion(), $pharPath); - } catch (NoUpdateAvailableException $e) { - echo sprintf('You are already running the latest version of workspace: %s', $e->getCurrentVersion()) . PHP_EOL; - exit(1); - } catch (NoVersionDeterminedException $e) { - echo 'Unable to determine your current workspace version. You are likely not using a tagged released.' . PHP_EOL; - exit(1); - } catch (\RuntimeException $e) { - echo sprintf('%s. Aborting self-update', $e->getMessage()) . PHP_EOL; - exit(1); + try { + if ($input->getArgument('version-constraint')) { + $this->updater->update(BaseApplication::getVersion(), $input->getArgument('version-constraint'), $pharPath); + } else { + $this->updater->updateLatest(BaseApplication::getVersion(), $pharPath); } - }; + } catch (NoUpdateAvailableException $e) { + echo sprintf('You are already running the latest version of workspace: %s', $e->getCurrentVersion()) . PHP_EOL; + exit(1); + } catch (NoVersionDeterminedException $e) { + echo 'Unable to determine your current workspace version. You are likely not using a tagged released.' . PHP_EOL; + exit(1); + } catch (\RuntimeException $e) { + echo sprintf('%s. Aborting self-update', $e->getMessage()) . PHP_EOL; + exit(1); + } } } diff --git a/src/Updater/Updater.php b/src/Updater/Updater.php index 1531ce30..6b3dc4c3 100644 --- a/src/Updater/Updater.php +++ b/src/Updater/Updater.php @@ -2,6 +2,8 @@ namespace my127\Workspace\Updater; +use Composer\Semver\Semver; +use Composer\Semver\VersionParser; use my127\Workspace\Updater\Exception\NoUpdateAvailableException; use my127\Workspace\Updater\Exception\NoVersionDeterminedException; @@ -11,6 +13,21 @@ class Updater public const CODE_NO_RELEASES = 101; public const CODE_ERR_FETCHING_NEXT_RELEASE = 102; + public const STABILITY_STABLE = 0; + public const STABILITY_RC = 5; + public const STABILITY_BETA = 10; + public const STABILITY_ALPHA = 15; + public const STABILITY_DEV = 20; + + /** @var array */ + public static $stabilities = [ + 'stable' => self::STABILITY_STABLE, + 'RC' => self::STABILITY_RC, + 'beta' => self::STABILITY_BETA, + 'alpha' => self::STABILITY_ALPHA, + 'dev' => self::STABILITY_DEV, + ]; + /** @var string */ private $apiUrl; @@ -23,24 +40,37 @@ public function __construct(string $apiUrl, ?Output $output = null) $this->output = $output ?: new StdOutput(); } - public function update(string $currentVersion, string $targetPath) + public function updateLatest(string $currentVersion, string $targetPath) { - if (empty($currentVersion)) { - throw new NoVersionDeterminedException(); - } - $latest = $this->getLatestRelease(); if (!$latest->isMoreRecentThan($currentVersion)) { throw new NoUpdateAvailableException($currentVersion); } + $this->doUpdate($currentVersion, $latest, $targetPath); + } + + public function update(string $currentVersion, string $targetConstraint, string $targetPath) + { + $latest = $this->getLatestReleaseByConstraint($targetConstraint); + if ($latest->getVersion() == $currentVersion) { + throw new NoUpdateAvailableException($currentVersion); + } + $this->doUpdate($currentVersion, $latest, $targetPath); + } + + private function doUpdate(string $currentVersion, Release $release, string $targetPath) + { + if (empty($currentVersion)) { + throw new NoVersionDeterminedException(); + } $temp = tempnam(sys_get_temp_dir(), 'workspace-update-') . '.phar'; try { - $this->output->infof('Downloading new version (%s) from %s', $latest->getVersion(), $latest->getUrl()); - $releaseData = @file_get_contents($latest->getUrl()); + $this->output->infof('Downloading new version (%s) from %s', $release->getVersion(), $release->getUrl()); + $releaseData = @file_get_contents($release->getUrl()); if ($releaseData === false) { - throw new \RuntimeException(sprintf('Unable to download latest release at %s', $latest->getUrl()), self::CODE_ERR_FETCHING_NEXT_RELEASE); + throw new \RuntimeException(sprintf('Unable to download latest release at %s', $release->getUrl()), self::CODE_ERR_FETCHING_NEXT_RELEASE); } $this->output->infof('Writing to %s', $temp); @@ -67,22 +97,70 @@ public function update(string $currentVersion, string $targetPath) private function getLatestRelease(): Release { try { - $releases = file_get_contents($this->apiUrl, false, $this->createStreamContext()); + $release = file_get_contents($this->apiUrl . '/latest', false, $this->createStreamContext()); + } catch (\Throwable $e) { + throw new \RuntimeException('Error fetching latest release from GitHub.', self::CODE_ERR_FETCHING_RELEASES); + } + + $latest = json_decode($release); + + return new Release($latest->assets[0]->browser_download_url, $latest->tag_name); + } + + private function getLatestReleaseByConstraint(string $targetConstraint): Release + { + try { + $releasesRaw = file_get_contents($this->apiUrl, false, $this->createStreamContext()); } catch (\Throwable $e) { - throw new \RuntimeException('Error fetching releases from GitHub.', self::CODE_ERR_FETCHING_RELEASES); + throw new \RuntimeException('Error fetching latest release from GitHub.', self::CODE_ERR_FETCHING_RELEASES); } - $releases = json_decode($releases); + $versionParser = new VersionParser(); - if (count($releases) === 0) { - throw new \RuntimeException('No releases present in the GitHub API response.', self::CODE_NO_RELEASES); + $parts = explode('@', $targetConstraint); + $constraint = $parts[0]; + if (count($parts) > 1) { + $minStability = VersionParser::normalizeStability($parts[1]); + } else { + $minStability = $versionParser->parseStability($constraint); } - $latest = $releases[0]; + $releases = json_decode($releasesRaw); + $sortedVersions = Semver::rsort(array_map(fn ($release) => $release->tag_name, $releases)); + $filteredVersions = Semver::satisfiedBy($sortedVersions, $constraint); + $filteredStabilityVersions = $this->filterVersionsByMinStability($versionParser, $filteredVersions, $minStability); + $filteredReleases = $this->filterReleasesByVersions($releases, $filteredStabilityVersions); + + if (count($filteredReleases) == 0) { + throw new \RuntimeException(sprintf('No releases match the version constraint "%s".', $targetConstraint), self::CODE_ERR_FETCHING_RELEASES); + } + + $latest = $filteredReleases[0]; return new Release($latest->assets[0]->browser_download_url, $latest->tag_name); } + /** + * @param string[] $versions + * + * @return string[] + */ + private function filterVersionsByMinStability(VersionParser $versionParser, array $versions, string $minStability): array + { + return array_filter($versions, fn ($version) => self::$stabilities[$versionParser->parseStability($version)] <= self::$stabilities[$minStability]); + } + + /** + * @param object[] $releases + * @param string[] $versions + * + * @return object[] + */ + private function filterReleasesByVersions(array $releases, array $versions): array + { + return array_values(array_filter($releases, fn ($release) => in_array($release->tag_name, $versions))); + } + /** * @return resource */ diff --git a/tests/Test/Updater/UpdaterTest.php b/tests/Test/Updater/UpdaterTest.php index ec9576c1..b267e54f 100644 --- a/tests/Test/Updater/UpdaterTest.php +++ b/tests/Test/Updater/UpdaterTest.php @@ -11,7 +11,27 @@ class UpdaterTest extends TestCase { public static function tearDownAfterClass(): void { - array_map('unlink', glob(__DIR__ . '/fixtures/generated/*.*')); + $files = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator(__DIR__ . '/fixtures/generated', \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($files as $file) { + if ($file->isDir()) { + rmdir($file); + } else { + unlink($file); + } + } + } + + protected function tearDown(): void + { + if (file_exists(__DIR__ . '/fixtures/generated/fake.phar')) { + \Phar::unlinkArchive(__DIR__ . '/fixtures/generated/fake.phar'); + } + if (file_exists(__DIR__ . '/fixtures/generated/fake.phar.tar')) { + \Phar::unlinkArchive(__DIR__ . '/fixtures/generated/fake.phar.tar'); + } } public static function setUpBeforeClass(): void @@ -28,76 +48,143 @@ public function exceptionThrownWhenErrorFetchingReleases() $this->expectExceptionCode(Updater::CODE_ERR_FETCHING_RELEASES); $updater = new Updater(__DIR__ . '/fixtures/foo.json', new NullOutput()); - $updater->update('1.0.0', ''); - } - - /** @test */ - public function exceptionThrownWhenThereAreNoReleases() - { - $this->expectException(\RuntimeException::class); - $this->expectExceptionCode(Updater::CODE_NO_RELEASES); - - $updater = new Updater(__DIR__ . '/fixtures/empty-releases.json'); - $updater->update('1.0.0', ''); + $updater->updateLatest('1.0.0', ''); } /** @test */ public function exceptionThrownWhenAlreadyOnLatest() { - $this->prepareReleasesFixture('latest.json', '', '1.0.0'); + $this->prepareLatestFixture('valid/latest', '', '1.0.0'); $this->expectException(NoUpdateAvailableException::class); - $updater = new Updater(__DIR__ . '/fixtures/generated/latest.json', new NullOutput()); - $updater->update('1.0.0', ''); + $updater = new Updater(__DIR__ . '/fixtures/generated/valid', new NullOutput()); + $updater->updateLatest('1.0.0', ''); } /** @test */ public function exceptionThrownWhenCurrentVersionIsEmpty() { - $this->prepareReleasesFixture('latest.json', '', '1.0.0'); + $this->prepareLatestFixture('valid/latest', '', '1.0.0'); $this->expectException(NoVersionDeterminedException::class); - $updater = new Updater(__DIR__ . '/fixtures/generated/latest.json', new NullOutput()); - $updater->update('', ''); + $updater = new Updater(__DIR__ . '/fixtures/generated/valid', new NullOutput()); + $updater->updateLatest('', ''); } /** @test */ public function exceptionThrownWhenOnMoreRecentVersion() { - $this->prepareReleasesFixture('older.json', '', '1.0.0'); + $this->prepareLatestFixture('older/latest', '', '1.0.0'); $this->expectException(NoUpdateAvailableException::class); - $updater = new Updater(__DIR__ . '/fixtures/generated/older.json', new NullOutput()); - $updater->update('1.1.0', ''); + $updater = new Updater(__DIR__ . '/fixtures/generated/older', new NullOutput()); + $updater->updateLatest('1.1.0', ''); } /** @test */ public function exceptionThrownWhenNextReleaseCannotBeDownloaded() { - $this->prepareReleasesFixture('invalid-release.json', __DIR__ . '/foo.baz.bar', '1.0.0'); + $this->prepareLatestFixture('invalid-release/latest', __DIR__ . '/foo.baz.bar', '1.0.0'); $this->expectException(\RuntimeException::class); $this->expectExceptionCode(Updater::CODE_ERR_FETCHING_NEXT_RELEASE); - $updater = new Updater(__DIR__ . '/fixtures/generated/invalid-release.json', new NullOutput()); - $updater->update('0.9.0', ''); + $updater = new Updater(__DIR__ . '/fixtures/generated/invalid-release', new NullOutput()); + $updater->updateLatest('0.9.0', ''); } /** @test */ public function downloadsLatestReleaseToDesiredTargetPath() { $this->prepareFakePhar(); - $this->prepareReleasesFixture('valid.json', __DIR__ . '/fixtures/generated/fake.phar', '1.0.0'); + $this->prepareLatestFixture('valid/latest', __DIR__ . '/fixtures/generated/fake.phar', '1.0.0'); + + $temp = sys_get_temp_dir() . '/test-ws-download'; + $updater = new Updater(__DIR__ . '/fixtures/generated/valid', new NullOutput()); + $updater->updateLatest('0.9.0', $temp); + + $this->assertFileExists($temp); + } + + /** @test */ + public function downloadsLatestCandidateOfConstraintToDesiredTargetPath() + { + $this->prepareFakePhar(); + $this->prepareReleasesFixture('valid.json', __DIR__ . '/fixtures/generated/fake.phar', '1.0.0', '0.9.1'); $temp = sys_get_temp_dir() . '/test-ws-download'; $updater = new Updater(__DIR__ . '/fixtures/generated/valid.json', new NullOutput()); - $updater->update('0.9.0', $temp); + $updater->update('0.9.0', '~0.9.1', $temp); $this->assertFileExists($temp); } - private function prepareReleasesFixture(string $name, string $releasePath, string $version): void + /** @test */ + public function downloadsLatestStableCandidateOfConstraintToDesiredTargetPath() + { + $this->prepareFakePhar(); + $this->prepareReleasesFixture('valid.json', __DIR__ . '/fixtures/generated/fake.phar', '1.0.0-alpha1', '0.9.1'); + + $temp = sys_get_temp_dir() . '/test-ws-download'; + $updater = new Updater(__DIR__ . '/fixtures/generated/valid.json', new NullOutput()); + $updater->update('0.9.0', '~0.9.1', $temp); + + $this->assertFileExists($temp); + } + + /** @test */ + public function exceptionThrownWhenNoStableCandidate() + { + $this->prepareFakePhar(); + $this->prepareReleasesFixture('valid.json', __DIR__ . '/fixtures/generated/fake.phar', '1.0.0-alpha1', '0.9.1-alpha1'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(Updater::CODE_ERR_FETCHING_RELEASES); + + $temp = sys_get_temp_dir() . '/test-ws-download'; + $updater = new Updater(__DIR__ . '/fixtures/generated/valid.json', new NullOutput()); + $updater->update('0.9.0', '~0.9.1', $temp); + } + + /** @test */ + public function exceptionThrownWhenNoRCCandidate() + { + $this->prepareFakePhar(); + $this->prepareReleasesFixture('valid.json', __DIR__ . '/fixtures/generated/fake.phar', '1.0.0-alpha1', '0.9.1-alpha1'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(Updater::CODE_ERR_FETCHING_RELEASES); + + $temp = sys_get_temp_dir() . '/test-ws-download'; + $updater = new Updater(__DIR__ . '/fixtures/generated/valid.json', new NullOutput()); + $updater->update('0.9.0', '~0.9.1@RC', $temp); + } + + /** @test */ + public function downloadsLatestAlphaCandidateOfConstraintToDesiredTargetPath() + { + $this->prepareFakePhar(); + $this->prepareReleasesFixture('valid.json', __DIR__ . '/fixtures/generated/fake.phar', '1.0.0-alpha1', '0.9.1-alpha1'); + + $temp = sys_get_temp_dir() . '/test-ws-download'; + $updater = new Updater(__DIR__ . '/fixtures/generated/valid.json', new NullOutput()); + $updater->update('0.9.0', '~0.9.0@alpha', $temp); + + $this->assertFileExists($temp); + } + + private function prepareReleasesFixture(string $name, string $releasePath, string $version1, string $version2): void { $contents = file_get_contents(__DIR__ . '/fixtures/tpl/releases.json'); + $contents = str_replace(['%%browserDownloadUrl%%', '%%versionTag1%%', '%%versionTag2%%'], [$releasePath, $version1, $version2], $contents); + + file_put_contents(__DIR__ . '/fixtures/generated/' . $name, $contents); + } + + private function prepareLatestFixture(string $name, string $releasePath, string $version): void + { + $dir = dirname(__DIR__ . '/fixtures/generated/' . $name); + if (!file_exists($dir)) { + mkdir($dir); + } + $contents = file_get_contents(__DIR__ . '/fixtures/tpl/latest.json'); $contents = str_replace(['%%browserDownloadUrl%%', '%%versionTag%%'], [$releasePath, $version], $contents); file_put_contents(__DIR__ . '/fixtures/generated/' . $name, $contents); diff --git a/tests/Test/Updater/fixtures/tpl/latest.json b/tests/Test/Updater/fixtures/tpl/latest.json new file mode 100644 index 00000000..748d5aa7 --- /dev/null +++ b/tests/Test/Updater/fixtures/tpl/latest.json @@ -0,0 +1,74 @@ +{ + "url": "https://api.github.com/repos/my127/workspace/releases/43190729", + "assets_url": "https://api.github.com/repos/my127/workspace/releases/43190729/assets", + "upload_url": "https://uploads.github.com/repos/my127/workspace/releases/43190729/assets{?name,label}", + "html_url": "https://github.com/my127/workspace/releases/tag/%%versionTag%%", + "id": 43190729, + "author": { + "login": "joe", + "id": 9999, + "node_id": "MDQ6VXNlcjE1NTQ3MDk=", + "avatar_url": "https://avatars.githubusercontent.com/u/1554709?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/joe", + "html_url": "https://github.com/joe", + "followers_url": "https://api.github.com/users/joe/followers", + "following_url": "https://api.github.com/users/joe/following{/other_user}", + "gists_url": "https://api.github.com/users/joe/gists{/gist_id}", + "starred_url": "https://api.github.com/users/joe/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/joe/subscriptions", + "organizations_url": "https://api.github.com/users/joe/orgs", + "repos_url": "https://api.github.com/users/joe/repos", + "events_url": "https://api.github.com/users/joe/events{/privacy}", + "received_events_url": "https://api.github.com/users/joe/received_events", + "type": "User", + "site_admin": false + }, + "node_id": "MDc6UmVsZWFzZTQzMTkwNzI5", + "tag_name": "%%versionTag%%", + "target_commitish": "1.x", + "name": "%%versionTag%%", + "draft": false, + "prerelease": true, + "created_at": "2021-05-19T06:18:24Z", + "published_at": "2021-05-19T06:24:32Z", + "assets": [ + { + "url": "https://api.github.com/repos/my127/workspace/releases/assets/37159196", + "id": 37159196, + "node_id": "MDEyOlJlbGVhc2VBc3NldDM3MTU5MTk2", + "name": "ws", + "label": null, + "uploader": { + "login": "joe", + "id": 1554709, + "node_id": "MDQ6VXNlcjE1NTQ3MDk=", + "avatar_url": "https://avatars.githubusercontent.com/u/1554709?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/joe", + "html_url": "https://github.com/joe", + "followers_url": "https://api.github.com/users/joe/followers", + "following_url": "https://api.github.com/users/joe/following{/other_user}", + "gists_url": "https://api.github.com/users/joe/gists{/gist_id}", + "starred_url": "https://api.github.com/users/joe/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/joe/subscriptions", + "organizations_url": "https://api.github.com/users/joe/orgs", + "repos_url": "https://api.github.com/users/joe/repos", + "events_url": "https://api.github.com/users/joe/events{/privacy}", + "received_events_url": "https://api.github.com/users/joe/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/octet-stream", + "state": "uploaded", + "size": 2944641, + "download_count": 20, + "created_at": "2021-05-19T06:22:50Z", + "updated_at": "2021-05-19T06:22:54Z", + "browser_download_url": "%%browserDownloadUrl%%" + } + ], + "tarball_url": "https://api.github.com/repos/my127/workspace/tarball/%%versionTag%%", + "zipball_url": "https://api.github.com/repos/my127/workspace/zipball/%%versionTag%%", + "body": "## [%%versionTag%%](https://github.com/my127/workspace/tree/%%versionTag%%) (2021-05-19)\r\n\r\n[Full Changelog](https://github.com/my127/workspace/compare/0.1.3...%%versionTag1%%)\r\n\r\n**Implemented enhancements:**\r\n\r\n- Add a cheatsheet [\\#83](https://github.com/my127/workspace/pull/83) ([rgpjones](https://github.com/rgpjones))\r\n- Add poweroff command [\\#82](https://github.com/my127/workspace/pull/82) ([joe](https://github.com/joe))\r\n- Add before/after overlay events and match with before for prepare [\\#78](https://github.com/my127/workspace/pull/78) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Improve integration test suite [\\#77](https://github.com/my127/workspace/pull/77) ([dantleech](https://github.com/dantleech))\r\n- Add PHP 8 support [\\#68](https://github.com/my127/workspace/pull/68) ([elvetemedve](https://github.com/elvetemedve))\r\n- Add config dump command [\\#67](https://github.com/my127/workspace/pull/67) ([hgajjar](https://github.com/hgajjar))\r\n- Add Jaeger daemon [\\#66](https://github.com/my127/workspace/pull/66) ([joe](https://github.com/joe))\r\n- Add more default symfony expression functions [\\#56](https://github.com/my127/workspace/pull/56) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Update symfony components and twig to latest minor version [\\#55](https://github.com/my127/workspace/pull/55) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Upgrade to mailhog image v1.0.1 [\\#54](https://github.com/my127/workspace/pull/54) ([joe](https://github.com/joe))\r\n- Fix multiple argument run/passthru escaping [\\#53](https://github.com/my127/workspace/pull/53) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Make commands and function errors debuggable by saying what name they are [\\#51](https://github.com/my127/workspace/pull/51) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Add missing Bash interpreter page [\\#49](https://github.com/my127/workspace/pull/49) ([opdavies](https://github.com/opdavies))\r\n- Misc: license as MIT [\\#47](https://github.com/my127/workspace/pull/47) ([dcole-inviqa](https://github.com/dcole-inviqa))\r\n- Note that curl is required [\\#46](https://github.com/my127/workspace/pull/46) ([joe](https://github.com/joe))\r\n- Add an after\\('harness.prepare'\\) event [\\#44](https://github.com/my127/workspace/pull/44) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Test on PHP 7.4 [\\#40](https://github.com/my127/workspace/pull/40) ([joe](https://github.com/joe))\r\n\r\n**Fixed bugs:**\r\n\r\n- Add requirement to use prefix space to avoid secrets in shell history [\\#65](https://github.com/my127/workspace/pull/65) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Handle sidekick errexiting correctly [\\#58](https://github.com/my127/workspace/pull/58) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n\r\n**Closed issues:**\r\n\r\n- Service commands provide no feedback if command not found [\\#62](https://github.com/my127/workspace/issues/62)\r\n- Support for self-update? [\\#60](https://github.com/my127/workspace/issues/60)\r\n- Executing a ws command that does not exist should return a non-zero exit code [\\#45](https://github.com/my127/workspace/issues/45)\r\n- Support for multiple harnesses to one workspace [\\#23](https://github.com/my127/workspace/issues/23)\r\n\r\n## Installation instructions\r\n```bash\r\ncurl --output ./ws --location https://github.com/my127/workspace/releases/download/%%versionTag1%%/ws\r\nchmod +x ws && sudo mv ws /usr/local/bin/ws\r\n```" +} diff --git a/tests/Test/Updater/fixtures/tpl/releases.json b/tests/Test/Updater/fixtures/tpl/releases.json index 7a159ea0..93f1cfa3 100644 --- a/tests/Test/Updater/fixtures/tpl/releases.json +++ b/tests/Test/Updater/fixtures/tpl/releases.json @@ -1,74 +1,150 @@ -[{ - "url": "https://api.github.com/repos/my127/workspace/releases/43190729", - "assets_url": "https://api.github.com/repos/my127/workspace/releases/43190729/assets", - "upload_url": "https://uploads.github.com/repos/my127/workspace/releases/43190729/assets{?name,label}", - "html_url": "https://github.com/my127/workspace/releases/tag/%%versionTag%%", - "id": 43190729, - "author": { - "login": "joe", - "id": 9999, - "node_id": "MDQ6VXNlcjE1NTQ3MDk=", - "avatar_url": "https://avatars.githubusercontent.com/u/1554709?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/joe", - "html_url": "https://github.com/joe", - "followers_url": "https://api.github.com/users/joe/followers", - "following_url": "https://api.github.com/users/joe/following{/other_user}", - "gists_url": "https://api.github.com/users/joe/gists{/gist_id}", - "starred_url": "https://api.github.com/users/joe/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/joe/subscriptions", - "organizations_url": "https://api.github.com/users/joe/orgs", - "repos_url": "https://api.github.com/users/joe/repos", - "events_url": "https://api.github.com/users/joe/events{/privacy}", - "received_events_url": "https://api.github.com/users/joe/received_events", - "type": "User", - "site_admin": false +[ + { + "url": "https://api.github.com/repos/my127/workspace/releases/43190729", + "assets_url": "https://api.github.com/repos/my127/workspace/releases/43190729/assets", + "upload_url": "https://uploads.github.com/repos/my127/workspace/releases/43190729/assets{?name,label}", + "html_url": "https://github.com/my127/workspace/releases/tag/%%versionTag1%%", + "id": 43190729, + "author": { + "login": "joe", + "id": 9999, + "node_id": "MDQ6VXNlcjE1NTQ3MDk=", + "avatar_url": "https://avatars.githubusercontent.com/u/1554709?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/joe", + "html_url": "https://github.com/joe", + "followers_url": "https://api.github.com/users/joe/followers", + "following_url": "https://api.github.com/users/joe/following{/other_user}", + "gists_url": "https://api.github.com/users/joe/gists{/gist_id}", + "starred_url": "https://api.github.com/users/joe/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/joe/subscriptions", + "organizations_url": "https://api.github.com/users/joe/orgs", + "repos_url": "https://api.github.com/users/joe/repos", + "events_url": "https://api.github.com/users/joe/events{/privacy}", + "received_events_url": "https://api.github.com/users/joe/received_events", + "type": "User", + "site_admin": false + }, + "node_id": "MDc6UmVsZWFzZTQzMTkwNzI5", + "tag_name": "%%versionTag1%%", + "target_commitish": "1.x", + "name": "%%versionTag1%%", + "draft": false, + "prerelease": true, + "created_at": "2021-05-19T06:18:24Z", + "published_at": "2021-05-19T06:24:32Z", + "assets": [ + { + "url": "https://api.github.com/repos/my127/workspace/releases/assets/37159196", + "id": 37159196, + "node_id": "MDEyOlJlbGVhc2VBc3NldDM3MTU5MTk2", + "name": "ws", + "label": null, + "uploader": { + "login": "joe", + "id": 1554709, + "node_id": "MDQ6VXNlcjE1NTQ3MDk=", + "avatar_url": "https://avatars.githubusercontent.com/u/1554709?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/joe", + "html_url": "https://github.com/joe", + "followers_url": "https://api.github.com/users/joe/followers", + "following_url": "https://api.github.com/users/joe/following{/other_user}", + "gists_url": "https://api.github.com/users/joe/gists{/gist_id}", + "starred_url": "https://api.github.com/users/joe/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/joe/subscriptions", + "organizations_url": "https://api.github.com/users/joe/orgs", + "repos_url": "https://api.github.com/users/joe/repos", + "events_url": "https://api.github.com/users/joe/events{/privacy}", + "received_events_url": "https://api.github.com/users/joe/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/octet-stream", + "state": "uploaded", + "size": 2944641, + "download_count": 20, + "created_at": "2021-05-19T06:22:50Z", + "updated_at": "2021-05-19T06:22:54Z", + "browser_download_url": "%%browserDownloadUrl%%" + } + ], + "tarball_url": "https://api.github.com/repos/my127/workspace/tarball/%%versionTag1%%", + "zipball_url": "https://api.github.com/repos/my127/workspace/zipball/%%versionTag1%%", + "body": "## [%%versionTag1%%](https://github.com/my127/workspace/tree/%%versionTag1%%) (2021-05-19)\r\n\r\n[Full Changelog](https://github.com/my127/workspace/compare/0.1.3...%%versionTag1%%)\r\n\r\n**Implemented enhancements:**\r\n\r\n- Add a cheatsheet [\\#83](https://github.com/my127/workspace/pull/83) ([rgpjones](https://github.com/rgpjones))\r\n- Add poweroff command [\\#82](https://github.com/my127/workspace/pull/82) ([joe](https://github.com/joe))\r\n- Add before/after overlay events and match with before for prepare [\\#78](https://github.com/my127/workspace/pull/78) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Improve integration test suite [\\#77](https://github.com/my127/workspace/pull/77) ([dantleech](https://github.com/dantleech))\r\n- Add PHP 8 support [\\#68](https://github.com/my127/workspace/pull/68) ([elvetemedve](https://github.com/elvetemedve))\r\n- Add config dump command [\\#67](https://github.com/my127/workspace/pull/67) ([hgajjar](https://github.com/hgajjar))\r\n- Add Jaeger daemon [\\#66](https://github.com/my127/workspace/pull/66) ([joe](https://github.com/joe))\r\n- Add more default symfony expression functions [\\#56](https://github.com/my127/workspace/pull/56) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Update symfony components and twig to latest minor version [\\#55](https://github.com/my127/workspace/pull/55) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Upgrade to mailhog image v1.0.1 [\\#54](https://github.com/my127/workspace/pull/54) ([joe](https://github.com/joe))\r\n- Fix multiple argument run/passthru escaping [\\#53](https://github.com/my127/workspace/pull/53) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Make commands and function errors debuggable by saying what name they are [\\#51](https://github.com/my127/workspace/pull/51) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Add missing Bash interpreter page [\\#49](https://github.com/my127/workspace/pull/49) ([opdavies](https://github.com/opdavies))\r\n- Misc: license as MIT [\\#47](https://github.com/my127/workspace/pull/47) ([dcole-inviqa](https://github.com/dcole-inviqa))\r\n- Note that curl is required [\\#46](https://github.com/my127/workspace/pull/46) ([joe](https://github.com/joe))\r\n- Add an after\\('harness.prepare'\\) event [\\#44](https://github.com/my127/workspace/pull/44) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Test on PHP 7.4 [\\#40](https://github.com/my127/workspace/pull/40) ([joe](https://github.com/joe))\r\n\r\n**Fixed bugs:**\r\n\r\n- Add requirement to use prefix space to avoid secrets in shell history [\\#65](https://github.com/my127/workspace/pull/65) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Handle sidekick errexiting correctly [\\#58](https://github.com/my127/workspace/pull/58) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n\r\n**Closed issues:**\r\n\r\n- Service commands provide no feedback if command not found [\\#62](https://github.com/my127/workspace/issues/62)\r\n- Support for self-update? [\\#60](https://github.com/my127/workspace/issues/60)\r\n- Executing a ws command that does not exist should return a non-zero exit code [\\#45](https://github.com/my127/workspace/issues/45)\r\n- Support for multiple harnesses to one workspace [\\#23](https://github.com/my127/workspace/issues/23)\r\n\r\n## Installation instructions\r\n```bash\r\ncurl --output ./ws --location https://github.com/my127/workspace/releases/download/%%versionTag1%%/ws\r\nchmod +x ws && sudo mv ws /usr/local/bin/ws\r\n```" }, - "node_id": "MDc6UmVsZWFzZTQzMTkwNzI5", - "tag_name": "%%versionTag%%", - "target_commitish": "1.x", - "name": "%%versionTag%%", - "draft": false, - "prerelease": true, - "created_at": "2021-05-19T06:18:24Z", - "published_at": "2021-05-19T06:24:32Z", - "assets": [ - { - "url": "https://api.github.com/repos/my127/workspace/releases/assets/37159196", - "id": 37159196, - "node_id": "MDEyOlJlbGVhc2VBc3NldDM3MTU5MTk2", - "name": "ws", - "label": null, - "uploader": { - "login": "joe", - "id": 1554709, - "node_id": "MDQ6VXNlcjE1NTQ3MDk=", - "avatar_url": "https://avatars.githubusercontent.com/u/1554709?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/joe", - "html_url": "https://github.com/joe", - "followers_url": "https://api.github.com/users/joe/followers", - "following_url": "https://api.github.com/users/joe/following{/other_user}", - "gists_url": "https://api.github.com/users/joe/gists{/gist_id}", - "starred_url": "https://api.github.com/users/joe/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/joe/subscriptions", - "organizations_url": "https://api.github.com/users/joe/orgs", - "repos_url": "https://api.github.com/users/joe/repos", - "events_url": "https://api.github.com/users/joe/events{/privacy}", - "received_events_url": "https://api.github.com/users/joe/received_events", - "type": "User", - "site_admin": false - }, - "content_type": "application/octet-stream", - "state": "uploaded", - "size": 2944641, - "download_count": 20, - "created_at": "2021-05-19T06:22:50Z", - "updated_at": "2021-05-19T06:22:54Z", - "browser_download_url": "%%browserDownloadUrl%%" - } - ], - "tarball_url": "https://api.github.com/repos/my127/workspace/tarball/%%versionTag%%", - "zipball_url": "https://api.github.com/repos/my127/workspace/zipball/%%versionTag%%", - "body": "## [%%versionTag%%](https://github.com/my127/workspace/tree/%%versionTag%%) (2021-05-19)\r\n\r\n[Full Changelog](https://github.com/my127/workspace/compare/0.1.3...%%versionTag%%)\r\n\r\n**Implemented enhancements:**\r\n\r\n- Add a cheatsheet [\\#83](https://github.com/my127/workspace/pull/83) ([rgpjones](https://github.com/rgpjones))\r\n- Add poweroff command [\\#82](https://github.com/my127/workspace/pull/82) ([joe](https://github.com/joe))\r\n- Add before/after overlay events and match with before for prepare [\\#78](https://github.com/my127/workspace/pull/78) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Improve integration test suite [\\#77](https://github.com/my127/workspace/pull/77) ([dantleech](https://github.com/dantleech))\r\n- Add PHP 8 support [\\#68](https://github.com/my127/workspace/pull/68) ([elvetemedve](https://github.com/elvetemedve))\r\n- Add config dump command [\\#67](https://github.com/my127/workspace/pull/67) ([hgajjar](https://github.com/hgajjar))\r\n- Add Jaeger daemon [\\#66](https://github.com/my127/workspace/pull/66) ([joe](https://github.com/joe))\r\n- Add more default symfony expression functions [\\#56](https://github.com/my127/workspace/pull/56) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Update symfony components and twig to latest minor version [\\#55](https://github.com/my127/workspace/pull/55) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Upgrade to mailhog image v1.0.1 [\\#54](https://github.com/my127/workspace/pull/54) ([joe](https://github.com/joe))\r\n- Fix multiple argument run/passthru escaping [\\#53](https://github.com/my127/workspace/pull/53) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Make commands and function errors debuggable by saying what name they are [\\#51](https://github.com/my127/workspace/pull/51) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Add missing Bash interpreter page [\\#49](https://github.com/my127/workspace/pull/49) ([opdavies](https://github.com/opdavies))\r\n- Misc: license as MIT [\\#47](https://github.com/my127/workspace/pull/47) ([dcole-inviqa](https://github.com/dcole-inviqa))\r\n- Note that curl is required [\\#46](https://github.com/my127/workspace/pull/46) ([joe](https://github.com/joe))\r\n- Add an after\\('harness.prepare'\\) event [\\#44](https://github.com/my127/workspace/pull/44) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Test on PHP 7.4 [\\#40](https://github.com/my127/workspace/pull/40) ([joe](https://github.com/joe))\r\n\r\n**Fixed bugs:**\r\n\r\n- Add requirement to use prefix space to avoid secrets in shell history [\\#65](https://github.com/my127/workspace/pull/65) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Handle sidekick errexiting correctly [\\#58](https://github.com/my127/workspace/pull/58) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n\r\n**Closed issues:**\r\n\r\n- Service commands provide no feedback if command not found [\\#62](https://github.com/my127/workspace/issues/62)\r\n- Support for self-update? [\\#60](https://github.com/my127/workspace/issues/60)\r\n- Executing a ws command that does not exist should return a non-zero exit code [\\#45](https://github.com/my127/workspace/issues/45)\r\n- Support for multiple harnesses to one workspace [\\#23](https://github.com/my127/workspace/issues/23)\r\n\r\n## Installation instructions\r\n```bash\r\ncurl --output ./ws --location https://github.com/my127/workspace/releases/download/%%versionTag%%/ws\r\nchmod +x ws && sudo mv ws /usr/local/bin/ws\r\n```" -}] \ No newline at end of file + { + "url": "https://api.github.com/repos/my127/workspace/releases/43190729", + "assets_url": "https://api.github.com/repos/my127/workspace/releases/43190729/assets", + "upload_url": "https://uploads.github.com/repos/my127/workspace/releases/43190729/assets{?name,label}", + "html_url": "https://github.com/my127/workspace/releases/tag/%%versionTag2%%", + "id": 43190729, + "author": { + "login": "joe", + "id": 9999, + "node_id": "MDQ6VXNlcjE1NTQ3MDk=", + "avatar_url": "https://avatars.githubusercontent.com/u/1554709?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/joe", + "html_url": "https://github.com/joe", + "followers_url": "https://api.github.com/users/joe/followers", + "following_url": "https://api.github.com/users/joe/following{/other_user}", + "gists_url": "https://api.github.com/users/joe/gists{/gist_id}", + "starred_url": "https://api.github.com/users/joe/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/joe/subscriptions", + "organizations_url": "https://api.github.com/users/joe/orgs", + "repos_url": "https://api.github.com/users/joe/repos", + "events_url": "https://api.github.com/users/joe/events{/privacy}", + "received_events_url": "https://api.github.com/users/joe/received_events", + "type": "User", + "site_admin": false + }, + "node_id": "MDc6UmVsZWFzZTQzMTkwNzI5", + "tag_name": "%%versionTag2%%", + "target_commitish": "1.x", + "name": "%%versionTag2%%", + "draft": false, + "prerelease": true, + "created_at": "2021-05-19T06:18:24Z", + "published_at": "2021-05-19T06:24:32Z", + "assets": [ + { + "url": "https://api.github.com/repos/my127/workspace/releases/assets/37159196", + "id": 37159196, + "node_id": "MDEyOlJlbGVhc2VBc3NldDM3MTU5MTk2", + "name": "ws", + "label": null, + "uploader": { + "login": "joe", + "id": 1554709, + "node_id": "MDQ6VXNlcjE1NTQ3MDk=", + "avatar_url": "https://avatars.githubusercontent.com/u/1554709?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/joe", + "html_url": "https://github.com/joe", + "followers_url": "https://api.github.com/users/joe/followers", + "following_url": "https://api.github.com/users/joe/following{/other_user}", + "gists_url": "https://api.github.com/users/joe/gists{/gist_id}", + "starred_url": "https://api.github.com/users/joe/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/joe/subscriptions", + "organizations_url": "https://api.github.com/users/joe/orgs", + "repos_url": "https://api.github.com/users/joe/repos", + "events_url": "https://api.github.com/users/joe/events{/privacy}", + "received_events_url": "https://api.github.com/users/joe/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/octet-stream", + "state": "uploaded", + "size": 2944641, + "download_count": 20, + "created_at": "2021-05-19T06:22:50Z", + "updated_at": "2021-05-19T06:22:54Z", + "browser_download_url": "%%browserDownloadUrl%%" + } + ], + "tarball_url": "https://api.github.com/repos/my127/workspace/tarball/%%versionTag2%%", + "zipball_url": "https://api.github.com/repos/my127/workspace/zipball/%%versionTag2%%", + "body": "## [%%versionTag2%%](https://github.com/my127/workspace/tree/%%versionTag2%%) (2021-05-19)\r\n\r\n[Full Changelog](https://github.com/my127/workspace/compare/0.1.3...%%versionTag1%%)\r\n\r\n**Implemented enhancements:**\r\n\r\n- Add a cheatsheet [\\#83](https://github.com/my127/workspace/pull/83) ([rgpjones](https://github.com/rgpjones))\r\n- Add poweroff command [\\#82](https://github.com/my127/workspace/pull/82) ([joe](https://github.com/joe))\r\n- Add before/after overlay events and match with before for prepare [\\#78](https://github.com/my127/workspace/pull/78) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Improve integration test suite [\\#77](https://github.com/my127/workspace/pull/77) ([dantleech](https://github.com/dantleech))\r\n- Add PHP 8 support [\\#68](https://github.com/my127/workspace/pull/68) ([elvetemedve](https://github.com/elvetemedve))\r\n- Add config dump command [\\#67](https://github.com/my127/workspace/pull/67) ([hgajjar](https://github.com/hgajjar))\r\n- Add Jaeger daemon [\\#66](https://github.com/my127/workspace/pull/66) ([joe](https://github.com/joe))\r\n- Add more default symfony expression functions [\\#56](https://github.com/my127/workspace/pull/56) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Update symfony components and twig to latest minor version [\\#55](https://github.com/my127/workspace/pull/55) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Upgrade to mailhog image v1.0.1 [\\#54](https://github.com/my127/workspace/pull/54) ([joe](https://github.com/joe))\r\n- Fix multiple argument run/passthru escaping [\\#53](https://github.com/my127/workspace/pull/53) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Make commands and function errors debuggable by saying what name they are [\\#51](https://github.com/my127/workspace/pull/51) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Add missing Bash interpreter page [\\#49](https://github.com/my127/workspace/pull/49) ([opdavies](https://github.com/opdavies))\r\n- Misc: license as MIT [\\#47](https://github.com/my127/workspace/pull/47) ([dcole-inviqa](https://github.com/dcole-inviqa))\r\n- Note that curl is required [\\#46](https://github.com/my127/workspace/pull/46) ([joe](https://github.com/joe))\r\n- Add an after\\('harness.prepare'\\) event [\\#44](https://github.com/my127/workspace/pull/44) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Test on PHP 7.4 [\\#40](https://github.com/my127/workspace/pull/40) ([joe](https://github.com/joe))\r\n\r\n**Fixed bugs:**\r\n\r\n- Add requirement to use prefix space to avoid secrets in shell history [\\#65](https://github.com/my127/workspace/pull/65) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n- Handle sidekick errexiting correctly [\\#58](https://github.com/my127/workspace/pull/58) ([andytson-inviqa](https://github.com/andytson-inviqa))\r\n\r\n**Closed issues:**\r\n\r\n- Service commands provide no feedback if command not found [\\#62](https://github.com/my127/workspace/issues/62)\r\n- Support for self-update? [\\#60](https://github.com/my127/workspace/issues/60)\r\n- Executing a ws command that does not exist should return a non-zero exit code [\\#45](https://github.com/my127/workspace/issues/45)\r\n- Support for multiple harnesses to one workspace [\\#23](https://github.com/my127/workspace/issues/23)\r\n\r\n## Installation instructions\r\n```bash\r\ncurl --output ./ws --location https://github.com/my127/workspace/releases/download/%%versionTag1%%/ws\r\nchmod +x ws && sudo mv ws /usr/local/bin/ws\r\n```" + } +] \ No newline at end of file