diff --git a/src/Updater/Plugin/Command.php b/src/Updater/Plugin/Command.php index 0283b6a0..8bb02e06 100644 --- a/src/Updater/Plugin/Command.php +++ b/src/Updater/Plugin/Command.php @@ -36,7 +36,7 @@ private function action() } try { - $this->updater->update(BaseApplication::getVersion(), $pharPath); + $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); diff --git a/src/Updater/Updater.php b/src/Updater/Updater.php index 1531ce30..2aef7b4d 100644 --- a/src/Updater/Updater.php +++ b/src/Updater/Updater.php @@ -23,24 +23,28 @@ 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); + } + + 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,18 +71,12 @@ 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 releases from GitHub.', self::CODE_ERR_FETCHING_RELEASES); - } - - $releases = json_decode($releases); - - if (count($releases) === 0) { - throw new \RuntimeException('No releases present in the GitHub API response.', self::CODE_NO_RELEASES); + throw new \RuntimeException('Error fetching latest release from GitHub.', self::CODE_ERR_FETCHING_RELEASES); } - $latest = $releases[0]; + $latest = json_decode($release); return new Release($latest->assets[0]->browser_download_url, $latest->tag_name); } diff --git a/tests/Test/Updater/UpdaterTest.php b/tests/Test/Updater/UpdaterTest.php index ec9576c1..b85195cc 100644 --- a/tests/Test/Updater/UpdaterTest.php +++ b/tests/Test/Updater/UpdaterTest.php @@ -7,11 +7,24 @@ use my127\Workspace\Updater\Updater; use PHPUnit\Framework\TestCase; +use \RecursiveIteratorIterator; +use \RecursiveDirectoryIterator; + 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); + } + } } public static function setUpBeforeClass(): void @@ -28,76 +41,70 @@ 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.json', new NullOutput()); - $updater->update('0.9.0', $temp); + $updater = new Updater(__DIR__ . '/fixtures/generated/valid', new NullOutput()); + $updater->updateLatest('0.9.0', $temp); $this->assertFileExists($temp); } - private function prepareReleasesFixture(string $name, string $releasePath, string $version): void + private function prepareLatestFixture(string $name, string $releasePath, string $version): void { - $contents = file_get_contents(__DIR__ . '/fixtures/tpl/releases.json'); + $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```" +}