feat(github-runners): add ephemeral self-hosted runners on build servers (#12054)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-09-30 12:13:11 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 03a056709d
commit fd85b740f5
40 changed files with 3605 additions and 20 deletions
+45
View File
@@ -2,6 +2,7 @@
namespace App\Models;
use App\Enums\GithubRunnerStatus;
use App\Traits\Auditable;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Facades\DB;
@@ -35,6 +36,10 @@ class GithubApp extends BaseModel
'metadata',
'pull_requests',
'administration',
'organization_self_hosted_runners',
'actions',
'webhook_events',
'runner_group_id',
];
protected $appends = ['type'];
@@ -43,6 +48,8 @@ class GithubApp extends BaseModel
'is_public' => 'boolean',
'is_system_wide' => 'boolean',
'type' => 'string',
'webhook_events' => 'array',
'runner_group_id' => 'integer',
];
protected $hidden = [
@@ -58,6 +65,15 @@ class GithubApp extends BaseModel
throw new \Exception('You cannot delete this GitHub App because it is in use by '.$applications_count.' application(s). Delete them first.');
}
// Runner rows cascade with the App, so live runner containers would lose their cleanup and keep taking jobs.
$hasLiveRunners = $github_app->runnerConfigs()->where('is_enabled', true)->exists()
|| GithubRunnerExecution::where('github_app_id', $github_app->id)
->whereIn('status', GithubRunnerStatus::occupying())
->exists();
if ($hasLiveRunners) {
throw new \Exception('You cannot delete this GitHub App because servers use it for GitHub Actions runners. Disable the runners on these servers and wait for running jobs to finish first.');
}
$privateKey = $github_app->privateKey;
if ($privateKey) {
// Check if key is used by anything EXCEPT this GitHub app
@@ -97,6 +113,35 @@ class GithubApp extends BaseModel
return $this->belongsTo(PrivateKey::class);
}
public function runnerConfigs()
{
return $this->hasMany(GithubRunnerConfig::class);
}
/**
* Permissions and webhook events that GitHub Actions runners need but the App does not have.
*
* @return array<int, string>
*/
public function missingRunnerRequirements(): array
{
$missing = [];
if (blank($this->organization)) {
$missing[] = 'An organization (runners are registered at organization level)';
}
if ($this->organization_self_hosted_runners !== 'write') {
$missing[] = 'Organization permission "Self-hosted runners": write';
}
if (! in_array($this->actions, ['read', 'write'], true)) {
$missing[] = 'Repository permission "Actions": read';
}
if (! in_array('workflow_job', $this->webhook_events ?? [], true)) {
$missing[] = 'Webhook event "Workflow job"';
}
return $missing;
}
public function type(): Attribute
{
return Attribute::make(