diff --git a/CHANGELOG.md b/CHANGELOG.md index 605c457e74..6ad154d717 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,8 +21,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Fixed - Fix the "Network error" when you sign in with email in AppFlowy Web. AppFlowy Web runs on its own domain and sends a `Redirect_to` header to GoTrue on the admin domain, and GoTrue blocked the CORS preflight because it did not allow that header. The AppFlowy service template now sets `GOTRUE_CORS_ALLOWED_HEADERS=redirect_to`. For an existing AppFlowy service, add this variable to the `gotrue` service in the Docker Compose file and restart the service ([#10545](https://github.com/coollabsio/coolify/issues/10545)) -- Use PostgreSQL 17 instead of MySQL in the Bugsink service template, as the Bugsink Docker Compose sample does, and pin Bugsink to version 2. Before, the template used `mysql:latest` with binary logging on, so the binary logs filled the disk after some weeks and Bugsink stopped. Only new Bugsink services get PostgreSQL. To stop the growth in an existing service, run `PURGE BINARY LOGS BEFORE NOW();` in MySQL, then add `command: ['--disable-log-bin']` to the `mysql` service and restart it ([#10626](https://github.com/coollabsio/coolify/issues/10626)) - List the commit images of Docker Compose applications on the **Rollback** page and in `GET /api/v1/applications/{uuid}/rollback-images`. Since v4.5.0 the list was always empty, because a Compose build names each service image `_`. The page also shows the running commit when the Compose file has services from other images, for example a database ([#12446](https://github.com/coollabsio/coolify/issues/12446)) +- Use PostgreSQL 17 instead of MySQL in the Bugsink service template, as the Bugsink Docker Compose sample does, and pin Bugsink to version 2. Before, the template used `mysql:latest` with binary logging on, so the binary logs filled the disk after some weeks and Bugsink stopped. Only new Bugsink services get PostgreSQL. To stop the growth in an existing service, run `PURGE BINARY LOGS BEFORE NOW();` in MySQL, then add `command: ['--disable-log-bin']` to the `mysql` service and restart it ([#10626](https://github.com/coollabsio/coolify/issues/10626)) - Finish the startup and the nightly cleanup of stuck resources on instances with many large deployment logs. Before, the cleanup loaded the logs of all deployments and stopped with "Allowed memory size exhausted", so stuck deployments, tasks, and backups were not marked as failed and the proxy configuration was not set up ([#12435](https://github.com/coollabsio/coolify/issues/12435)) - Open the database backup pages and **Settings > Backup** faster when a backup has many executions. Before, these pages loaded all executions of the backup on each request, but they did not show them - Queue a deployment from the MCP `deploy` tool and the MCP `control` start action while a deployment of the same application is in progress, as the API and the **Deploy** button do. Before, both started at once, so two rolling updates could remove the new container of each other and stop the application. A deployment of the same commit is now skipped, and the response gives the UUID of the deployment in progress. A rolling update now also removes only the containers that ran before it started ([#12442](https://github.com/coollabsio/coolify/issues/12442)) @@ -38,10 +38,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Keep the dashboard proxy configuration of a server with the IP `host.docker.internal` that is not the default localhost server. Before, Coolify did not see this server as localhost after it saved the server, for example after a proxy restart, and deleted `coolify.yaml`, so the dashboard was not reachable at its domain ([#12444](https://github.com/coollabsio/coolify/issues/12444)) - Show the disk usage of a server on the **Metrics** tab, on the **Fleet** page, and in the MCP `get_server_metrics` tool when Sentinel reports the Coolify data disk under a different mount point than `/`, `/data`, or `/data/coolify`. For example, this occurs on a NAS where `/data` is a symlink into a data volume. Before, the disk usage was always empty on these servers ([#12448](https://github.com/coollabsio/coolify/issues/12448)) - Pin the Usesend service template to `usesend/usesend:v1.10.0`, which has the Next.js, Node.js 24, and nodemailer security updates. Before, the template used `latest`, so a server kept an old cached image until you pulled the images again. The template does not use MinIO, so the MinIO-to-RustFS change of v1.10.0 does not affect it ([#12443](https://github.com/coollabsio/coolify/issues/12443)) -- Use PostgreSQL 16 in the Outline service template. New Outline versions need PostgreSQL 14 or later, so a new Outline service restarted in a loop on `postgres:12-alpine` with `syntax error at or near "TRIGGER"`. An existing Outline service keeps PostgreSQL 12. To upgrade it, follow the steps in the Outline service documentation ([#12345](https://github.com/coollabsio/coolify/issues/12345)) - Collect only the logs of its own stack in the Supabase service template. Before, the Vector container read the logs of all containers on the server, so with two Supabase services on one server (for example a clone for production), Supabase Studio showed the API logs of the other stack. An existing Supabase service keeps its `vector.yml`. To fix it, on the server, add `include_labels: ['com.docker.compose.project=${COOLIFY_RESOURCE_UUID}']` under `sources.docker_host` in `/data/coolify/services//volumes/logs/vector.yml`, then restart the service ([#10324](https://github.com/coollabsio/coolify/issues/10324)) -- Keep an application that uses a server only as an additional destination when you delete that server with **Delete all resources** or with `DELETE /api/v1/servers/{uuid}?force=true`. Before, Coolify deleted the application on all its servers, also after **Promote** made another server its main server. Coolify now removes the application only from the deleted server and stops its containers there. A server that has only such applications can be deleted without **Delete all resources**. The delete confirmation lists the resources that it deletes and the applications that keep running on another server +- Use PostgreSQL 16 in the Outline service template. New Outline versions need PostgreSQL 14 or later, so a new Outline service restarted in a loop on `postgres:12-alpine` with `syntax error at or near "TRIGGER"`. An existing Outline service keeps PostgreSQL 12. To upgrade it, follow the steps in the Outline service documentation ([#12345](https://github.com/coollabsio/coolify/issues/12345)) - Fix the LiteLLM service template for new services. The master key now starts with `sk-`, as LiteLLM requires: use `sk-` followed by the value of `SERVICE_PASSWORD_MASTERKEY` as the admin API key. The template also sets a separate `LITELLM_SALT_KEY`, so stored model credentials stay readable when you change the master key, and sets `STORE_MODEL_IN_DB` to `True` by default, so you can add models in the LiteLLM UI ([#10521](https://github.com/coollabsio/coolify/issues/10521)) +- Send one request less to GitHub before each deployment and each list of repositories of a GitHub App. Before, Coolify always called `/zen` to compare its clock with GitHub before it requested an installation token, so a short network problem between the Coolify instance and GitHub failed the deployment with `cURL error 28: Connection timed out ... for https://api.github.com/zen`. Coolify now compares the clocks only when GitHub rejects a request, from the `Date` header of that response, and still shows "System time is out of sync with GitHub API time" ([#9785](https://github.com/coollabsio/coolify/issues/9785)) +- Keep an application that uses a server only as an additional destination when you delete that server with **Delete all resources** or with `DELETE /api/v1/servers/{uuid}?force=true`. Before, Coolify deleted the application on all its servers, also after **Promote** made another server its main server. Coolify now removes the application only from the deleted server and stops its containers there. A server that has only such applications can be deleted without **Delete all resources**. The delete confirmation lists the resources that it deletes and the applications that keep running on another server ## [v4.6.0] - 2026-11-02 diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index c69487031d..3915b2ed95 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -841,6 +841,9 @@ class Github extends Controller ]) ->timeout(10) ->get("{$github_app->api_url}/app/installations/{$installation_id}"); + if (! $response->successful()) { + assertGithubClockInSync($response); + } } catch (\Throwable $e) { return strip_tags(str_replace('
', ' ', $e->getMessage())); } diff --git a/app/Jobs/GithubAppPermissionJob.php b/app/Jobs/GithubAppPermissionJob.php index 71920d6d76..95968868ce 100644 --- a/app/Jobs/GithubAppPermissionJob.php +++ b/app/Jobs/GithubAppPermissionJob.php @@ -35,6 +35,7 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue ])->get("{$this->github_app->api_url}/app"); if (! $response->successful()) { + assertGithubClockInSync($response); throw new \RuntimeException('Failed to fetch GitHub app permissions: '.$response->body()); } diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 60d65804e2..1f8596374f 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -272,6 +272,7 @@ class Change extends Component ])->timeout(10)->get("{$this->github_app->api_url}/app"); if (! $appResponse->successful()) { + assertGithubClockInSync($appResponse); $error = data_get($appResponse->json(), 'message', 'Unknown error'); $this->dispatch('error', "Connection failed: {$error}"); diff --git a/app/Services/ServerTransfer/TransferFinishChecklist.php b/app/Services/ServerTransfer/TransferFinishChecklist.php index 25d6d19e61..3f1793a3b4 100644 --- a/app/Services/ServerTransfer/TransferFinishChecklist.php +++ b/app/Services/ServerTransfer/TransferFinishChecklist.php @@ -464,6 +464,7 @@ class TransferFinishChecklist ->withHeaders(['Authorization' => 'Bearer '.generateGithubJwt($githubApp), 'Accept' => 'application/vnd.github+json']) ->{$method}(rtrim($githubApp->api_url, '/').$path, $data); if (! $response->successful()) { + assertGithubClockInSync($response); throw new RuntimeException('GitHub answered with HTTP '.$response->status().': '.data_get($response->json(), 'message', 'no details')); } diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php index 04eebdb505..1cc463ddbb 100644 --- a/bootstrap/helpers/github.php +++ b/bootstrap/helpers/github.php @@ -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.')); } diff --git a/tests/Feature/Api/ApplicationBuildSecretsSettingApiTest.php b/tests/Feature/Api/ApplicationBuildSecretsSettingApiTest.php index 1847c00221..9fb36df5d0 100644 --- a/tests/Feature/Api/ApplicationBuildSecretsSettingApiTest.php +++ b/tests/Feature/Api/ApplicationBuildSecretsSettingApiTest.php @@ -224,9 +224,6 @@ describe('other application creation endpoints use_build_secrets', function () { ]); Http::fake([ - 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [ - 'Date' => now()->toRfc7231String(), - ]), 'https://api.github.com/app/installations/67890/access_tokens' => Http::response([ 'token' => 'github-installation-token', ], 201), @@ -278,9 +275,6 @@ describe('other application creation endpoints use_build_secrets', function () { ]); Http::fake([ - 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [ - 'Date' => now()->toRfc7231String(), - ]), 'https://api.github.com/app/installations/9876/access_tokens' => Http::response([ 'token' => 'github-installation-token', ], 201), diff --git a/tests/Feature/Api/GithubAppsListApiTest.php b/tests/Feature/Api/GithubAppsListApiTest.php index 181bfde4f6..c031702eb2 100644 --- a/tests/Feature/Api/GithubAppsListApiTest.php +++ b/tests/Feature/Api/GithubAppsListApiTest.php @@ -469,9 +469,6 @@ describe('GitHub app API url normalization', function () { Http::preventStrayRequests(); Http::fake([ - 'https://api.octocorp.ghe.com/zen' => Http::response('Keep it logically awesome.', 200, [ - 'Date' => now()->toRfc7231String(), - ]), 'https://api.octocorp.ghe.com/app/installations/67890/access_tokens' => Http::response([ 'token' => 'installation-token', ]), diff --git a/tests/Feature/Application/GithubSourceChangeTest.php b/tests/Feature/Application/GithubSourceChangeTest.php index c42139c396..9dcf149f8c 100644 --- a/tests/Feature/Application/GithubSourceChangeTest.php +++ b/tests/Feature/Application/GithubSourceChangeTest.php @@ -231,7 +231,6 @@ describe('GitHub Source Change Component', function () { test('syncGithubAppName persists the github slug and renames the private key', function () { Http::fake([ '*/app' => Http::response(['slug' => 'actual-github-slug']), - '*/zen' => Http::response('Keep it logically awesome.'), ]); $privateKey = PrivateKey::create([ @@ -680,9 +679,6 @@ describe('GitHub Source Change Component', function () { Http::preventStrayRequests(); Http::fake([ - 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [ - 'date' => now()->toRfc7231String(), - ]), 'https://api.github.com/app' => Http::response([ 'permissions' => [ 'contents' => 'read', @@ -735,9 +731,6 @@ describe('GitHub Source Change Component', function () { Http::preventStrayRequests(); Http::fake([ - 'https://api.github.ghe.com/zen' => Http::response('Keep it logically awesome.', 200, [ - 'date' => now()->toRfc7231String(), - ]), 'https://api.github.ghe.com/app' => Http::response([ 'slug' => 'octocorp-app', ]), @@ -837,9 +830,6 @@ describe('GitHub Source Change Component', function () { Http::preventStrayRequests(); Http::fake([ - 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [ - 'date' => now()->toRfc7231String(), - ]), 'https://api.github.com/app' => Http::response([ 'name' => 'Coolify GitHub App', 'slug' => 'coolify-github-app', @@ -932,7 +922,6 @@ describe('GitHub App name sync after the user switches the session team', functi Http::preventStrayRequests(); Http::fake([ - 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, ['date' => now()->toRfc7231String()]), 'https://api.github.com/app' => Http::response(['slug' => 'synced-app']), ]); diff --git a/tests/Feature/GithubAppInstallationTokenTest.php b/tests/Feature/GithubAppInstallationTokenTest.php index 9ea349c460..4811cd12be 100644 --- a/tests/Feature/GithubAppInstallationTokenTest.php +++ b/tests/Feature/GithubAppInstallationTokenTest.php @@ -35,11 +35,7 @@ beforeEach(function () { 'team_id' => $this->team->id, ]); - Http::fake([ - 'https://api.github.com/zen' => fn () => Http::response('Keep it logically awesome.', 200, [ - 'Date' => now()->toRfc7231String(), - ]), - ]); + Http::preventStrayRequests(); }); function fakeGithubInstallationTokenResponses(mixed $response): void @@ -119,3 +115,21 @@ it('reports the HTTP status when GitHub keeps failing without an error message', ->toThrow(RuntimeException::class, 'Failed to get installation token for Private GitHub App with error: HTTP 503') ->and(githubInstallationTokenRequests())->toBe(3); }); + +it('requests the installation token without a separate GitHub clock request', function () { + fakeGithubInstallationTokenResponses(Http::response(['token' => 'only-token'], 201)); + + expect(generateGithubInstallationToken($this->githubApp))->toBe('only-token') + ->and(Http::recorded())->toHaveCount(1); +}); + +it('reports the clock difference when GitHub rejects the token request of an out-of-sync server', function () { + fakeGithubInstallationTokenResponses(Http::response( + ['message' => "'Issued at' claim ('iat') must be an Integer representing a time in the past"], + 401, + ['Date' => now()->addMinutes(5)->toRfc7231String()], + )); + + expect(fn () => generateGithubInstallationToken($this->githubApp)) + ->toThrow(Exception::class, 'System time is out of sync with GitHub API time'); +}); diff --git a/tests/Feature/GithubAppPushWatchPathsTest.php b/tests/Feature/GithubAppPushWatchPathsTest.php index ddc0985c8a..b48c04bf41 100644 --- a/tests/Feature/GithubAppPushWatchPathsTest.php +++ b/tests/Feature/GithubAppPushWatchPathsTest.php @@ -105,9 +105,6 @@ function sendGithubAppMergePush(TestCase $test, string $before = PUSH_WATCH_PATH function fakeGithubAppPushApi(array $compareResponse): void { Http::fake([ - 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [ - 'Date' => now()->toRfc7231String(), - ]), 'https://api.github.com/app/installations/67890/access_tokens' => Http::response([ 'token' => 'fake-installation-token', ], 201), diff --git a/tests/Feature/GithubPrivateRepositoryTest.php b/tests/Feature/GithubPrivateRepositoryTest.php index de455553a1..d859ab0094 100644 --- a/tests/Feature/GithubPrivateRepositoryTest.php +++ b/tests/Feature/GithubPrivateRepositoryTest.php @@ -53,9 +53,6 @@ beforeEach(function () { function fakeGithubHttp(array $repositories): void { Http::fake([ - 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [ - 'Date' => now()->toRfc7231String(), - ]), 'https://api.github.com/app/installations/67890/access_tokens' => Http::response([ 'token' => 'fake-installation-token', ], 201), @@ -129,9 +126,6 @@ describe('GitHub Private Repository Component', function () { fakeGithubHttp([$repository]); Http::fake([ - 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [ - 'Date' => now()->toRfc7231String(), - ]), 'https://api.github.com/app/installations/67890/access_tokens' => Http::response([ 'token' => 'fake-installation-token', ], 201), @@ -281,9 +275,6 @@ describe('GitHub Private Repository Component', function () { $callCount = 0; Http::fake([ - 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [ - 'Date' => now()->toRfc7231String(), - ]), 'https://api.github.com/app/installations/67890/access_tokens' => Http::response([ 'token' => 'fake-installation-token', ], 201), @@ -346,9 +337,6 @@ describe('GitHub Private Repository Component', function () { test('loadRepositories shows an error when the installation is not found', function () { Http::fake([ - 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [ - 'Date' => now()->toRfc7231String(), - ]), 'https://api.github.com/app/installations/67890/access_tokens' => Http::response(['message' => 'Not Found'], 404), ]); @@ -360,9 +348,6 @@ describe('GitHub Private Repository Component', function () { test('loadRepositories shows the GitHub error when listing repositories fails', function () { Http::fake([ - 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [ - 'Date' => now()->toRfc7231String(), - ]), 'https://api.github.com/app/installations/67890/access_tokens' => Http::response([ 'token' => 'fake-installation-token', ], 201), diff --git a/tests/Feature/GithubRunnerFlowTest.php b/tests/Feature/GithubRunnerFlowTest.php index 360c2196ee..ec2565b24e 100644 --- a/tests/Feature/GithubRunnerFlowTest.php +++ b/tests/Feature/GithubRunnerFlowTest.php @@ -127,7 +127,6 @@ function sendWorkflowJobWebhook($test, GithubApp $githubApp, string $secret, str function fakeRunnerGithubApi(GithubApp $githubApp): void { Http::fake([ - 'https://api.github.com/zen' => Http::response('ok', 200, ['Date' => now()->toRfc7231String()]), "https://api.github.com/app/installations/{$githubApp->installation_id}/access_tokens" => Http::response(['token' => 'installation-token'], 201), 'https://api.github.com/orgs/acme/actions/runners/generate-jitconfig' => Http::response([ 'runner' => ['id' => 42, 'name' => 'runner'], @@ -140,7 +139,6 @@ function fakeRunnerGithubApi(GithubApp $githubApp): void function fakeWorkflowRunApi(GithubApp $githubApp, mixed $runResponse): void { Http::fake([ - 'https://api.github.com/zen' => Http::response('ok', 200, ['Date' => now()->toRfc7231String()]), "https://api.github.com/app/installations/{$githubApp->installation_id}/access_tokens" => Http::response(['token' => 'installation-token'], 201), 'https://api.github.com/repos/acme/api/actions/runs/321' => $runResponse, ]); @@ -584,7 +582,6 @@ describe('provisioning', function () { runnerTestConfig($server, $this->githubApp); $execution = runnerTestExecution($this->githubApp); Http::fake([ - 'https://api.github.com/zen' => Http::response('ok', 200, ['Date' => now()->toRfc7231String()]), 'https://api.github.com/app/installations/*' => Http::response(['token' => 'installation-token'], 201), 'https://api.github.com/orgs/acme/actions/runners/generate-jitconfig' => function () use ($server) { DeleteServer::run($server->id); @@ -659,7 +656,6 @@ describe('provisioning', function () { runnerTestConfig(runnerTestServer($this->team), $this->githubApp); $execution = runnerTestExecution($this->githubApp, ['provision_attempts' => $attempts]); Http::fake([ - 'https://api.github.com/zen' => Http::response('ok', 200, ['Date' => now()->toRfc7231String()]), 'https://api.github.com/app/installations/*' => Http::response(['token' => 'installation-token'], 201), 'https://api.github.com/orgs/acme/actions/runners/42' => Http::response(null, 204), 'https://api.github.com/orgs/acme/actions/runners/generate-jitconfig' => function () use ($execution, $status) { @@ -687,7 +683,6 @@ describe('provisioning', function () { runnerTestConfig(runnerTestServer($this->team), $this->githubApp); $execution = runnerTestExecution($this->githubApp); Http::fake([ - 'https://api.github.com/zen' => Http::response('ok', 200, ['Date' => now()->toRfc7231String()]), 'https://api.github.com/app/installations/*' => Http::response(['token' => 'installation-token'], 201), 'https://api.github.com/orgs/acme/actions/runners/42' => Http::response(null, 204), 'https://api.github.com/orgs/acme/actions/runners/generate-jitconfig' => function () use ($execution) { @@ -712,7 +707,6 @@ describe('provisioning', function () { runnerTestConfig($server, $this->githubApp); $execution = runnerTestExecution($this->githubApp); Http::fake([ - 'https://api.github.com/zen' => Http::response('ok', 200, ['Date' => now()->toRfc7231String()]), 'https://api.github.com/app/installations/*' => Http::response(['token' => 'installation-token'], 201), 'https://api.github.com/orgs/acme/actions/runners/42' => Http::response(null, 204), 'https://api.github.com/orgs/acme/actions/runners/generate-jitconfig' => function () use ($server, $execution, $cancelled) { @@ -942,7 +936,6 @@ describe('server deletion', function () { it('removes the runner from GitHub and ignores GitHub errors', function (int $status) { Http::fake([ - 'https://api.github.com/zen' => Http::response('ok', 200, ['Date' => now()->toRfc7231String()]), 'https://api.github.com/app/installations/*' => Http::response(['token' => 'installation-token'], 201), 'https://api.github.com/orgs/acme/actions/runners/42' => Http::response(null, $status), ]); @@ -977,7 +970,6 @@ describe('server deletion', function () { function fakeRejectedJitConfig(): void { Http::fake([ - 'https://api.github.com/zen' => Http::response('ok', 200, ['Date' => now()->toRfc7231String()]), 'https://api.github.com/app/installations/*' => Http::response(['token' => 'installation-token'], 201), 'https://api.github.com/orgs/acme/actions/runners/generate-jitconfig' => Http::response(['message' => 'Resource not accessible by integration'], 403), ]); diff --git a/tests/Feature/GithubRunnerSettingsTest.php b/tests/Feature/GithubRunnerSettingsTest.php index 6bfc8e142d..a5c27b098f 100644 --- a/tests/Feature/GithubRunnerSettingsTest.php +++ b/tests/Feature/GithubRunnerSettingsTest.php @@ -99,7 +99,6 @@ function unreadyRunnerApp(): array function fakeRunnerGroupApi(GithubApp $githubApp, int $createStatus = 201): void { Http::fake([ - 'https://api.github.com/zen' => Http::response('ok', 200, ['Date' => now()->toRfc7231String()]), "https://api.github.com/app/installations/{$githubApp->installation_id}/access_tokens" => Http::response(['token' => 'installation-token'], 201), 'https://api.github.com/orgs/acme/actions/runner-groups' => fn ($request) => $request->method() === 'POST' ? Http::response($createStatus === 201 ? ['id' => 55, 'default' => false] : ['message' => 'Upgrade to create runner groups'], $createStatus) @@ -137,7 +136,6 @@ describe('runner settings page', function () { it('keeps the restrictions an organization admin set on an existing runner group', function () { $this->githubApp->update(['runner_group_id' => 77]); Http::fake([ - 'https://api.github.com/zen' => Http::response('ok', 200, ['Date' => now()->toRfc7231String()]), "https://api.github.com/app/installations/{$this->githubApp->installation_id}/access_tokens" => Http::response(['token' => 'installation-token'], 201), 'https://api.github.com/orgs/acme/actions/runner-groups/77' => Http::response(['id' => 77, 'default' => false, 'visibility' => 'selected', 'allows_public_repositories' => false]), ]); @@ -625,7 +623,6 @@ describe('build server role', function () { describe('GitHub App runner permissions', function () { it('stores runner permissions and webhook events from GitHub', function () { Http::fake([ - 'https://api.github.com/zen' => Http::response('ok', 200, ['Date' => now()->toRfc7231String()]), 'https://api.github.com/app' => Http::response([ 'permissions' => ['contents' => 'read', 'metadata' => 'read', 'organization_self_hosted_runners' => 'write', 'actions' => 'read'], 'events' => ['push', 'workflow_job'], diff --git a/tests/Feature/NewApplicationRepositoryFlowsTest.php b/tests/Feature/NewApplicationRepositoryFlowsTest.php index bc0eb08165..f612960023 100644 --- a/tests/Feature/NewApplicationRepositoryFlowsTest.php +++ b/tests/Feature/NewApplicationRepositoryFlowsTest.php @@ -81,7 +81,6 @@ function githubAppRepositoryFlow(): Testable 'is_system_wide' => false, ]); Http::fake([ - 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, ['Date' => now()->toRfc7231String()]), 'https://api.github.com/app/installations/67890/access_tokens' => Http::response(['token' => 'installation-token'], 201), 'https://api.github.com/installation/repositories*' => Http::response([ 'total_count' => 1, diff --git a/tests/Feature/ProcessGithubPullRequestWebhookTest.php b/tests/Feature/ProcessGithubPullRequestWebhookTest.php index 8a5199e942..4b5aacb75c 100644 --- a/tests/Feature/ProcessGithubPullRequestWebhookTest.php +++ b/tests/Feature/ProcessGithubPullRequestWebhookTest.php @@ -383,9 +383,6 @@ it('uses one GitHub App installation token for all GitHub API calls of a pull re $firstPage = collect(range(1, 100))->map(fn (int $i): array => ['filename' => ".github/workflows/workflow-{$i}.yml"])->all(); Http::fake([ - 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [ - 'Date' => now()->toRfc7231String(), - ]), 'https://api.github.com/app/installations/67890/access_tokens' => Http::response([ 'token' => 'fake-installation-token', ], 201), diff --git a/tests/Feature/Security/GithubAppSetupCallbackTest.php b/tests/Feature/Security/GithubAppSetupCallbackTest.php index 93ba0e533f..56ecb7c917 100644 --- a/tests/Feature/Security/GithubAppSetupCallbackTest.php +++ b/tests/Feature/Security/GithubAppSetupCallbackTest.php @@ -96,9 +96,6 @@ function fakeGithubInstallationVerification(int $appId, string $apiUrl = 'https: { Http::preventStrayRequests(); Http::fake([ - "{$apiUrl}/zen" => Http::response('Keep it logically awesome.', 200, [ - 'Date' => now()->toRfc7231String(), - ]), "{$apiUrl}/app/installations/*" => Http::response([ 'id' => 555, 'app_id' => $appId, @@ -110,9 +107,6 @@ function fakeGithubInstallationVerificationFailure(): void { Http::preventStrayRequests(); Http::fake([ - 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [ - 'Date' => now()->toRfc7231String(), - ]), 'https://api.github.com/app/installations/*' => Http::response(['message' => 'Not Found'], 404), ]); } @@ -367,7 +361,6 @@ it('verifies ghe dot com installations through the data residency api host', fun $this->get('/webhooks/source/github/install?state=valid-install-state&setup_action=install&installation_id=123456') ->assertRedirect(route('source.github.show', ['github_app_uuid' => $this->githubApp->uuid])); - Http::assertSent(fn ($request) => $request->url() === 'https://api.octocorp.ghe.com/zen'); Http::assertSent(fn ($request) => $request->url() === 'https://api.octocorp.ghe.com/app/installations/123456'); }); @@ -420,9 +413,11 @@ it('redirects to the github app with the reason when the server clock is out of configureGithubAppCredentials($this->githubApp); Http::preventStrayRequests(); Http::fake([ - 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [ - 'Date' => now()->addMinutes(5)->toRfc7231String(), - ]), + 'https://api.github.com/app/installations/*' => Http::response( + ['message' => "'Issued at' claim ('iat') must be an Integer representing a time in the past"], + 401, + ['Date' => now()->addMinutes(5)->toRfc7231String()], + ), ]); cacheGithubAppSetupState('valid-install-state', 'install', $this->githubApp); diff --git a/tests/Unit/ServerTransfer/TransferFinishChecklistTest.php b/tests/Unit/ServerTransfer/TransferFinishChecklistTest.php index ad6491152f..1a0c1cdec0 100644 --- a/tests/Unit/ServerTransfer/TransferFinishChecklistTest.php +++ b/tests/Unit/ServerTransfer/TransferFinishChecklistTest.php @@ -101,7 +101,6 @@ test('the GitHub App webhook step checks the URL at GitHub and sets it with one $key = "github_app_webhook:{$this->githubApp->uuid}"; $hookUrl = 'https://coolify-a.test/webhooks/source/github/events'; Http::fake([ - 'https://api.github.com/zen' => Http::response('ok', 200, ['Date' => now()->toRfc7231String()]), 'https://api.github.com/app/hook/config' => function ($request) use (&$hookUrl) { if ($request->method() === 'PATCH') { $hookUrl = $request['url']; @@ -195,7 +194,6 @@ test('a failed check shows a short message and keeps the technical reason as det finishTestApplication($this, ['source_type' => GithubApp::class, 'source_id' => $this->githubApp->id]); $this->checklist->start($this->server); Http::fake([ - 'https://api.github.com/zen' => Http::response('ok', 200, ['Date' => now()->toRfc7231String()]), 'https://api.github.com/app/hook/config' => Http::response(['message' => 'Bad credentials'], 401), ]);