fix: reduce GitHub API calls and add configurable timeout (#7769)

- Pre-generate GitHub installation token once per deployment instead of
  multiple times, reducing API calls from 4-6 down to 2
- Add configurable timeout (GITHUB_API_TIMEOUT env var, default 30s)
- Add retry logic (3 retries with 200ms delay) for resilience
- Pass token through deployment context to avoid regeneration
- Fixes timeout issues on slow networks

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai 2026-01-05 19:24:38 +01:00
parent c406afddaf
commit d104b8b343
5 changed files with 49 additions and 13 deletions

View file

@ -175,6 +175,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private Collection|string $build_secrets;
private ?string $github_access_token = null;
public function tags()
{
// Do not remove this one, it needs to properly identify which worker is running the job
@ -212,6 +214,16 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($source) {
$this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first();
}
// Pre-generate GitHub installation token once to avoid multiple API calls during deployment
if ($this->source instanceof GithubApp && ! $this->source->is_public) {
try {
$this->github_access_token = generateGithubInstallationToken($this->source);
} catch (\Exception $e) {
// Token generation will be retried later if needed
$this->github_access_token = null;
}
}
$this->server = Server::find($this->application_deployment_queue->server_id);
$this->timeout = $this->server->settings->dynamic_timeout;
$this->destination = $this->server->destinations()->where('id', $this->application_deployment_queue->destination_id)->first();
@ -2065,7 +2077,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function check_git_if_build_needed()
{
if (is_object($this->source) && $this->source->getMorphClass() === \App\Models\GithubApp::class && $this->source->is_public === false) {
$repository = githubApi($this->source, "repos/{$this->customRepository}");
$repository = githubApi($this->source, "repos/{$this->customRepository}", token: $this->github_access_token);
$data = data_get($repository, 'data');
$repository_project_id = data_get($data, 'id');
if (isset($repository_project_id)) {
@ -2190,7 +2202,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
deployment_uuid: $this->deployment_uuid,
pull_request_id: $this->pull_request_id,
git_type: $this->git_type,
commit: $this->commit
commit: $this->commit,
github_access_token: $this->github_access_token
);
return $commands;

View file

@ -1143,7 +1143,7 @@ class Application extends BaseModel
}
}
public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_in_docker = true)
public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_in_docker = true, ?string $github_access_token = null)
{
$branch = $this->git_branch;
['repository' => $customRepository, 'port' => $customPort] = $this->customRepository();
@ -1163,7 +1163,8 @@ class Application extends BaseModel
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$base_command = "{$base_command} {$escapedRepoUrl}";
} else {
$github_access_token = generateGithubInstallationToken($this->source);
// Use provided token or generate a new one
$github_access_token = $github_access_token ?? generateGithubInstallationToken($this->source);
if ($exec_in_docker) {
$repoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
@ -1250,7 +1251,7 @@ class Application extends BaseModel
}
}
public function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null, ?string $commit = null)
public function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null, ?string $commit = null, ?string $github_access_token = null)
{
$branch = $this->git_branch;
['repository' => $customRepository, 'port' => $customPort] = $this->customRepository();
@ -1301,7 +1302,8 @@ class Application extends BaseModel
$commands->push($git_clone_command);
}
} else {
$github_access_token = generateGithubInstallationToken($this->source);
// Use provided token or generate a new one
$github_access_token = $github_access_token ?? generateGithubInstallationToken($this->source);
if ($exec_in_docker) {
$repoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
$escapedRepoUrl = escapeshellarg($repoUrl);

View file

@ -66,16 +66,18 @@ class AppServiceProvider extends ServiceProvider
private function configureGitHubHttp(): void
{
Http::macro('GitHub', function (string $api_url, ?string $github_access_token = null) {
$timeout = config('constants.github.api_timeout', 30);
if ($github_access_token) {
return Http::withHeaders([
'X-GitHub-Api-Version' => '2022-11-28',
'Accept' => 'application/vnd.github.v3+json',
'Authorization' => "Bearer $github_access_token",
])->baseUrl($api_url);
])->baseUrl($api_url)->timeout($timeout);
} else {
return Http::withHeaders([
'Accept' => 'application/vnd.github.v3+json',
])->baseUrl($api_url);
])->baseUrl($api_url)->timeout($timeout);
}
});
}

View file

@ -14,7 +14,19 @@ use Lcobucci\JWT\Token\Builder;
function generateGithubToken(GithubApp $source, string $type)
{
$response = Http::get("{$source->api_url}/zen");
$timeout = config('constants.github.api_timeout', 30);
$response = Http::timeout($timeout)
->retry(3, 200, throw: false)
->get("{$source->api_url}/zen");
if (! $response->successful()) {
throw new \Exception(
'Failed to connect to GitHub API to sync time. '.
'Please check your network connection and try again.'
);
}
$serverTime = CarbonImmutable::now()->setTimezone('UTC');
$githubTime = Carbon::parse($response->header('date'));
$timeDiff = abs($serverTime->diffInSeconds($githubTime));
@ -44,11 +56,13 @@ function generateGithubToken(GithubApp $source, string $type)
return match ($type) {
'jwt' => $jwt,
'installation' => (function () use ($source, $jwt) {
'installation' => (function () use ($source, $jwt, $timeout) {
$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");
])->timeout($timeout)
->retry(3, 200, throw: false)
->post("{$source->api_url}/app/installations/{$source->installation_id}/access_tokens");
if (! $response->successful()) {
$error = data_get($response->json(), 'message', 'no error message found');
@ -74,7 +88,7 @@ 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)
function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $method = 'get', ?array $data = null, bool $throwError = true, ?string $token = null)
{
if (is_null($source)) {
throw new \Exception('Source is required for API calls');
@ -87,7 +101,8 @@ function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $m
if ($source->is_public) {
$response = Http::GitHub($source->api_url)->$method($endpoint);
} else {
$token = generateGithubInstallationToken($source);
// Use provided token or generate a new one
$token = $token ?? generateGithubInstallationToken($source);
if ($data && in_array(strtolower($method), ['post', 'patch', 'put'])) {
$response = Http::GitHub($source->api_url, $token)->$method($endpoint, $data);
} else {

View file

@ -59,6 +59,10 @@ return [
'minimum_required_version' => '24.0',
],
'github' => [
'api_timeout' => env('GITHUB_API_TIMEOUT', 30),
],
'ssh' => [
'mux_enabled' => env('MUX_ENABLED', env('SSH_MUX_ENABLED', true)),
'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', 3600),