fix(github): check clock skew only from failed GitHub App responses

Stop calling /zen before each installation token request. A short
network problem to GitHub failed deployments with a cURL timeout on
/zen. assertGithubClockInSync now takes the failed response and reads
its Date header, so the "System time is out of sync" error still shows
when GitHub rejects the JWT.

Call it on failed JWT requests in the webhook installation check, the
permission job, the source connection test, and the server transfer
checklist. Remove the /zen fakes from the tests.
This commit is contained in:
Andras Bacsai
2026-11-03 19:29:45 +01:00
parent 7f521e496d
commit b7a2c75739
18 changed files with 54 additions and 80 deletions
+20 -7
View File
@@ -6,6 +6,7 @@ use Carbon\Carbon;
use Carbon\CarbonImmutable;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
@@ -150,11 +151,25 @@ function encodeGithubPathSegment(string $segment): string
return rawurlencode($segment);
}
function assertGithubClockInSync(string $apiUrl): void
/**
* GitHub rejects an App JWT when the clocks of Coolify and GitHub differ.
* Call this with the failed response of a JWT request: its Date header shows the difference without an extra request.
*/
function assertGithubClockInSync(Response $response): void
{
$response = Http::GitSource($apiUrl)->get("{$apiUrl}/zen");
$date = $response->header('Date');
if (blank($date)) {
return;
}
try {
$githubTime = Carbon::parse($date);
} catch (Throwable) {
return;
}
$serverTime = CarbonImmutable::now()->setTimezone('UTC');
$githubTime = Carbon::parse($response->header('date'));
$timeDiff = abs($serverTime->diffInSeconds($githubTime));
if ($timeDiff > 50) {
@@ -179,8 +194,6 @@ function generateGithubToken(GithubApp $source, string $type)
{
$privateKey = githubAppPrivateKey($source) ?? throw new RuntimeException('Private key not found for this GitHub App.');
assertGithubClockInSync($source->api_url);
$signingKey = InMemory::plainText($privateKey->private_key);
$algorithm = new Sha256;
$tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
@@ -210,6 +223,7 @@ function generateGithubToken(GithubApp $source, string $type)
)->post("{$source->api_url}/app/installations/{$source->installation_id}/access_tokens");
if (! $response->successful()) {
assertGithubClockInSync($response);
$error = data_get($response->json(), 'message', "HTTP {$response->status()}");
if ($error === 'Not Found') {
$error = "GitHub App installation {$source->installation_id} was not found. Install the GitHub App again or update its installation ID.";
@@ -303,8 +317,6 @@ function syncGithubAppName(GithubApp $source, bool $throw = false): ?string
return null;
}
assertGithubClockInSync($source->api_url);
$jwt = generateGithubAppJwt($privateKey->private_key, $source->app_id);
$response = Http::GitSource($source->api_url)->withHeaders([
@@ -314,6 +326,7 @@ function syncGithubAppName(GithubApp $source, bool $throw = false): ?string
])->get("{$source->api_url}/app");
if (! $response->successful()) {
assertGithubClockInSync($response);
throw new RuntimeException(data_get($response->json(), 'message', 'Failed to fetch GitHub App information.'));
}