From e58badf70ea0188eecaa973504ec6d9ac162865d Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 15 Aug 2026 10:07:40 +0200 Subject: [PATCH 01/11] 152 - Give factories unique values for constrained columns --- database/factories/FeedFactory.php | 2 +- database/factories/LanguageFactory.php | 4 +++- database/factories/PlatformAccountFactory.php | 2 +- database/factories/PlatformInstanceFactory.php | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/database/factories/FeedFactory.php b/database/factories/FeedFactory.php index fc0cb805..0d59ef3c 100644 --- a/database/factories/FeedFactory.php +++ b/database/factories/FeedFactory.php @@ -17,7 +17,7 @@ public function definition(): array { return [ 'name' => $this->faker->words(3, true), - 'url' => $this->faker->url(), + 'url' => $this->faker->unique()->url(), 'type' => $this->faker->randomElement(['website', 'rss']), 'provider' => $this->faker->randomElement(['vrt', 'belga']), 'language_id' => null, diff --git a/database/factories/LanguageFactory.php b/database/factories/LanguageFactory.php index c5ea2bf0..e7ece2e3 100644 --- a/database/factories/LanguageFactory.php +++ b/database/factories/LanguageFactory.php @@ -15,7 +15,9 @@ class LanguageFactory extends Factory public function definition(): array { return [ - 'short_code' => $this->faker->unique()->languageCode(), + // Not a real language code: tests hardcode 'en', 'fr', 'nl' and + // others, and faker's pool would collide with them. + 'short_code' => 'x-'.$this->faker->unique()->numerify('####'), 'name' => $this->faker->unique()->word(), 'native_name' => $this->faker->optional()->word(), 'is_active' => true, diff --git a/database/factories/PlatformAccountFactory.php b/database/factories/PlatformAccountFactory.php index 630446ae..92f1f544 100644 --- a/database/factories/PlatformAccountFactory.php +++ b/database/factories/PlatformAccountFactory.php @@ -19,7 +19,7 @@ public function definition(): array return [ 'platform' => PlatformEnum::LEMMY, 'instance_url' => 'https://lemmy.'.$this->faker->domainName(), - 'username' => $this->faker->userName(), + 'username' => $this->faker->unique()->userName(), 'password' => 'test-password', 'settings' => [], 'is_active' => true, diff --git a/database/factories/PlatformInstanceFactory.php b/database/factories/PlatformInstanceFactory.php index 956427d2..dc1a82e6 100644 --- a/database/factories/PlatformInstanceFactory.php +++ b/database/factories/PlatformInstanceFactory.php @@ -17,7 +17,7 @@ public function definition(): array return [ 'platform' => 'lemmy', 'name' => $this->faker->words(2, true), - 'url' => $this->faker->url(), + 'url' => $this->faker->unique()->url(), 'is_active' => true, ]; } @@ -34,7 +34,7 @@ public function lemmy(): static return $this->state(fn (array $attributes) => [ 'platform' => 'lemmy', 'name' => 'Lemmy '.$this->faker->word(), - 'url' => 'https://lemmy.'.$this->faker->domainName(), + 'url' => 'https://lemmy.'.$this->faker->unique()->domainName(), ]); } } From d17d98a5ecd45a0ee43000970c73844ec9f2f10c Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 15 Aug 2026 10:12:41 +0200 Subject: [PATCH 02/11] 152 - Freeze time in the publish job tests and cover the midnight boundary --- tests/Unit/Jobs/PublishNextArticleJobTest.php | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/Unit/Jobs/PublishNextArticleJobTest.php b/tests/Unit/Jobs/PublishNextArticleJobTest.php index c813d9d6..75d350b9 100644 --- a/tests/Unit/Jobs/PublishNextArticleJobTest.php +++ b/tests/Unit/Jobs/PublishNextArticleJobTest.php @@ -23,6 +23,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Carbon; use Mockery; use Tests\TestCase; @@ -35,6 +36,11 @@ class PublishNextArticleJobTest extends TestCase protected function setUp(): void { parent::setUp(); + + // The daily cap counts from startOfDay, so tests placing publications a + // couple of hours back land on the previous day when run after midnight. + Carbon::setTestNow('2026-07-15 12:00:00'); + $this->notificationService = new NotificationService; } @@ -319,6 +325,54 @@ public function test_daily_cap_counts_each_channel_publication_separately(): voi $this->assertTrue(true); } + public function test_daily_cap_counts_from_midnight_not_a_rolling_window(): void + { + Carbon::setTestNow('2026-07-15 01:00:00'); + + $this->createApprovedRouteArticle(); + + ArticlePublication::factory()->count(3)->create(['published_at' => Carbon::parse('2026-07-14 23:00:00')]); + Setting::setArticlePublishingInterval(0); + Setting::setDailyPublishCap(3); + + $articleFetcherMock = Mockery::mock(FetchArticleDataAction::class); + $articleFetcherMock->shouldReceive('execute') + ->once() + ->andReturn(['title' => 'Test Article', 'description' => 'Test description']); + + $publishingServiceMock = Mockery::mock(ArticlePublishingService::class); + $publishingServiceMock->shouldReceive('publishRouteArticle') + ->once() + ->andReturn(PublishOutcome::published($this->makePublication())); + + $job = new PublishNextArticleJob; + $job->handle(new PublishRouteArticleAction($articleFetcherMock, $publishingServiceMock, $this->notificationService)); + + $this->assertTrue(true); + } + + public function test_daily_cap_counts_a_publication_just_after_midnight(): void + { + Carbon::setTestNow('2026-07-15 01:00:00'); + + $this->createApprovedRouteArticle(); + + ArticlePublication::factory()->count(3)->create(['published_at' => Carbon::parse('2026-07-15 00:30:00')]); + Setting::setArticlePublishingInterval(0); + Setting::setDailyPublishCap(3); + + $articleFetcherMock = Mockery::mock(FetchArticleDataAction::class); + $publishingServiceMock = Mockery::mock(ArticlePublishingService::class); + + $articleFetcherMock->shouldNotReceive('execute'); + $publishingServiceMock->shouldNotReceive('publishRouteArticle'); + + $job = new PublishNextArticleJob; + $job->handle(new PublishRouteArticleAction($articleFetcherMock, $publishingServiceMock, $this->notificationService)); + + $this->assertTrue(true); + } + public function test_handle_ignores_publications_from_previous_days(): void { $this->createApprovedRouteArticle(); @@ -572,6 +626,7 @@ public function test_job_can_be_serialized(): void protected function tearDown(): void { + Carbon::setTestNow(); Mockery::close(); parent::tearDown(); } From ddd560b2044fea9d5ade56de0bcc95288c22cc2b Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 15 Aug 2026 10:35:33 +0200 Subject: [PATCH 03/11] 150 - Delete unconvertible channels instead of resolving them via Lemmy --- ...eric_community_id_on_platform_channels.php | 67 ++++++++-------- .../Feature/StoreCommunityIdMigrationTest.php | 77 +++++++++---------- 2 files changed, 66 insertions(+), 78 deletions(-) diff --git a/database/migrations/2024_01_01_000015_store_numeric_community_id_on_platform_channels.php b/database/migrations/2024_01_01_000015_store_numeric_community_id_on_platform_channels.php index 4f907e4f..da461755 100644 --- a/database/migrations/2024_01_01_000015_store_numeric_community_id_on_platform_channels.php +++ b/database/migrations/2024_01_01_000015_store_numeric_community_id_on_platform_channels.php @@ -1,26 +1,26 @@ orderBy('id') - ->get() - ->mapWithKeys(fn (object $channel) => [$channel->id => $this->resolve($channel)]); + $this->deleteChannelsWithUnconvertibleIds(); Schema::table('platform_channels', function (Blueprint $table) { $table->dropUnique('platform_channels_channel_id_unique'); @@ -30,11 +30,9 @@ public function up(): void $table->unsignedBigInteger('remote_community_id')->nullable()->after('channel_id'); }); - foreach ($resolved as $id => $communityId) { - DB::table('platform_channels') - ->where('id', $id) - ->update(['remote_community_id' => $communityId]); - } + DB::table('platform_channels')->update([ + 'remote_community_id' => DB::raw('CAST(channel_id AS UNSIGNED)'), + ]); Schema::table('platform_channels', function (Blueprint $table) { $table->dropColumn('channel_id'); @@ -67,33 +65,28 @@ public function down(): void }); } - private function resolve(object $channel): int + /** + * Routes, keywords, route articles and channel posts cascade from the + * database. article_publications does not have its foreign key until + * 000023, which deletes whatever this leaves orphaned. + */ + private function deleteChannelsWithUnconvertibleIds(): void { - if (is_numeric($channel->channel_id)) { - return (int) $channel->channel_id; + $doomed = DB::table('platform_channels') + ->get(['id', 'name', 'channel_id']) + ->reject(fn (object $channel) => ctype_digit((string) $channel->channel_id)) + ->pluck('name', 'id'); + + if ($doomed->isEmpty()) { + return; } - $instance = DB::table('platform_instances')->find($channel->platform_instance_id); + Log::warning(sprintf( + 'Deleting %d channel(s) whose community id is a slug rather than a numeric id: %s. Recreate them from the Channels page.', + $doomed->count(), + $doomed->implode(', '), + )); - if (! $instance) { - throw new RuntimeException("Channel {$channel->id} has no platform instance; cannot resolve its community id."); - } - - $account = PlatformAccount::where('instance_url', $instance->url) - ->where('is_active', true) - ->first(); - - if (! $account) { - throw new RuntimeException("No active account for {$instance->url}; cannot resolve community '{$channel->channel_id}'."); - } - - $api = new LemmyApiService($instance->url); - $token = $api->login($account->username, $account->password); - - if (! $token) { - throw new RuntimeException("Could not authenticate against {$instance->url} to resolve community '{$channel->channel_id}'."); - } - - return $api->getCommunityId($channel->channel_id, $token); + DB::table('platform_channels')->whereIn('id', $doomed->keys())->delete(); } }; diff --git a/tests/Feature/StoreCommunityIdMigrationTest.php b/tests/Feature/StoreCommunityIdMigrationTest.php index 6dc9c759..e0112840 100644 --- a/tests/Feature/StoreCommunityIdMigrationTest.php +++ b/tests/Feature/StoreCommunityIdMigrationTest.php @@ -2,7 +2,6 @@ namespace Tests\Feature; -use App\Models\PlatformAccount; use App\Models\PlatformInstance; use Illuminate\Database\Schema\Blueprint; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -37,39 +36,31 @@ private function restoreSlugColumn(): void }); } - private function seedChannel(PlatformInstance $instance, string $slug): int + private function seedChannel(PlatformInstance $instance, string $channelId, string $name): int { return DB::table('platform_channels')->insertGetId([ 'platform_instance_id' => $instance->id, - 'name' => $slug, - 'display_name' => ucfirst($slug), - 'channel_id' => $slug, + 'name' => $name, + 'display_name' => ucfirst($name), + 'channel_id' => $channelId, 'is_active' => true, 'created_at' => now(), 'updated_at' => now(), ]); } - private function instanceWithAccount(): PlatformInstance - { - $instance = PlatformInstance::factory()->create(['url' => 'https://lemmy.test']); - PlatformAccount::factory()->create(['instance_url' => 'https://lemmy.test', 'is_active' => true]); - - return $instance; - } - - public function test_it_replaces_the_slug_with_the_resolved_community_id(): void + private function prepare(): PlatformInstance { $this->restoreSlugColumn(); DB::table('platform_channels')->delete(); - $instance = $this->instanceWithAccount(); - $id = $this->seedChannel($instance, 'news'); + return PlatformInstance::factory()->create(['url' => 'https://lemmy.test']); + } - Http::fake([ - '*/api/v3/user/login*' => Http::response(['jwt' => 'token']), - '*/api/v3/community*' => Http::response(['community_view' => ['community' => ['id' => 8]]]), - ]); + public function test_it_keeps_a_channel_whose_id_is_already_numeric(): void + { + $instance = $this->prepare(); + $id = $this->seedChannel($instance, '8', 'news'); $this->runMigration(); @@ -77,34 +68,38 @@ public function test_it_replaces_the_slug_with_the_resolved_community_id(): void $this->assertSame('news', DB::table('platform_channels')->where('id', $id)->value('name')); } - public function test_it_aborts_when_a_community_cannot_be_resolved(): void + public function test_it_deletes_a_channel_whose_id_is_still_a_slug(): void { - $this->restoreSlugColumn(); - DB::table('platform_channels')->delete(); - - $instance = $this->instanceWithAccount(); - $this->seedChannel($instance, 'gone'); - - Http::fake([ - '*/api/v3/user/login*' => Http::response(['jwt' => 'token']), - '*/api/v3/community*' => Http::response('not found', 404), - ]); - - $this->expectException(\Exception::class); + $instance = $this->prepare(); + $id = $this->seedChannel($instance, 'news', 'news'); $this->runMigration(); + + $this->assertDatabaseMissing('platform_channels', ['id' => $id]); } - public function test_it_aborts_when_the_instance_has_no_active_account(): void + public function test_it_keeps_numeric_channels_while_deleting_slug_ones(): void { - $this->restoreSlugColumn(); - DB::table('platform_channels')->delete(); - - $instance = PlatformInstance::factory()->create(['url' => 'https://no-account.test']); - $this->seedChannel($instance, 'news'); - - $this->expectException(\RuntimeException::class); + $instance = $this->prepare(); + $kept = $this->seedChannel($instance, '8', 'news'); + $deleted = $this->seedChannel($instance, 'nieuws', 'nieuws'); $this->runMigration(); + + $this->assertDatabaseHas('platform_channels', ['id' => $kept]); + $this->assertDatabaseMissing('platform_channels', ['id' => $deleted]); + } + + public function test_it_makes_no_http_requests(): void + { + Http::preventStrayRequests(); + + $instance = $this->prepare(); + $this->seedChannel($instance, 'news', 'news'); + $this->seedChannel($instance, '8', 'other'); + + $this->runMigration(); + + Http::assertNothingSent(); } } From 20bc7655d66bec73b627422723b2f165dbc77445 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 15 Aug 2026 00:51:46 +0200 Subject: [PATCH 04/11] 147 - Drop coverage from CI and run PHPUnit directly --- .forgejo/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index fada4116..90b25726 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: with: php-version: '8.3' extensions: pdo_sqlite, mbstring, xml, dom - coverage: pcov + coverage: none - name: Cache Composer dependencies uses: https://data.forgejo.org/actions/cache@v4 @@ -41,4 +41,4 @@ jobs: run: vendor/bin/phpstan analyse --memory-limit=1G - name: Tests - run: php artisan test --coverage-clover coverage.xml --coverage-text + run: php -d memory_limit=512M vendor/bin/phpunit From 209efc31b24e88b3605de8b2fd96e218c409323e Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 15 Aug 2026 00:59:05 +0200 Subject: [PATCH 05/11] 147 - Run CI in a prebuilt image instead of installing PHP each run --- .forgejo/workflows/ci.yml | 9 +------ .forgejo/workflows/images.yml | 44 +++++++++++++++++++++++++++++++++++ docker/build/Dockerfile.ci | 25 ++++++++++++++++++++ 3 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 .forgejo/workflows/images.yml create mode 100644 docker/build/Dockerfile.ci diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 90b25726..7e98af9c 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -10,17 +10,10 @@ jobs: ci: runs-on: docker container: - image: catthehacker/ubuntu:act-latest + image: forge.lvl0.xyz/lvl0/fedi-feed-router-ci:latest steps: - uses: https://data.forgejo.org/actions/checkout@v4 - - name: Set up PHP - uses: https://github.com/shivammathur/setup-php@v2 - with: - php-version: '8.3' - extensions: pdo_sqlite, mbstring, xml, dom - coverage: none - - name: Cache Composer dependencies uses: https://data.forgejo.org/actions/cache@v4 with: diff --git a/.forgejo/workflows/images.yml b/.forgejo/workflows/images.yml new file mode 100644 index 00000000..2bdd4009 --- /dev/null +++ b/.forgejo/workflows/images.yml @@ -0,0 +1,44 @@ +name: Build and Push Base Images + +on: + push: + branches: [main] + paths: + - 'docker/build/**' + - '.forgejo/workflows/images.yml' + workflow_dispatch: + +jobs: + images: + runs-on: docker + container: + image: catthehacker/ubuntu:act-latest + strategy: + matrix: + include: + - name: fedi-feed-router-base + file: docker/build/Dockerfile.base + - name: fedi-feed-router-ci + file: docker/build/Dockerfile.ci + steps: + - uses: https://data.forgejo.org/actions/checkout@v4 + + - name: Set up Docker Buildx + uses: https://data.forgejo.org/docker/setup-buildx-action@v3 + + - name: Login to Forgejo Registry + uses: https://data.forgejo.org/docker/login-action@v3 + with: + registry: forge.lvl0.xyz + username: ${{ github.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Build and push + uses: https://data.forgejo.org/docker/build-push-action@v5 + with: + context: . + file: ${{ matrix.file }} + push: true + tags: | + forge.lvl0.xyz/lvl0/${{ matrix.name }}:latest + forge.lvl0.xyz/lvl0/${{ matrix.name }}:${{ github.sha }} diff --git a/docker/build/Dockerfile.ci b/docker/build/Dockerfile.ci new file mode 100644 index 00000000..05481602 --- /dev/null +++ b/docker/build/Dockerfile.ci @@ -0,0 +1,25 @@ +# Image for CI: PHP and Composer only, no runtime server or frontend toolchain. +# Tests run against sqlite in memory (see .env.testing), so no database client +# or cache extension is needed. +# +# Extensions here are the ext-* requirements from composer.lock that php:alpine +# does not already bundle. Regenerate that list before changing this file. +FROM php:8.3-alpine + +COPY --from=mlocati/php-extension-installer:2 /usr/bin/install-php-extensions /usr/local/bin/ + +RUN install-php-extensions \ + pdo_sqlite \ + mbstring \ + dom \ + xml \ + fileinfo \ + iconv \ + pcntl \ + posix + +# nodejs is not used by the app's tests; the Forgejo/GitHub JavaScript actions +# (checkout, cache) are executed with it inside this container. +RUN apk add --no-cache git unzip nodejs + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer From 1e120a4bac05459f61eaeb5d21dc9d87305cb28b Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 15 Aug 2026 01:08:34 +0200 Subject: [PATCH 06/11] 147 - Fix the Composer cache path so it caches anything --- .forgejo/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 7e98af9c..50ec54ed 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - name: Cache Composer dependencies uses: https://data.forgejo.org/actions/cache@v4 with: - path: ~/.composer/cache + path: ~/.cache/composer key: composer-${{ hashFiles('composer.lock') }} restore-keys: composer- From 5cd51f9b43bd688f36580b8f403139f66c0fbdd9 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 15 Aug 2026 01:12:24 +0200 Subject: [PATCH 07/11] 147 - Run CI on pull requests into release branches --- .forgejo/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 50ec54ed..85f3f27e 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -4,7 +4,7 @@ on: push: branches: ['release/*'] pull_request: - branches: [main] + branches: [main, 'release/*'] jobs: ci: From 5400bb0582075d74e2609aa3b17993235dac5e46 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 15 Aug 2026 01:22:18 +0200 Subject: [PATCH 08/11] 147 - Add gd to the CI image and declare ext-gd --- composer.json | 1 + composer.lock | 5 +++-- docker/build/Dockerfile.ci | 12 +++--------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 0254fbae..99b45229 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "license": "AGPL-3.0-only", "require": { "php": "^8.2", + "ext-gd": "*", "blade-ui-kit/blade-heroicons": "^2.6", "laravel/framework": "^12.0", "laravel/horizon": "^5.29", diff --git a/composer.lock b/composer.lock index 665abac6..fabaf870 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e6ce5effb7f8c4d5a3f6d8cd04b6e299", + "content-hash": "67923d1e9e79798f3ebe2e79954f4795", "packages": [ { "name": "blade-ui-kit/blade-heroicons", @@ -8747,7 +8747,8 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.2" + "php": "^8.2", + "ext-gd": "*" }, "platform-dev": {}, "plugin-api-version": "2.6.0" diff --git a/docker/build/Dockerfile.ci b/docker/build/Dockerfile.ci index 05481602..b63abaf4 100644 --- a/docker/build/Dockerfile.ci +++ b/docker/build/Dockerfile.ci @@ -2,21 +2,15 @@ # Tests run against sqlite in memory (see .env.testing), so no database client # or cache extension is needed. # -# Extensions here are the ext-* requirements from composer.lock that php:alpine -# does not already bundle. Regenerate that list before changing this file. +# php:alpine already bundles the rest of what composer.lock requires. gd is not +# declared anywhere but ThumbnailUploader calls it directly. FROM php:8.3-alpine COPY --from=mlocati/php-extension-installer:2 /usr/bin/install-php-extensions /usr/local/bin/ RUN install-php-extensions \ - pdo_sqlite \ - mbstring \ - dom \ - xml \ - fileinfo \ - iconv \ pcntl \ - posix + gd # nodejs is not used by the app's tests; the Forgejo/GitHub JavaScript actions # (checkout, cache) are executed with it inside this container. From 7eaa32f7a079ea6cbae9237edd57e8da320e00d9 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 15 Aug 2026 01:31:14 +0200 Subject: [PATCH 09/11] 147 - Pin the CI image to a version tag instead of latest --- .forgejo/workflows/ci.yml | 2 +- .forgejo/workflows/images.yml | 3 +++ docker/build/Dockerfile.ci | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 85f3f27e..a99a86a9 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: ci: runs-on: docker container: - image: forge.lvl0.xyz/lvl0/fedi-feed-router-ci:latest + image: forge.lvl0.xyz/lvl0/fedi-feed-router-ci:php8.3-1 steps: - uses: https://data.forgejo.org/actions/checkout@v4 diff --git a/.forgejo/workflows/images.yml b/.forgejo/workflows/images.yml index 2bdd4009..643e7528 100644 --- a/.forgejo/workflows/images.yml +++ b/.forgejo/workflows/images.yml @@ -18,8 +18,10 @@ jobs: include: - name: fedi-feed-router-base file: docker/build/Dockerfile.base + version: php8.3-1 - name: fedi-feed-router-ci file: docker/build/Dockerfile.ci + version: php8.3-1 steps: - uses: https://data.forgejo.org/actions/checkout@v4 @@ -40,5 +42,6 @@ jobs: file: ${{ matrix.file }} push: true tags: | + forge.lvl0.xyz/lvl0/${{ matrix.name }}:${{ matrix.version }} forge.lvl0.xyz/lvl0/${{ matrix.name }}:latest forge.lvl0.xyz/lvl0/${{ matrix.name }}:${{ github.sha }} diff --git a/docker/build/Dockerfile.ci b/docker/build/Dockerfile.ci index b63abaf4..d63a4c1c 100644 --- a/docker/build/Dockerfile.ci +++ b/docker/build/Dockerfile.ci @@ -2,6 +2,10 @@ # Tests run against sqlite in memory (see .env.testing), so no database client # or cache extension is needed. # +# Published as fedi-feed-router-ci:php-, not :latest. Runners +# cache mutable tags and will not re-pull them, so bump the revision in the tag +# and in ci.yml whenever this file changes. +# # php:alpine already bundles the rest of what composer.lock requires. gd is not # declared anywhere but ThumbnailUploader calls it directly. FROM php:8.3-alpine From d07e58207f3c8c3e7edb55a39fabdbac01eb1cdb Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 15 Aug 2026 01:48:47 +0200 Subject: [PATCH 10/11] 147 - Base the CI image on Debian instead of Alpine --- .forgejo/workflows/ci.yml | 2 +- .forgejo/workflows/images.yml | 2 +- docker/build/Dockerfile.ci | 17 +++++++++++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index a99a86a9..1f16d305 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: ci: runs-on: docker container: - image: forge.lvl0.xyz/lvl0/fedi-feed-router-ci:php8.3-1 + image: forge.lvl0.xyz/lvl0/fedi-feed-router-ci:php8.3-2 steps: - uses: https://data.forgejo.org/actions/checkout@v4 diff --git a/.forgejo/workflows/images.yml b/.forgejo/workflows/images.yml index 643e7528..ba694bfd 100644 --- a/.forgejo/workflows/images.yml +++ b/.forgejo/workflows/images.yml @@ -21,7 +21,7 @@ jobs: version: php8.3-1 - name: fedi-feed-router-ci file: docker/build/Dockerfile.ci - version: php8.3-1 + version: php8.3-2 steps: - uses: https://data.forgejo.org/actions/checkout@v4 diff --git a/docker/build/Dockerfile.ci b/docker/build/Dockerfile.ci index d63a4c1c..50857a02 100644 --- a/docker/build/Dockerfile.ci +++ b/docker/build/Dockerfile.ci @@ -6,18 +6,27 @@ # cache mutable tags and will not re-pull them, so bump the revision in the tag # and in ci.yml whenever this file changes. # -# php:alpine already bundles the rest of what composer.lock requires. gd is not -# declared anywhere but ThumbnailUploader calls it directly. -FROM php:8.3-alpine +# Debian-based rather than alpine: the alpine build hit repeated DNS resolution +# timeouts against codeload.github.com during composer install. +# +# gd is not declared in composer.lock but ThumbnailUploader calls it directly. +FROM php:8.3-cli COPY --from=mlocati/php-extension-installer:2 /usr/bin/install-php-extensions /usr/local/bin/ RUN install-php-extensions \ + pdo_sqlite \ + mbstring \ + dom \ + xml \ + fileinfo \ pcntl \ gd # nodejs is not used by the app's tests; the Forgejo/GitHub JavaScript actions # (checkout, cache) are executed with it inside this container. -RUN apk add --no-cache git unzip nodejs +RUN apt-get update \ + && apt-get install -y --no-install-recommends git unzip nodejs \ + && rm -rf /var/lib/apt/lists/* COPY --from=composer:2 /usr/bin/composer /usr/bin/composer From 85fe585f8d6cbae96ddd61f37d3fb43b84afc42a Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 15 Aug 2026 11:10:46 +0200 Subject: [PATCH 11/11] 147 - Bump the CI image tag to force a fresh pull --- .forgejo/workflows/ci.yml | 2 +- .forgejo/workflows/images.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 1f16d305..80a1b1fd 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: ci: runs-on: docker container: - image: forge.lvl0.xyz/lvl0/fedi-feed-router-ci:php8.3-2 + image: forge.lvl0.xyz/lvl0/fedi-feed-router-ci:php8.3-3 steps: - uses: https://data.forgejo.org/actions/checkout@v4 diff --git a/.forgejo/workflows/images.yml b/.forgejo/workflows/images.yml index ba694bfd..f0769ce1 100644 --- a/.forgejo/workflows/images.yml +++ b/.forgejo/workflows/images.yml @@ -21,7 +21,7 @@ jobs: version: php8.3-1 - name: fedi-feed-router-ci file: docker/build/Dockerfile.ci - version: php8.3-2 + version: php8.3-3 steps: - uses: https://data.forgejo.org/actions/checkout@v4