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:
Andras Bacsai
2026-08-18 09:16:29 +02:00
parent 17d9d1a370
commit 0d23b29775
7 changed files with 123 additions and 22 deletions
@@ -134,8 +134,9 @@ class GithubPrivateRepository extends Component
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 @@ class GithubPrivateRepository extends Component
}
}
$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()
+12 -11
View File
@@ -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 $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 @@
<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 @@
</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'),
+44 -4
View File
@@ -105,19 +105,59 @@ describe('GitHub Private Repository Component', function () {
->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 @@ describe('GitHub Private Repository Component', function () {
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');
});
});
@@ -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 @@ test('searchable listbox renders search field and filters options client-side',
->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')