diff --git a/app/Support/GitHub.php b/app/Support/GitHub.php index 87c797f8..3daf6382 100644 --- a/app/Support/GitHub.php +++ b/app/Support/GitHub.php @@ -17,6 +17,8 @@ class GitHub public const PACKAGE_PHP_BIN = 'nativephp/php-bin'; + public const PACKAGE_MOBILE_AIR = 'nativephp/mobile-air'; + public function __construct( private string $package ) {} @@ -43,6 +45,11 @@ public static function phpBin(): static return new static(static::PACKAGE_PHP_BIN); } + public static function mobileAir(): static + { + return new static(static::PACKAGE_MOBILE_AIR); + } + public function latestVersion() { $release = Cache::remember( @@ -63,6 +70,15 @@ public function releases(): Collection ) ?? collect(); } + public function releasesAfter(string $version): Collection + { + $version = ltrim($version, 'v'); + + return $this->releases()->filter( + fn (Release $release) => version_compare(ltrim((string) $release->tag_name, 'v'), $version, '>') + )->values(); + } + private function fetchLatestVersion(): ?Release { // Make a request to GitHub @@ -83,14 +99,24 @@ private function getCacheKey(string $string): string private function fetchReleases(): ?Collection { - // Make a request to GitHub - $response = Http::get('https://api.github.com/repos/'.$this->package.'/releases'); + $releases = collect(); + $page = 1; - // Check if the request was successful - if ($response->failed()) { - return collect(); - } + do { + $response = Http::get('https://api.github.com/repos/'.$this->package.'/releases', [ + 'per_page' => 100, + 'page' => $page, + ]); + + if ($response->failed()) { + return $releases; + } + + $pageReleases = $response->json(); + $releases = $releases->concat($pageReleases); + $page++; + } while (count($pageReleases) === 100); - return collect($response->json())->map(fn (array $release) => new Release($release)); + return $releases->map(fn (array $release) => new Release($release)); } } diff --git a/resources/views/docs/mobile/3/getting-started/changelog.md b/resources/views/docs/mobile/3/getting-started/changelog.md index ba5a4d78..ec985749 100644 --- a/resources/views/docs/mobile/3/getting-started/changelog.md +++ b/resources/views/docs/mobile/3/getting-started/changelog.md @@ -5,6 +5,15 @@ order: 2 For changes prior to v3, see the [v2 documentation](/docs/mobile/2/getting-started/changelog). +@forelse (\App\Support\GitHub::mobileAir()->releasesAfter('3.1.0') as $release) +## {{ $release->name ?: $release->tag_name }} +**Released: {{ \Carbon\Carbon::parse($release->published_at)->format('F j, Y') }}** + +{{ $release->getBodyForMarkdown() }} +--- +@empty +@endforelse + ## v3.1 — Persistent Runtime & Performance ### New Features diff --git a/tests/Feature/Support/GitHubTest.php b/tests/Feature/Support/GitHubTest.php new file mode 100644 index 00000000..de171aa5 --- /dev/null +++ b/tests/Feature/Support/GitHubTest.php @@ -0,0 +1,109 @@ + Http::response([ + [ + 'id' => 1, + 'name' => 'v3.2.0', + 'tag_name' => 'v3.2.0', + 'body' => 'New release', + 'published_at' => '2026-05-01T00:00:00Z', + ], + ]), + ]); + + $releases = GitHub::mobileAir()->releases(); + + $this->assertCount(1, $releases); + $this->assertEquals('v3.2.0', $releases->first()->tag_name); + + Http::assertSent(fn ($request) => str_starts_with($request->url(), 'https://api.github.com/repos/nativephp/mobile-air/releases?')); + } + + public function test_releases_paginates_through_every_page(): void + { + $firstPage = array_map( + fn (int $i) => ['id' => $i, 'tag_name' => "v1.{$i}.0", 'name' => "v1.{$i}.0", 'body' => '', 'published_at' => '2026-01-01T00:00:00Z'], + range(1, 100), + ); + $secondPage = [ + ['id' => 101, 'tag_name' => 'v0.9.0', 'name' => 'v0.9.0', 'body' => '', 'published_at' => '2025-12-01T00:00:00Z'], + ]; + + $sequence = Http::sequence() + ->push($firstPage) + ->push($secondPage); + + Http::fake([ + 'api.github.com/repos/nativephp/mobile-air/releases*' => $sequence, + ]); + + $releases = GitHub::mobileAir()->releases(); + + $this->assertCount(101, $releases); + $this->assertEquals('v0.9.0', $releases->last()->tag_name); + Http::assertSentCount(2); + } + + public function test_releases_after_filters_out_versions_at_or_below_the_given_version(): void + { + Http::fake([ + 'api.github.com/repos/nativephp/mobile-air/releases*' => Http::response([ + ['id' => 1, 'tag_name' => 'v4.0.0', 'name' => 'v4.0.0', 'body' => '', 'published_at' => '2026-06-01T00:00:00Z'], + ['id' => 2, 'tag_name' => 'v3.2.0', 'name' => 'v3.2.0', 'body' => '', 'published_at' => '2026-05-01T00:00:00Z'], + ['id' => 3, 'tag_name' => 'v3.1.1', 'name' => 'v3.1.1', 'body' => '', 'published_at' => '2026-04-15T00:00:00Z'], + ['id' => 4, 'tag_name' => 'v3.1.0', 'name' => 'v3.1.0', 'body' => '', 'published_at' => '2026-04-01T00:00:00Z'], + ['id' => 5, 'tag_name' => 'v3.0.0', 'name' => 'v3.0.0', 'body' => '', 'published_at' => '2026-03-01T00:00:00Z'], + ]), + ]); + + $releases = GitHub::mobileAir()->releasesAfter('3.1.0'); + + $this->assertEquals( + ['v4.0.0', 'v3.2.0', 'v3.1.1'], + $releases->pluck('tag_name')->all() + ); + } + + public function test_releases_after_handles_tag_names_without_a_leading_v(): void + { + Http::fake([ + 'api.github.com/repos/nativephp/mobile-air/releases*' => Http::response([ + ['id' => 1, 'tag_name' => '3.2.0', 'name' => '3.2.0', 'body' => '', 'published_at' => '2026-05-01T00:00:00Z'], + ['id' => 2, 'tag_name' => '3.1.0', 'name' => '3.1.0', 'body' => '', 'published_at' => '2026-04-01T00:00:00Z'], + ]), + ]); + + $releases = GitHub::mobileAir()->releasesAfter('v3.1.0'); + + $this->assertCount(1, $releases); + $this->assertEquals('3.2.0', $releases->first()->tag_name); + } + + public function test_releases_after_returns_empty_collection_when_request_fails(): void + { + Http::fake([ + 'api.github.com/repos/nativephp/mobile-air/releases*' => Http::response(null, 500), + ]); + + $this->assertTrue(GitHub::mobileAir()->releasesAfter('3.1.0')->isEmpty()); + } +}