Skip to content

Commit 47157d5

Browse files
committed
refactor: renamed milestone command to release
Fixed issue where the sync could break the repo for a brief moment
1 parent 2c68010 commit 47157d5

14 files changed

Lines changed: 468 additions & 93 deletions

File tree

README.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ While most values are self-explanatory, there are a few that you need to manuall
189189
- `APP_URL`: The full URL where the application is reached from, while most of the functionalities will work with a
190190
wrong value, the url generation is based off this value.
191191
- `FILESYSTEM_DISK`: Disk to use during production, works same as development, more info in the development setup.
192-
- `REPOSITORY_MILESTONE_TOKEN`: Token used to trigger from remote the milestone creation, you can set this to a random
192+
- `REPOSITORY_RELEASE_TOKEN`: Token used to trigger from remote a clean release, you can set this to a random
193193
value, it's used to avoid unwanted requests.
194194
- `NIGHTWATCH_TOKEN`: In case you want to enable nightwatch stats, you need to set this to the token provided by
195195
nightwatch.
@@ -358,26 +358,25 @@ be provided the folder that are snapshotted and which one is currently being ser
358358
php artisan repository:snapshots {repository_name}
359359
```
360360

361-
### Milestone release
361+
### Fresh release
362362

363-
A Milestone Release is a process that wipes all the snapshots of a repository and then creates one with the latest sync.
363+
A Fresh Release is a process that wipes all the snapshots of a repository and then creates one with the latest sync.
364364
This is useful when you want to release a new version of a repository, or when you want to force the release of a
365365
specific set of packages.
366366

367-
To trigger a milestone release, this can be done by both of the following:
368-
367+
To trigger a fresh release, this can be done by both of the following:
369368
- CLI
370369

371370
```bash
372-
php artisan repository:milestone {repository_name}
371+
php artisan repository:release {repository_name}
373372
```
374373

375374
- CURL
376375

377-
Additional authentication must be provided, the token is set in the `.env` file under the `REPOSITORY_MILESTONE_TOKEN`.
376+
Additional authentication must be provided, the token is set in the `.env` file under the `REPOSITORY_RELEASE_TOKEN`.
378377

379378
```bash
380-
curl -X POST -H Accept:application/json -H Authorization:Bearer <token> <url>/repository/<repository_name>/milestone
379+
curl -X POST -H Accept:application/json -H Authorization:Bearer <token> <url>/repository/<repository_name>/release
381380
```
382381

383382
## List of behaviors when faulty packages get accidentally distributed

app/Console/Commands/MilestoneRepository.php renamed to app/Console/Commands/ReleaseRepository.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,27 @@
22

33
namespace App\Console\Commands;
44

5-
use App\Jobs\MilestoneRelease;
5+
use App\Jobs\Release;
66
use App\Models\Repository;
77
use Illuminate\Console\Command;
88
use Illuminate\Database\Eloquent\ModelNotFoundException;
99
use Throwable;
1010

11-
class MilestoneRepository extends Command
11+
class ReleaseRepository extends Command
1212
{
1313
/**
1414
* The name and signature of the console command.
1515
*
1616
* @var string
1717
*/
18-
protected $signature = 'repository:milestone {repository}';
18+
protected $signature = 'repository:release {repository}';
1919

2020
/**
2121
* The console command description.
2222
*
2323
* @var string
2424
*/
25-
protected $description = 'Dispatch a milestone reset job for the given repository.';
25+
protected $description = 'Dispatch a release job for the given repository.';
2626

2727
/**
2828
* Execute the console command.
@@ -33,8 +33,8 @@ public function handle(): void
3333
{
3434
try {
3535
$repository = Repository::where('name', $this->argument('repository'))->firstOrFail();
36-
MilestoneRelease::dispatch($repository);
37-
$this->info("Milestone release for $repository->name dispatched.");
36+
Release::dispatch($repository);
37+
$this->info("Release for $repository->name dispatched.");
3838
} catch (ModelNotFoundException) {
3939
$this->fail("Repository '{$this->argument('repository')}' not found.");
4040
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@
77
use Symfony\Component\HttpFoundation\Response;
88
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
99

10-
class MilestoneAuth
10+
class ReleaseAuth
1111
{
1212
/**
1313
* Using the Authorization header to authenticate the user, the token is passed as a Bearer token.
14-
* Check against the configuration milestone_token to allow the request.
14+
* Check against the configuration release_token to allow the request.
1515
*
1616
* @param Closure(Request): (Response) $next
1717
*/
1818
public function handle(Request $request, Closure $next): Response
1919
{
20-
if ($request->header('Authorization') !== 'Bearer '.config('repositories.milestone_token')) {
20+
if ($request->header('Authorization') !== 'Bearer '.config('repositories.release_token')) {
2121
throw new UnauthorizedHttpException('Bearer', 'Unauthenticated');
2222
}
2323

app/Jobs/DeleteSnapshot.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
//
4+
// Copyright (C) 2026 Nethesis S.r.l.
5+
// SPDX-License-Identifier: AGPL-3.0-or-later
6+
//
7+
8+
namespace App\Jobs;
9+
10+
use Illuminate\Bus\Batchable;
11+
use Illuminate\Contracts\Queue\ShouldQueue;
12+
use Illuminate\Foundation\Queue\Queueable;
13+
use Illuminate\Support\Facades\Log;
14+
use Illuminate\Support\Facades\Storage;
15+
16+
class DeleteSnapshot implements ShouldQueue
17+
{
18+
use Batchable, Queueable;
19+
20+
/**
21+
* Delete a snapshot directory.
22+
*/
23+
public function __construct(public readonly string $directory) {}
24+
25+
/**
26+
* Execute the job.
27+
*/
28+
public function handle(): void
29+
{
30+
if ($this->batch()?->cancelled()) {
31+
return;
32+
}
33+
34+
Log::debug("Deleting snapshot directory: {$this->directory}");
35+
Storage::deleteDirectory($this->directory);
36+
}
37+
}

app/Jobs/MilestoneRelease.php

Lines changed: 0 additions & 39 deletions
This file was deleted.

app/Jobs/Release.php

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
//
4+
// Copyright (C) 2026 Nethesis S.r.l.
5+
// SPDX-License-Identifier: AGPL-3.0-or-later
6+
//
7+
8+
namespace App\Jobs;
9+
10+
use App\Models\Repository;
11+
use Carbon\Carbon;
12+
use Illuminate\Bus\Batch;
13+
use Illuminate\Contracts\Queue\ShouldQueue;
14+
use Illuminate\Foundation\Queue\Queueable;
15+
use Illuminate\Support\Facades\Bus;
16+
use Illuminate\Support\Facades\Log;
17+
use Illuminate\Support\Facades\Storage;
18+
use Throwable;
19+
20+
class Release implements ShouldQueue
21+
{
22+
use Queueable;
23+
24+
/**
25+
* Sync the release.
26+
*/
27+
public function __construct(public readonly Repository $repository) {}
28+
29+
/**
30+
* Execute the job.
31+
*/
32+
public function handle(): void
33+
{
34+
Log::debug("Releasing {$this->repository->name}.");
35+
36+
// Sync the repository to create a fresh snapshot
37+
SyncRepository::dispatchSync($this->repository);
38+
39+
// Get all snapshot directories and filter to only valid snapshot timestamps
40+
$allDirectories = Storage::directories($this->repository->snapshotDir());
41+
$snapshots = collect($allDirectories)->filter(function ($dir) {
42+
$basename = basename($dir);
43+
try {
44+
Carbon::createFromFormat(DATE_ATOM, $basename);
45+
46+
return true;
47+
} catch (\Exception $e) {
48+
return false;
49+
}
50+
})->sortByDesc(function ($dir) {
51+
return Carbon::createFromFormat(DATE_ATOM, basename($dir));
52+
})->values();
53+
54+
if ($snapshots->isEmpty()) {
55+
Log::warning("No valid snapshots found for {$this->repository->name}.");
56+
57+
return;
58+
}
59+
60+
// Freeze to the most recent snapshot
61+
$lastSnapshot = $snapshots->first();
62+
$this->repository->freeze = basename($lastSnapshot);
63+
$this->repository->save();
64+
Log::info("Froze {$this->repository->name} to snapshot: {$this->repository->freeze}");
65+
66+
// Prepare deletion jobs for all other snapshots
67+
$snapshotsToDelete = $snapshots->slice(1);
68+
69+
if ($snapshotsToDelete->isEmpty()) {
70+
Log::info("No snapshots to delete for {$this->repository->name}.");
71+
72+
return;
73+
}
74+
75+
$deletionJobs = $snapshotsToDelete->map(fn ($dir) => new DeleteSnapshot($dir))->all();
76+
77+
// Dispatch batch with unfreeze on success
78+
Bus::batch($deletionJobs)
79+
->then(function (Batch $batch) {
80+
$repo = $this->repository->fresh();
81+
$repo->freeze = null;
82+
$repo->save();
83+
Log::info("Repository {$repo->name} unfrozen after successful cleanup.");
84+
})
85+
->catch(function (Batch $batch, Throwable $e) {
86+
Log::error("Deletion batch failed for {$this->repository->name}", [
87+
'batch_id' => $batch->id,
88+
'error' => $e->getMessage(),
89+
]);
90+
})
91+
->name("Release cleanup for {$this->repository->name}")
92+
->dispatch();
93+
94+
Log::debug("Release completed for {$this->repository->name}.");
95+
}
96+
}

bootstrap/app.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
)
1414
->withMiddleware(function (Middleware $middleware) {
1515
$middleware->validateCsrfTokens(except: [
16-
'/repository/*/milestone',
16+
'/repository/*/release',
1717
]);
1818
})
1919
->withExceptions(function (Exceptions $exceptions) {

config/repositories.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
'snapshots' => env('REPOSITORY_BASE_FOLDER', 'snapshots'),
2323

2424
/*
25-
* Milestone release authentication token
25+
* Fresh release authentication token
2626
*/
27-
'milestone_token' => env('REPOSITORY_MILESTONE_TOKEN'),
27+
'release_token' => env('REPOSITORY_RELEASE_TOKEN'),
2828
];

phpunit.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
<env name="QUEUE_CONNECTION" value="sync"/>
3131
<env name="SESSION_DRIVER" value="array"/>
3232
<env name="TELESCOPE_ENABLED" value="false"/>
33-
<env name="REPOSITORY_MILESTONE_TOKEN" value="testing" />
33+
<env name="REPOSITORY_RELEASE_TOKEN" value="testing" />
3434
<env name="NETIFYD_API_KEY" value="key" />
3535
</php>
3636
</phpunit>

routes/web.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
use App\Http\Middleware\CommunityLicenceCheck;
55
use App\Http\Middleware\EnterpriseLicenceCheck;
66
use App\Http\Middleware\ForceBasicAuth;
7-
use App\Http\Middleware\MilestoneAuth;
8-
use App\Jobs\MilestoneRelease;
7+
use App\Http\Middleware\ReleaseAuth;
8+
use App\Jobs\Release;
99
use App\Models\Repository;
1010
use Illuminate\Support\Facades\Route;
1111
use Laravel\Nightwatch\Http\Middleware\Sample;
@@ -14,11 +14,11 @@
1414
return view('welcome');
1515
});
1616

17-
Route::middleware(MilestoneAuth::class)
18-
->post('/repository/{repository:name}/milestone', function (Repository $repository) {
19-
MilestoneRelease::dispatch($repository);
17+
Route::middleware(ReleaseAuth::class)
18+
->post('/repository/{repository:name}/release', function (Repository $repository) {
19+
Release::dispatch($repository);
2020

21-
return response()->json(['message' => 'Milestone release job dispatched.']);
21+
return response()->json(['message' => 'Release job dispatched.']);
2222
});
2323

2424
Route::middleware([ForceBasicAuth::class, Sample::rate(0)])->group(function () {

0 commit comments

Comments
 (0)