mirror of
https://github.com/getgrav/grav.git
synced 2026-05-08 01:36:00 +02:00
feat: surface remote next_major hint in Upgrader for cross-major migration notices
Reads the new `next_major` block from grav.json (sent by a family-aware resources server when a 1.x client is served a 1.x release alongside a 2.x hint) and exposes it via Upgrader::isNextMajorAvailable(), getNextMajorVersion(), getMigrationUrl(). The admin plugin uses these to show a dashboard migration banner without implying an automatic upgrade. isNextMajorAvailable() no longer compares raw remote vs local majors — under family-aware serving, the remote version is already the client's family, so the old logic would silently stop firing. It now requires the server hint and verifies it truly points to a newer major. Tests updated to inject the hint via an extended TestableUpgrader.
This commit is contained in:
@@ -29,6 +29,8 @@ class GravCore extends AbstractPackageCollection
|
||||
private $date;
|
||||
/** @var string|null */
|
||||
private $min_php;
|
||||
/** @var array|null */
|
||||
private $next_major;
|
||||
|
||||
/**
|
||||
* @param bool $refresh
|
||||
@@ -45,10 +47,13 @@ class GravCore extends AbstractPackageCollection
|
||||
|
||||
$this->fetch($refresh, $callback);
|
||||
|
||||
$this->data = json_decode((string) $this->raw, true);
|
||||
$this->version = $this->data['version'] ?? '-';
|
||||
$this->date = $this->data['date'] ?? '-';
|
||||
$this->min_php = $this->data['min_php'] ?? null;
|
||||
$this->data = json_decode((string) $this->raw, true);
|
||||
$this->version = $this->data['version'] ?? '-';
|
||||
$this->date = $this->data['date'] ?? '-';
|
||||
$this->min_php = $this->data['min_php'] ?? null;
|
||||
$this->next_major = isset($this->data['next_major']) && is_array($this->data['next_major'])
|
||||
? $this->data['next_major']
|
||||
: null;
|
||||
|
||||
if (isset($this->data['assets'])) {
|
||||
foreach ((array)$this->data['assets'] as $slug => $data) {
|
||||
@@ -147,4 +152,15 @@ class GravCore extends AbstractPackageCollection
|
||||
{
|
||||
return is_link(GRAV_ROOT . DS . 'index.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the `next_major` hint block from the remote response, if present.
|
||||
* Expected shape: ['version' => '2.0.0', 'migration_url' => 'https://...'].
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getNextMajor(): ?array
|
||||
{
|
||||
return $this->next_major;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,17 +135,49 @@ class Upgrader
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when a newer major version is available (e.g. currently on 1.x, remote offers 2.x).
|
||||
* Intended for informational notices — does not imply an automatic upgrade will occur.
|
||||
* Returns true when the remote has advertised a newer major via the `next_major` hint.
|
||||
* Used for informational notices — never implies an automatic upgrade.
|
||||
*
|
||||
* The server sends `next_major` to older-family clients (e.g. 1.8.x) alongside the
|
||||
* family-appropriate release, so comparing raw remote/local versions here would miss it.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isNextMajorAvailable(): bool
|
||||
{
|
||||
$localMajor = (int) explode('.', $this->getLocalVersion())[0];
|
||||
$remoteMajor = (int) explode('.', $this->getRemoteVersion())[0];
|
||||
$next = $this->remote->getNextMajor();
|
||||
if (!$next || empty($next['version'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $remoteMajor > $localMajor;
|
||||
$localMajor = (int) explode('.', $this->getLocalVersion())[0];
|
||||
$nextMajor = (int) explode('.', (string) $next['version'])[0];
|
||||
|
||||
return $nextMajor > $localMajor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next-major version advertised by the remote, or null when none is offered.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getNextMajorVersion(): ?string
|
||||
{
|
||||
$next = $this->remote->getNextMajor();
|
||||
|
||||
return $next['version'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the migration URL advertised by the remote alongside the next-major hint.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getMigrationUrl(): ?string
|
||||
{
|
||||
$next = $this->remote->getNextMajor();
|
||||
|
||||
return $next['migration_url'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,9 @@ use PHPUnit\Framework\TestCase;
|
||||
/**
|
||||
* Testable subclass that bypasses the GravCore HTTP constructor so we can
|
||||
* inject arbitrary local/remote version strings without any HTTP calls.
|
||||
*
|
||||
* Also stubs the `next_major` hint path — the real Upgrader reads that via
|
||||
* $this->remote->getNextMajor(), which we emulate with an injected array.
|
||||
*/
|
||||
class TestableUpgrader extends Upgrader
|
||||
{
|
||||
@@ -14,12 +17,15 @@ class TestableUpgrader extends Upgrader
|
||||
private $localVersion;
|
||||
/** @var string */
|
||||
private $remoteVersion;
|
||||
/** @var array|null */
|
||||
private $nextMajor;
|
||||
|
||||
public function __construct(string $local, string $remote)
|
||||
public function __construct(string $local, string $remote, ?array $nextMajor = null)
|
||||
{
|
||||
// Intentionally skip parent constructor — no HTTP, no Grav bootstrap needed.
|
||||
$this->localVersion = $local;
|
||||
$this->remoteVersion = $remote;
|
||||
$this->nextMajor = $nextMajor;
|
||||
}
|
||||
|
||||
public function getLocalVersion(): string
|
||||
@@ -31,13 +37,26 @@ class TestableUpgrader extends Upgrader
|
||||
{
|
||||
return $this->remoteVersion;
|
||||
}
|
||||
|
||||
// Override isNextMajorAvailable so tests don't hit the (unset) GravCore remote.
|
||||
public function isNextMajorAvailable(): bool
|
||||
{
|
||||
if (!$this->nextMajor || empty($this->nextMajor['version'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$localMajor = (int) explode('.', $this->getLocalVersion())[0];
|
||||
$nextMajor = (int) explode('.', (string) $this->nextMajor['version'])[0];
|
||||
|
||||
return $nextMajor > $localMajor;
|
||||
}
|
||||
}
|
||||
|
||||
class UpgraderFamilyTest extends TestCase
|
||||
{
|
||||
private function make(string $local, string $remote): TestableUpgrader
|
||||
private function make(string $local, string $remote, ?array $nextMajor = null): TestableUpgrader
|
||||
{
|
||||
return new TestableUpgrader($local, $remote);
|
||||
return new TestableUpgrader($local, $remote, $nextMajor);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
@@ -46,15 +65,18 @@ class UpgraderFamilyTest extends TestCase
|
||||
|
||||
public function testOneEightToTwoZeroIsBlocked(): void
|
||||
{
|
||||
$u = $this->make('1.8.0-beta.28', '2.0.0');
|
||||
// Pre family-aware server (or client not yet supporting the hint) — cross-family raw remote.
|
||||
// Upgrade still must be blocked; notice fires only when server sends next_major hint.
|
||||
$u = $this->make('1.8.0-beta.28', '2.0.0', ['version' => '2.0.0']);
|
||||
$this->assertFalse($u->isUpgradable(), '1.8→2.0 must be blocked');
|
||||
$this->assertTrue($u->isNextMajorAvailable(), '2.0 notice must fire');
|
||||
$this->assertTrue($u->isNextMajorAvailable(), '2.0 notice must fire when server hints next_major');
|
||||
}
|
||||
|
||||
public function testOneSevenToTwoZeroIsBlocked(): void
|
||||
{
|
||||
$u = $this->make('1.7.49', '2.0.0');
|
||||
$this->assertFalse($u->isUpgradable(), '1.7→2.0 must be blocked');
|
||||
// Family-aware: server returns 1.7.x to a 1.7.x client and hints next_major=2.0.0.
|
||||
$u = $this->make('1.7.49', '1.7.49', ['version' => '2.0.0']);
|
||||
$this->assertFalse($u->isUpgradable(), '1.7 client must not self-upgrade across majors');
|
||||
$this->assertTrue($u->isNextMajorAvailable());
|
||||
}
|
||||
|
||||
@@ -114,13 +136,23 @@ class UpgraderFamilyTest extends TestCase
|
||||
|
||||
public function testNextMajorFiredCorrectly(): void
|
||||
{
|
||||
$this->assertTrue($this->make('1.9.0', '2.0.0')->isNextMajorAvailable());
|
||||
$this->assertTrue($this->make('2.1.0', '3.0.0')->isNextMajorAvailable());
|
||||
// Hint-driven: notice fires when server advertises a newer major than local.
|
||||
$this->assertTrue($this->make('1.9.0', '1.9.0', ['version' => '2.0.0'])->isNextMajorAvailable());
|
||||
$this->assertTrue($this->make('2.1.0', '2.1.0', ['version' => '3.0.0'])->isNextMajorAvailable());
|
||||
}
|
||||
|
||||
public function testNextMajorNotFiredWhenOnTwoZero(): void
|
||||
{
|
||||
// On 2.0, the 2.0.x server would not advertise a next_major pointing back at 2.x.
|
||||
$this->assertFalse($this->make('2.0.0', '2.0.1')->isNextMajorAvailable());
|
||||
$this->assertFalse($this->make('2.0.0', '2.1.0')->isNextMajorAvailable());
|
||||
// Even if a stale hint slipped through, a same-major hint must not fire.
|
||||
$this->assertFalse($this->make('2.0.0', '2.0.1', ['version' => '2.0.1'])->isNextMajorAvailable());
|
||||
}
|
||||
|
||||
public function testNextMajorNotFiredWithoutHint(): void
|
||||
{
|
||||
// No next_major hint from server → notice must not fire regardless of remote version.
|
||||
$this->assertFalse($this->make('1.9.0', '2.0.0')->isNextMajorAvailable());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user