[PHP-WEBMASTER] [web-downloads] main: Allow publishing Winlibs artifacts for one architecture

Author: Shivam Mathur (shivammathur)
Date: 2026-10-13T19:22:22+05:30

Commit: https://github.com/php/web-downloads/commit/d8171aca08092c46104b9845388c24ef457b4798
Raw diff: https://github.com/php/web-downloads/commit/d8171aca08092c46104b9845388c24ef457b4798.diff

Allow publishing Winlibs artifacts for one architecture

Changed paths:
  M .github/workflows/winlibs.yml
  M API.md
  M src/Console/Command/WinlibsCommand.php
  M src/Http/Controllers/WinlibsController.php
  M tests/Console/Command/WinlibsCommandTest.php
  M tests/Http/Controllers/WinlibsControllerTest.php

Diff:

diff --git a/.github/workflows/winlibs.yml b/.github/workflows/winlibs.yml
index c10ccd3..39b0d48 100644
--- a/.github/workflows/winlibs.yml
+++ b/.github/workflows/winlibs.yml
@@ -35,6 +35,11 @@ on:
         options:
           - 'true'
           - 'false'
+ arch:
+ description: 'Artifact architecture to publish'
+ type: choice
+ default: all
+ options: [all, x64, x86, arm64]
jobs:
   print-inputs:
     runs-on: ubuntu-latest
@@ -53,14 +58,26 @@ jobs:
           echo "| vs_version_targets | ${{ inputs.vs_version_targets }} |" >> $GITHUB_STEP_SUMMARY
           echo "| stability | ${{ inputs.stability }} |" >> $GITHUB_STEP_SUMMARY
           echo "| update_series | ${{ inputs.update_series }} |" >> $GITHUB_STEP_SUMMARY
+ echo "| arch | ${{ inputs.arch }} |" >> $GITHUB_STEP_SUMMARY
   test:
     runs-on: ubuntu-latest
     environment: downloads.php.net
     steps:
+ - name: Check architecture filtering support
+ if: inputs.arch != 'all'
+ run: |
+ response=$(curl --silent --show-error --location --request POST \
+ --header 'Authorization: Bearer ${{ secrets.AUTH_TOKEN }}' \
+ --data '{ "arch": "unsupported" }' --write-out '\n%{http_code}' \
+ https://downloads.php.net/api/winlibs)
+ if [[ "$response" != *'The arch field must match'* || "$response" != *$'\n400' ]]; then
+ echo 'The downloads server does not yet support filtering Winlibs artifacts by architecture.'
+ exit 1
+ fi
       - name: Upload
         run: |
- curl \
+ curl --fail-with-body \
           --request POST \
           --location https://downloads.php.net/api/winlibs \
           --header 'Authorization: Bearer ${{ secrets.AUTH_TOKEN }}' \
- --data '{ "library": "${{ inputs.library }}", "ref": "${{ inputs.ref }}", "type": "${{ inputs.type }}", "workflow_run_id": "${{ inputs.workflow_run_id }}", "php_versions": "${{ inputs.php_versions }}", "vs_version_targets": "${{ inputs.vs_version_targets }}", "stability": "${{ inputs.stability }}", "token": "${{ secrets.TOKEN }}", "update_series": "${{ inputs.update_series }}" }'
+ --data '{ "library": "${{ inputs.library }}", "ref": "${{ inputs.ref }}", "type": "${{ inputs.type }}", "workflow_run_id": "${{ inputs.workflow_run_id }}", "php_versions": "${{ inputs.php_versions }}", "vs_version_targets": "${{ inputs.vs_version_targets }}", "stability": "${{ inputs.stability }}", "token": "${{ secrets.TOKEN }}", "update_series": "${{ inputs.update_series }}", "arch": "${{ inputs.arch }}" }'
diff --git a/API.md b/API.md
index 7c28d40..9376c69 100644
--- a/API.md
+++ b/API.md
@@ -159,6 +159,7 @@ curl -i -X POST \
     - `vs_version_targets` (string, required): Comma-separated list matching `^(v[c|s]\d{2})(,v[c|s]\d{2})*$`.
     - `stability` (string, required): `stable`, `staging`, or both (comma-separated) matching `^(stable|staging)(,(stable|staging))?$`.
     - `update_series` (string, required): `true` or `false`.
+ - `arch` (string, optional): `all` (default), `x64`, `x86`, or `arm64`. Only matching artifacts are published and their series entries updated; other architectures remain unchanged.
     - `token` (string, required): GitHub token used to download artifacts.
- Success: `200 OK`, empty body.
- Errors:
diff --git a/src/Console/Command/WinlibsCommand.php b/src/Console/Command/WinlibsCommand.php
index 3be1677..e0360e0 100644
--- a/src/Console/Command/WinlibsCommand.php
+++ b/src/Console/Command/WinlibsCommand.php
@@ -58,6 +58,10 @@ public function handle(): int
                     $data = json_decode((string) file_get_contents($directoryPath . '/data.json'), true, 512, JSON_THROW_ON_ERROR);
                     $files = glob($directoryPath . '/*.zip');
                     $files = $this->parseFiles($files);
+ $arch = $data['arch'] ?? 'all';
+ if ($arch !== 'all') {
+ $files = array_filter($files, static fn (array $file): bool => $file['arch'] === $arch);
+ }
                     if (empty($files)) {
                         throw new Exception('No valid files found in ' . basename($directoryPath));
                     }
diff --git a/src/Http/Controllers/WinlibsController.php b/src/Http/Controllers/WinlibsController.php
index 56334a5..e7b359e 100644
--- a/src/Http/Controllers/WinlibsController.php
+++ b/src/Http/Controllers/WinlibsController.php
@@ -20,10 +20,11 @@ protected function validate(array $data): bool
             'vs_version_targets' => 'required|string|regex:/^(v[c|s]\d{2})(,v[c|s]\d{2})*$/',
             'stability' => 'required|string|regex:/^(stable|staging)(,(stable|staging))?$/',
             'update_series' => 'required|string|regex:/^(true|false)$/',
+ 'arch' => 'required|string|regex:/\A(all|x64|x86|arm64)\z/',
             'token' => 'required|string',
         ]);

