feat(release): ReleasePlanner — proposed beta targets from the current version

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-22 23:07:41 +02:00
parent 39ccf0d539
commit 05c98a7b9e
2 changed files with 96 additions and 0 deletions

View File

@ -0,0 +1,45 @@
<?php
namespace App\Services;
/**
* Pure semver arithmetic for the Release page: given the current version, what beta-target versions
* may the operator cut? The dashboard computes the target X.Y.Z; the host bridge computes -betaN.
*/
class ReleasePlanner
{
/**
* Proposed target versions (bare X.Y.Z) for the current version. A beta also offers continuing
* its own base (another beta of the same target).
*
* @return array{patch:string, minor:string, major:string, continueBeta?:string}
*/
public function proposedTargets(string $current): array
{
$current = ltrim($current, 'vV');
$base = preg_replace('/-.*$/', '', $current);
[$maj, $min, $pat] = array_map('intval', array_pad(explode('.', (string) $base), 3, '0'));
$targets = [
'patch' => "{$maj}.{$min}.".($pat + 1),
'minor' => "{$maj}.".($min + 1).'.0',
'major' => ($maj + 1).'.0.0',
];
if (str_contains($current, '-beta')) {
$targets['continueBeta'] = "{$maj}.{$min}.{$pat}";
}
return $targets;
}
/**
* The flat list of allowed target versions the server-side allow-list for a posted target.
*
* @return array<int,string>
*/
public function allowedTargets(string $current): array
{
return array_values($this->proposedTargets($current));
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Tests\Unit;
use App\Services\ReleasePlanner;
use PHPUnit\Framework\TestCase;
class ReleasePlannerTest extends TestCase
{
private ReleasePlanner $planner;
protected function setUp(): void
{
parent::setUp();
$this->planner = new ReleasePlanner;
}
public function test_stable_version_offers_patch_minor_major_without_continue_beta(): void
{
$t = $this->planner->proposedTargets('0.9.58');
$this->assertSame('0.9.59', $t['patch']);
$this->assertSame('0.10.0', $t['minor']);
$this->assertSame('1.0.0', $t['major']);
$this->assertArrayNotHasKey('continueBeta', $t);
}
public function test_beta_version_also_offers_continue_beta_of_its_base(): void
{
$t = $this->planner->proposedTargets('0.10.0-beta3');
$this->assertSame('0.10.0', $t['continueBeta']);
$this->assertSame('0.10.1', $t['patch']);
$this->assertSame('0.11.0', $t['minor']);
$this->assertSame('1.0.0', $t['major']);
}
public function test_strips_a_leading_v(): void
{
$this->assertSame('0.9.59', $this->planner->proposedTargets('v0.9.58')['patch']);
}
public function test_allowed_targets_are_the_proposed_values(): void
{
$allowed = $this->planner->allowedTargets('0.10.0-beta1');
$this->assertContains('0.10.0', $allowed); // continueBeta
$this->assertContains('0.11.0', $allowed); // minor
$this->assertNotContains('0.9.0', $allowed);
}
}