Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions app/Support/GitHub.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {}
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -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));
}
}
9 changes: 9 additions & 0 deletions resources/views/docs/mobile/3/getting-started/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
109 changes: 109 additions & 0 deletions tests/Feature/Support/GitHubTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

namespace Tests\Feature\Support;

use App\Support\GitHub;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;

class GitHubTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();

Cache::flush();
}

public function test_mobile_air_fetches_releases_from_the_correct_repo(): void
{
Http::fake([
'api.github.com/repos/nativephp/mobile-air/releases*' => 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());
}
}
Loading