mirror of
https://github.com/coollabsio/coolify.git
synced 2026-06-14 03:19:51 +00:00
a047971bc1
Generate GitHub App installation links and setup cache state from the current app instance, and keep the Livewire app name in sync after permission checks.
223 lines
7.5 KiB
PHP
223 lines
7.5 KiB
PHP
<?php
|
|
|
|
use App\Models\GithubApp;
|
|
use App\Models\GitlabApp;
|
|
use Carbon\Carbon;
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Str;
|
|
use Lcobucci\JWT\Encoding\ChainedFormatter;
|
|
use Lcobucci\JWT\Encoding\JoseEncoder;
|
|
use Lcobucci\JWT\Signer\Key\InMemory;
|
|
use Lcobucci\JWT\Signer\Rsa\Sha256;
|
|
use Lcobucci\JWT\Token\Builder;
|
|
|
|
function generateGithubToken(GithubApp $source, string $type)
|
|
{
|
|
$response = Http::get("{$source->api_url}/zen");
|
|
$serverTime = CarbonImmutable::now()->setTimezone('UTC');
|
|
$githubTime = Carbon::parse($response->header('date'));
|
|
$timeDiff = abs($serverTime->diffInSeconds($githubTime));
|
|
|
|
if ($timeDiff > 50) {
|
|
throw new Exception(
|
|
'System time is out of sync with GitHub API time:<br>'.
|
|
'- System time: '.$serverTime->format('Y-m-d H:i:s').' UTC<br>'.
|
|
'- GitHub time: '.$githubTime->format('Y-m-d H:i:s').' UTC<br>'.
|
|
'- Difference: '.$timeDiff.' seconds<br>'.
|
|
'Please synchronize your system clock.'
|
|
);
|
|
}
|
|
|
|
$signingKey = InMemory::plainText($source->privateKey->private_key);
|
|
$algorithm = new Sha256;
|
|
$tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
|
|
$now = CarbonImmutable::now()->setTimezone('UTC');
|
|
$now = $now->setTime($now->format('H'), $now->format('i'), $now->format('s'));
|
|
|
|
$jwt = $tokenBuilder
|
|
->issuedBy($source->app_id)
|
|
->issuedAt($now->modify('-1 minute'))
|
|
->expiresAt($now->modify('+8 minutes'))
|
|
->getToken($algorithm, $signingKey)
|
|
->toString();
|
|
|
|
return match ($type) {
|
|
'jwt' => $jwt,
|
|
'installation' => (function () use ($source, $jwt) {
|
|
$response = Http::withHeaders([
|
|
'Authorization' => "Bearer $jwt",
|
|
'Accept' => 'application/vnd.github.machine-man-preview+json',
|
|
])->post("{$source->api_url}/app/installations/{$source->installation_id}/access_tokens");
|
|
|
|
if (! $response->successful()) {
|
|
$error = data_get($response->json(), 'message', 'no error message found');
|
|
if ($error === 'Not Found') {
|
|
$error = 'Repository not found. Is it moved or deleted?';
|
|
}
|
|
throw new RuntimeException("Failed to get installation token for {$source->name} with error: ".$error);
|
|
}
|
|
|
|
return $response->json()['token'];
|
|
})(),
|
|
default => throw new InvalidArgumentException("Unsupported token type: {$type}")
|
|
};
|
|
}
|
|
|
|
function generateGithubInstallationToken(GithubApp $source)
|
|
{
|
|
return generateGithubToken($source, 'installation');
|
|
}
|
|
|
|
function generateGithubJwt(GithubApp $source)
|
|
{
|
|
return generateGithubToken($source, 'jwt');
|
|
}
|
|
|
|
function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $method = 'get', ?array $data = null, bool $throwError = true)
|
|
{
|
|
if (is_null($source)) {
|
|
throw new Exception('Source is required for API calls');
|
|
}
|
|
|
|
if ($source->getMorphClass() !== GithubApp::class) {
|
|
throw new InvalidArgumentException("Unsupported source type: {$source->getMorphClass()}");
|
|
}
|
|
|
|
if ($source->is_public) {
|
|
$response = Http::GitHub($source->api_url)->$method($endpoint);
|
|
} else {
|
|
$token = generateGithubInstallationToken($source);
|
|
if ($data && in_array(strtolower($method), ['post', 'patch', 'put'])) {
|
|
$response = Http::GitHub($source->api_url, $token)->$method($endpoint, $data);
|
|
} else {
|
|
$response = Http::GitHub($source->api_url, $token)->$method($endpoint);
|
|
}
|
|
}
|
|
|
|
if (! $response->successful() && $throwError) {
|
|
$resetTime = Carbon::parse((int) $response->header('X-RateLimit-Reset'))->format('Y-m-d H:i:s');
|
|
$errorMessage = data_get($response->json(), 'message', 'no error message found');
|
|
$remainingCalls = $response->header('X-RateLimit-Remaining', '0');
|
|
|
|
throw new Exception(
|
|
'GitHub API call failed:<br>'.
|
|
"Error: {$errorMessage}<br>".
|
|
'Rate Limit Status:<br>'.
|
|
"- Remaining Calls: {$remainingCalls}<br>".
|
|
"- Reset Time: {$resetTime} UTC"
|
|
);
|
|
}
|
|
|
|
return [
|
|
'rate_limit_remaining' => $response->header('X-RateLimit-Remaining'),
|
|
'rate_limit_reset' => $response->header('X-RateLimit-Reset'),
|
|
'data' => collect($response->json()),
|
|
];
|
|
}
|
|
|
|
function getInstallationPath(GithubApp $source): string
|
|
{
|
|
$name = str(Str::kebab($source->name));
|
|
$installation_path = $source->html_url === 'https://github.com' ? 'apps' : 'github-apps';
|
|
$state = Str::random(64);
|
|
|
|
Cache::put('github-app-setup-state:'.hash('sha256', $state), [
|
|
'action' => 'install',
|
|
'github_app_id' => $source->id,
|
|
'team_id' => $source->team_id,
|
|
], now()->addMinutes(60));
|
|
|
|
return "$source->html_url/$installation_path/$name/installations/new?".http_build_query(['state' => $state]);
|
|
}
|
|
|
|
function getPermissionsPath(GithubApp $source)
|
|
{
|
|
$github = GithubApp::where('uuid', $source->uuid)->first();
|
|
$name = str(Str::kebab($github->name));
|
|
|
|
return "$github->html_url/settings/apps/$name/permissions";
|
|
}
|
|
|
|
function loadRepositoryByPage(GithubApp $source, string $token, int $page)
|
|
{
|
|
$response = Http::GitHub($source->api_url, $token)
|
|
->timeout(20)
|
|
->retry(3, 200, throw: false)
|
|
->get('/installation/repositories', [
|
|
'per_page' => 100,
|
|
'page' => $page,
|
|
]);
|
|
$json = $response->json();
|
|
if ($response->status() !== 200) {
|
|
return [
|
|
'total_count' => 0,
|
|
'repositories' => [],
|
|
];
|
|
}
|
|
|
|
if ($json['total_count'] === 0) {
|
|
return [
|
|
'total_count' => 0,
|
|
'repositories' => [],
|
|
];
|
|
}
|
|
|
|
return [
|
|
'total_count' => $json['total_count'],
|
|
'repositories' => $json['repositories'],
|
|
];
|
|
}
|
|
function getGithubCommitRangeFiles(?GithubApp $source, string $owner, string $repo, string $beforeSha, string $afterSha): array
|
|
{
|
|
try {
|
|
if (! $source) {
|
|
// Manual webhooks don't have GitHub App authentication
|
|
// Return empty array so watch paths are ignored (current behavior)
|
|
return [];
|
|
}
|
|
|
|
$endpoint = "/repos/{$owner}/{$repo}/compare/{$beforeSha}...{$afterSha}";
|
|
$response = githubApi($source, $endpoint, 'get', null, false);
|
|
|
|
if (! $response) {
|
|
return [];
|
|
}
|
|
|
|
$files = collect(data_get($response, 'data.files', []));
|
|
|
|
return $files->pluck('filename')->filter()->values()->toArray();
|
|
} catch (Exception $e) {
|
|
ray('Error fetching GitHub commit range files: '.$e->getMessage());
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function getGithubPullRequestFiles(?GithubApp $source, string $owner, string $repo, int $pullRequestId): array
|
|
{
|
|
try {
|
|
if (! $source) {
|
|
// Manual webhooks don't have GitHub App authentication
|
|
// Return empty array so watch paths are ignored (current behavior)
|
|
return [];
|
|
}
|
|
|
|
$endpoint = "/repos/{$owner}/{$repo}/pulls/{$pullRequestId}/files";
|
|
$response = githubApi($source, $endpoint, 'get', null, false);
|
|
|
|
if (! $response) {
|
|
return [];
|
|
}
|
|
|
|
$files = collect(data_get($response, 'data', []));
|
|
|
|
return $files->pluck('filename')->filter()->values()->toArray();
|
|
} catch (Exception $e) {
|
|
ray('Error fetching GitHub PR files: '.$e->getMessage());
|
|
|
|
return [];
|
|
}
|
|
}
|