Files
coolify/bootstrap/helpers/github-runner.php
Andras BacsaiandClaude 8b90ae3428 feat: Add GitHub Actions self-hosted runner pool system
Implement backend infrastructure and initial UI for managing GitHub Actions
runners on user-owned servers, similar to Blacksmith.sh but self-hosted.

**Backend Implementation:**
- Database schema: github_runner_sources (pools), github_runner_servers (pivot), github_runners (instances)
- Models: GitHubRunnerSource, GitHubRunner with relationships to Server
- GitHub API helpers: JIT config generation, token management, webhook validation
- Webhook handler: Processes workflow_job events, matches labels, provisions runners
- Job queue: ProvisionGithubRunnerJob (server selection, Docker provisioning), CleanupGithubRunnerJob
- Ephemeral runners using JIT configuration for enhanced security

**Testing:**
- Unit tests: Helper functions, webhook validation, model methods
- Feature tests: Webhook flow, provisioning, server selection, relationships

**UI Foundation:**
- Runner Sources index page with source listing
- Routes configured for index, create, and detail views
- Integration into server management section

**How it works:**
1. User creates a GitHub Runner Source (GitHub App + custom label)
2. User adds multiple servers to the runner pool
3. GitHub Actions jobs with matching runs-on label trigger webhook
4. Coolify provisions ephemeral Docker runner on available server
5. Job executes, container auto-removes on completion

**Next steps:**
- Complete UI components (create, detail, server management)
- Add runner history and monitoring
- User documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 09:52:08 +01:00

189 lines
5.9 KiB
PHP

<?php
use App\Models\GitHubRunnerSource;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* Generate a JIT (Just-In-Time) configuration for an ephemeral GitHub Actions runner
*
* @param GitHubRunnerSource $source The runner source containing GitHub App credentials
* @param array $labels Array of labels for the runner
* @param string $runnerName Unique name for the runner
* @return array|null Returns array with 'encoded_jit_config' and 'runner' keys, or null on failure
*/
function generateRunnerJitConfig(GitHubRunnerSource $source, array $labels, string $runnerName): ?array
{
try {
$token = $source->generateInstallationToken();
if (! $token) {
Log::error('Failed to generate installation token for runner source: '.$source->id);
return null;
}
$url = $source->is_organization_level
? "https://api.github.com/orgs/{$source->organization}/actions/runners/generate-jitconfig"
: "https://api.github.com/repos/{$source->organization}/actions/runners/generate-jitconfig";
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$token,
'Accept' => 'application/vnd.github+json',
'X-GitHub-Api-Version' => '2022-11-28',
])->post($url, [
'name' => $runnerName,
'runner_group_id' => 1, // Default runner group
'labels' => $labels,
'work_folder' => '_work',
]);
if ($response->successful()) {
return $response->json();
}
Log::error('Failed to generate JIT config', [
'source_id' => $source->id,
'status' => $response->status(),
'body' => $response->body(),
]);
return null;
} catch (\Exception $e) {
Log::error('Exception generating JIT config: '.$e->getMessage(), [
'source_id' => $source->id,
]);
return null;
}
}
/**
* Generate a registration token for a GitHub Actions runner
* This is an alternative to JIT config for non-ephemeral runners
*
* @param GitHubRunnerSource $source The runner source containing GitHub App credentials
* @return string|null The registration token, or null on failure
*/
function generateRunnerRegistrationToken(GitHubRunnerSource $source): ?string
{
try {
$token = $source->generateInstallationToken();
if (! $token) {
return null;
}
$url = $source->is_organization_level
? "https://api.github.com/orgs/{$source->organization}/actions/runners/registration-token"
: "https://api.github.com/repos/{$source->organization}/actions/runners/registration-token";
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$token,
'Accept' => 'application/vnd.github+json',
'X-GitHub-Api-Version' => '2022-11-28',
])->post($url);
if ($response->successful()) {
return $response->json()['token'];
}
Log::error('Failed to generate registration token', [
'source_id' => $source->id,
'status' => $response->status(),
]);
return null;
} catch (\Exception $e) {
Log::error('Exception generating registration token: '.$e->getMessage());
return null;
}
}
/**
* List all runners registered to a GitHub organization or repository
*
* @param GitHubRunnerSource $source The runner source
* @return array Array of runners
*/
function listGitHubRunners(GitHubRunnerSource $source): array
{
try {
$token = $source->generateInstallationToken();
if (! $token) {
return [];
}
$url = $source->is_organization_level
? "https://api.github.com/orgs/{$source->organization}/actions/runners"
: "https://api.github.com/repos/{$source->organization}/actions/runners";
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$token,
'Accept' => 'application/vnd.github+json',
'X-GitHub-Api-Version' => '2022-11-28',
])->get($url);
if ($response->successful()) {
return $response->json()['runners'] ?? [];
}
return [];
} catch (\Exception $e) {
Log::error('Exception listing runners: '.$e->getMessage());
return [];
}
}
/**
* Remove a runner from GitHub
*
* @param GitHubRunnerSource $source The runner source
* @param string $runnerId The GitHub runner ID
* @return bool Success status
*/
function removeGitHubRunner(GitHubRunnerSource $source, string $runnerId): bool
{
try {
$token = $source->generateInstallationToken();
if (! $token) {
return false;
}
$url = $source->is_organization_level
? "https://api.github.com/orgs/{$source->organization}/actions/runners/{$runnerId}"
: "https://api.github.com/repos/{$source->organization}/actions/runners/{$runnerId}";
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$token,
'Accept' => 'application/vnd.github+json',
'X-GitHub-Api-Version' => '2022-11-28',
])->delete($url);
return $response->successful();
} catch (\Exception $e) {
Log::error('Exception removing runner: '.$e->getMessage());
return false;
}
}
/**
* Validate a GitHub webhook signature
*
* @param string $payload The raw webhook payload
* @param string $signature The X-Hub-Signature-256 header value
* @param string $secret The webhook secret
* @return bool True if signature is valid
*/
function validateRunnerWebhookSignature(string $payload, string $signature, string $secret): bool
{
if (empty($signature) || empty($secret)) {
return false;
}
$computedSignature = 'sha256='.hash_hmac('sha256', $payload, $secret);
return hash_equals($computedSignature, $signature);
}