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:
'. '- System time: '.$serverTime->format('Y-m-d H:i:s').' UTC
'. '- GitHub time: '.$githubTime->format('Y-m-d H:i:s').' UTC
'. '- Difference: '.$timeDiff.' seconds
'. '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:
'. "Error: {$errorMessage}
". 'Rate Limit Status:
'. "- Remaining Calls: {$remainingCalls}
". "- 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) { $github = GithubApp::where('uuid', $source->uuid)->first(); $name = str(Str::kebab($github->name)); $installation_path = $github->html_url === 'https://github.com' ? 'apps' : 'github-apps'; return "$github->html_url/$installation_path/$name/installations/new"; } 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'], ]; }