diff --git a/README.md b/README.md index bf8b381..ef92abc 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ functionality for your Laravel application. - GitHub - Gitlab +- Gitea - Http-based archives Usually you need this when distributing a self-hosted Laravel application @@ -206,6 +207,21 @@ The archive URL should contain nothing more than a simple directory listing with The target archive files must be zip archives and should contain all files on root level, not within an additional folder named like the archive itself. +### Using Gitea + +With _Gitea_ you can use your own Gitea-Instance with tag-releases. + +To use it, use the following settings in your `.env` file: + +| Config name | Value / Description | +| --------------------------------------- | --------------------------------------- | +| SELF_UPDATER_SOURCE | `gitea` | +| SELF_UPDATER_GITEA_URL | URL of Gitea Server | +| SELF_UPDATER_REPO_VENDOR | Repo Vendor Name | +| SELF_UPDATER_REPO_NAME | Repo Name | +| SELF_UPDATER_GITEA_PRIVATE_ACCESS_TOKEN | Access Token from Gitea | +| SELF_UPDATER_DOWNLOAD_PATH | Download path on the webapp host server | + ## Contributing Please see the [contributing guide](CONTRIBUTING.md). diff --git a/config/self-update.php b/config/self-update.php index f3e15fe..eef1702 100644 --- a/config/self-update.php +++ b/config/self-update.php @@ -62,6 +62,14 @@ 'download_path' => env('SELF_UPDATER_DOWNLOAD_PATH', '/tmp'), 'private_access_token' => env('SELF_UPDATER_HTTP_PRIVATE_ACCESS_TOKEN', ''), ], + 'gitea' => [ + 'type' => 'gitea', + 'repository_vendor' => env('SELF_UPDATER_REPO_VENDOR', ''), + 'gitea_url' => env('SELF_UPDATER_GITEA_URL', ''), + 'repository_name' => env('SELF_UPDATER_REPO_NAME', ''), + 'download_path' => env('SELF_UPDATER_DOWNLOAD_PATH', '/tmp'), + 'private_access_token' => env('SELF_UPDATER_GITEA_PRIVATE_ACCESS_TOKEN', ''), + ], ], /* diff --git a/src/SourceRepositoryTypes/GiteaRepositoryType.php b/src/SourceRepositoryTypes/GiteaRepositoryType.php new file mode 100644 index 0000000..93a3c5a --- /dev/null +++ b/src/SourceRepositoryTypes/GiteaRepositoryType.php @@ -0,0 +1,165 @@ +config = config('self-update.repository_types.gitea'); + + $this->release = resolve(Release::class); + $this->release->setStoragePath(Str::finish($this->config['download_path'], DIRECTORY_SEPARATOR)) + ->setUpdatePath(base_path(), config('self-update.exclude_folders')) + ->setAccessToken($this->config['private_access_token']); + $this->release->setAccessTokenPrefix('token '); + + $this->updateExecutor = $updateExecutor; + } + + public function update(Release $release): bool + { + return $this->updateExecutor->run($release); + } + + public function isNewVersionAvailable(string $currentVersion = ''): bool + { + $version = $currentVersion ?: $this->getVersionInstalled(); + + if (!$version) { + throw VersionException::versionInstalledNotFound(); + } + + $versionAvailable = $this->getVersionAvailable(); + + if (version_compare($version, $versionAvailable, '<')) { + if (!$this->versionFileExists()) { + $this->setVersionFile($versionAvailable); + } + event(new UpdateAvailable($versionAvailable)); + + return true; + } + + return false; + } + + public function getVersionInstalled(): string + { + return (string) config('self-update.version_installed'); + } + + /** + * Get the latest version that has been published in a certain repository. + * Example: 2.6.5 or v2.6.5. + * + * @param string $prepend Prepend a string to the latest version + * @param string $append Append a string to the latest version + * + * @throws Exception + */ + public function getVersionAvailable(string $prepend = '', string $append = ''): string + { + if ($this->versionFileExists()) { + $version = $prepend.$this->getVersionFile().$append; + } else { + $response = $this->getReleases(); + + $releaseCollection = collect(json_decode($response->body())); + $version = $prepend.$releaseCollection->first()->tag_name.$append; + } + + return $version; + } + + /** + * @throws ReleaseException + */ + public function fetch(string $version = ''): Release + { + $response = $this->getReleases(); + + try { + $releases = collect(Utils::jsonDecode($response->body())); + } catch (InvalidArgumentException $e) { + throw ReleaseException::noReleaseFound($version); + } + + if ($releases->isEmpty()) { + throw ReleaseException::noReleaseFound($version); + } + + $release = $this->selectRelease($releases, $version); + + $url = '/api/v1/repos/'.$this->config['repository_vendor'].'/'.$this->config['repository_name'].'/archive/'.$release->tag_name.'.zip'; + $downloadUrl = $this->config['gitea_url'].$url; + + $this->release->setVersion($release->tag_name) + ->setRelease($release->tag_name.'.zip') + ->updateStoragePath() + ->setDownloadUrl($downloadUrl); + + if (!$this->release->isSourceAlreadyFetched()) { + $this->release->download(); + $this->release->extract(); + } + + return $this->release; + } + + public function selectRelease(Collection $collection, string $version) + { + $release = $collection->first(); + + if (!empty($version)) { + if ($collection->contains('tag_name', $version)) { + $release = $collection->where('tag_name', $version)->first(); + } else { + Log::info('No release for version "'.$version.'" found. Selecting latest.'); + } + } + + return $release; + } + + final public function getReleases(): Response + { + $url = '/api/v1/repos/'.$this->config['repository_vendor'].'/'.$this->config['repository_name'].'/releases'; + + $headers = []; + + if ($this->release->hasAccessToken()) { + $headers = [ + 'Authorization' => $this->release->getAccessToken(), + ]; + } + + return Http::withHeaders($headers)->get($this->config['gitea_url'].$url); + } +} diff --git a/src/UpdaterManager.php b/src/UpdaterManager.php index d5bb390..6c4ab3e 100644 --- a/src/UpdaterManager.php +++ b/src/UpdaterManager.php @@ -7,6 +7,7 @@ use Codedge\Updater\Contracts\SourceRepositoryTypeContract; use Codedge\Updater\Contracts\UpdaterContract; use Codedge\Updater\Models\UpdateExecutor; +use Codedge\Updater\SourceRepositoryTypes\GiteaRepositoryType; use Codedge\Updater\SourceRepositoryTypes\GithubRepositoryType; use Codedge\Updater\SourceRepositoryTypes\GitlabRepositoryType; use Codedge\Updater\SourceRepositoryTypes\HttpRepositoryType; @@ -106,4 +107,9 @@ protected function createHttpRepository(): SourceRepositoryTypeContract { return $this->sourceRepository($this->app->make(HttpRepositoryType::class)); } + + protected function createGiteaRepository(): SourceRepositoryTypeContract + { + return $this->sourceRepository($this->app->make(GiteaRepositoryType::class)); + } } diff --git a/tests/Data/releases-gitea.json b/tests/Data/releases-gitea.json new file mode 100644 index 0000000..ba74fce --- /dev/null +++ b/tests/Data/releases-gitea.json @@ -0,0 +1,80 @@ +[ + { + "id": 5801852, + "tag_name": "0.0.2", + "target_commitish": "master", + "name": "Testrelease", + "body": "", + "url": "https://try.gitea.io/api/v1/repos/phillopp/emptyRepo/releases/5801851", + "html_url": "https://try.gitea.io/phillopp/emptyRepo/releases/tag/0.0.2", + "tarball_url": "https://try.gitea.io/phillopp/emptyRepo/archive/0.0.2.tar.gz", + "zipball_url": "https://try.gitea.io/phillopp/emptyRepo/archive/0.0.2.zip", + "draft": false, + "prerelease": false, + "created_at": "2022-08-18T09:34:20Z", + "published_at": "2022-08-18T09:34:20Z", + "author": { + "id": 515025, + "login": "phillopp", + "login_name": "", + "full_name": "", + "email": "phillopp@noreply.try.gitea.io", + "avatar_url": "https://try.gitea.io/avatar/330ff1fe1bb56077547730562f9bb038", + "language": "", + "is_admin": false, + "last_login": "0001-01-01T00:00:00Z", + "created": "2022-08-18T09:21:28Z", + "restricted": false, + "active": false, + "prohibit_login": false, + "location": "", + "website": "", + "description": "", + "visibility": "public", + "followers_count": 0, + "following_count": 0, + "starred_repos_count": 0, + "username": "phillopp" + }, + "assets": [] + }, + { + "id": 5801851, + "tag_name": "0.0.1", + "target_commitish": "master", + "name": "Testrelease", + "body": "", + "url": "https://try.gitea.io/api/v1/repos/phillopp/emptyRepo/releases/5801851", + "html_url": "https://try.gitea.io/phillopp/emptyRepo/releases/tag/0.0.1", + "tarball_url": "https://try.gitea.io/phillopp/emptyRepo/archive/0.0.1.tar.gz", + "zipball_url": "https://try.gitea.io/phillopp/emptyRepo/archive/0.0.1.zip", + "draft": false, + "prerelease": false, + "created_at": "2022-08-17T09:34:20Z", + "published_at": "2022-08-17T09:34:20Z", + "author": { + "id": 515025, + "login": "phillopp", + "login_name": "", + "full_name": "", + "email": "phillopp@noreply.try.gitea.io", + "avatar_url": "https://try.gitea.io/avatar/330ff1fe1bb56077547730562f9bb038", + "language": "", + "is_admin": false, + "last_login": "0001-01-01T00:00:00Z", + "created": "2022-08-18T09:21:28Z", + "restricted": false, + "active": false, + "prohibit_login": false, + "location": "", + "website": "", + "description": "", + "visibility": "public", + "followers_count": 0, + "following_count": 0, + "starred_repos_count": 0, + "username": "phillopp" + }, + "assets": [] + } +] \ No newline at end of file diff --git a/tests/SourceRepositoryTypes/GiteaRepositoryTypeTest.php b/tests/SourceRepositoryTypes/GiteaRepositoryTypeTest.php new file mode 100644 index 0000000..f59ae23 --- /dev/null +++ b/tests/SourceRepositoryTypes/GiteaRepositoryTypeTest.php @@ -0,0 +1,177 @@ +resetDownloadDir(); + } + + /** @test */ + public function it_can_instantiate(): void + { + $this->assertInstanceOf(GiteaRepositoryType::class, resolve(GiteaRepositoryType::class)); + } + + /** @test */ + public function it_can_run_update(): void + { + /** @var GiteaRepositoryType $gitea */ + $gitea = resolve(GiteaRepositoryType::class); + + /** @var Release $release */ + $release = resolve(Release::class); + $release->setStoragePath((string) config('self-update.repository_types.gitea.download_path')) + ->setVersion('1.0') + ->setRelease('release-1.0.zip') + ->updateStoragePath() + ->setDownloadUrl('https://gitea.com/download/target'); + + Event::fake(); + Http::fake([ + 'gitea.com/*' => $this->getResponse200ZipFile(), + ]); + $release->download(); + $release->extract(); + + $this->assertTrue($gitea->update($release)); + + Event::assertDispatched(UpdateSucceeded::class, 1); + Event::assertDispatched(UpdateSucceeded::class, function (UpdateSucceeded $e) use ($release) { + return $e->getVersionUpdatedTo() === $release->getVersion(); + }); + } + + /** @test */ + public function it_can_get_the_version_installed(): void + { + /** @var GiteaRepositoryType $gitea */ + $gitea = resolve(GiteaRepositoryType::class); + $this->assertEmpty($gitea->getVersionInstalled()); + + config(['self-update.version_installed' => '1.0']); + $this->assertEquals('1.0', $gitea->getVersionInstalled()); + } + + /** @test */ + public function it_cannot_get_new_version_available_and_fails_with_exception(): void + { + $this->expectException(VersionException::class); + $this->expectExceptionMessage('Version installed not found.'); + + /** @var GiteaRepositoryType $gitea */ + $gitea = resolve(GiteaRepositoryType::class); + $gitea->isNewVersionAvailable(); + } + + /** @test */ + public function it_can_get_new_version_available_without_version_file(): void + { + /** @var GiteaRepositoryType $gitea */ + $gitea = resolve(GiteaRepositoryType::class); + $gitea->deleteVersionFile(); + + Event::fake(); + Http::fake([ + '*' => $this->getResponse200Type('gitea'), + ]); + + $this->assertFalse($gitea->isNewVersionAvailable('2.7')); + $this->assertTrue($gitea->isNewVersionAvailable('0.0.1')); + + Event::assertDispatched(UpdateAvailable::class, 1); + Event::assertDispatched(UpdateAvailable::class, function (UpdateAvailable $e) use ($gitea) { + return $e->getVersionAvailable() === $gitea->getVersionAvailable(); + }); + } + + /** @test */ + public function it_cannot_fetch_releases_because_there_is_no_release(): void + { + /** @var GiteaRepositoryType $gitea */ + $gitea = resolve(GiteaRepositoryType::class); + + Http::fake([ + '*' => $this->getResponseEmpty(), + ]); + + $this->expectException(ReleaseException::class); + $this->expectExceptionMessage('No release found for version "latest version". Please check the repository you\'re pulling from'); + + $this->assertInstanceOf(Release::class, $gitea->fetch()); + } + + /** @test */ + public function it_can_fetch_releases(): void + { + /** @var GiteaRepositoryType $gitea */ + $gitea = resolve(GiteaRepositoryType::class); + + Http::fakeSequence() + ->pushResponse($this->getResponse200Type('gitea')) + ->pushResponse($this->getResponse200ZipFile()) + ->pushResponse($this->getResponse200Type('gitea')); + + $release = $gitea->fetch(); + + $this->assertInstanceOf(Release::class, $release); + } + + /** @test */ + public function it_can_get_specific_release_from_collection(): void + { + $items = [ + [ + 'tag_name' => '1.3', + 'name' => 'New version 1.3', + ], + [ + 'tag_name' => '1.2', + 'name' => 'New version 1.2', + ], + ]; + + /** @var GiteaRepositoryType $gitea */ + $gitea = resolve(GiteaRepositoryType::class); + + $this->assertEquals($items[1], $gitea->selectRelease(collect($items), '1.2')); + } + + /** @test */ + public function it_takes_latest_release_if_no_other_found(): void + { + $items = [ + [ + 'tag_name' => '1.3', + 'name' => 'New version 1.3', + ], + [ + 'tag_name' => '1.2', + 'name' => 'New version 1.2', + ], + ]; + + /** @var GiteaRepositoryType $gitea */ + $gitea = resolve(GiteaRepositoryType::class); + + Log::shouldReceive('info')->once()->with('No release for version "1.7" found. Selecting latest.'); + + $this->assertEquals('1.3', $gitea->selectRelease(collect($items), '1.7')['tag_name']); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 2c46e27..92c37a2 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -23,6 +23,7 @@ abstract class TestCase extends Orchestra 'branch' => 'releases-branch.json', 'http' => 'releases-http_gh.json', 'gitlab' => 'releases-gitlab.json', + 'gitea' => 'releases-gitea.json', ]; /** @@ -53,6 +54,14 @@ protected function getEnvironmentSetUp($app): void 'download_path' => self::DOWNLOAD_PATH, 'private_access_token' => '', ], + 'gitea' => [ + 'type' => 'gitea', + 'repository_vendor' => 'phillopp', + 'gitea_url' => 'https://try.gitea.io', + 'repository_name' => 'emptyRepo', + 'download_path' => self::DOWNLOAD_PATH, + 'private_access_token' => '', + ], ]); }