mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-17 17:15:08 +00:00
8f12c83d21
ProjectSend prints the update instructions for the way this server was installed, and it knew two answers where it needed three: anything inside a container was handed `docker compose pull && docker compose up -d`. On the Compose stack that builds from a checkout there is no image behind those containers, so `pull` skips every ProjectSend service and `up -d` then finds them all current — the update reports success, changes nothing, and the dashboard goes on offering the same release. Reported by @mueller7382, who stayed on 2.0.0 that way while 2.1.0 was out (#1661). Those installations are now their own kind, told to `git pull` and rebuild, with the two steps a checkout needs that an image does not: its dependencies and its compiled frontend live outside git, so a release that moved either leaves them stale. Two signals decide it, in that order. The published image now declares itself with PROJECTSEND_IMAGE, which is the only evidence an operator bind-mounting over /var/www/html can neither hide nor forge; failing that — images published before this — a working tree in the install directory, which the image never has and the repository's own stack always does. getenv() rather than env(), because a cached configuration makes env() outside a config file return null, and the answer would flip silently on exactly the installs most likely to have cached it. The stale-code banner keeps treating both container kinds alike: what clears it is recreating the container, whichever way its image was built. The changelog also credits the reporter of #1663, which was missed when that entry was written.
139 lines
4.6 KiB
PHP
139 lines
4.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\User;
|
|
use App\Modules\Identity\Models\Role;
|
|
use App\Modules\Identity\Permissions\SystemRole;
|
|
use App\Modules\Platform\Capabilities\Edition;
|
|
use App\Modules\Platform\Installation\Installation;
|
|
use App\Modules\Platform\Installation\InstallationKind;
|
|
use App\Modules\Platform\Settings\Setting;
|
|
use App\Modules\Platform\Settings\Settings;
|
|
|
|
/**
|
|
* There is no way to be in a container and not in one within a single test
|
|
* run, so the detection is answered for — same seam and same reasoning as
|
|
* InstallationKindTest's own fake.
|
|
*/
|
|
class NoticeInstallation extends Installation
|
|
{
|
|
public function __construct(
|
|
private readonly bool $container,
|
|
private readonly bool $source = false,
|
|
) {}
|
|
|
|
protected function inContainer(): bool
|
|
{
|
|
return $this->container;
|
|
}
|
|
|
|
protected function builtFromSource(): bool
|
|
{
|
|
return $this->source;
|
|
}
|
|
}
|
|
|
|
function applied(string $version): void
|
|
{
|
|
app(Settings::class)->set(Setting::AppliedVersion, $version);
|
|
app(Settings::class)->set(Setting::AppliedVersionAt, now()->toIso8601String());
|
|
}
|
|
|
|
/**
|
|
* The shared prop as the page would receive it. Read out of the root
|
|
* view's data rather than through assertInertia's callback, because most
|
|
* of these assert the notice's *absence* — and a callback that never runs
|
|
* proves nothing.
|
|
*
|
|
* @return array<string, string>|null
|
|
*/
|
|
function noticeFor(User $user): ?array
|
|
{
|
|
$page = test()->actingAs($user)
|
|
->get('/dashboard')
|
|
->assertSuccessful()
|
|
->viewData('page');
|
|
|
|
return $page['props']['code_notice'] ?? null;
|
|
}
|
|
|
|
beforeEach(function () {
|
|
$this->admin = User::factory()->create();
|
|
config()->set('projectsend.version', '2.1.0');
|
|
});
|
|
|
|
test('nothing is said when the applied version is the running one', function () {
|
|
applied('2.1.0');
|
|
|
|
expect(noticeFor($this->admin))->toBeNull();
|
|
});
|
|
|
|
test('nothing is said on an installation that has never applied an update', function () {
|
|
expect(app(Settings::class)->get(Setting::AppliedVersion))->toBe('')
|
|
->and(noticeFor($this->admin))->toBeNull();
|
|
});
|
|
|
|
// The failure this exists for: files replaced, PHP never reloaded, so the
|
|
// database is ahead of the code every visitor is being served.
|
|
test('an applied version ahead of the running code reports stale code', function () {
|
|
applied('2.2.0');
|
|
|
|
$notice = noticeFor($this->admin);
|
|
|
|
expect($notice)->not->toBeNull()
|
|
->and($notice['reason'])->toBe('stale_code')
|
|
->and($notice['applied'])->toBe('2.2.0')
|
|
->and($notice['running'])->toBe('2.1.0');
|
|
});
|
|
|
|
// The other half of the same mistake: new files unpacked, the update never
|
|
// run, so the schema is behind the code.
|
|
test('an applied version behind the running code reports a pending update', function () {
|
|
applied('2.0.0');
|
|
|
|
expect(noticeFor($this->admin)['reason'])->toBe('pending_update');
|
|
});
|
|
|
|
test('it is gated on view_system_info', function () {
|
|
applied('2.2.0');
|
|
|
|
$uploader = User::factory()->create([
|
|
'role_id' => Role::query()->where('name', SystemRole::AccountManager->value)->value('id'),
|
|
]);
|
|
|
|
expect(noticeFor($uploader))->toBeNull()
|
|
->and(noticeFor($this->admin))->not->toBeNull();
|
|
})->skip(fn (): bool => Role::query()->where('name', SystemRole::AccountManager->value)->doesntExist(), 'needs the seeded roles');
|
|
|
|
// Unlike update_notice, which is edition-gated: what code a server is
|
|
// executing is a fact about the machine, not a feature of an edition.
|
|
test('it is not gated on the edition', function () {
|
|
config()->set('projectsend.edition', Edition::Cloud);
|
|
applied('2.2.0');
|
|
|
|
expect(noticeFor($this->admin))->not->toBeNull();
|
|
});
|
|
|
|
test('it names the command this kind of installation can actually run', function (bool $container, bool $source, string $expected) {
|
|
app()->instance(Installation::class, new NoticeInstallation($container, $source));
|
|
applied('2.2.0');
|
|
|
|
expect(noticeFor($this->admin)['install_kind'])->toBe($expected);
|
|
})->with([
|
|
'a container from the published image' => [true, false, InstallationKind::Container->value],
|
|
'a container built from a checkout' => [true, true, InstallationKind::ContainerSource->value],
|
|
'a server somebody administers' => [false, false, InstallationKind::Manual->value],
|
|
]);
|
|
|
|
// The rollback story, asserted end to end: whatever the marker said, the
|
|
// command rewrites it to whatever is actually running.
|
|
test('running the update clears the notice', function () {
|
|
applied('2.2.0');
|
|
expect(noticeFor($this->admin))->not->toBeNull();
|
|
|
|
$this->artisan('projectsend:update')->assertSuccessful();
|
|
|
|
expect(noticeFor($this->admin))->toBeNull();
|
|
});
|