From fc890d5f0c4258848e30d5cb501bcfd172f24639 Mon Sep 17 00:00:00 2001 From: Tommaso Bailetti Date: Wed, 20 Nov 2024 17:52:11 +0100 Subject: [PATCH] feat: added milestone dispatch feature --- README.md | 38 ++++++++++++----- app/Console/Commands/MilestoneRepository.php | 42 +++++++++++++++++++ app/Http/Middleware/MilestoneAuth.php | 26 ++++++++++++ app/Jobs/MilestoneRelease.php | 39 +++++++++++++++++ bootstrap/app.php | 4 +- config/repositories.php | 5 +++ phpunit.xml | 1 + routes/web.php | 10 +++++ .../Feature/Console/MilestoneReleaseTest.php | 27 ++++++++++++ tests/Feature/Http/MilestoneJobTest.php | 34 +++++++++++++++ 10 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 app/Console/Commands/MilestoneRepository.php create mode 100644 app/Http/Middleware/MilestoneAuth.php create mode 100644 app/Jobs/MilestoneRelease.php create mode 100644 tests/Feature/Console/MilestoneReleaseTest.php create mode 100644 tests/Feature/Http/MilestoneJobTest.php diff --git a/README.md b/README.md index 1731db9..cf0ced7 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,8 @@ While some of the values are self-explanatory, there are a few that you need to - `APP_URL`: The full URL where the application is reached from, while most of the functionalities will work with a wrong value, the url generation is based off this value. - `FILESYSTEM_DISK`: Disk to use during production, works same as development, more info in the development setup. +- `REPOSITORY_MILESTONE_TOKEN`: Token used to trigger from remote the milestone creation, you can set this to a random + value, it's used to avoid unwanted requests. ### Container Configuration @@ -213,14 +215,8 @@ otherwise you will lose reference to endpoints and snapshots (or files, if you'r ### First deploy -To deploy the service, you can find the related files needed in the `deploy` directory: - -- `compose`: Contains the `docker-compose.yml` file, this file can deploy the full stack once copied to the server and - then using `docker compose up -d`, remember to copy the `.env` production file to the same directory where you store - the compose file. -- `systemd`: Uses `systemd` and `podman` to deploy the service, you can find the service files in the `systemd`. Use - the `v4` or `v5` files, depending on the podman version you want to deploy. Remember to copy the `.env` production - file to the `%S/parceler.env` directory. +An example of a deployment can be found under the `deploy` directory, you can use the `deploy/docker-compose.yml` file +to deploy the full stack. And replicate the same structure in your server. ### Additional Configuration @@ -267,8 +263,8 @@ The command will guide you through the process of adding a repository, here's th - `command`: the command the worker will run to sync the repository it can be anything available in the container. Save the content of the repository under the path `source/{repository_name}` in the disk you're using. (e.g. if you're using the local disk, save the content of the repository - under `storage/app/source/repository_name`). `rclone` binary is available in the container, to add configuration file - follow the [Additional Configuration](#additional-configuration) section. + under `storage/app/source/{repository_name}`). `rclone` binary is available in the container, to add configuration + file follow the [Additional Configuration](#additional-configuration) section. - `source_folder`: if repository files are stored in a subfolder, you can specify it here, otherwise leave it empty. - `delay`: how many days the upstream files are delayed. @@ -360,6 +356,28 @@ be provided the folder that are snapshotted and which one is currently being ser php artisan repository:snapshots {repository_name} ``` +### Milestone release + +A Milestone Release is a process that wipes all the snapshots of a repository and then creates one with the latest sync. +This is useful when you want to release a new version of a repository, or when you want to force the release of a +specific set of packages. + +To trigger a milestone release, this can be done by both of the following: + +- CLI + +```bash +php artisan repository:milestone {repository_name} +``` + +- CURL + +Additional authentication must be provided, the token is set in the `.env` file under the `REPOSITORY_MILESTONE_TOKEN`. + +```bash +curl -X POST -H Accept:application/json -H Authorization:Bearer /repository//milestone +``` + ## List of behaviours in case of distribution of faulty packages The following list is a guide on how to handle the distribution of faulty packages, to find which of the snapshots has a diff --git a/app/Console/Commands/MilestoneRepository.php b/app/Console/Commands/MilestoneRepository.php new file mode 100644 index 0000000..ed14882 --- /dev/null +++ b/app/Console/Commands/MilestoneRepository.php @@ -0,0 +1,42 @@ +argument('repository'))->firstOrFail(); + MilestoneRelease::dispatch($repository); + $this->info("Milestone release for $repository->name dispatched."); + } catch (ModelNotFoundException) { + $this->fail("Repository '{$this->argument('repository')}' not found."); + } + } +} diff --git a/app/Http/Middleware/MilestoneAuth.php b/app/Http/Middleware/MilestoneAuth.php new file mode 100644 index 0000000..1185f72 --- /dev/null +++ b/app/Http/Middleware/MilestoneAuth.php @@ -0,0 +1,26 @@ +header('Authorization') !== 'Bearer '.config('repositories.milestone_token')) { + throw new UnauthorizedHttpException('Bearer', 'Unauthenticated'); + } + + return $next($request); + } +} diff --git a/app/Jobs/MilestoneRelease.php b/app/Jobs/MilestoneRelease.php new file mode 100644 index 0000000..cfc64e9 --- /dev/null +++ b/app/Jobs/MilestoneRelease.php @@ -0,0 +1,39 @@ +repository->name}."); + $toPurge = Storage::directories($this->repository->snapshotDir()); + SyncRepository::dispatchSync($this->repository); + foreach ($toPurge as $dir) { + Log::debug("Purging directory $dir."); + Storage::deleteDirectory($dir); + } + Log::debug("Milestone released for {$this->repository->name}."); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 7b162da..8fda6c7 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -11,7 +11,9 @@ health: '/up', ) ->withMiddleware(function (Middleware $middleware) { - // + $middleware->validateCsrfTokens(except: [ + '/repository/*/milestone', + ]); }) ->withExceptions(function (Exceptions $exceptions) { // diff --git a/config/repositories.php b/config/repositories.php index b6eb822..ccb96a6 100644 --- a/config/repositories.php +++ b/config/repositories.php @@ -20,4 +20,9 @@ * This directory contains the snapshots of synced repositories. */ 'snapshots' => env('REPOSITORY_BASE_FOLDER', 'snapshots'), + + /* + * Milestone release authentication token + */ + 'milestone_token' => env('REPOSITORY_MILESTONE_TOKEN'), ]; diff --git a/phpunit.xml b/phpunit.xml index 04ec163..daac24f 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -30,5 +30,6 @@ + diff --git a/routes/web.php b/routes/web.php index 927db93..54f5c0a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -4,12 +4,22 @@ use App\Http\Middleware\CommunityLicenceCheck; use App\Http\Middleware\EnterpriseLicenceCheck; use App\Http\Middleware\ForceBasicAuth; +use App\Http\Middleware\MilestoneAuth; +use App\Jobs\MilestoneRelease; +use App\Models\Repository; use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); }); +Route::middleware(MilestoneAuth::class) + ->post('/repository/{repository:name}/milestone', function (Repository $repository) { + MilestoneRelease::dispatch($repository); + + return response()->json(['message' => 'Milestone release job dispatched.']); + }); + Route::middleware(ForceBasicAuth::class)->group(function () { Route::get('/repository/community/{repository:name}/{path}', RepositoryController::class) ->where('path', '.*') diff --git a/tests/Feature/Console/MilestoneReleaseTest.php b/tests/Feature/Console/MilestoneReleaseTest.php new file mode 100644 index 0000000..af78bc6 --- /dev/null +++ b/tests/Feature/Console/MilestoneReleaseTest.php @@ -0,0 +1,27 @@ +assertFailed(); +})->throws(RuntimeException::class); + +it('cannot release milestone for a non-existing repository', function () { + artisan('repository:milestone', ['repository' => 'non-existing-repo']) + ->assertFailed(); +}); + +it('ensure MilestoneRelease job is dispatched', function () { + $repo = Repository::factory()->create(); + Queue::fake(); + artisan('repository:milestone', ['repository' => $repo->name]) + ->expectsOutput("Milestone release for $repo->name dispatched.") + ->assertExitCode(0); + Queue::assertPushed(function (MilestoneRelease $job) use ($repo): bool { + return $job->repository->is($repo); + }); +}); diff --git a/tests/Feature/Http/MilestoneJobTest.php b/tests/Feature/Http/MilestoneJobTest.php new file mode 100644 index 0000000..03735cd --- /dev/null +++ b/tests/Feature/Http/MilestoneJobTest.php @@ -0,0 +1,34 @@ +create(); + post("repository/$repo->name/milestone") + ->assertUnauthorized() + ->assertHeader('WWW-Authenticate', 'Bearer'); + withToken('random')->post("repository/$repo->name/milestone") + ->assertUnauthorized() + ->assertHeader('WWW-Authenticate', 'Bearer'); +}); + +it('will dispatch a MilestoneRelease job', function () { + $repo = Repository::factory()->create(); + Queue::fake(); + withToken(config('repositories.milestone_token')) + ->post("repository/$repo->name/milestone") + ->assertOk(); + Queue::assertPushed(function (MilestoneRelease $job) use ($repo): bool { + return $job->repository->is($repo); + }); +}); + +it('cannot dispatch a MilestoneRelease job for a non-existent repository', function () { + withToken(config('repositories.milestone_token')) + ->post('repository/non-existent/milestone') + ->assertNotFound(); +});