- $validator->validate($data);
+ $validator->validate($data + ['arch' => 'all']);

         $valid = $validator->isValid;

diff --git a/tests/Console/Command/WinlibsCommandTest.php b/tests/Console/Command/WinlibsCommandTest.php
index ebcdba7..a7308af 100644
--- a/tests/Console/Command/WinlibsCommandTest.php
+++ b/tests/Console/Command/WinlibsCommandTest.php
@@ -481,6 +481,74 @@ public function testSortsPhpSeriesFileAfterAddingLibrary(): void
         );
     }

+ #[DataProvider('architectureFilterProvider')]
+ public function testPublishesOnlySelectedArchitecture(string $type, string $arch): void
+ {
+ $job = $this->winlibsDirectory . '/lib';
+ $deps = $this->baseDirectory . ($type === 'php' ? '/php-sdk/deps' : '/pecl/deps');
+ mkdir($job, 0755, true);
+ mkdir($deps . '/series', 0755, true);
+ file_put_contents($job . '/data.json', json_encode([
+ 'type' => $type, 'library' => 'lib', 'ref' => '1.0',
+ 'vs_version_targets' => 'vs17', 'php_versions' => '8.4',
+ 'stability' => 'staging', 'update_series' => 'true', 'arch' => $arch,
+ ]));
+ foreach (['x64', 'x86', 'arm64'] as $artifactArch) {
+ $filename = "lib-1.0-vs17-$artifactArch.zip";
+ $destination = $type === 'php' ? "$deps/vs17/$artifactArch" : $deps;
+ if (!is_dir($destination)) {
+ mkdir($destination, 0755, true);
+ }
+ file_put_contents("$destination/$filename", 'published');
+ file_put_contents("$job/$filename", 'rebuilt');
+ file_put_contents("$deps/series/packages-8.4-vs17-$artifactArch-staging.txt", "lib-0.9-vs17-$artifactArch.zip");
+ }
+
+ $command = new WinlibsCommand();
+ $command->options = ['base-directory' => $this->baseDirectory, 'builds-directory' => $this->buildsDirectory];
+ $this->assertSame(0, $command->handle());
+
+ foreach (['x64', 'x86', 'arm64'] as $artifactArch) {
+ $selected = $arch === 'all' || $artifactArch === $arch;
+ $destination = $type === 'php' ? "$deps/vs17/$artifactArch" : $deps;
+ $this->assertStringEqualsFile("$destination/lib-1.0-vs17-$artifactArch.zip", $selected ? 'rebuilt' : 'published');
+ if ($type === 'php') {
+ $version = $selected ? '1.0' : '0.9';
+ $this->assertStringEqualsFile("$deps/series/packages-8.4-vs17-$artifactArch-staging.txt", "lib-$version-vs17-$artifactArch.zip");
+ }
+ }
+ $this->assertDirectoryDoesNotExist($job);
+ }
+
+ public static function architectureFilterProvider(): array
+ {
+ return [
+ ['php', 'arm64'], ['php', 'x64'], ['php', 'x86'], ['php', 'all'],
+ ['pecl', 'arm64'],
+ ];
+ }
+
+ public function testMissingSelectedArchitectureDoesNotPublishOtherArtifacts(): void
+ {
+ $job = $this->winlibsDirectory . '/lib';
+ mkdir($job, 0755, true);
+ file_put_contents($job . '/data.json', json_encode([
+ 'type' => 'php', 'library' => 'lib', 'ref' => '1.0',
+ 'vs_version_targets' => 'vs17', 'php_versions' => '8.4',
+ 'stability' => 'staging', 'update_series' => 'true', 'arch' => 'arm64',
+ ]));
+ file_put_contents($job . '/lib-1.0-vs17-x64.zip', 'rebuilt');
+ $command = new WinlibsCommand();
+ $command->options = ['base-directory' => $this->baseDirectory, 'builds-directory' => $this->buildsDirectory];
+ ob_start();
+ $result = $command->handle();
+ $output = ob_get_clean();
+ $this->assertSame(1, $result);
+ $this->assertStringContainsString('No valid files found', $output);
+ $this->assertDirectoryDoesNotExist($this->baseDirectory . '/php-sdk');
+ $this->assertFileExists($job . '/lib-1.0-vs17-x64.zip');
+ }
+
     public function testCommandHandlesMissingBaseDirectory(): void
     {
         $command = new WinlibsCommand();
diff --git a/tests/Http/Controllers/WinlibsControllerTest.php b/tests/Http/Controllers/WinlibsControllerTest.php
index f1ef347..5f70833 100644
--- a/tests/Http/Controllers/WinlibsControllerTest.php
+++ b/tests/Http/Controllers/WinlibsControllerTest.php
@@ -6,6 +6,7 @@
use App\Http\Controllers\WinlibsController;
use JsonException;
use Override;
+use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class MockWinlibsController extends WinlibsController {
@@ -31,6 +32,37 @@ public function handle(): void
}

class WinlibsControllerTest extends TestCase {
+ #[DataProvider('architectureProvider')]
+ public function testArchitectureValidation(array $override, bool $expected): void
+ {
+ $controller = new class extends WinlibsController {
+ public function validateRequest(array $data): bool { return parent::validate($data); }
+ };
+ $data = $override + [
+ 'library' => 'zlib', 'ref' => '1.3.2', 'type' => 'php',
+ 'workflow_run_id' => '123', 'php_versions' => '8.4',
+ 'vs_version_targets' => 'vs17', 'stability' => 'staging',
+ 'update_series' => 'true', 'token' => 'test',
+ ];
+ ob_start();
+ try {
+ $this->assertSame($expected, $controller->validateRequest($data));
+ } finally {
+ ob_end_clean();
+ http_response_code(200);
+ }
+ }
+
+ public static function architectureProvider(): array
+ {
+ return [
+ [[], true], [['arch' => 'all'], true], [['arch' => 'arm64'], true],
+ [['arch' => 'x64'], true], [['arch' => 'x86'], true],
+ [['arch' => ''], false], [['arch' => 'x64,arm64'], false],
+ [['arch' => "arm64\n"], false], [['arch' => null], false],
+ ];
+ }
+
     /**
      * @throws JsonException
      */