mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-03-14 20:33:40 +00:00
feat: add update page, refactor update checking process
Some checks failed
tests / ci (push) Has been cancelled
Some checks failed
tests / ci (push) Has been cancelled
This commit is contained in:
parent
eb767fa6d0
commit
297a17d00b
17 changed files with 1212 additions and 241 deletions
216
app/Jobs/CheckVersionUpdateJob.php
Normal file
216
app/Jobs/CheckVersionUpdateJob.php
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Settings\UpdateSettings;
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Http\Client\ConnectionException;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class CheckVersionUpdateJob
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
private const CACHE_KEY = 'latest_release';
|
||||||
|
|
||||||
|
private const BACKOFF_KEY = 'github_api_backoff';
|
||||||
|
|
||||||
|
private const BACKOFF_MINUTES = 10;
|
||||||
|
|
||||||
|
private const CACHE_TTL = 86400;
|
||||||
|
|
||||||
|
public function __construct(private bool $forceRefresh = false) {}
|
||||||
|
|
||||||
|
public function handle(UpdateSettings $updateSettings): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$currentVersion = config('app.version');
|
||||||
|
|
||||||
|
if (! $currentVersion) {
|
||||||
|
return $this->errorResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
$backoffUntil = Cache::get(self::BACKOFF_KEY);
|
||||||
|
if ($this->isInBackoffPeriod($backoffUntil)) {
|
||||||
|
return $this->rateLimitResponse($backoffUntil);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cachedResponse = Cache::get(self::CACHE_KEY);
|
||||||
|
$response = $this->fetchOrUseCache($cachedResponse, $updateSettings->prereleases, $backoffUntil);
|
||||||
|
|
||||||
|
if (! $response) {
|
||||||
|
return $this->errorResponse('fetch_failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
[$latestVersion, $releaseData] = $this->extractLatestVersion($response, $updateSettings->prereleases);
|
||||||
|
$isNewer = $latestVersion && version_compare($latestVersion, $currentVersion, '>');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'latest_version' => $latestVersion,
|
||||||
|
'is_newer' => $isNewer,
|
||||||
|
'release_data' => $releaseData,
|
||||||
|
'error' => null,
|
||||||
|
];
|
||||||
|
} catch (ConnectionException $e) {
|
||||||
|
Log::error('Version check failed: '.$e->getMessage());
|
||||||
|
|
||||||
|
return $this->errorResponse('connection_failed');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Unexpected error in version check: '.$e->getMessage());
|
||||||
|
|
||||||
|
return $this->errorResponse('unexpected_error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isInBackoffPeriod(?\Illuminate\Support\Carbon $backoffUntil): bool
|
||||||
|
{
|
||||||
|
return $backoffUntil !== null && now()->isBefore($backoffUntil);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function rateLimitResponse(\Illuminate\Support\Carbon $backoffUntil): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'latest_version' => null,
|
||||||
|
'is_newer' => false,
|
||||||
|
'release_data' => null,
|
||||||
|
'error' => 'rate_limit',
|
||||||
|
'backoff_until' => $backoffUntil->timestamp,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function errorResponse(?string $error = null): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'latest_version' => null,
|
||||||
|
'is_newer' => false,
|
||||||
|
'release_data' => null,
|
||||||
|
'error' => $error,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fetchOrUseCache(?array $cachedResponse, bool $enablePrereleases, ?\Illuminate\Support\Carbon $backoffUntil): ?array
|
||||||
|
{
|
||||||
|
if ($cachedResponse && ! $this->forceRefresh) {
|
||||||
|
return $cachedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isInBackoffPeriod($backoffUntil)) {
|
||||||
|
return $cachedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$httpResponse = $this->fetchReleases($enablePrereleases);
|
||||||
|
|
||||||
|
if ($httpResponse->status() === 429) {
|
||||||
|
return $this->handleRateLimit($cachedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpResponse->successful()) {
|
||||||
|
$responseData = $httpResponse->json();
|
||||||
|
Cache::put(self::CACHE_KEY, $responseData, self::CACHE_TTL);
|
||||||
|
|
||||||
|
return $responseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::warning('GitHub API request failed', [
|
||||||
|
'status' => $httpResponse->status(),
|
||||||
|
'body' => $httpResponse->body(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $cachedResponse;
|
||||||
|
} catch (ConnectionException $e) {
|
||||||
|
Log::debug('Failed to fetch releases: '.$e->getMessage());
|
||||||
|
|
||||||
|
return $cachedResponse ?? null;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::debug('Failed to fetch releases: '.$e->getMessage());
|
||||||
|
|
||||||
|
return $cachedResponse ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fetchReleases(bool $enablePrereleases)
|
||||||
|
{
|
||||||
|
$githubRepo = config('app.github_repo');
|
||||||
|
$apiBaseUrl = "https://api.github.com/repos/{$githubRepo}";
|
||||||
|
$endpoint = $enablePrereleases ? "{$apiBaseUrl}/releases" : "{$apiBaseUrl}/releases/latest";
|
||||||
|
|
||||||
|
return Http::timeout(10)->connectTimeout(5)->get($endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleRateLimit(?array $cachedResponse): ?array
|
||||||
|
{
|
||||||
|
$backoffUntil = now()->addMinutes(self::BACKOFF_MINUTES);
|
||||||
|
Cache::put(self::BACKOFF_KEY, $backoffUntil, 600);
|
||||||
|
Log::warning('GitHub API rate limit exceeded. Backing off for 10 minutes.');
|
||||||
|
|
||||||
|
return $cachedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractLatestVersion(array $response, bool $enablePrereleases): array
|
||||||
|
{
|
||||||
|
if (! $enablePrereleases || ! is_array($response) || ! isset($response[0])) {
|
||||||
|
return [
|
||||||
|
Arr::get($response, 'tag_name'),
|
||||||
|
$response,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
[$stableRelease, $prerelease] = $this->findReleases($response);
|
||||||
|
|
||||||
|
if ($prerelease && $stableRelease) {
|
||||||
|
$prereleaseVersion = Arr::get($prerelease, 'tag_name');
|
||||||
|
$stableVersion = Arr::get($stableRelease, 'tag_name');
|
||||||
|
|
||||||
|
if (version_compare($prereleaseVersion, $stableVersion, '>')) {
|
||||||
|
return [$prereleaseVersion, $prerelease];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$stableVersion, $stableRelease];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($prerelease) {
|
||||||
|
return [Arr::get($prerelease, 'tag_name'), $prerelease];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stableRelease) {
|
||||||
|
return [Arr::get($stableRelease, 'tag_name'), $stableRelease];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [null, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findReleases(array $allReleases): array
|
||||||
|
{
|
||||||
|
$stableRelease = null;
|
||||||
|
$prerelease = null;
|
||||||
|
|
||||||
|
foreach ($allReleases as $release) {
|
||||||
|
$tagName = Arr::get($release, 'tag_name');
|
||||||
|
if (! $tagName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isPrerelease = (bool) Arr::get($release, 'prerelease', false);
|
||||||
|
|
||||||
|
if ($isPrerelease && ! $prerelease) {
|
||||||
|
$prerelease = $release;
|
||||||
|
} elseif (! $isPrerelease && ! $stableRelease) {
|
||||||
|
$stableRelease = $release;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stableRelease && $prerelease) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$stableRelease, $prerelease];
|
||||||
|
}
|
||||||
|
}
|
||||||
15
app/Settings/UpdateSettings.php
Normal file
15
app/Settings/UpdateSettings.php
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Settings;
|
||||||
|
|
||||||
|
use Spatie\LaravelSettings\Settings;
|
||||||
|
|
||||||
|
class UpdateSettings extends Settings
|
||||||
|
{
|
||||||
|
public bool $prereleases = false;
|
||||||
|
|
||||||
|
public static function group(): string
|
||||||
|
{
|
||||||
|
return 'update';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
"livewire/livewire": "^4.0",
|
"livewire/livewire": "^4.0",
|
||||||
"om/icalparser": "^3.2",
|
"om/icalparser": "^3.2",
|
||||||
"spatie/browsershot": "^5.0",
|
"spatie/browsershot": "^5.0",
|
||||||
|
"spatie/laravel-settings": "^3.6",
|
||||||
"stevebauman/purify": "^6.3",
|
"stevebauman/purify": "^6.3",
|
||||||
"symfony/yaml": "^7.3",
|
"symfony/yaml": "^7.3",
|
||||||
"wnx/sidecar-browsershot": "^2.6"
|
"wnx/sidecar-browsershot": "^2.6"
|
||||||
|
|
|
||||||
499
composer.lock
generated
499
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "cd2e6e87598cd99111e73d9ce8e0a9b8",
|
"content-hash": "23163ec9d3efca34357cab8ee5219529",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "aws/aws-crt-php",
|
"name": "aws/aws-crt-php",
|
||||||
|
|
@ -621,6 +621,54 @@
|
||||||
},
|
},
|
||||||
"time": "2024-07-08T12:26:09+00:00"
|
"time": "2024-07-08T12:26:09+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "doctrine/deprecations",
|
||||||
|
"version": "1.1.5",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/doctrine/deprecations.git",
|
||||||
|
"reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
|
||||||
|
"reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"phpunit/phpunit": "<=7.5 || >=13"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/coding-standard": "^9 || ^12 || ^13",
|
||||||
|
"phpstan/phpstan": "1.4.10 || 2.1.11",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.0 || ^2",
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12",
|
||||||
|
"psr/log": "^1 || ^2 || ^3"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Doctrine\\Deprecations\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
|
||||||
|
"homepage": "https://www.doctrine-project.org/",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/doctrine/deprecations/issues",
|
||||||
|
"source": "https://github.com/doctrine/deprecations/tree/1.1.5"
|
||||||
|
},
|
||||||
|
"time": "2025-04-07T20:06:18+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "doctrine/inflector",
|
"name": "doctrine/inflector",
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
|
|
@ -4041,6 +4089,117 @@
|
||||||
},
|
},
|
||||||
"time": "2020-10-15T08:29:30+00:00"
|
"time": "2020-10-15T08:29:30+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "phpdocumentor/reflection-common",
|
||||||
|
"version": "2.2.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
|
||||||
|
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
|
||||||
|
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.2 || ^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-2.x": "2.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"phpDocumentor\\Reflection\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jaap van Otterdijk",
|
||||||
|
"email": "opensource@ijaap.nl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common reflection classes used by phpdocumentor to reflect the code structure",
|
||||||
|
"homepage": "http://www.phpdoc.org",
|
||||||
|
"keywords": [
|
||||||
|
"FQSEN",
|
||||||
|
"phpDocumentor",
|
||||||
|
"phpdoc",
|
||||||
|
"reflection",
|
||||||
|
"static analysis"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
|
||||||
|
"source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
|
||||||
|
},
|
||||||
|
"time": "2020-06-27T09:03:43+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "phpdocumentor/type-resolver",
|
||||||
|
"version": "1.12.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/phpDocumentor/TypeResolver.git",
|
||||||
|
"reference": "92a98ada2b93d9b201a613cb5a33584dde25f195"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195",
|
||||||
|
"reference": "92a98ada2b93d9b201a613cb5a33584dde25f195",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"doctrine/deprecations": "^1.0",
|
||||||
|
"php": "^7.3 || ^8.0",
|
||||||
|
"phpdocumentor/reflection-common": "^2.0",
|
||||||
|
"phpstan/phpdoc-parser": "^1.18|^2.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"ext-tokenizer": "*",
|
||||||
|
"phpbench/phpbench": "^1.2",
|
||||||
|
"phpstan/extension-installer": "^1.1",
|
||||||
|
"phpstan/phpstan": "^1.8",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.1",
|
||||||
|
"phpunit/phpunit": "^9.5",
|
||||||
|
"rector/rector": "^0.13.9",
|
||||||
|
"vimeo/psalm": "^4.25"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-1.x": "1.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"phpDocumentor\\Reflection\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mike van Riel",
|
||||||
|
"email": "me@mikevanriel.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
|
||||||
|
"source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0"
|
||||||
|
},
|
||||||
|
"time": "2025-11-21T15:09:14+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "phpoption/phpoption",
|
"name": "phpoption/phpoption",
|
||||||
"version": "1.9.5",
|
"version": "1.9.5",
|
||||||
|
|
@ -4226,6 +4385,53 @@
|
||||||
],
|
],
|
||||||
"time": "2025-12-15T11:51:42+00:00"
|
"time": "2025-12-15T11:51:42+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "phpstan/phpdoc-parser",
|
||||||
|
"version": "2.3.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/phpstan/phpdoc-parser.git",
|
||||||
|
"reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/16dbf9937da8d4528ceb2145c9c7c0bd29e26374",
|
||||||
|
"reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.4 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/annotations": "^2.0",
|
||||||
|
"nikic/php-parser": "^5.3.0",
|
||||||
|
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||||
|
"phpstan/extension-installer": "^1.0",
|
||||||
|
"phpstan/phpstan": "^2.0",
|
||||||
|
"phpstan/phpstan-phpunit": "^2.0",
|
||||||
|
"phpstan/phpstan-strict-rules": "^2.0",
|
||||||
|
"phpunit/phpunit": "^9.6",
|
||||||
|
"symfony/process": "^5.2"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"PHPStan\\PhpDocParser\\": [
|
||||||
|
"src/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "PHPDoc parser with support for nullable, intersection and generic types",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
|
||||||
|
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.1"
|
||||||
|
},
|
||||||
|
"time": "2026-01-12T11:33:04+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "pragmarx/google2fa",
|
"name": "pragmarx/google2fa",
|
||||||
"version": "v9.0.0",
|
"version": "v9.0.0",
|
||||||
|
|
@ -5096,6 +5302,91 @@
|
||||||
],
|
],
|
||||||
"time": "2025-07-17T15:46:43+00:00"
|
"time": "2025-07-17T15:46:43+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "spatie/laravel-settings",
|
||||||
|
"version": "3.6.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/spatie/laravel-settings.git",
|
||||||
|
"reference": "fae93dadb8f748628ecaf5710f494adf790255b2"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/spatie/laravel-settings/zipball/fae93dadb8f748628ecaf5710f494adf790255b2",
|
||||||
|
"reference": "fae93dadb8f748628ecaf5710f494adf790255b2",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-json": "*",
|
||||||
|
"illuminate/database": "^11.0|^12.0",
|
||||||
|
"php": "^8.2",
|
||||||
|
"phpdocumentor/type-resolver": "^1.5",
|
||||||
|
"spatie/temporary-directory": "^1.3|^2.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"ext-redis": "*",
|
||||||
|
"mockery/mockery": "^1.4",
|
||||||
|
"orchestra/testbench": "^9.0|^10.0",
|
||||||
|
"pestphp/pest": "^2.0|^3.0",
|
||||||
|
"pestphp/pest-plugin-laravel": "^2.0|^3.0",
|
||||||
|
"phpstan/extension-installer": "^1.1",
|
||||||
|
"phpstan/phpstan-deprecation-rules": "^1.0",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.0",
|
||||||
|
"spatie/laravel-data": "^2.0.0|^4.0.0",
|
||||||
|
"spatie/pest-plugin-snapshots": "^2.0",
|
||||||
|
"spatie/phpunit-snapshot-assertions": "^4.2|^5.0",
|
||||||
|
"spatie/ray": "^1.36"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"spatie/data-transfer-object": "Allows for DTO casting to settings. (deprecated)"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Spatie\\LaravelSettings\\LaravelSettingsServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Spatie\\LaravelSettings\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Ruben Van Assche",
|
||||||
|
"email": "ruben@spatie.be",
|
||||||
|
"homepage": "https://spatie.be",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Store your application settings",
|
||||||
|
"homepage": "https://github.com/spatie/laravel-settings",
|
||||||
|
"keywords": [
|
||||||
|
"laravel-settings",
|
||||||
|
"spatie"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/spatie/laravel-settings/issues",
|
||||||
|
"source": "https://github.com/spatie/laravel-settings/tree/3.6.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://spatie.be/open-source/support-us",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/spatie",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-12-03T10:29:27+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/temporary-directory",
|
"name": "spatie/temporary-directory",
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
|
|
@ -8336,54 +8627,6 @@
|
||||||
],
|
],
|
||||||
"time": "2026-01-08T07:23:06+00:00"
|
"time": "2026-01-08T07:23:06+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "doctrine/deprecations",
|
|
||||||
"version": "1.1.5",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/doctrine/deprecations.git",
|
|
||||||
"reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
|
|
||||||
"reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": "^7.1 || ^8.0"
|
|
||||||
},
|
|
||||||
"conflict": {
|
|
||||||
"phpunit/phpunit": "<=7.5 || >=13"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"doctrine/coding-standard": "^9 || ^12 || ^13",
|
|
||||||
"phpstan/phpstan": "1.4.10 || 2.1.11",
|
|
||||||
"phpstan/phpstan-phpunit": "^1.0 || ^2",
|
|
||||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12",
|
|
||||||
"psr/log": "^1 || ^2 || ^3"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Doctrine\\Deprecations\\": "src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
|
|
||||||
"homepage": "https://www.doctrine-project.org/",
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/doctrine/deprecations/issues",
|
|
||||||
"source": "https://github.com/doctrine/deprecations/tree/1.1.5"
|
|
||||||
},
|
|
||||||
"time": "2025-04-07T20:06:18+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "fakerphp/faker",
|
"name": "fakerphp/faker",
|
||||||
"version": "v1.24.1",
|
"version": "v1.24.1",
|
||||||
|
|
@ -10121,59 +10364,6 @@
|
||||||
},
|
},
|
||||||
"time": "2022-02-21T01:04:05+00:00"
|
"time": "2022-02-21T01:04:05+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "phpdocumentor/reflection-common",
|
|
||||||
"version": "2.2.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
|
|
||||||
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
|
|
||||||
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": "^7.2 || ^8.0"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-2.x": "2.x-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"phpDocumentor\\Reflection\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Jaap van Otterdijk",
|
|
||||||
"email": "opensource@ijaap.nl"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Common reflection classes used by phpdocumentor to reflect the code structure",
|
|
||||||
"homepage": "http://www.phpdoc.org",
|
|
||||||
"keywords": [
|
|
||||||
"FQSEN",
|
|
||||||
"phpDocumentor",
|
|
||||||
"phpdoc",
|
|
||||||
"reflection",
|
|
||||||
"static analysis"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
|
|
||||||
"source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
|
|
||||||
},
|
|
||||||
"time": "2020-06-27T09:03:43+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "phpdocumentor/reflection-docblock",
|
"name": "phpdocumentor/reflection-docblock",
|
||||||
"version": "5.6.6",
|
"version": "5.6.6",
|
||||||
|
|
@ -10238,111 +10428,6 @@
|
||||||
},
|
},
|
||||||
"time": "2025-12-22T21:13:58+00:00"
|
"time": "2025-12-22T21:13:58+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "phpdocumentor/type-resolver",
|
|
||||||
"version": "1.12.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/phpDocumentor/TypeResolver.git",
|
|
||||||
"reference": "92a98ada2b93d9b201a613cb5a33584dde25f195"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195",
|
|
||||||
"reference": "92a98ada2b93d9b201a613cb5a33584dde25f195",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"doctrine/deprecations": "^1.0",
|
|
||||||
"php": "^7.3 || ^8.0",
|
|
||||||
"phpdocumentor/reflection-common": "^2.0",
|
|
||||||
"phpstan/phpdoc-parser": "^1.18|^2.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"ext-tokenizer": "*",
|
|
||||||
"phpbench/phpbench": "^1.2",
|
|
||||||
"phpstan/extension-installer": "^1.1",
|
|
||||||
"phpstan/phpstan": "^1.8",
|
|
||||||
"phpstan/phpstan-phpunit": "^1.1",
|
|
||||||
"phpunit/phpunit": "^9.5",
|
|
||||||
"rector/rector": "^0.13.9",
|
|
||||||
"vimeo/psalm": "^4.25"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-1.x": "1.x-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"phpDocumentor\\Reflection\\": "src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Mike van Riel",
|
|
||||||
"email": "me@mikevanriel.com"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
|
|
||||||
"source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0"
|
|
||||||
},
|
|
||||||
"time": "2025-11-21T15:09:14+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "phpstan/phpdoc-parser",
|
|
||||||
"version": "2.3.1",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/phpstan/phpdoc-parser.git",
|
|
||||||
"reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/16dbf9937da8d4528ceb2145c9c7c0bd29e26374",
|
|
||||||
"reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": "^7.4 || ^8.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"doctrine/annotations": "^2.0",
|
|
||||||
"nikic/php-parser": "^5.3.0",
|
|
||||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
|
||||||
"phpstan/extension-installer": "^1.0",
|
|
||||||
"phpstan/phpstan": "^2.0",
|
|
||||||
"phpstan/phpstan-phpunit": "^2.0",
|
|
||||||
"phpstan/phpstan-strict-rules": "^2.0",
|
|
||||||
"phpunit/phpunit": "^9.6",
|
|
||||||
"symfony/process": "^5.2"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"PHPStan\\PhpDocParser\\": [
|
|
||||||
"src/"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"description": "PHPDoc parser with support for nullable, intersection and generic types",
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
|
|
||||||
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.1"
|
|
||||||
},
|
|
||||||
"time": "2026-01-12T11:33:04+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "phpstan/phpstan",
|
"name": "phpstan/phpstan",
|
||||||
"version": "2.1.33",
|
"version": "2.1.33",
|
||||||
|
|
|
||||||
|
|
@ -153,4 +153,6 @@ return [
|
||||||
'version' => env('APP_VERSION', null),
|
'version' => env('APP_VERSION', null),
|
||||||
|
|
||||||
'catalog_url' => env('CATALOG_URL', 'https://raw.githubusercontent.com/bnussbau/trmnl-recipe-catalog/refs/heads/main/catalog.yaml'),
|
'catalog_url' => env('CATALOG_URL', 'https://raw.githubusercontent.com/bnussbau/trmnl-recipe-catalog/refs/heads/main/catalog.yaml'),
|
||||||
|
|
||||||
|
'github_repo' => env('GITHUB_REPO', 'usetrmnl/byos_laravel'),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
94
config/settings.php
Normal file
94
config/settings.php
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Each settings class used in your application must be registered, you can
|
||||||
|
* put them (manually) here.
|
||||||
|
*/
|
||||||
|
'settings' => [
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The path where the settings classes will be created.
|
||||||
|
*/
|
||||||
|
'setting_class_path' => app_path('Settings'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* In these directories settings migrations will be stored and ran when migrating. A settings
|
||||||
|
* migration created via the make:settings-migration command will be stored in the first path or
|
||||||
|
* a custom defined path when running the command.
|
||||||
|
*/
|
||||||
|
'migrations_paths' => [
|
||||||
|
database_path('settings'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When no repository was set for a settings class the following repository
|
||||||
|
* will be used for loading and saving settings.
|
||||||
|
*/
|
||||||
|
'default_repository' => 'database',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Settings will be stored and loaded from these repositories.
|
||||||
|
*/
|
||||||
|
'repositories' => [
|
||||||
|
'database' => [
|
||||||
|
'type' => Spatie\LaravelSettings\SettingsRepositories\DatabaseSettingsRepository::class,
|
||||||
|
'model' => null,
|
||||||
|
'table' => null,
|
||||||
|
'connection' => null,
|
||||||
|
],
|
||||||
|
'redis' => [
|
||||||
|
'type' => Spatie\LaravelSettings\SettingsRepositories\RedisSettingsRepository::class,
|
||||||
|
'connection' => null,
|
||||||
|
'prefix' => null,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The encoder and decoder will determine how settings are stored and
|
||||||
|
* retrieved in the database. By default, `json_encode` and `json_decode`
|
||||||
|
* are used.
|
||||||
|
*/
|
||||||
|
'encoder' => null,
|
||||||
|
'decoder' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The contents of settings classes can be cached through your application,
|
||||||
|
* settings will be stored within a provided Laravel store and can have an
|
||||||
|
* additional prefix.
|
||||||
|
*/
|
||||||
|
'cache' => [
|
||||||
|
'enabled' => env('SETTINGS_CACHE_ENABLED', false),
|
||||||
|
'store' => null,
|
||||||
|
'prefix' => null,
|
||||||
|
'ttl' => null,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* These global casts will be automatically used whenever a property within
|
||||||
|
* your settings class isn't a default PHP type.
|
||||||
|
*/
|
||||||
|
'global_casts' => [
|
||||||
|
DateTimeInterface::class => Spatie\LaravelSettings\SettingsCasts\DateTimeInterfaceCast::class,
|
||||||
|
DateTimeZone::class => Spatie\LaravelSettings\SettingsCasts\DateTimeZoneCast::class,
|
||||||
|
// Spatie\DataTransferObject\DataTransferObject::class => Spatie\LaravelSettings\SettingsCasts\DtoCast::class,
|
||||||
|
Spatie\LaravelData\Data::class => Spatie\LaravelSettings\SettingsCasts\DataCast::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The package will look for settings in these paths and automatically
|
||||||
|
* register them.
|
||||||
|
*/
|
||||||
|
'auto_discover_settings' => [
|
||||||
|
app_path('Settings'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Automatically discovered settings classes can be cached, so they don't
|
||||||
|
* need to be searched each time the application boots up.
|
||||||
|
*/
|
||||||
|
'discovered_settings_cache_path' => base_path('bootstrap/cache'),
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('settings', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->string('group');
|
||||||
|
$table->string('name');
|
||||||
|
$table->boolean('locked')->default(false);
|
||||||
|
$table->json('payload');
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['group', 'name']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Spatie\LaravelSettings\Migrations\SettingsMigration;
|
||||||
|
|
||||||
|
return new class extends SettingsMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$this->migrator->add('update.prereleases', false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -70,6 +70,12 @@
|
||||||
<flux:menu.radio.group>
|
<flux:menu.radio.group>
|
||||||
<flux:menu.item href="{{ route('settings.preferences') }}" wire:navigate icon="cog">Settings</flux:menu.item>
|
<flux:menu.item href="{{ route('settings.preferences') }}" wire:navigate icon="cog">Settings</flux:menu.item>
|
||||||
<flux:menu.item href="{{ route('settings.support') }}" wire:navigate icon="heart">Support</flux:menu.item>
|
<flux:menu.item href="{{ route('settings.support') }}" wire:navigate icon="heart">Support</flux:menu.item>
|
||||||
|
<flux:menu.item href="{{ route('settings.update') }}" wire:navigate icon="arrow-down-circle">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>Updates</span>
|
||||||
|
<livewire:update-badge />
|
||||||
|
</div>
|
||||||
|
</flux:menu.item>
|
||||||
</flux:menu.radio.group>
|
</flux:menu.radio.group>
|
||||||
|
|
||||||
<flux:menu.separator/>
|
<flux:menu.separator/>
|
||||||
|
|
|
||||||
48
resources/views/livewire/update-badge.blade.php
Normal file
48
resources/views/livewire/update-badge.blade.php
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
new class extends Component
|
||||||
|
{
|
||||||
|
public bool $hasUpdate = false;
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->checkForUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkForUpdate(): void
|
||||||
|
{
|
||||||
|
$currentVersion = config('app.version');
|
||||||
|
if (! $currentVersion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = Cache::get('latest_release');
|
||||||
|
if (! $response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle both single release object and array of releases
|
||||||
|
if (is_array($response) && isset($response[0])) {
|
||||||
|
// Array of releases - find the latest one
|
||||||
|
$latestRelease = $response[0];
|
||||||
|
$latestVersion = Arr::get($latestRelease, 'tag_name');
|
||||||
|
} else {
|
||||||
|
// Single release object
|
||||||
|
$latestVersion = Arr::get($response, 'tag_name');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($latestVersion && version_compare($latestVersion, $currentVersion, '>')) {
|
||||||
|
$this->hasUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
@if($hasUpdate)
|
||||||
|
<flux:badge color="yellow"><flux:icon name="sparkles" class="size-4"/></flux:badge>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
66
resources/views/livewire/update-check.blade.php
Normal file
66
resources/views/livewire/update-check.blade.php
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Jobs\CheckVersionUpdateJob;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
new class extends Component
|
||||||
|
{
|
||||||
|
public ?string $latestVersion = null;
|
||||||
|
|
||||||
|
public bool $isUpdateAvailable = false;
|
||||||
|
|
||||||
|
public ?array $releaseData = null;
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$currentVersion = config('app.version');
|
||||||
|
if (! $currentVersion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache first - only show content if cached
|
||||||
|
$cachedResponse = Cache::get('latest_release');
|
||||||
|
if ($cachedResponse) {
|
||||||
|
$this->processCachedResponse($cachedResponse, $currentVersion);
|
||||||
|
} else {
|
||||||
|
// Defer job in background using dispatchAfterResponse
|
||||||
|
CheckVersionUpdateJob::dispatchAfterResponse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processCachedResponse($response, string $currentVersion): void
|
||||||
|
{
|
||||||
|
$latestVersion = null;
|
||||||
|
|
||||||
|
// Handle both single release object and array of releases
|
||||||
|
if (is_array($response) && isset($response[0])) {
|
||||||
|
// Array of releases - find the latest one
|
||||||
|
$latestRelease = $response[0];
|
||||||
|
$latestVersion = Arr::get($latestRelease, 'tag_name');
|
||||||
|
$this->releaseData = $latestRelease;
|
||||||
|
} else {
|
||||||
|
// Single release object
|
||||||
|
$latestVersion = Arr::get($response, 'tag_name');
|
||||||
|
$this->releaseData = $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($latestVersion && version_compare($latestVersion, $currentVersion, '>')) {
|
||||||
|
$this->latestVersion = $latestVersion;
|
||||||
|
$this->isUpdateAvailable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
@if(config('app.version') && $isUpdateAvailable && $latestVersion)
|
||||||
|
<flux:callout class="text-xs mt-6" icon="arrow-down-circle">
|
||||||
|
<flux:callout.heading>Update available</flux:callout.heading>
|
||||||
|
<flux:callout.text>
|
||||||
|
There is a newer version {{ $latestVersion }} available. Update to the latest version for the best experience.
|
||||||
|
<flux:callout.link href="{{route('settings.update')}}" wire:navigate>Release notes</flux:callout.link>
|
||||||
|
</flux:callout.text>
|
||||||
|
</flux:callout>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
<flux:navlist.item :href="route('user-password.edit')" wire:navigate>{{ __('Password') }}</flux:navlist.item>
|
<flux:navlist.item :href="route('user-password.edit')" wire:navigate>{{ __('Password') }}</flux:navlist.item>
|
||||||
@endif
|
@endif
|
||||||
<flux:navlist.item :href="route('settings.support')" wire:navigate>{{ __('Support') }}</flux:navlist.item>
|
<flux:navlist.item :href="route('settings.support')" wire:navigate>{{ __('Support') }}</flux:navlist.item>
|
||||||
|
<flux:navlist.item :href="route('settings.update')" wire:navigate>{{ __('Updates') }}</flux:navlist.item>
|
||||||
</flux:navlist>
|
</flux:navlist>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
258
resources/views/pages/settings/update.blade.php
Normal file
258
resources/views/pages/settings/update.blade.php
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Jobs\CheckVersionUpdateJob;
|
||||||
|
use App\Settings\UpdateSettings;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
new class extends Component
|
||||||
|
{
|
||||||
|
public ?string $latestVersion = null;
|
||||||
|
|
||||||
|
public bool $isUpdateAvailable = false;
|
||||||
|
|
||||||
|
public ?string $releaseNotes = null;
|
||||||
|
|
||||||
|
public ?string $releaseUrl = null;
|
||||||
|
|
||||||
|
public bool $prereleases = false;
|
||||||
|
|
||||||
|
public bool $isChecking = false;
|
||||||
|
|
||||||
|
public ?string $errorMessage = null;
|
||||||
|
|
||||||
|
public ?int $backoffUntil = null;
|
||||||
|
|
||||||
|
private UpdateSettings $updateSettings;
|
||||||
|
|
||||||
|
public function boot(UpdateSettings $updateSettings): void
|
||||||
|
{
|
||||||
|
$this->updateSettings = $updateSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->prereleases = $this->updateSettings->prereleases;
|
||||||
|
$this->loadFromCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadFromCache(): void
|
||||||
|
{
|
||||||
|
$currentVersion = config('app.version');
|
||||||
|
if (! $currentVersion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load from cache without fetching
|
||||||
|
$cachedRelease = Cache::get('latest_release');
|
||||||
|
if ($cachedRelease) {
|
||||||
|
$this->processCachedRelease($cachedRelease, $currentVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processCachedRelease($cachedRelease, string $currentVersion): void
|
||||||
|
{
|
||||||
|
$latestVersion = null;
|
||||||
|
$releaseData = null;
|
||||||
|
|
||||||
|
// Handle both single release object and array of releases
|
||||||
|
if (is_array($cachedRelease) && isset($cachedRelease[0])) {
|
||||||
|
// Array of releases - find the latest one
|
||||||
|
$releaseData = $cachedRelease[0];
|
||||||
|
$latestVersion = Arr::get($releaseData, 'tag_name');
|
||||||
|
} else {
|
||||||
|
// Single release object
|
||||||
|
$releaseData = $cachedRelease;
|
||||||
|
$latestVersion = Arr::get($releaseData, 'tag_name');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($latestVersion) {
|
||||||
|
$this->latestVersion = $latestVersion;
|
||||||
|
$this->isUpdateAvailable = version_compare($latestVersion, $currentVersion, '>');
|
||||||
|
$this->releaseUrl = Arr::get($releaseData, 'html_url');
|
||||||
|
$this->loadReleaseNotes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkForUpdates(): void
|
||||||
|
{
|
||||||
|
$this->isChecking = true;
|
||||||
|
$this->errorMessage = null;
|
||||||
|
$this->backoffUntil = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = CheckVersionUpdateJob::dispatchSync();
|
||||||
|
|
||||||
|
$this->latestVersion = $result['latest_version'];
|
||||||
|
$this->isUpdateAvailable = $result['is_newer'];
|
||||||
|
$this->releaseUrl = Arr::get($result['release_data'] ?? [], 'html_url');
|
||||||
|
|
||||||
|
// Handle errors
|
||||||
|
if (isset($result['error'])) {
|
||||||
|
if ($result['error'] === 'rate_limit') {
|
||||||
|
$this->backoffUntil = $result['backoff_until'] ?? null;
|
||||||
|
$this->errorMessage = 'GitHub API rate limit exceeded. Please try again later.';
|
||||||
|
} elseif ($result['error'] === 'connection_failed') {
|
||||||
|
$this->errorMessage = 'Request timed out or failed to connect to GitHub. Please check your internet connection and try again.';
|
||||||
|
} elseif ($result['error'] === 'fetch_failed') {
|
||||||
|
$this->errorMessage = 'Failed to fetch update information from GitHub. Please try again later.';
|
||||||
|
} else {
|
||||||
|
$this->errorMessage = 'An unexpected error occurred while checking for updates. Please try again later.';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Reload release notes if we have a new version
|
||||||
|
if ($this->latestVersion) {
|
||||||
|
$this->loadReleaseNotes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Illuminate\Http\Client\ConnectionException $e) {
|
||||||
|
$this->errorMessage = 'Request timed out or failed to connect to GitHub. Please check your internet connection and try again.';
|
||||||
|
Log::error('Update check connection failed: '.$e->getMessage());
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->errorMessage = 'Request timed out or failed. Please check your internet connection and try again.';
|
||||||
|
Log::error('Update check failed: '.$e->getMessage());
|
||||||
|
} finally {
|
||||||
|
$this->isChecking = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedPrereleases(): void
|
||||||
|
{
|
||||||
|
$this->updateSettings->prereleases = $this->prereleases;
|
||||||
|
$this->updateSettings->save();
|
||||||
|
|
||||||
|
// Clear cache and recheck for updates with new preference
|
||||||
|
Cache::forget('latest_release');
|
||||||
|
$this->checkForUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadReleaseNotes(): void
|
||||||
|
{
|
||||||
|
if (! $this->latestVersion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cacheKey = "release_notes_{$this->latestVersion}";
|
||||||
|
$currentVersionKey = 'release_notes_current_version';
|
||||||
|
|
||||||
|
// Check if we have a previous version cached
|
||||||
|
$previousVersion = Cache::get($currentVersionKey);
|
||||||
|
|
||||||
|
// Clean up old version cache if different
|
||||||
|
if ($previousVersion && $previousVersion !== $this->latestVersion) {
|
||||||
|
Cache::forget("release_notes_{$previousVersion}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get from cache first - always load from cache if available
|
||||||
|
$cachedNotes = Cache::get($cacheKey);
|
||||||
|
if ($cachedNotes) {
|
||||||
|
$this->releaseNotes = $cachedNotes;
|
||||||
|
// Update current version tracker
|
||||||
|
Cache::put($currentVersionKey, $this->latestVersion, 86400);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch release notes if we have a version but no cache
|
||||||
|
// This will fetch on mount if an update is available, or when explicitly checking
|
||||||
|
$githubRepo = config('app.github_repo');
|
||||||
|
$apiBaseUrl = "https://api.github.com/repos/{$githubRepo}";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch release notes for the specific version with HTML format
|
||||||
|
$response = Http::withHeaders([
|
||||||
|
'Accept' => 'application/vnd.github.v3.html+json',
|
||||||
|
])->timeout(10)->get("{$apiBaseUrl}/releases/tags/{$this->latestVersion}");
|
||||||
|
if ($response->successful()) {
|
||||||
|
$releaseData = $response->json();
|
||||||
|
$bodyHtml = Arr::get($releaseData, 'body_html');
|
||||||
|
|
||||||
|
if ($bodyHtml) {
|
||||||
|
// Cache for 24 hours
|
||||||
|
Cache::put($cacheKey, $bodyHtml, 86400);
|
||||||
|
$this->releaseNotes = $bodyHtml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::debug('Failed to fetch release notes: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current version tracker
|
||||||
|
Cache::put($currentVersionKey, $this->latestVersion, 86400);
|
||||||
|
}
|
||||||
|
} ?>
|
||||||
|
|
||||||
|
<section class="w-full">
|
||||||
|
@include('partials.settings-heading')
|
||||||
|
|
||||||
|
<x-pages::settings.layout heading="Updates">
|
||||||
|
<div class="my-6 w-full space-y-6">
|
||||||
|
@if(config('app.version'))
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<flux:heading size="sm">Current Version</flux:heading>
|
||||||
|
<flux:text class="text-sm">
|
||||||
|
<a href="https://github.com/{{ config('app.github_repo') }}/releases/" target="_blank" class="text-primary-600 dark:text-primary-400 hover:underline">
|
||||||
|
{{ config('app.version') }}
|
||||||
|
</a>
|
||||||
|
</flux:text>
|
||||||
|
</div>
|
||||||
|
<flux:button wire:click="checkForUpdates">
|
||||||
|
Check for Updates
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<flux:switch wire:model.live="prereleases" label="Include Pre-Releases"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
@if($errorMessage)
|
||||||
|
<flux:callout icon="exclamation-triangle" variant="danger">
|
||||||
|
<flux:callout.heading>Error</flux:callout.heading>
|
||||||
|
<flux:callout.text>
|
||||||
|
{{ $errorMessage }}
|
||||||
|
@if($backoffUntil)
|
||||||
|
<br><small class="text-xs opacity-75">You can try again after {{ \Carbon\Carbon::createFromTimestamp($backoffUntil)->format('H:i') }}.</small>
|
||||||
|
@endif
|
||||||
|
</flux:callout.text>
|
||||||
|
</flux:callout>
|
||||||
|
@elseif($isUpdateAvailable && $latestVersion)
|
||||||
|
<flux:callout icon="arrow-down-circle" variant="info">
|
||||||
|
<flux:callout.heading>Update Available</flux:callout.heading>
|
||||||
|
<flux:callout.text>
|
||||||
|
A newer version {{ $latestVersion }} is available. Update to the latest version for the best experience.
|
||||||
|
</flux:callout.text>
|
||||||
|
@if($releaseNotes)
|
||||||
|
<div class="mt-4 [&_h2]:text-sm [&_h2]:font-semibold [&_h2]:text-zinc-900 [&_h2]:dark:text-white [&_h2]:mb-2 [&_h2]:mt-4 [&_h2:first-child]:mt-0 [&_p]:text-sm [&_p]:text-zinc-500 [&_p]:dark:text-white/70 [&_p]:mb-2 [&_ul]:text-sm [&_ul]:text-zinc-500 [&_ul]:dark:text-white/70 [&_ul]:list-disc [&_ul]:ml-6 [&_ul]:mb-2 [&_ul>li]:mb-1 [&_li]:text-sm [&_li]:text-zinc-500 [&_li]:dark:text-white/70 [&_a]:text-primary-600 [&_a]:dark:text-primary-400 [&_a]:hover:underline">
|
||||||
|
{!! $releaseNotes !!}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@if($releaseUrl)
|
||||||
|
<div class="mt-4">
|
||||||
|
<flux:button
|
||||||
|
href="{{ $releaseUrl }}"
|
||||||
|
target="_blank"
|
||||||
|
icon:trailing="arrow-up-right"
|
||||||
|
class="w-full">
|
||||||
|
View on GitHub
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</flux:callout>
|
||||||
|
@elseif($latestVersion && !$isUpdateAvailable)
|
||||||
|
<flux:callout icon="check-circle" variant="success">
|
||||||
|
<flux:callout.heading>Up to Date</flux:callout.heading>
|
||||||
|
<flux:callout.text>
|
||||||
|
You are running the latest version {{ $latestVersion }}.
|
||||||
|
</flux:callout.text>
|
||||||
|
</flux:callout>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-pages::settings.layout>
|
||||||
|
</section>
|
||||||
|
|
@ -32,39 +32,6 @@
|
||||||
@endif
|
@endif
|
||||||
</header>
|
</header>
|
||||||
@auth
|
@auth
|
||||||
@if(config('app.version'))
|
<livewire:update-check />
|
||||||
<flux:text class="text-xs">Version: <a href="https://github.com/usetrmnl/byos_laravel/releases/"
|
|
||||||
target="_blank">{{ config('app.version') }}</a>
|
|
||||||
</flux:text>
|
|
||||||
|
|
||||||
@php
|
|
||||||
$response = Cache::remember('latest_release', 86400, function () {
|
|
||||||
try {
|
|
||||||
$response = Http::get('https://api.github.com/repos/usetrmnl/byos_laravel/releases/latest');
|
|
||||||
if ($response->successful()) {
|
|
||||||
return $response->json();
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::debug('Failed to fetch latest release: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
$latestVersion = Arr::get($response, 'tag_name');
|
|
||||||
|
|
||||||
if ($latestVersion && version_compare($latestVersion, config('app.version'), '>')) {
|
|
||||||
$newVersion = $latestVersion;
|
|
||||||
}
|
|
||||||
@endphp
|
|
||||||
|
|
||||||
@if(isset($newVersion))
|
|
||||||
<flux:callout class="text-xs mt-6" icon="arrow-down-circle">
|
|
||||||
<flux:callout.heading>Update available</flux:callout.heading>
|
|
||||||
<flux:callout.text>
|
|
||||||
There is a newer version {{ $newVersion }} available. Update to the latest version for the best experience.
|
|
||||||
<flux:callout.link href="https://github.com/usetrmnl/byos_laravel/releases/" target="_blank">Release notes</flux:callout.link>
|
|
||||||
</flux:callout.text>
|
|
||||||
</flux:callout>
|
|
||||||
@endif
|
|
||||||
@endif
|
|
||||||
@endauth
|
@endauth
|
||||||
</x-layouts::auth.card>
|
</x-layouts::auth.card>
|
||||||
|
|
|
||||||
|
|
@ -20,4 +20,5 @@ Route::middleware(['auth'])->group(function () {
|
||||||
)
|
)
|
||||||
->name('two-factor.show');
|
->name('two-factor.show');
|
||||||
Route::livewire('settings/support', 'pages::settings.support')->name('settings.support');
|
Route::livewire('settings/support', 'pages::settings.support')->name('settings.support');
|
||||||
|
Route::livewire('settings/update', 'pages::settings.update')->name('settings.update');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
147
tests/Feature/Jobs/CheckVersionUpdateJobTest.php
Normal file
147
tests/Feature/Jobs/CheckVersionUpdateJobTest.php
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Jobs\CheckVersionUpdateJob;
|
||||||
|
use App\Settings\UpdateSettings;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
|
beforeEach(function (): void {
|
||||||
|
Http::preventStrayRequests();
|
||||||
|
Cache::flush();
|
||||||
|
config(['app.version' => '1.0.0']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it returns latest version when update is available', function (): void {
|
||||||
|
$githubRepo = config('app.github_repo');
|
||||||
|
$apiBaseUrl = "https://api.github.com/repos/{$githubRepo}";
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
"{$apiBaseUrl}/releases/latest" => Http::response([
|
||||||
|
'tag_name' => '1.1.0',
|
||||||
|
'body' => 'Release notes',
|
||||||
|
], 200),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = (new CheckVersionUpdateJob)->handle(app(UpdateSettings::class));
|
||||||
|
|
||||||
|
expect($result)
|
||||||
|
->latest_version->toBe('1.1.0')
|
||||||
|
->is_newer->toBeTrue()
|
||||||
|
->release_data->not->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it returns false when no update is available', function (): void {
|
||||||
|
$githubRepo = config('app.github_repo');
|
||||||
|
$apiBaseUrl = "https://api.github.com/repos/{$githubRepo}";
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
"{$apiBaseUrl}/releases/latest" => Http::response([
|
||||||
|
'tag_name' => '1.0.0',
|
||||||
|
'body' => 'Release notes',
|
||||||
|
], 200),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = (new CheckVersionUpdateJob)->handle(app(UpdateSettings::class));
|
||||||
|
|
||||||
|
expect($result)
|
||||||
|
->latest_version->toBe('1.0.0')
|
||||||
|
->is_newer->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it caches the release data', function (): void {
|
||||||
|
$githubRepo = config('app.github_repo');
|
||||||
|
$apiBaseUrl = "https://api.github.com/repos/{$githubRepo}";
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
"{$apiBaseUrl}/releases/latest" => Http::response([
|
||||||
|
'tag_name' => '1.1.0',
|
||||||
|
'body' => 'Release notes',
|
||||||
|
], 200),
|
||||||
|
]);
|
||||||
|
|
||||||
|
(new CheckVersionUpdateJob)->handle(app(UpdateSettings::class));
|
||||||
|
|
||||||
|
expect(Cache::has('latest_release'))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it forces refresh when forceRefresh is true', function (): void {
|
||||||
|
$githubRepo = config('app.github_repo');
|
||||||
|
$apiBaseUrl = "https://api.github.com/repos/{$githubRepo}";
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
"{$apiBaseUrl}/releases/latest" => Http::sequence()
|
||||||
|
->push([
|
||||||
|
'tag_name' => '1.1.0',
|
||||||
|
'body' => 'Release notes',
|
||||||
|
], 200)
|
||||||
|
->push([
|
||||||
|
'tag_name' => '1.2.0',
|
||||||
|
'body' => 'New release notes',
|
||||||
|
], 200),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// First call caches
|
||||||
|
(new CheckVersionUpdateJob)->handle(app(UpdateSettings::class));
|
||||||
|
|
||||||
|
// Second call with force refresh should make new request
|
||||||
|
$result = (new CheckVersionUpdateJob(true))->handle(app(UpdateSettings::class));
|
||||||
|
|
||||||
|
expect($result['latest_version'])->toBe('1.2.0');
|
||||||
|
Http::assertSentCount(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it handles pre-releases when enabled', function (): void {
|
||||||
|
$updateSettings = app(UpdateSettings::class);
|
||||||
|
$updateSettings->prereleases = true;
|
||||||
|
$updateSettings->save();
|
||||||
|
|
||||||
|
$githubRepo = config('app.github_repo');
|
||||||
|
$apiBaseUrl = "https://api.github.com/repos/{$githubRepo}";
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
"{$apiBaseUrl}/releases" => Http::response([
|
||||||
|
[
|
||||||
|
'tag_name' => '1.2.0-beta',
|
||||||
|
'body' => 'Beta release',
|
||||||
|
'prerelease' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'tag_name' => '1.1.0',
|
||||||
|
'body' => 'Stable release',
|
||||||
|
'prerelease' => false,
|
||||||
|
],
|
||||||
|
], 200),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = (new CheckVersionUpdateJob)->handle($updateSettings);
|
||||||
|
|
||||||
|
// Should prefer pre-release if newer
|
||||||
|
expect($result['latest_version'])->toBe('1.2.0-beta');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it returns null when no version is configured', function (): void {
|
||||||
|
config(['app.version' => null]);
|
||||||
|
|
||||||
|
$result = (new CheckVersionUpdateJob)->handle(app(UpdateSettings::class));
|
||||||
|
|
||||||
|
expect($result)
|
||||||
|
->latest_version->toBeNull()
|
||||||
|
->is_newer->toBeFalse()
|
||||||
|
->release_data->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it handles API failures gracefully', function (): void {
|
||||||
|
$githubRepo = config('app.github_repo');
|
||||||
|
$apiBaseUrl = "https://api.github.com/repos/{$githubRepo}";
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
"{$apiBaseUrl}/releases/latest" => Http::response([], 500),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = (new CheckVersionUpdateJob)->handle(app(UpdateSettings::class));
|
||||||
|
|
||||||
|
expect($result)
|
||||||
|
->latest_version->toBeNull()
|
||||||
|
->is_newer->toBeFalse()
|
||||||
|
->release_data->toBeNull();
|
||||||
|
});
|
||||||
29
tests/Feature/Settings/UpdateSettingsTest.php
Normal file
29
tests/Feature/Settings/UpdateSettingsTest.php
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Settings\UpdateSettings;
|
||||||
|
|
||||||
|
test('it has default value for prereleases', function (): void {
|
||||||
|
$settings = app(UpdateSettings::class);
|
||||||
|
|
||||||
|
expect($settings->prereleases)->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it can update prereleases', function (): void {
|
||||||
|
$settings = app(UpdateSettings::class);
|
||||||
|
$settings->prereleases = true;
|
||||||
|
$settings->save();
|
||||||
|
|
||||||
|
$settings->refresh();
|
||||||
|
|
||||||
|
expect($settings->prereleases)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it persists prereleases across instances', function (): void {
|
||||||
|
$settings1 = app(UpdateSettings::class);
|
||||||
|
$settings1->prereleases = true;
|
||||||
|
$settings1->save();
|
||||||
|
|
||||||
|
$settings2 = app(UpdateSettings::class);
|
||||||
|
|
||||||
|
expect($settings2->prereleases)->toBeTrue();
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue