From 1469865e45ecd2fc615af33d5ba6d3a0d0198d5d Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 31 Aug 2026 07:24:46 +0100 Subject: [PATCH 1/4] 715: use GH API url instead of browser_download_url The easy route to use `browser_download_url` to grab the asset only works if the repo is a public repo. However, the docs do indeed say: - If within a browser, fetch the location specified in the `browser_download_url` key provided in the response. - Alternatively, set the `Accept` header of the request to `application/octet-stream`. The API will either redirect the client to the location, or stream it directly if possible. API clients should handle both a `200` or `302` response. So, we'll update PIE to use `url` and handle the other complexity with this. ref: https://docs.github.com/en/rest/releases/assets?apiVersion=2026-03-10#get-a-release-asset --- src/Downloading/GithubPackageReleaseAssets.php | 12 ++++++------ .../OverrideDownloadUrlInstallListenerTest.php | 4 ++-- .../Downloading/GithubPackageReleaseAssetsTest.php | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php index ad1bd13a..d35380de 100644 --- a/src/Downloading/GithubPackageReleaseAssets.php +++ b/src/Downloading/GithubPackageReleaseAssets.php @@ -42,16 +42,16 @@ public function findMatchingReleaseAssetUrl( $possibleReleaseAssetNames, ); - return $releaseAsset['browser_download_url']; + return $releaseAsset['url']; } /** @link https://github.com/squizlabs/PHP_CodeSniffer/issues/3734 */ // phpcs:disable Squiz.Commenting.FunctionComment.MissingParamName /** - * @param list $releaseAssets + * @param list $releaseAssets * @param non-empty-list $possibleReleaseAssetNames * - * @return array{name: non-empty-string, browser_download_url: non-empty-string, ...} + * @return array{name: non-empty-string, url: non-empty-string, ...} */ // phpcs:enable private function selectMatchingReleaseAsset( @@ -70,7 +70,7 @@ private function selectMatchingReleaseAsset( throw Exception\CouldNotFindReleaseAsset::forPackage($targetPlatform, $package, $downloadUrlMethod, $possibleReleaseAssetNames); } - /** @return list */ + /** @return list */ private function getReleaseAssetsForPackage( Package $package, HttpDownloader $httpDownloader, @@ -106,8 +106,8 @@ private function getReleaseAssetsForPackage( static function (array $asset): array { Assert::keyExists($asset, 'name'); Assert::stringNotEmpty($asset['name']); - Assert::keyExists($asset, 'browser_download_url'); - Assert::stringNotEmpty($asset['browser_download_url']); + Assert::keyExists($asset, 'url'); + Assert::stringNotEmpty($asset['url']); return $asset; }, diff --git a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php index f7ce589a..44ebe617 100644 --- a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php +++ b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php @@ -405,7 +405,7 @@ public function testDistUrlIsUpdatedForPrePackagedTgzBinaryWhenBinaryIsFound(): $packageReleaseAssets ->expects(self::once()) ->method('findMatchingReleaseAssetUrl') - ->willReturn('https://example.com/pre-packaged-binary-download-url.tgz'); + ->willReturn('https://api.github.com/repos/foo/bar/releases/assets/12345'); $this->container ->method('get') @@ -436,7 +436,7 @@ public function testDistUrlIsUpdatedForPrePackagedTgzBinaryWhenBinaryIsFound(): ))($installerEvent); self::assertSame( - 'https://example.com/pre-packaged-binary-download-url.tgz', + 'https://api.github.com/repos/foo/bar/releases/assets/12345', $composerPackage->getDistUrl(), ); self::assertSame(DownloadUrlMethod::PrePackagedBinary, DownloadUrlMethod::fromComposerPackage($composerPackage)); diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php index e36347b5..87804a5d 100644 --- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php @@ -56,11 +56,11 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void 'assets' => [ [ 'name' => 'php_foo-1.2.3-8.3-vc14-nts-x86.zip', - 'browser_download_url' => 'wrong_download_url', + 'url' => 'wrong_download_url', ], [ 'name' => 'php_foo-1.2.3-8.3-vc14-ts-x86.zip', - 'browser_download_url' => 'actual_download_url', + 'url' => 'actual_download_url', ], ], ]); @@ -123,11 +123,11 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrlWithCompilerAndThr 'assets' => [ [ 'name' => 'php_foo-1.2.3-8.3-nts-vc14-x86.zip', - 'browser_download_url' => 'wrong_download_url', + 'url' => 'wrong_download_url', ], [ 'name' => 'php_foo-1.2.3-8.3-ts-vc14-x86.zip', - 'browser_download_url' => 'actual_download_url', + 'url' => 'actual_download_url', ], ], ]); From 378122a89fbf6cd9602a5b9ac7db035db46629a9 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 31 Aug 2026 07:53:09 +0100 Subject: [PATCH 2/4] 715: add Accept header as per GH docs --- .../Listeners/OverrideDownloadUrlInstallListener.php | 6 ++++++ .../Listeners/OverrideDownloadUrlInstallListenerTest.php | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php index 46272f47..ef5a0630 100644 --- a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php +++ b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php @@ -20,6 +20,7 @@ use Psr\Container\ContainerInterface; use Throwable; +use function array_merge_recursive; use function array_walk; use function in_array; use function pathinfo; @@ -150,6 +151,11 @@ function (OperationInterface $operation): void { $this->composerRequest->pieOutput->write('Found prebuilt archive: ' . $url); $composerPackage->setDistUrl($url); + $composerPackage->setTransportOptions(array_merge_recursive( + $composerPackage->getTransportOptions(), + ['http' => ['header' => ['Accept: application/octet-stream']]], + )); + // Composer's dist-sha was computed against the original // Packagist URL; once we swap to a release-asset URL the // FileDownloader has nothing to validate the new bytes diff --git a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php index 44ebe617..62e24d19 100644 --- a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php +++ b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php @@ -319,6 +319,7 @@ public function testDistUrlIsUpdatedForWindowsInstallers(): void $composerPackage->getDistUrl(), ); self::assertSame(DownloadUrlMethod::WindowsBinaryDownload, DownloadUrlMethod::fromComposerPackage($composerPackage)); + self::assertSame(['http' => ['header' => ['Accept: application/octet-stream']]], $composerPackage->getTransportOptions()); } public function testDistUrlIsUpdatedForPrePackagedTgzSource(): void @@ -441,6 +442,7 @@ public function testDistUrlIsUpdatedForPrePackagedTgzBinaryWhenBinaryIsFound(): ); self::assertSame(DownloadUrlMethod::PrePackagedBinary, DownloadUrlMethod::fromComposerPackage($composerPackage)); self::assertSame('tar', $composerPackage->getDistType()); + self::assertSame(['http' => ['header' => ['Accept: application/octet-stream']]], $composerPackage->getTransportOptions()); } public function testDistUrlIsUpdatedForPrePackagedTgzBinaryWhenBinaryIsNotFound(): void From d1cda0b732985af8f788b886944f72c2eefe0173 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 31 Aug 2026 08:04:32 +0100 Subject: [PATCH 3/4] 715: add MatchedReleaseAsset VO to carry both the asset name and URL back from PackageReleaseAssets implementors --- .../OverrideDownloadUrlInstallListener.php | 8 ++--- .../GithubPackageReleaseAssets.php | 12 +++---- src/Downloading/MatchedReleaseAsset.php | 23 +++++++++++++ src/Downloading/PackageReleaseAssets.php | 10 ++---- ...OverrideDownloadUrlInstallListenerTest.php | 33 +++++++++++++------ .../GithubPackageReleaseAssetsTest.php | 10 +++--- 6 files changed, 62 insertions(+), 34 deletions(-) create mode 100644 src/Downloading/MatchedReleaseAsset.php diff --git a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php index ef5a0630..641916a8 100644 --- a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php +++ b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php @@ -135,7 +135,7 @@ function (OperationInterface $operation): void { $packageReleaseAssets = $this->container->get(PackageReleaseAssets::class); try { - $url = $packageReleaseAssets->findMatchingReleaseAssetUrl( + $matchedReleaseAsset = $packageReleaseAssets->findMatchingReleaseAsset( $targetPlatform, $piePackage, new HttpDownloader($this->io, $this->composer->getConfig()), @@ -148,8 +148,8 @@ function (OperationInterface $operation): void { continue; } - $this->composerRequest->pieOutput->write('Found prebuilt archive: ' . $url); - $composerPackage->setDistUrl($url); + $this->composerRequest->pieOutput->write('Found prebuilt archive: ' . $matchedReleaseAsset->url); + $composerPackage->setDistUrl($matchedReleaseAsset->url); $composerPackage->setTransportOptions(array_merge_recursive( $composerPackage->getTransportOptions(), @@ -165,7 +165,7 @@ function (OperationInterface $operation): void { 'Note: dist-sha integrity check is not available for prebuilt-binary URLs; HTTPS to the release-asset origin is the only integrity guarantee.', ); - if (pathinfo($url, PATHINFO_EXTENSION) === 'tgz') { + if (pathinfo($matchedReleaseAsset->filename, PATHINFO_EXTENSION) === 'tgz') { $composerPackage->setDistType('tar'); } diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php index d35380de..3826a7ce 100644 --- a/src/Downloading/GithubPackageReleaseAssets.php +++ b/src/Downloading/GithubPackageReleaseAssets.php @@ -22,18 +22,14 @@ public function __construct( ) { } - /** - * @param non-empty-list $possibleReleaseAssetNames - * - * @return non-empty-string - */ - public function findMatchingReleaseAssetUrl( + /** @param non-empty-list $possibleReleaseAssetNames */ + public function findMatchingReleaseAsset( TargetPlatform $targetPlatform, Package $package, HttpDownloader $httpDownloader, DownloadUrlMethod $downloadUrlMethod, array $possibleReleaseAssetNames, - ): string { + ): MatchedReleaseAsset { $releaseAsset = $this->selectMatchingReleaseAsset( $targetPlatform, $package, @@ -42,7 +38,7 @@ public function findMatchingReleaseAssetUrl( $possibleReleaseAssetNames, ); - return $releaseAsset['url']; + return new MatchedReleaseAsset($releaseAsset['url'], $releaseAsset['name']); } /** @link https://github.com/squizlabs/PHP_CodeSniffer/issues/3734 */ diff --git a/src/Downloading/MatchedReleaseAsset.php b/src/Downloading/MatchedReleaseAsset.php new file mode 100644 index 00000000..f10554e5 --- /dev/null +++ b/src/Downloading/MatchedReleaseAsset.php @@ -0,0 +1,23 @@ + $possibleReleaseAssetNames - * - * @return non-empty-string - */ - public function findMatchingReleaseAssetUrl( + /** @param non-empty-list $possibleReleaseAssetNames */ + public function findMatchingReleaseAsset( TargetPlatform $targetPlatform, Package $package, HttpDownloader $httpDownloader, DownloadUrlMethod $downloadUrlMethod, array $possibleReleaseAssetNames, - ): string; + ): MatchedReleaseAsset; } diff --git a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php index 62e24d19..e7d5b89c 100644 --- a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php +++ b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php @@ -21,6 +21,7 @@ use Php\Pie\DependencyResolver\RequestedPackageAndVersion; use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset; +use Php\Pie\Downloading\MatchedReleaseAsset; use Php\Pie\Downloading\PackageReleaseAssets; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; @@ -283,8 +284,11 @@ public function testDistUrlIsUpdatedForWindowsInstallers(): void $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); $packageReleaseAssets ->expects(self::once()) - ->method('findMatchingReleaseAssetUrl') - ->willReturn('https://example.com/windows-download-url'); + ->method('findMatchingReleaseAsset') + ->willReturn(new MatchedReleaseAsset( + 'https://api.github.com/repos/foo/bar/releases/assets/12345', + 'php_foo-1.2.3-8.3-vc14-ts-x86.zip', + )); $this->container ->method('get') @@ -344,8 +348,11 @@ public function testDistUrlIsUpdatedForPrePackagedTgzSource(): void $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); $packageReleaseAssets ->expects(self::once()) - ->method('findMatchingReleaseAssetUrl') - ->willReturn('https://example.com/pre-packaged-source-download-url.tgz'); + ->method('findMatchingReleaseAsset') + ->willReturn(new MatchedReleaseAsset( + 'https://api.github.com/repos/foo/bar/releases/assets/12345', + 'php_foobar-1.2.3-src.tgz', + )); $this->container ->method('get') @@ -405,8 +412,11 @@ public function testDistUrlIsUpdatedForPrePackagedTgzBinaryWhenBinaryIsFound(): $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); $packageReleaseAssets ->expects(self::once()) - ->method('findMatchingReleaseAssetUrl') - ->willReturn('https://api.github.com/repos/foo/bar/releases/assets/12345'); + ->method('findMatchingReleaseAsset') + ->willReturn(new MatchedReleaseAsset( + 'https://api.github.com/repos/foo/bar/releases/assets/12345', + 'php_foobar-1.2.3_php8.3-x86_64-linux-glibc-zts.tgz', + )); $this->container ->method('get') @@ -467,7 +477,7 @@ public function testDistUrlIsUpdatedForPrePackagedTgzBinaryWhenBinaryIsNotFound( $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); $packageReleaseAssets ->expects(self::once()) - ->method('findMatchingReleaseAssetUrl') + ->method('findMatchingReleaseAsset') ->willThrowException(new CouldNotFindReleaseAsset('nope not found')); $this->container @@ -585,8 +595,11 @@ public function testDistUrlIsUpdatedForWindowsInstallersOnUpdateOperations(): vo $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); $packageReleaseAssets ->expects(self::once()) - ->method('findMatchingReleaseAssetUrl') - ->willReturn('https://example.com/windows-download-url'); + ->method('findMatchingReleaseAsset') + ->willReturn(new MatchedReleaseAsset( + 'https://api.github.com/repos/foo/bar/releases/assets/12345', + 'php_foo-1.2.3-8.3-vc14-ts-x86.zip', + )); $this->container ->method('get') @@ -645,7 +658,7 @@ public function testNoSelectedDownloadUrlMethodWillThrowException(): void $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); $packageReleaseAssets ->expects(self::once()) - ->method('findMatchingReleaseAssetUrl') + ->method('findMatchingReleaseAsset') ->willThrowException(new CouldNotFindReleaseAsset('nope not found')); $this->container diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php index 87804a5d..2260075a 100644 --- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php @@ -84,7 +84,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void self::assertSame( 'actual_download_url', - $releaseAssets->findMatchingReleaseAssetUrl( + $releaseAssets->findMatchingReleaseAsset( $targetPlatform, $package, $httpDownloader, @@ -93,7 +93,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void $targetPlatform, $package, ), - ), + )->url, ); } @@ -151,7 +151,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrlWithCompilerAndThr self::assertSame( 'actual_download_url', - $releaseAssets->findMatchingReleaseAssetUrl( + $releaseAssets->findMatchingReleaseAsset( $targetPlatform, $package, $httpDownloader, @@ -160,7 +160,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrlWithCompilerAndThr $targetPlatform, $package, ), - ), + )->url, ); } @@ -198,7 +198,7 @@ public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotF $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation'); $this->expectException(CouldNotFindReleaseAsset::class); - $releaseAssets->findMatchingReleaseAssetUrl( + $releaseAssets->findMatchingReleaseAsset( $targetPlatform, $package, $httpDownloader, From 678b8be2b19509030342693e396772c13a5745b2 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 31 Aug 2026 08:38:31 +0100 Subject: [PATCH 4/4] 715: clean up integration test from MatchedReleaseAsset addition --- .../Downloading/GithubPackageReleaseAssetsTest.php | 8 ++++---- .../OverrideDownloadUrlInstallListenerTest.php | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php index 2d04ec80..a2153701 100644 --- a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php @@ -60,10 +60,10 @@ public function testDeterminingReleaseAssetUrlForWindows(): void $config = Factory::createConfig(); $io->loadConfiguration($config); - self::assertSame( - 'https://github.com/asgrim/example-pie-extension/releases/download/2.0.2/php_example_pie_extension-2.0.2-8.3-ts-vs16-x86_64.zip', + self::assertMatchesRegularExpression( + '~^https://api\.github\.com/repos/asgrim/example-pie-extension/releases/assets/\d+$~', (new GithubPackageReleaseAssets('https://api.github.com')) - ->findMatchingReleaseAssetUrl( + ->findMatchingReleaseAsset( $targetPlatform, $package, new HttpDownloader($io, $config), @@ -72,7 +72,7 @@ public function testDeterminingReleaseAssetUrlForWindows(): void $targetPlatform, $package, ), - ), + )->url, ); } } diff --git a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php index e7d5b89c..2e706841 100644 --- a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php +++ b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php @@ -286,7 +286,7 @@ public function testDistUrlIsUpdatedForWindowsInstallers(): void ->expects(self::once()) ->method('findMatchingReleaseAsset') ->willReturn(new MatchedReleaseAsset( - 'https://api.github.com/repos/foo/bar/releases/assets/12345', + 'https://api.github.com/repos/foo/bar/releases/assets/11111', 'php_foo-1.2.3-8.3-vc14-ts-x86.zip', )); @@ -319,7 +319,7 @@ public function testDistUrlIsUpdatedForWindowsInstallers(): void ))($installerEvent); self::assertSame( - 'https://example.com/windows-download-url', + 'https://api.github.com/repos/foo/bar/releases/assets/11111', $composerPackage->getDistUrl(), ); self::assertSame(DownloadUrlMethod::WindowsBinaryDownload, DownloadUrlMethod::fromComposerPackage($composerPackage)); @@ -350,7 +350,7 @@ public function testDistUrlIsUpdatedForPrePackagedTgzSource(): void ->expects(self::once()) ->method('findMatchingReleaseAsset') ->willReturn(new MatchedReleaseAsset( - 'https://api.github.com/repos/foo/bar/releases/assets/12345', + 'https://api.github.com/repos/foo/bar/releases/assets/22222', 'php_foobar-1.2.3-src.tgz', )); @@ -383,7 +383,7 @@ public function testDistUrlIsUpdatedForPrePackagedTgzSource(): void ))($installerEvent); self::assertSame( - 'https://example.com/pre-packaged-source-download-url.tgz', + 'https://api.github.com/repos/foo/bar/releases/assets/22222', $composerPackage->getDistUrl(), ); self::assertSame(DownloadUrlMethod::PrePackagedSourceDownload, DownloadUrlMethod::fromComposerPackage($composerPackage)); @@ -597,7 +597,7 @@ public function testDistUrlIsUpdatedForWindowsInstallersOnUpdateOperations(): vo ->expects(self::once()) ->method('findMatchingReleaseAsset') ->willReturn(new MatchedReleaseAsset( - 'https://api.github.com/repos/foo/bar/releases/assets/12345', + 'https://api.github.com/repos/foo/bar/releases/assets/33333', 'php_foo-1.2.3-8.3-vc14-ts-x86.zip', )); @@ -630,7 +630,7 @@ public function testDistUrlIsUpdatedForWindowsInstallersOnUpdateOperations(): vo ))($installerEvent); self::assertSame( - 'https://example.com/windows-download-url', + 'https://api.github.com/repos/foo/bar/releases/assets/33333', $targetPackage->getDistUrl(), ); self::assertSame(DownloadUrlMethod::WindowsBinaryDownload, DownloadUrlMethod::fromComposerPackage($targetPackage));