chore: inspect commit message guidance
This commit is contained in:
parent
d443758b03
commit
9b996b4dc9
8 changed files with 185 additions and 30 deletions
|
|
@ -6,6 +6,7 @@
|
|||
use App\Jobs\PushServerUpdateJob;
|
||||
use App\Models\Server;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Cache\LockTimeoutException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
|
@ -119,20 +120,24 @@ private function shouldDispatchUpdate(Server $server, array $data): bool
|
|||
$forceKey = "sentinel:push-force:{$server->id}";
|
||||
$lockKey = "sentinel:push-lock:{$server->id}";
|
||||
|
||||
return Cache::lock($lockKey, 10)->block(5, function () use ($hashKey, $forceKey, $hash): bool {
|
||||
$cachedHash = Cache::get($hashKey);
|
||||
$forceActive = Cache::has($forceKey);
|
||||
try {
|
||||
return Cache::lock($lockKey, 10)->block(5, function () use ($hashKey, $forceKey, $hash): bool {
|
||||
$cachedHash = Cache::get($hashKey);
|
||||
$forceActive = Cache::has($forceKey);
|
||||
|
||||
$shouldDispatch = $cachedHash === null || $cachedHash !== $hash || ! $forceActive;
|
||||
$shouldDispatch = $cachedHash === null || $cachedHash !== $hash || ! $forceActive;
|
||||
|
||||
if ($shouldDispatch) {
|
||||
// Day-long TTL bounds memory if a server stops pushing entirely.
|
||||
Cache::put($hashKey, $hash, now()->addDay());
|
||||
Cache::put($forceKey, true, config('constants.sentinel.push_force_interval_seconds', 300));
|
||||
}
|
||||
if ($shouldDispatch) {
|
||||
// Day-long TTL bounds memory if a server stops pushing entirely.
|
||||
Cache::put($hashKey, $hash, now()->addDay());
|
||||
Cache::put($forceKey, true, config('constants.sentinel.push_force_interval_seconds', 300));
|
||||
}
|
||||
|
||||
return $shouldDispatch;
|
||||
});
|
||||
return $shouldDispatch;
|
||||
});
|
||||
} catch (LockTimeoutException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
|
|
@ -116,17 +115,19 @@ public function promote(int $network_id, int $server_id)
|
|||
$network = StandaloneDocker::ownedByCurrentTeam()->where('server_id', $server->id)->findOrFail($network_id);
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
DB::transaction(function () use ($network, $server) {
|
||||
$this->resource->getConnection()->transaction(function () use ($network, $server) {
|
||||
$main_destination = $this->resource->destination;
|
||||
$this->resource->update([
|
||||
'destination_id' => $network->id,
|
||||
'destination_type' => StandaloneDocker::class,
|
||||
]);
|
||||
$this->resource->additional_networks()->detach($network->id, ['server_id' => $server->id]);
|
||||
$this->resource->additional_networks()
|
||||
->wherePivot('server_id', $server->id)
|
||||
->detach($network->id);
|
||||
$this->resource->additional_networks()->attach($main_destination->id, ['server_id' => $main_destination->server->id]);
|
||||
$this->refreshServers();
|
||||
$this->resource->refresh();
|
||||
});
|
||||
$this->resource->refresh();
|
||||
$this->refreshServers();
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
|
@ -167,7 +168,9 @@ public function removeServer(int $network_id, int $server_id, $password, $select
|
|||
}
|
||||
$server = Server::ownedByCurrentTeam()->findOrFail($server_id);
|
||||
StopApplicationOneServer::run($this->resource, $server);
|
||||
$this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]);
|
||||
$this->resource->additional_networks()
|
||||
->wherePivot('server_id', $server_id)
|
||||
->detach($network_id);
|
||||
$this->loadData();
|
||||
$this->dispatch('refresh');
|
||||
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
|
||||
|
|
|
|||
|
|
@ -104,6 +104,12 @@ protected static function boot()
|
|||
$new_team->forceFill($team);
|
||||
$new_team->save();
|
||||
|
||||
if (! $user->teams()->whereKey($new_team->id)->exists()) {
|
||||
$user->teams()->attach($new_team, ['role' => 'owner']);
|
||||
} else {
|
||||
$user->teams()->updateExistingPivot($new_team->id, ['role' => 'owner']);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,14 +70,14 @@
|
|||
</x-forms.button>
|
||||
@can('update', $github_app)
|
||||
<a href="{{ $this->getGithubAppNameUpdatePath() }}">
|
||||
<x-forms.button
|
||||
<x-forms.button canGate="update" :canResource="$github_app"
|
||||
class="bg-transparent border-transparent hover:bg-transparent hover:border-transparent hover:underline">
|
||||
Rename
|
||||
<x-external-link />
|
||||
</x-forms.button>
|
||||
</a>
|
||||
<a href="{{ getInstallationPath($github_app) }}" class="w-fit">
|
||||
<x-forms.button
|
||||
<x-forms.button canGate="update" :canResource="$github_app"
|
||||
class="bg-transparent border-transparent hover:bg-transparent hover:border-transparent hover:underline whitespace-nowrap">
|
||||
Update Repositories
|
||||
<x-external-link />
|
||||
|
|
@ -141,9 +141,9 @@ class="bg-transparent border-transparent hover:bg-transparent hover:border-trans
|
|||
<div class="flex flex-col sm:flex-row items-start sm:items-end gap-2">
|
||||
<h2>Permissions</h2>
|
||||
@can('view', $github_app)
|
||||
<x-forms.button wire:click.prevent="checkPermissions">Refetch</x-forms.button>
|
||||
<x-forms.button canGate="view" :canResource="$github_app" wire:click.prevent="checkPermissions">Refetch</x-forms.button>
|
||||
<a href="{{ getPermissionsPath($github_app) }}">
|
||||
<x-forms.button class="bg-transparent border-transparent hover:bg-transparent hover:border-transparent hover:underline">
|
||||
<x-forms.button canGate="view" :canResource="$github_app" class="bg-transparent border-transparent hover:bg-transparent hover:border-transparent hover:underline">
|
||||
Update
|
||||
<x-external-link />
|
||||
</x-forms.button>
|
||||
|
|
@ -151,11 +151,11 @@ class="bg-transparent border-transparent hover:bg-transparent hover:border-trans
|
|||
@endcan
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<x-forms.input id="contents" helper="read - mandatory." label="Content" readonly
|
||||
<x-forms.input canGate="view" :canResource="$github_app" id="contents" helper="read - mandatory." label="Content" readonly
|
||||
placeholder="N/A" />
|
||||
<x-forms.input id="metadata" helper="read - mandatory." label="Metadata" readonly
|
||||
<x-forms.input canGate="view" :canResource="$github_app" id="metadata" helper="read - mandatory." label="Metadata" readonly
|
||||
placeholder="N/A" />
|
||||
<x-forms.input id="pullRequests"
|
||||
<x-forms.input canGate="view" :canResource="$github_app" id="pullRequests"
|
||||
helper="write access needed to use deployment status update in previews."
|
||||
label="Pull Request" readonly placeholder="N/A" />
|
||||
</div>
|
||||
|
|
@ -167,7 +167,7 @@ class="bg-transparent border-transparent hover:bg-transparent hover:border-trans
|
|||
No resources are currently using this GitHub App.
|
||||
</div>
|
||||
@else
|
||||
<x-forms.input placeholder="Search resources..." x-model="search" id="null" />
|
||||
<x-forms.input canGate="view" :canResource="$github_app" placeholder="Search resources..." x-model="search" id="null" />
|
||||
<div class="overflow-x-auto pt-4">
|
||||
<div class="inline-block min-w-full">
|
||||
<div class="overflow-hidden">
|
||||
|
|
@ -259,7 +259,7 @@ class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-coollabs/10 dark:b
|
|||
</div>
|
||||
<div class="flex flex-col gap-3 pt-4 border-t border-neutral-200 dark:border-coolgray-400">
|
||||
@if (!isCloud() || isDev())
|
||||
<x-forms.select wire:model.live='webhook_endpoint' x-model="webhookEndpoint" label="Webhook Endpoint"
|
||||
<x-forms.select canGate="create" :canResource="$github_app" wire:model.live='webhook_endpoint' x-model="webhookEndpoint" label="Webhook Endpoint"
|
||||
helper="All Git webhooks will be sent to this endpoint. <br><br>If you would like to use domain instead of IP address, set your Coolify instance's FQDN in the Settings menu.">
|
||||
@if ($fqdn)
|
||||
<option value="{{ $fqdn }}">Use {{ $fqdn }}</option>
|
||||
|
|
@ -279,14 +279,14 @@ class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-coollabs/10 dark:b
|
|||
@endif
|
||||
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<x-forms.checkbox disabled id="default_permissions" label="Mandatory"
|
||||
<x-forms.checkbox canGate="create" :canResource="$github_app" disabled id="default_permissions" label="Mandatory"
|
||||
helper="Contents: read<br>Metadata: read<br>Email: read" />
|
||||
<x-forms.checkbox id="preview_deployment_permissions" label="Preview Deployments"
|
||||
<x-forms.checkbox canGate="create" :canResource="$github_app" id="preview_deployment_permissions" label="Preview Deployments"
|
||||
helper="Necessary for updating pull requests with useful comments (deployment status, links, etc.)<br><br>Pull Request: read & write" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-auto pt-2">
|
||||
<x-forms.button class="w-full justify-center" isHighlighted
|
||||
<x-forms.button canGate="create" :canResource="$github_app" class="w-full justify-center" isHighlighted
|
||||
x-on:click.prevent="createGithubApp(webhookEndpoint, {{ Illuminate\Support\Js::from($preview_deployment_permissions) }}, {{ Illuminate\Support\Js::from($administration) }})">
|
||||
Register Now
|
||||
</x-forms.button>
|
||||
|
|
@ -314,7 +314,7 @@ class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-neutral-100 dark:b
|
|||
</p>
|
||||
</div>
|
||||
<div class="mt-auto pt-2">
|
||||
<x-forms.button class="w-full justify-center" wire:click.prevent="createGithubAppManually">
|
||||
<x-forms.button canGate="create" :canResource="$github_app" class="w-full justify-center" wire:click.prevent="createGithubAppManually">
|
||||
Continue
|
||||
</x-forms.button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Actions\Docker\GetContainersStatus;
|
||||
use App\Livewire\Project\Shared\Destination;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
|
|
@ -10,6 +11,7 @@
|
|||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Livewire\Livewire;
|
||||
|
||||
|
|
@ -65,6 +67,10 @@
|
|||
session(['currentTeam' => $this->teamA]);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
GetContainersStatus::clearFake();
|
||||
});
|
||||
|
||||
describe('Destination::addServer GHSA-j395-3pqh-9r5g', function () {
|
||||
test('cannot attach another team\'s server + network to own application', function () {
|
||||
try {
|
||||
|
|
@ -158,4 +164,69 @@
|
|||
expect($additional->first()->id)->toBe($this->destinationA->id);
|
||||
expect($additional->first()->pivot->server_id)->toBe($this->serverA->id);
|
||||
});
|
||||
|
||||
test('refresh failures after promote do not roll back promoted destination', function () {
|
||||
$this->applicationA->additional_networks()->attach($this->destinationA2->id, ['server_id' => $this->serverA2->id]);
|
||||
|
||||
GetContainersStatus::shouldRun()
|
||||
->once()
|
||||
->andThrow(new RuntimeException('refresh failed'));
|
||||
|
||||
try {
|
||||
Livewire::test(Destination::class, ['resource' => $this->applicationA])
|
||||
->call('promote', $this->destinationA2->id, $this->serverA2->id);
|
||||
} catch (Throwable $e) {
|
||||
// The refresh failure is intentionally outside the transaction; persistence is the assertion.
|
||||
}
|
||||
|
||||
$application = $this->applicationA->fresh();
|
||||
$additional = $application->additional_networks;
|
||||
|
||||
expect($application->destination_id)->toBe($this->destinationA2->id);
|
||||
expect($additional)->toHaveCount(1);
|
||||
expect($additional->first()->id)->toBe($this->destinationA->id);
|
||||
expect($additional->first()->pivot->server_id)->toBe($this->serverA->id);
|
||||
});
|
||||
|
||||
test('only detaches the promoted network for the selected pivot server', function () {
|
||||
$this->applicationA->additional_networks()->attach($this->destinationA2->id, ['server_id' => $this->serverA2->id]);
|
||||
$this->applicationA->additional_networks()->attach($this->destinationA2->id, ['server_id' => $this->serverA->id]);
|
||||
|
||||
Livewire::test(Destination::class, ['resource' => $this->applicationA])
|
||||
->call('promote', $this->destinationA2->id, $this->serverA2->id);
|
||||
|
||||
expect(DB::table('additional_destinations')
|
||||
->where('application_id', $this->applicationA->id)
|
||||
->where('standalone_docker_id', $this->destinationA2->id)
|
||||
->where('server_id', $this->serverA->id)
|
||||
->exists())->toBeTrue();
|
||||
|
||||
expect(DB::table('additional_destinations')
|
||||
->where('application_id', $this->applicationA->id)
|
||||
->where('standalone_docker_id', $this->destinationA2->id)
|
||||
->where('server_id', $this->serverA2->id)
|
||||
->exists())->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Destination::removeServer', function () {
|
||||
test('only detaches the removed network for the selected pivot server', function () {
|
||||
$this->applicationA->additional_networks()->attach($this->destinationA2->id, ['server_id' => $this->serverA2->id]);
|
||||
$this->applicationA->additional_networks()->attach($this->destinationA2->id, ['server_id' => $this->serverA->id]);
|
||||
|
||||
Livewire::test(Destination::class, ['resource' => $this->applicationA])
|
||||
->call('removeServer', $this->destinationA2->id, $this->serverA2->id, 'password');
|
||||
|
||||
expect(DB::table('additional_destinations')
|
||||
->where('application_id', $this->applicationA->id)
|
||||
->where('standalone_docker_id', $this->destinationA2->id)
|
||||
->where('server_id', $this->serverA->id)
|
||||
->exists())->toBeTrue();
|
||||
|
||||
expect(DB::table('additional_destinations')
|
||||
->where('application_id', $this->applicationA->id)
|
||||
->where('standalone_docker_id', $this->destinationA2->id)
|
||||
->where('server_id', $this->serverA2->id)
|
||||
->exists())->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,6 +23,23 @@
|
|||
});
|
||||
|
||||
describe('GitHub Source Change Component', function () {
|
||||
test('all github app form controls declare explicit authorization', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/source/github/change.blade.php'));
|
||||
|
||||
preg_match_all(
|
||||
'/<x-forms\.(button|input|select|checkbox)\b(?![^>]*\bcanGate=)[^>]*>/s',
|
||||
$view,
|
||||
$matches,
|
||||
PREG_OFFSET_CAPTURE
|
||||
);
|
||||
|
||||
$missingAuthorization = collect($matches[0])
|
||||
->map(fn (array $match): string => 'Line '.(substr_count(substr($view, 0, $match[1]), PHP_EOL) + 1).': '.trim(preg_replace('/\s+/', ' ', $match[0])))
|
||||
->all();
|
||||
|
||||
expect($missingAuthorization)->toBeEmpty();
|
||||
});
|
||||
|
||||
test('can mount with newly created github app with null app_id', function () {
|
||||
// Create a GitHub app without app_id (simulating a newly created source)
|
||||
$githubApp = GithubApp::create([
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\SentinelController;
|
||||
use App\Jobs\PushServerUpdateJob;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Cache\LockTimeoutException;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
|
@ -47,6 +49,25 @@ function sentinelPayload(array $containers, ?float $diskPercentage = 42.0): arra
|
|||
|
||||
$running = fn () => [['name' => 'app-1', 'state' => 'running', 'health_status' => 'healthy']];
|
||||
|
||||
it('skips dispatch decision when sentinel lock acquisition times out', function () use ($running) {
|
||||
$lock = Mockery::mock();
|
||||
$lock->shouldReceive('block')
|
||||
->once()
|
||||
->with(5, Mockery::type('callable'))
|
||||
->andThrow(LockTimeoutException::class);
|
||||
|
||||
Cache::shouldReceive('lock')
|
||||
->once()
|
||||
->with('sentinel:push-lock:'.$this->server->id, 10)
|
||||
->andReturn($lock);
|
||||
|
||||
$controller = new SentinelController;
|
||||
$method = new ReflectionMethod($controller, 'shouldDispatchUpdate');
|
||||
$method->setAccessible(true);
|
||||
|
||||
expect($method->invoke($controller, $this->server, sentinelPayload($running())))->toBeFalse();
|
||||
});
|
||||
|
||||
it('dispatches the job on the first push', function () use ($running) {
|
||||
pushSentinel($this->token, sentinelPayload($running()))->assertOk();
|
||||
|
||||
|
|
|
|||
32
tests/Feature/UserRootTeamTest.php
Normal file
32
tests/Feature/UserRootTeamTest.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('attaches the root user as owner when reusing an existing root team', function () {
|
||||
Team::factory()->create(['id' => 0, 'name' => 'Existing Root Team']);
|
||||
|
||||
$rootUser = User::factory()->create(['id' => 0]);
|
||||
|
||||
expect($rootUser->teams()->whereKey(0)->first()?->pivot?->role)->toBe('owner');
|
||||
});
|
||||
|
||||
it('promotes the root user to owner when the reused root team pivot already exists', function () {
|
||||
Team::factory()->create(['id' => 0, 'name' => 'Existing Root Team']);
|
||||
|
||||
DB::table('team_user')->insert([
|
||||
'team_id' => 0,
|
||||
'user_id' => 0,
|
||||
'role' => 'member',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$rootUser = User::factory()->create(['id' => 0]);
|
||||
|
||||
expect($rootUser->teams()->whereKey(0)->first()?->pivot?->role)->toBe('owner');
|
||||
});
|
||||
Loading…
Reference in a new issue