mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-24 10:05:47 -05:00
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:
@@ -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
@@ -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'),
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user