fix(github): retry GitHub API reads on server and connection errors

Retry GET requests of githubApi() up to three times when GitHub answers
with a 5xx error or the connection fails. One 502 or one timeout on the
repository lookup before a deployment failed the deployment. Writes are
not retried, so a pull request comment is not posted two times. The
installation token request uses the same retry rule.

Closes #9785

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-11-03 19:42:04 +01:00
co-authored by Claude Opus 5.5
parent b7a2c75739
commit d70801aaa1
3 changed files with 64 additions and 16 deletions
+22 -16
View File
@@ -183,6 +183,15 @@ function assertGithubClockInSync(Response $response): void
}
}
/**
* GitHub sometimes answers with a 5xx page or drops the connection. A repeated read or token request is harmless.
*/
function isTransientGithubFailure(Throwable $exception): bool
{
return $exception instanceof ConnectionException
|| ($exception instanceof RequestException && $exception->response->serverError());
}
function githubAppPrivateKey(GithubApp $source): ?PrivateKey
{
$privateKey = $source->privateKey;
@@ -210,17 +219,10 @@ function generateGithubToken(GithubApp $source, string $type)
return match ($type) {
'jwt' => $jwt,
'installation' => (function () use ($source, $jwt) {
// GitHub sometimes answers with a 5xx page or drops the connection; a new token request is harmless.
$response = Http::GitSource($source->api_url)->withHeaders([
'Authorization' => "Bearer $jwt",
'Accept' => 'application/vnd.github.machine-man-preview+json',
])->retry(
3,
fn (int $attempt) => $attempt * 1000,
fn (Throwable $exception) => $exception instanceof ConnectionException
|| ($exception instanceof RequestException && $exception->response->serverError()),
throw: false,
)->post("{$source->api_url}/app/installations/{$source->installation_id}/access_tokens");
])->retry(3, fn (int $attempt) => $attempt * 1000, isTransientGithubFailure(...), throw: false)->post("{$source->api_url}/app/installations/{$source->installation_id}/access_tokens");
if (! $response->successful()) {
assertGithubClockInSync($response);
@@ -257,15 +259,19 @@ function githubApi(?GithubApp $source, string $endpoint, string $method = 'get',
throw new InvalidArgumentException("Unsupported source type: {$source->getMorphClass()}");
}
if ($source->is_public) {
$response = Http::GitHub($source->api_url)->$method($endpoint);
$request = $source->is_public
? Http::GitHub($source->api_url)
: Http::GitHub($source->api_url, generateGithubInstallationToken($source));
// Only reads are retried: a repeated POST could create a second pull request comment.
if (strtolower($method) === 'get') {
$request = $request->retry(3, fn (int $attempt) => $attempt * 1000, isTransientGithubFailure(...), throw: false);
}
if ($data && in_array(strtolower($method), ['post', 'patch', 'put'])) {
$response = $request->$method($endpoint, $data);
} 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);
}
$response = $request->$method($endpoint);
}
if (! $response->successful() && $throwError) {