File::delete(storage_path('app/deployment.json'))); it('reads the release number from the VERSION file', function () { // One source of truth, and it is a file — a source archive, a shallow CI // checkout and a container built without .git have no tags to describe. expect(config('app.version'))->toBe(trim(File::get(base_path('VERSION')))) ->and(config('app.version'))->toMatch('/^\d+\.\d+\.\d+$/'); }); it('reports a pinned release as that release', function () { writeManifest([ 'version' => config('app.version'), 'commit' => 'abc1234567890', 'source' => 'refs/tags/v'.config('app.version'), 'mode' => 'release', 'deployed_at' => now()->toIso8601String(), ]); $release = Release::current(); expect($release->isTaggedRelease)->toBeTrue() ->and($release->label())->toBe(config('app.version').' (abc1234)'); }); it('does not call a deployment after the release by the release\'s name', function () { // Every commit after v1.0.0 still carries VERSION=1.0.0 and is not 1.0.0. writeManifest([ 'commit' => 'def4567890123', 'source' => 'main', 'mode' => 'branch', 'deployed_at' => now()->toIso8601String(), ]); $release = Release::current(); expect($release->isTaggedRelease)->toBeFalse() ->and($release->label())->toBe(config('app.version').'-dev (def4567) · main'); }); it('keeps reporting the deployed version when an update fails halfway', function () { // The checkout moves before the migrations run, so a run that dies in // between leaves a newer VERSION on disk than is actually serving. Pairing // that number with the still-deployed commit would describe a build that // never existed. writeManifest([ 'version' => '0.9.0', 'commit' => 'aaa1111222333', 'source' => 'refs/tags/v0.9.0', 'mode' => 'release', 'deployed_at' => now()->subDay()->toIso8601String(), ]); $release = Release::current(); expect($release->version)->toBe('0.9.0') ->and($release->version)->not->toBe(config('app.version')) // Still a clean release label: 0.9.0 is genuinely what is running. ->and($release->isTaggedRelease)->toBeTrue() ->and($release->label())->toBe('0.9.0 (aaa1111)'); }); it('says something sensible before anything has been deployed', function () { File::delete(storage_path('app/deployment.json')); expect(Release::current()->commit)->toBeNull() ->and(Release::current()->label())->toBe(config('app.version').'-dev'); }); it('does not take the console down over an unparseable timestamp', function () { writeManifest([ 'version' => '1.0.0', 'commit' => 'abc1234567890', 'source' => 'main', 'mode' => 'branch', 'deployed_at' => 'not-a-date', ]); // A line of small print in the sidebar must not 500 every admin page. expect(Release::current()->deployedAt)->toBeNull() ->and(Release::current()->label())->toBe('1.0.0-dev (abc1234) · main'); $this->actingAs(User::factory()->operator()->create()) ->get(route('admin.overview')) ->assertOk(); }); it('survives a manifest that is not readable json', function () { File::ensureDirectoryExists(storage_path('app')); File::put(storage_path('app/deployment.json'), '{ half-written'); // The console reporting nothing is a nuisance; the console 500ing because // a deployment was interrupted mid-write is an outage. expect(Release::current()->label())->toBe(config('app.version').'-dev'); }); it('shows the running version in the console', function () { writeManifest([ 'commit' => 'abc1234567890', 'source' => 'refs/tags/v'.config('app.version'), 'mode' => 'release', 'deployed_at' => now()->toIso8601String(), ]); $this->actingAs(User::factory()->operator()->create()) ->get(route('admin.overview')) ->assertOk() ->assertSee(config('app.version').' (abc1234)'); });