coolify/resources/views/livewire/server/github-runners.blade.php
Andras Bacsai 5c50ab6162 feat(github-runners): improve runner lifecycle and GitHub sync
Add runner execution observability and lifecycle hardening for self-hosted
GitHub Actions runners, including:
- scheduled artifact cleanup job for cached runner tarballs/templates
- workflow_job in_progress handling to mark executions as running
- safer cleanup failure handling and non-functional server fallback
- persisted workflow job HTML URLs with execution "Open" links in UI
- configurable runner group name sync (UI + provisioning + GitHub API)
- webhook events persistence and auto-fix for missing required events
- strict enum/status checks and related model cast/query improvements

Also includes migrations and expanded feature/unit coverage for webhook,
provisioning, cleanup, and runner group/event sync behaviors.
2026-03-03 22:21:13 +01:00

190 lines
12 KiB
PHP

<div wire:init="initializeRepositories">
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > GitHub Runners | Coolify
</x-slot>
<livewire:server.navbar :server="$server" />
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="github-runners" />
<div class="w-full">
<div class="flex items-center gap-2">
<h2>GitHub Actions Runners</h2>
@if ($this->config)
<x-forms.button wire:click="toggleEnabled" canGate="update" :canResource="$server">
{{ $this->config->is_enabled ? 'Disable' : 'Enable' }}
</x-forms.button>
@endif
</div>
<div class="mt-1 mb-6">Use this server as a GitHub Actions self-hosted runner host. Runners are ephemeral (JIT) spun up per workflow job and cleaned up automatically.</div>
{{-- Permission Warning --}}
@if ($selectedGithubAppId && $this->selectedAppHasRunnerPermission === false)
<div class="mb-4">
<x-callout type="warning" title="Missing Permission">
<p>The selected GitHub App does not have the <code>organization_self_hosted_runners: write</code> permission.</p>
<p class="mt-1">
1. Add it in your <a href="{{ getPermissionsPath($this->selectedApp) }}" target="_blank" class="underline">GitHub App settings</a>,
then 2. re-sync permissions in <a href="{{ route('source.github.show', ['github_app_uuid' => $this->selectedApp->uuid]) }}" class="underline">Coolify's Source settings</a>.
</p>
</x-callout>
</div>
@endif
{{-- Accessible Repositories --}}
@if ($selectedGithubAppId && $this->selectedApp && !$this->selectedApp->is_public)
<div class="mb-4">
<div class="flex items-center justify-between mb-1">
<label class="text-sm font-medium">Accessible Repositories</label>
<div class="flex items-center gap-3">
<button type="button" wire:click="loadAccessibleRepositories"
class="text-xs text-neutral-400 hover:text-white transition-colors">
Refresh
</button>
<a href="{{ getInstallationPath($this->selectedApp) }}" target="_blank"
class="text-xs text-warning hover:underline">
Manage Accessible Repositories →
</a>
</div>
</div>
<div x-data="{
open: false,
search: '',
get repos() {
return $wire.accessibleRepositories ?? [];
},
get filtered() {
if (!this.search) return this.repos;
const q = this.search.toLowerCase();
return this.repos.filter(r => r.toLowerCase().includes(q));
}
}" @click.outside="open = false" class="relative mt-1">
<div @click="open = !open"
class="flex items-center gap-2 w-full input cursor-pointer">
<input type="text" x-model="search" @click.stop @focus="open = true"
@keydown.escape="open = false"
placeholder="{{ $repositoriesLoading || ! $repositoriesLoaded ? 'Loading repositories...' : count($accessibleRepositories).' '.Str::plural('repository', count($accessibleRepositories)).' accessible type to search...' }}"
class="flex-1 text-sm border-0 outline-none bg-transparent px-2 py-0 focus:ring-0 placeholder:text-neutral-400 dark:placeholder:text-neutral-600 text-white" />
<svg class="w-4 h-4 shrink-0 mr-2.5 duration-200 ease-out text-neutral-500"
:class="{ 'rotate-180': open }" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
<div x-show="open" x-transition
class="absolute z-50 w-full mt-1 bg-coolgray-100 border border-coolgray-400 rounded shadow-lg max-h-60 overflow-auto scrollbar">
<template x-if="filtered.length === 0">
<div class="px-3 py-2 text-sm text-neutral-400">No matching repositories</div>
</template>
<template x-for="repo in filtered" :key="repo">
<div class="px-3 py-2 text-sm font-mono text-neutral-300" x-text="repo"></div>
</template>
</div>
</div>
@if ($repositoryError)
<x-callout type="error" title="Could Not Load Repositories">
{{ $repositoryError }}
</x-callout>
@elseif ($repositoriesLoaded && count($accessibleRepositories) === 0)
<x-callout type="warning" title="No Repositories Loaded">
<p>No repositories are accessible yet, or the GitHub App is set to "All repositories" (all org repos are covered automatically).</p>
<p class="mt-1">If you expect specific repositories to appear, <a href="{{ getInstallationPath($this->selectedApp) }}" target="_blank" class="underline">manage accessible repositories</a> in your GitHub App settings.</p>
</x-callout>
@endif
</div>
@endif
{{-- Configuration Form --}}
<form wire:submit="submit">
<div class="flex flex-col gap-4">
<div>
<label for="selectedGithubAppId" class="block text-sm font-medium">GitHub App</label>
<select wire:model.live="selectedGithubAppId" id="selectedGithubAppId"
class="w-full mt-1 input">
<option value="">Select a GitHub App...</option>
@foreach ($this->githubApps as $app)
<option value="{{ $app->id }}">
{{ $app->name }}
@if ($app->organization)
({{ $app->organization }})
@endif
@if ($app->organization_self_hosted_runners === 'write')
✓ Runner Permission
@endif
</option>
@endforeach
</select>
</div>
<div class="flex gap-4">
<x-forms.input canGate="update" :canResource="$server" id="runnerGroupName"
label="Runner Group Name (optional)"
helper="If set, Coolify will enforce this GitHub organization runner group name during sync." />
</div>
<div class="flex gap-4">
<x-forms.input canGate="update" :canResource="$server" id="labels"
label="Labels (comma-separated)" required
helper="Labels for routing workflow jobs to this server. Workflows use runs-on to match these labels." />
</div>
<div class="flex gap-2">
<x-forms.input canGate="update" :canResource="$server" id="maxRunners" type="number"
label="Max Concurrent Runners" required
helper="Maximum number of runners that can run simultaneously on this server." />
<x-forms.input canGate="update" :canResource="$server" id="capacityWaitTimeout" type="number"
label="Capacity Wait Timeout (minutes)" required
helper="How long queued jobs wait for a runner slot to become available before giving up. Default: 60 minutes (1 hour)." />
</div>
<div class="flex gap-4">
<x-forms.input canGate="update" :canResource="$server" id="runnerUser"
label="Runner User" required
helper="Linux user to run the runner process as. Will be created if it doesn't exist." />
</div>
<div class="flex gap-2">
<x-forms.input canGate="update" :canResource="$server" id="runnerBaseDir"
label="Base Directory" required
helper="Directory on the server where runner binaries and working directories will be stored." />
<x-forms.input canGate="update" :canResource="$server" id="runnerVersion"
label="Runner Version (optional)"
helper="Pin to a specific runner version (e.g. 2.321.0). Leave empty for latest." />
</div>
<div class="flex items-center gap-2 mt-2">
<x-forms.button type="submit" canGate="update" :canResource="$server">Save</x-forms.button>
@if ($this->config)
<x-modal-confirmation title="Delete Runner Configuration?" buttonTitle="Delete Configuration"
submitAction="deleteConfig"
:actions="['This will remove the runner configuration from this server.', 'Active runners will not be affected until they complete.']"
:confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Delete Configuration" />
@endif
</div>
</div>
</form>
{{-- Status --}}
@if ($this->config)
<div class="mt-8">
<h3 class="mb-2">Status</h3>
<div class="flex items-center gap-4 text-sm">
<span class="inline-flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full {{ $this->config->is_enabled ? 'bg-success' : 'bg-error' }}"></span>
{{ $this->config->is_enabled ? 'Enabled' : 'Disabled' }}
</span>
<span>Active Runners: {{ $this->activeRunnerCount }} / {{ $this->config->max_runners }}</span>
<span>Organization: {{ $this->config->githubApp?->organization }}</span>
<span>Labels: {{ implode(', ', $this->config->labels ?? []) }}</span>
</div>
</div>
@endif
@if ($this->config)
<livewire:server.github-runner-executions :server="$server" />
@endif
</div>
</div>
</div>