fix: honor GitHub default branches and reconcile proxy networks
Use searchable repository and branch selectors, select each repository's default branch when available, and discover running container networks during proxy reconciliation.
This commit is contained in:
parent
17d9d1a370
commit
0d23b29775
7 changed files with 123 additions and 22 deletions
|
|
@ -134,8 +134,9 @@ public function loadRepositories(int $github_app_id): void
|
|||
|
||||
public function loadBranches()
|
||||
{
|
||||
$this->selected_repository_owner = $this->repositories->where('id', $this->selected_repository_id)->first()['owner']['login'];
|
||||
$this->selected_repository_repo = $this->repositories->where('id', $this->selected_repository_id)->first()['name'];
|
||||
$repository = $this->repositories->firstWhere('id', $this->selected_repository_id);
|
||||
$this->selected_repository_owner = data_get($repository, 'owner.login');
|
||||
$this->selected_repository_repo = data_get($repository, 'name');
|
||||
$this->branches = collect();
|
||||
$this->page = 1;
|
||||
$this->loadBranchByPage();
|
||||
|
|
@ -146,7 +147,10 @@ public function loadBranches()
|
|||
}
|
||||
}
|
||||
$this->branches = sortBranchesByPriority($this->branches);
|
||||
$this->selected_branch_name = data_get($this->branches, '0.name', 'main');
|
||||
$defaultBranch = data_get($repository, 'default_branch', 'main');
|
||||
$this->selected_branch_name = $this->branches->contains('name', $defaultBranch)
|
||||
? $defaultBranch
|
||||
: data_get($this->branches, '0.name', 'main');
|
||||
}
|
||||
|
||||
protected function loadBranchByPage()
|
||||
|
|
|
|||
|
|
@ -107,8 +107,8 @@ function collectDockerNetworksByServer(Server $server)
|
|||
}
|
||||
function connectProxyToNetworks(Server $server)
|
||||
{
|
||||
['networks' => $networks] = collectDockerNetworksByServer($server);
|
||||
if ($server->isSwarm()) {
|
||||
['networks' => $networks] = collectDockerNetworksByServer($server);
|
||||
$commands = $networks->map(function ($network) {
|
||||
$safe = escapeshellarg($network);
|
||||
|
||||
|
|
@ -118,19 +118,20 @@ function connectProxyToNetworks(Server $server)
|
|||
"echo 'Successfully connected coolify-proxy to {$safe} network.'",
|
||||
];
|
||||
});
|
||||
} else {
|
||||
$commands = $networks->map(function ($network) {
|
||||
$safe = escapeshellarg($network);
|
||||
|
||||
return [
|
||||
"docker network ls --format '{{.Name}}' | grep '^{$network}$' >/dev/null || docker network create --attachable {$safe} >/dev/null",
|
||||
"docker network connect {$safe} coolify-proxy >/dev/null 2>&1 || true",
|
||||
"echo 'Successfully connected coolify-proxy to {$safe} network.'",
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
return $commands->flatten();
|
||||
}
|
||||
|
||||
return collect([
|
||||
'for network in $(docker inspect $(docker ps --filter label=coolify.managed=true --format "{{.ID}}") --format=\'{{range $network, $_ := .NetworkSettings.Networks}}{{println $network}}{{end}}\' 2>/dev/null | sort -u); do',
|
||||
' if [ -z "$network" ] || [ "$network" = "bridge" ] || [ "$network" = "host" ] || [ "$network" = "none" ] || [ "$network" = "default" ]; then',
|
||||
' continue',
|
||||
' fi',
|
||||
' if docker network inspect "$network" >/dev/null 2>&1; then',
|
||||
' docker network connect "$network" coolify-proxy >/dev/null 2>&1 || true',
|
||||
' fi',
|
||||
'done',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@
|
|||
@click.stop>
|
||||
<div class="searchable-listbox-search">
|
||||
<x-reicon name="search"
|
||||
class="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-neutral-400 dark:text-fg-faint" />
|
||||
class="pointer-events-none absolute top-1/2 left-3 size-3 -translate-y-1/2 text-neutral-400 dark:text-fg-faint" />
|
||||
<input x-ref="search" type="search" x-model="query" autocomplete="off"
|
||||
placeholder="{{ $searchPlaceholder }}"
|
||||
class="searchable-listbox-search-input"
|
||||
|
|
|
|||
|
|
@ -72,10 +72,11 @@ class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-10
|
|||
<div class="application-settings-section-body">
|
||||
@if ($repositories->isNotEmpty())
|
||||
<div class="flex items-end gap-2">
|
||||
<x-forms.listbox id="selected_repository_id" label="Repository" required live
|
||||
<x-forms.searchable-listbox id="selected_repository_id" label="Repository" required live
|
||||
searchPlaceholder="Search repositories…"
|
||||
:options="$repositories->map(fn ($repository) => [
|
||||
'value' => data_get($repository, 'id'),
|
||||
'label' => data_get($repository, 'name'),
|
||||
'label' => data_get($repository, 'full_name', data_get($repository, 'name')),
|
||||
])->values()->all()" />
|
||||
<x-forms.button :showLoadingIndicator="false" wire:click.prevent="loadBranches"
|
||||
wire:loading.attr="disabled"
|
||||
|
|
@ -104,7 +105,8 @@ class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-10
|
|||
</div>
|
||||
<div class="application-settings-section-body space-y-5">
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<x-forms.listbox id="selected_branch_name" label="Branch" required
|
||||
<x-forms.searchable-listbox id="selected_branch_name" label="Branch" required
|
||||
searchPlaceholder="Search branches…"
|
||||
:options="$branches->map(fn ($branch) => [
|
||||
'value' => data_get($branch, 'name'),
|
||||
'label' => data_get($branch, 'name'),
|
||||
|
|
|
|||
|
|
@ -105,19 +105,59 @@ function githubPrivateRepositoryTestPrivateKeyForTeam(Team $team): PrivateKey
|
|||
->assertSet('selected_repository_id', 1);
|
||||
});
|
||||
|
||||
test('repository selection uses the shared listbox and disables loading action', function () {
|
||||
test('repository selection uses the searchable listbox and disables loading action', function () {
|
||||
fakeGithubHttp([
|
||||
['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']],
|
||||
]);
|
||||
|
||||
Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
||||
->call('loadRepositories', $this->githubApp->id)
|
||||
->assertSee('id="selected_repository_id-trigger"', false)
|
||||
->assertSee('class="listbox-panel searchable-listbox-panel"', false)
|
||||
->assertSee('placeholder="Search repositories…"', false)
|
||||
->assertSee('wire:loading.attr="disabled"', false)
|
||||
->assertSee('wire:target="loadBranches,selected_repository_id"', false)
|
||||
->assertDontSee('<datalist', false);
|
||||
});
|
||||
|
||||
test('loadBranches fetches available branches and selects the repository default branch', function () {
|
||||
$repository = [
|
||||
'id' => 1,
|
||||
'name' => 'alpha-repo',
|
||||
'default_branch' => 'production',
|
||||
'owner' => ['login' => 'testuser'],
|
||||
];
|
||||
|
||||
fakeGithubHttp([$repository]);
|
||||
Http::fake([
|
||||
'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [
|
||||
'Date' => now()->toRfc7231String(),
|
||||
]),
|
||||
'https://api.github.com/app/installations/67890/access_tokens' => Http::response([
|
||||
'token' => 'fake-installation-token',
|
||||
], 201),
|
||||
'https://api.github.com/installation/repositories*' => Http::response([
|
||||
'total_count' => 1,
|
||||
'repositories' => [$repository],
|
||||
], 200),
|
||||
'https://api.github.com/repos/testuser/alpha-repo/branches*' => Http::response([
|
||||
['name' => 'main'],
|
||||
['name' => 'feature/login'],
|
||||
['name' => 'production'],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
||||
->call('loadRepositories', $this->githubApp->id)
|
||||
->call('loadBranches')
|
||||
->assertSet('selected_branch_name', 'production')
|
||||
->assertSet('branches', collect([
|
||||
['name' => 'main'],
|
||||
['name' => 'feature/login'],
|
||||
['name' => 'production'],
|
||||
]))
|
||||
->assertSee('placeholder="Search branches…"', false);
|
||||
});
|
||||
|
||||
test('continue button uses the shared submit loading indicator', function () {
|
||||
fakeGithubHttp([
|
||||
['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']],
|
||||
|
|
@ -301,11 +341,11 @@ function githubPrivateRepositoryTestPrivateKeyForTeam(Team $team): PrivateKey
|
|||
|
||||
Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
||||
->call('loadRepositories', $this->githubApp->id)
|
||||
->assertSee('Refresh Repository List');
|
||||
->assertSee('Refresh');
|
||||
});
|
||||
|
||||
test('refresh button is not visible before repositories are loaded', function () {
|
||||
Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
||||
->assertDontSee('Refresh Repository List');
|
||||
->assertDontSee('Refresh');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
53
tests/Feature/Proxy/RuntimeNetworkReconciliationTest.php
Normal file
53
tests/Feature/Proxy/RuntimeNetworkReconciliationTest.php
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('discovers running container networks instead of relying on cached resource statuses', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
|
||||
$commands = connectProxyToNetworks($server)->implode("\n");
|
||||
|
||||
expect($commands)
|
||||
->toContain('docker ps --filter label=coolify.managed=true')
|
||||
->toContain('.NetworkSettings.Networks')
|
||||
->toContain('docker network inspect "$network"')
|
||||
->toContain('docker network connect "$network" coolify-proxy')
|
||||
->not->toContain('docker network create');
|
||||
});
|
||||
|
||||
it('skips Docker system networks during runtime reconciliation', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
|
||||
$commands = connectProxyToNetworks($server)->implode("\n");
|
||||
|
||||
expect($commands)
|
||||
->toContain('"$network" = "bridge"')
|
||||
->toContain('"$network" = "host"')
|
||||
->toContain('"$network" = "none"')
|
||||
->toContain('"$network" = "default"');
|
||||
});
|
||||
|
||||
it('preserves runtime reconciliation loops for non-root SSH users', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $team->id,
|
||||
'user' => 'ubuntu',
|
||||
]);
|
||||
|
||||
$commands = collect(parseCommandsByLineForSudo(connectProxyToNetworks($server), $server))->implode("\n");
|
||||
|
||||
expect($commands)
|
||||
->toContain('$(sudo docker ps')
|
||||
->toMatch('/sudo\s+docker inspect/')
|
||||
->toMatch('/sudo\s+docker network inspect/')
|
||||
->toMatch('/sudo\s+docker network connect/')
|
||||
->not->toContain('sudo while')
|
||||
->not->toContain('sudo done')
|
||||
->not->toContain('sudo continue');
|
||||
});
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
->toContain('No matching timezone')
|
||||
->toContain('serverTimezone-trigger')
|
||||
->toContain('x-ref="search"')
|
||||
->toContain('left-3 size-3')
|
||||
->toContain('get filtered()')
|
||||
->toContain('searchable-listbox-panel')
|
||||
->toContain('Berlin')
|
||||
|
|
|
|||
Loading…
Reference in a new issue