Author: Shivam Mathur (shivammathur)
Date: 2026-10-13T14:47:26+05:30

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

Add support to init arch series files

Changed paths:
  M  .github/workflows/series-init.yml
  M  API.md
  M  src/Console/Command/PhpCommand.php
  M  src/Console/Command/SeriesDeleteCommand.php
  M  src/Console/Command/SeriesInitCommand.php
  M  src/Console/Command/SeriesStabilityCommand.php
  M  src/Console/Command/SeriesUpdateCommand.php
  M  src/Http/Controllers/SeriesInitController.php
  M  tests/Console/Command/PhpCommandTest.php
  M  tests/Console/Command/SeriesDeleteCommandTest.php
  M  tests/Console/Command/SeriesInitCommandTest.php
  M  tests/Console/Command/SeriesStabilityCommandTest.php
  M  tests/Console/Command/SeriesUpdateCommandTest.php
  M  tests/Http/Controllers/SeriesInitControllerTest.php


Diff:

diff --git a/.github/workflows/series-init.yml 
b/.github/workflows/series-init.yml
index 371216a..f7fbb5f 100644
--- a/.github/workflows/series-init.yml
+++ b/.github/workflows/series-init.yml
@@ -1,10 +1,18 @@
-name: Create series files for new PHP version
-run-name: Series init ${{ inputs.php_version }}
+name: Create PHP or architecture series files
+run-name: Series init ${{ inputs.php_version }} (${{ inputs.subject }})
 on:
   workflow_dispatch:
     inputs:
+      subject:
+        description: 'Initialize a VS/PHP series or copy an architecture 
series'
+        type: choice
+        default: vs
+        required: true
+        options:
+          - vs
+          - arch
       php_version:
-        description: 'PHP Version'
+        description: 'PHP series (e.g. 8.4; master is also supported for 
architecture copies)'
         required: true
       source_vs:
         description: 'Source VS Version'
@@ -12,6 +20,22 @@ on:
       target_vs:
         description: 'Target VS Version'
         required: true
+      source_arch:
+        description: 'Source architecture (used when subject is arch)'
+        type: choice
+        default: x64
+        options:
+          - x86
+          - x64
+          - arm64
+      target_arch:
+        description: 'Target architecture (used when subject is arch)'
+        type: choice
+        default: arm64
+        options:
+          - x86
+          - x64
+          - arm64
 jobs:
   test:
     runs-on: ubuntu-latest
@@ -23,4 +47,4 @@ jobs:
           --request POST \
           --location https://downloads.php.net/api/series-init \
           --header 'Authorization: Bearer ${{ secrets.AUTH_TOKEN }}' \
-          --data '{ "php_version": "${{ inputs.php_version }}", "source_vs": 
"${{ inputs.source_vs }}", "target_vs": "${{ inputs.target_vs }}" }'
+          --data '{ "subject": "${{ inputs.subject }}", "php_version": "${{ 
inputs.php_version }}", "source_vs": "${{ inputs.source_vs }}", "target_vs": 
"${{ inputs.target_vs }}", "source_arch": "${{ inputs.source_arch }}", 
"target_arch": "${{ inputs.target_arch }}" }'
diff --git a/API.md b/API.md
index 8b72718..d9564fa 100644
--- a/API.md
+++ b/API.md
@@ -217,21 +217,30 @@ curl -i -X POST \
 ### POST /api/series-init
 
 - Auth: Required
-- Purpose: Initialize a series configuration by writing a JSON file into 
`BUILDS_DIRECTORY/series`.
+- Purpose: Queue initialization of a new PHP series or copy an existing PHP 
series to another architecture. The API writes a JSON job into 
`BUILDS_DIRECTORY/series`.
+- Processor: `php runner.php series:init --base-directory=<downloads-root> 
--builds-directory=<builds-root>`.
 - Request body (JSON):
-    - `php_version` (string, required): Matches `^\d+.\d+$`.
+    - `subject` (string, optional, default `vs`): `vs` uses the existing 
initialization path from master; `arch` copies an architecture within the 
selected PHP series.
+    - `php_version` (string, required): PHP series matching `^\d+\.\d+$`, such 
as `8.2`. With `subject=arch`, also accepts `master`.
     - `source_vs` (string, required): Matches `^v[c|s]\d{2}$`.
     - `target_vs` (string, required): Matches `^v[c|s]\d{2}$`.
-- Success: `200 OK`, empty body.
-- Errors: `400` with validation details if input is invalid.
+    - `source_arch` (string, required when `subject=arch`): One of `x86`, 
`x64`, or `arm64`. Ignored when `subject=vs`.
+    - `target_arch` (string, required when `subject=arch`): One of `x86`, 
`x64`, or `arm64`. Ignored when `subject=vs`.
+- Success: `200 OK`, empty body, meaning the initialization job was queued.
+- Errors: `400` with validation details if input is invalid. Missing source 
files and existing destinations are checked later by the processor, as 
described below.
 
-Example
+With `subject=vs` (or `subject` omitted), the processor follows the existing 
path. For each architecture, it copies 
`packages-master-<source_vs>-<arch>-<stability>.txt` to 
`packages-<php_version>-<target_vs>-<arch>-<stability>.txt` under 
`php-sdk/deps/series`, for both `stable` and `staging`. The x86/x64 source 
files are required; ARM64 files are copied when present and skipped 
individually when missing. Package entries are copied unchanged. An existing 
destination causes the job to fail.
+
+With `subject=arch`, the processor copies within the selected `php_version`, 
including `master`. It reads 
`packages-<php_version>-<source_vs>-<source_arch>-<stability>.txt` and creates 
`packages-<php_version>-<target_vs>-<target_arch>-<stability>.txt` for both 
`stable` and `staging`. Set `source_vs` and `target_vs` to the same value to 
change only the architecture. In each file, `-<source_arch>.zip` is replaced 
with `-<target_arch>.zip`; library names, versions, and VS labels in package 
entries are preserved. Existing target files are skipped, so repeated 
initialization preserves any subsequent updates. Missing source files for 
targets that need creation fail the job before any series files are written. 
Target ZIPs need not exist yet.
+
+Example (`subject=vs`: initialize a new PHP series from master)
 
 ```bash
 curl -i -X POST \
     -H "Authorization: Bearer $AUTH_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{
+            "subject": "vs",
             "php_version": "8.4",
             "source_vs": "vc15",
             "target_vs": "vs16"
@@ -239,6 +248,25 @@ curl -i -X POST \
     https://downloads.php.net/api/series-init
 ```
 
+Example (`subject=arch`: initialize ARM64 from the existing PHP 8.2 x64 series)
+
+```bash
+curl -i -X POST \
+    -H "Authorization: Bearer $AUTH_TOKEN" \
+    -H "Content-Type: application/json" \
+    -d '{
+            "subject": "arch",
+            "php_version": "8.2",
+            "source_vs": "vs16",
+            "target_vs": "vs16",
+            "source_arch": "x64",
+            "target_arch": "arm64"
+        }' \
+    https://downloads.php.net/api/series-init
+```
+
+This creates `packages-8.2-vs16-arm64-stable.txt` and 
`packages-8.2-vs16-arm64-staging.txt` from the corresponding x64 files. For 
example, `libffi-3.8.0-1-vs16-x64.zip` becomes `libffi-3.8.0-1-vs16-arm64.zip`. 
The source series files remain unchanged.
+
 ---
 
 ### POST /api/series-delete
diff --git a/src/Console/Command/PhpCommand.php 
b/src/Console/Command/PhpCommand.php
index a091373..da2da67 100644
--- a/src/Console/Command/PhpCommand.php
+++ b/src/Console/Command/PhpCommand.php
@@ -215,10 +215,13 @@ private function promoteSeriesFiles(string $phpVersion): 
void
         if (!is_dir($baseDirectory)) {
             mkdir($baseDirectory, 0755, true);
         }
-        foreach(['x86', 'x64'] as $arch) {
+        foreach(['x86', 'x64', 'arm64'] as $arch) {
             $sourceFile = $baseDirectory . '/packages-' . $phpVersion . '-' . 
$vsVersion . '-' . $arch . '-staging.txt';
             $destinationFile = $baseDirectory . '/packages-' . $phpVersion . 
'-' . $vsVersion . '-' . $arch . '-stable.txt';
             if(!file_exists($sourceFile)) {
+                if ($arch === 'arm64') {
+                    continue;
+                }
                 throw new Exception($sourceFile . ' does not exist');
             }
             copy($sourceFile, $destinationFile);
diff --git a/src/Console/Command/SeriesDeleteCommand.php 
b/src/Console/Command/SeriesDeleteCommand.php
index 9a2c8dd..9375fa6 100644
--- a/src/Console/Command/SeriesDeleteCommand.php
+++ b/src/Console/Command/SeriesDeleteCommand.php
@@ -73,7 +73,7 @@ private function deleteSeriesFiles(
         if (!is_dir($baseDirectory)) {
             mkdir($baseDirectory, 0755, true);
         }
-        foreach(['x86', 'x64'] as $arch) {
+        foreach(['x86', 'x64', 'arm64'] as $arch) {
             foreach(['stable', 'staging'] as $stability) {
                 $filePath = $baseDirectory . '/packages-' . $php_version . '-' 
. $vs_version . '-' . $arch . '-' . $stability . '.txt';
                 if(file_exists($filePath)) {
diff --git a/src/Console/Command/SeriesInitCommand.php 
b/src/Console/Command/SeriesInitCommand.php
index 00ea201..4a45e28 100644
--- a/src/Console/Command/SeriesInitCommand.php
+++ b/src/Console/Command/SeriesInitCommand.php
@@ -46,11 +46,24 @@ public function handle(): int
 
             foreach ($filteredFiles as $filepath) {
                 $data = json_decode(file_get_contents($filepath), true, 512, 
JSON_THROW_ON_ERROR);
-                $this->initSeriesFiles(
-                    $data['php_version'],
-                    $data['source_vs'],
-                    $data['target_vs']
-                );
+                $subject = $data['subject'] ?? 'vs';
+                if ($subject === 'vs') {
+                    $this->initSeriesFiles(
+                        $data['php_version'],
+                        $data['source_vs'],
+                        $data['target_vs']
+                    );
+                } elseif ($subject === 'arch') {
+                    $this->initArchitectureSeriesFiles(
+                        $data['php_version'],
+                        $data['source_vs'],
+                        $data['target_vs'],
+                        $data['source_arch'] ?? '',
+                        $data['target_arch'] ?? ''
+                    );
+                } else {
+                    throw new Exception('Subject must be vs or arch');
+                }
                 unlink($filepath);
                 unlink($filepath . '.lock');
             }
@@ -61,6 +74,46 @@ public function handle(): int
         }
     }
 
+    /**
+     * @throws Exception
+     */
+    private function initArchitectureSeriesFiles(
+        string $phpVersion,
+        string $sourceVs,
+        string $targetVs,
+        string $sourceArch,
+        string $targetArch
+    ): void {
+        $architectures = ['x86', 'x64', 'arm64'];
+        if (!in_array($sourceArch, $architectures, true) || 
!in_array($targetArch, $architectures, true)) {
+            throw new Exception('Source and target architectures must both be 
x86, x64, or arm64');
+        }
+
+        $seriesDirectory = $this->baseDirectory . '/php-sdk/deps/series';
+        $files = [];
+        foreach (['stable', 'staging'] as $stability) {
+            $sourceFile = 
"$seriesDirectory/packages-$phpVersion-$sourceVs-$sourceArch-$stability.txt";
+            $targetFile = 
"$seriesDirectory/packages-$phpVersion-$targetVs-$targetArch-$stability.txt";
+            if (file_exists($targetFile)) {
+                continue;
+            }
+            if (!is_file($sourceFile)) {
+                throw new Exception("$sourceFile not found");
+            }
+            $contents = file_get_contents($sourceFile);
+            if ($contents === false) {
+                throw new Exception("Unable to read $sourceFile");
+            }
+            $files[$targetFile] = str_replace("-$sourceArch.zip", 
"-$targetArch.zip", $contents);
+        }
+
+        foreach ($files as $targetFile => $contents) {
+            if (file_put_contents($targetFile, $contents) === false) {
+                throw new Exception("Unable to write $targetFile");
+            }
+        }
+    }
+
     /**
      * @throws Exception
      */
@@ -75,10 +128,13 @@ private function initSeriesFiles(
         if (!is_dir($baseDirectory)) {
             mkdir($baseDirectory, 0755, true);
         }
-        foreach(['x86', 'x64'] as $arch) {
+        foreach(['x86', 'x64', 'arm64'] as $arch) {
             foreach(['stable', 'staging'] as $stability) {
                 $sourceSeries = 'packages-master-' . $source_vs . '-' . $arch 
. '-' . $stability . '.txt';
                 if(!file_exists($baseDirectory . '/' . $sourceSeries)) {
+                    if ($arch === 'arm64') {
+                        continue;
+                    }
                     throw new Exception("$baseDirectory/$sourceSeries not 
found");
                 }
                 $destinationFileName = 'packages-' . $php_version . '-' . 
$target_vs . '-' . $arch . '-' . $stability . '.txt';
diff --git a/src/Console/Command/SeriesStabilityCommand.php 
b/src/Console/Command/SeriesStabilityCommand.php
index 10fe913..bbd4eeb 100644
--- a/src/Console/Command/SeriesStabilityCommand.php
+++ b/src/Console/Command/SeriesStabilityCommand.php
@@ -73,10 +73,13 @@ private function promoteSeriesFiles(
         if (!is_dir($baseDirectory)) {
             mkdir($baseDirectory, 0755, true);
         }
-        foreach(['x86', 'x64'] as $arch) {
+        foreach(['x86', 'x64', 'arm64'] as $arch) {
             $sourceFile = $baseDirectory . '/packages-' . $php_version . '-' . 
$vs_version . '-' . $arch . '-staging.txt';
             $destinationFile = $baseDirectory . '/packages-' . $php_version . 
'-' . $vs_version . '-' . $arch . '-stable.txt';
             if(!file_exists($sourceFile)) {
+                if ($arch === 'arm64') {
+                    continue;
+                }
                 throw new Exception($sourceFile . ' does not exist');
             }
             copy($sourceFile, $destinationFile);
diff --git a/src/Console/Command/SeriesUpdateCommand.php 
b/src/Console/Command/SeriesUpdateCommand.php
index b5f4373..f52b7f9 100644
--- a/src/Console/Command/SeriesUpdateCommand.php
+++ b/src/Console/Command/SeriesUpdateCommand.php
@@ -94,19 +94,24 @@ private function updateSeriesFiles(
             mkdir($seriesDirectory, 0755, true);
         }
 
-        $arches = ['x86', 'x64'];
+        $arches = ['x86', 'x64', 'arm64'];
 
         foreach ($arches as $arch) {
             $filePath = $seriesDirectory . 
"/packages-$phpVersion-$vsVersion-$arch-$stability.txt";
+            $refValue = trim($ref);
+            $package = $refValue === '' ? null : sprintf('%s-%s-%s-%s.zip', 
$library, $refValue, $vsVersion, $arch);
+
+            // ARM64 is populated incrementally; preserve existing entries 
until the requested build is uploaded.
+            if ($arch === 'arm64' && $package !== null
+                && (!is_file($filePath) || !is_file($this->baseDirectory . 
"/php-sdk/deps/$vsVersion/$arch/$package"))) {
+                continue;
+            }
 
             $lines = [];
             if (file_exists($filePath)) {
                 $lines = file($filePath, FILE_IGNORE_NEW_LINES);
             }
 
-            $refValue = trim($ref);
-            $package = $refValue === '' ? null : sprintf('%s-%s-%s-%s.zip', 
$library, $refValue, $vsVersion, $arch);
-
             $replaced = false;
             foreach ($lines as $index => $line) {
                 if (str_starts_with($line, $library . '-')) {
diff --git a/src/Http/Controllers/SeriesInitController.php 
b/src/Http/Controllers/SeriesInitController.php
index 63ca1cd..2373b22 100644
--- a/src/Http/Controllers/SeriesInitController.php
+++ b/src/Http/Controllers/SeriesInitController.php
@@ -10,11 +10,20 @@ class SeriesInitController extends BaseController
 {
     protected function validate(array $data): bool
     {
-        $validator = new Validator([
-            'php_version' => 'required|string|regex:/^\d+\.\d+$/',
+        $data += ['subject' => 'vs'];
+        $rules = [
+            'subject' => 'required|string|regex:/\A(?:vs|arch)\z/',
+            'php_version' => $data['subject'] === 'arch'
+                ? 'required|string|regex:/\A(?:\d+\.\d+|master)\z/'
+                : 'required|string|regex:/^\d+\.\d+$/',
             'source_vs' => 'required|string|regex:/^v[c|s]\d{2}$/',
             'target_vs' => 'required|string|regex:/^v[c|s]\d{2}$/',
-        ]);
+        ];
+        if ($data['subject'] === 'arch') {
+            $rules['source_arch'] = 
'required|string|regex:/\A(?:x86|x64|arm64)\z/';
+            $rules['target_arch'] = 
'required|string|regex:/\A(?:x86|x64|arm64)\z/';
+        }
+        $validator = new Validator($rules);
 
         $validator->validate($data);
 
diff --git a/tests/Console/Command/PhpCommandTest.php 
b/tests/Console/Command/PhpCommandTest.php
index b731efb..d0b490b 100644
--- a/tests/Console/Command/PhpCommandTest.php
+++ b/tests/Console/Command/PhpCommandTest.php
@@ -45,7 +45,7 @@ protected function tearDown(): void
 
     public static function buildsProvider(): array
     {
-        return [
+        $builds = [
             [[
                 'php-8.4.1-Win32-vs17-x64.zip',
                 'php-8.4.1-Win32-vs17-x64.zip.cdx.json',
@@ -83,6 +83,13 @@ public static function buildsProvider(): array
                 'php-test-pack-8.4.0-dev.zip',
             ]]
         ];
+        foreach ($builds as [$files]) {
+            $builds[] = [array_map(
+                static fn (string $file): string => str_replace('-x64', 
'-arm64', $file),
+                array_filter($files, static fn (string $file): bool => 
!str_contains($file, '-x86'))
+            )];
+        }
+        return $builds;
     }
 
     private function stageBuilds(array $phpZips, $zipPath): void
@@ -130,6 +137,21 @@ public function 
testCommandHandlesSuccessfulExecution(array $phpZips): void
         $this->assertStringNotContainsString('.json', 
file_get_contents($expectedDestination . '/sha256sum.txt'));
         $this->assertStringNotContainsString('.json', 
file_get_contents($expectedDestination . '/sha1sum.txt'));
         $this->assertStringNotContainsString('.json', 
file_get_contents($expectedDestination . '/releases.json'));
+        if (str_contains($phpZips[0], '-arm64')) {
+            $releases = json_decode(file_get_contents($expectedDestination . 
'/releases.json'), true);
+            foreach (['ts', 'nts'] as $threadSafety) {
+                $build = $releases['8.4']["$threadSafety-vs17-arm64"];
+                foreach (['zip', 'debug_pack', 'devel_pack'] as $type) {
+                    $path = $build[$type]['path'];
+                    $this->assertSame(hash_file('sha256', 
"$expectedDestination/$path"), $build[$type]['sha256']);
+                    $this->assertFileEquals("$expectedDestination/$path", 
"$expectedDestination/archives/$path");
+                }
+                if ($expectedDestination === $this->baseDirectory . 
'/releases') {
+                    $latest = str_replace([$releases['8.4']['version'], 
'.zip'], ['8.4', '-latest.zip'], $build['zip']['path']);
+                    $this->assertFileEquals("$expectedDestination/" . 
$build['zip']['path'], "$expectedDestination/latest/$latest");
+                }
+            }
+        }
     }
 
     public function testCommandHandlerWithMissingTestPackZip(): void
@@ -176,8 +198,27 @@ public function testCleanupAfterCommand(): void
         $this->assertEmpty(glob($tempDirectory));
     }
 
-    public function testStableReleasePromotesSeriesFiles(): void
+    public static function seriesPromotionProvider(): array
+    {
+        return [
+            'no ARM64 files' => [null, null],
+            'ARM64 staging only' => 
["libffi-3.8.0-1-vs17-arm64.zip\nzlib-1.3.2-vs17-x64.zip", null],
+            'ARM64 stable only' => [null, 'existing-arm64-stable'],
+            'both ARM64 files' => ['new-arm64-staging', 
'existing-arm64-stable'],
+        ];
+    }
+
+    #[DataProvider('seriesPromotionProvider')]
+    public function testStableReleasePromotesSeriesFiles(?string 
$arm64Staging, ?string $arm64Stable): void
     {
+        $seriesBase = $this->baseDirectory . '/php-sdk/deps/series';
+        $dstArm64 = "$seriesBase/packages-8.4-vs17-arm64-stable.txt";
+        if ($arm64Staging !== null) {
+            
file_put_contents("$seriesBase/packages-8.4-vs17-arm64-staging.txt", 
$arm64Staging);
+        }
+        if ($arm64Stable !== null) {
+            file_put_contents($dstArm64, $arm64Stable);
+        }
         $command = new PhpCommand(new GetListing(), new UpdateReleasesJson());
         $command->options = ['base-directory' => $this->baseDirectory, 
'builds-directory' => $this->buildsDirectory];
 
@@ -190,10 +231,16 @@ public function testStableReleasePromotesSeriesFiles(): 
void
         $this->assertFileExists($this->baseDirectory . 
'/php-sdk/deps/series/packages-8.4-vs17-x64-stable.txt');
         $this->assertSame('x86-staging', 
file_get_contents($this->baseDirectory . 
'/php-sdk/deps/series/packages-8.4-vs17-x86-stable.txt'));
         $this->assertSame('x64-staging', 
file_get_contents($this->baseDirectory . 
'/php-sdk/deps/series/packages-8.4-vs17-x64-stable.txt'));
+        if ($arm64Staging !== null || $arm64Stable !== null) {
+            $this->assertStringEqualsFile($dstArm64, $arm64Staging ?? 
$arm64Stable);
+        } else {
+            $this->assertFileDoesNotExist($dstArm64);
+        }
     }
 
     public function testQaReleaseDoesNotPromoteSeriesFiles(): void
     {
+        file_put_contents($this->baseDirectory . 
'/php-sdk/deps/series/packages-8.4-vs17-arm64-staging.txt', 'arm64-staging');
         $command = new PhpCommand(new GetListing(), new UpdateReleasesJson());
         $command->options = ['base-directory' => $this->baseDirectory, 
'builds-directory' => $this->buildsDirectory];
 
@@ -204,5 +251,6 @@ public function testQaReleaseDoesNotPromoteSeriesFiles(): 
void
         $this->assertSame(0, $result);
         $this->assertFileDoesNotExist($this->baseDirectory . 
'/php-sdk/deps/series/packages-8.4-vs17-x86-stable.txt');
         $this->assertFileDoesNotExist($this->baseDirectory . 
'/php-sdk/deps/series/packages-8.4-vs17-x64-stable.txt');
+        $this->assertFileDoesNotExist($this->baseDirectory . 
'/php-sdk/deps/series/packages-8.4-vs17-arm64-stable.txt');
     }
 }
diff --git a/tests/Console/Command/SeriesDeleteCommandTest.php 
b/tests/Console/Command/SeriesDeleteCommandTest.php
index 65246b7..9ce469d 100644
--- a/tests/Console/Command/SeriesDeleteCommandTest.php
+++ b/tests/Console/Command/SeriesDeleteCommandTest.php
@@ -88,6 +88,8 @@ public function 
testDeletesAllSeriesFilesAndCleansUpTask(string $phpVersion, str
             "$seriesBase/packages-$phpVersion-$vsVersion-x86-staging.txt",
             "$seriesBase/packages-$phpVersion-$vsVersion-x64-stable.txt",
             "$seriesBase/packages-$phpVersion-$vsVersion-x64-staging.txt",
+            "$seriesBase/packages-$phpVersion-$vsVersion-arm64-stable.txt",
+            "$seriesBase/packages-$phpVersion-$vsVersion-arm64-staging.txt",
         ];
         foreach ($paths as $p) {
             file_put_contents($p, "dummy");
@@ -133,6 +135,8 @@ public function testIdempotentWhenSomeFilesMissing(string 
$phpVersion, string $v
         $missing = [
             "$seriesBase/packages-$phpVersion-$vsVersion-x86-staging.txt",
             "$seriesBase/packages-$phpVersion-$vsVersion-x64-stable.txt",
+            "$seriesBase/packages-$phpVersion-$vsVersion-arm64-stable.txt",
+            "$seriesBase/packages-$phpVersion-$vsVersion-arm64-staging.txt",
         ];
 
         $seriesDir = $this->buildsDirectory . '/series';
diff --git a/tests/Console/Command/SeriesInitCommandTest.php 
b/tests/Console/Command/SeriesInitCommandTest.php
index 1f7199a..70d1ff6 100644
--- a/tests/Console/Command/SeriesInitCommandTest.php
+++ b/tests/Console/Command/SeriesInitCommandTest.php
@@ -36,6 +36,9 @@ public static function caseProvider(): array
             ['8.3', 'vs17', 'vs17'],
             ['8.2', 'vs16', 'vs16'],
             ['8.7', 'vs18', 'vs18'],
+            ['8.7', 'vs18', 'vs18', ['stable', 'staging']],
+            ['8.7', 'vs18', 'vs18', ['stable']],
+            ['8.7', 'vs18', 'vs18', ['staging']],
         ];
     }
 
@@ -79,7 +82,7 @@ public function testMissingBuildsDirectory(): void
     }
 
     #[DataProvider('caseProvider')]
-    public function testProcessesJsonCopiesSeriesFilesAndCleansUp(string 
$series, string $series_vs, string $target_vs): void
+    public function testProcessesJsonCopiesSeriesFilesAndCleansUp(string 
$series, string $series_vs, string $target_vs, array $arm64Stabilities = []): 
void
     {
         $seriesBase = $this->baseDirectory . '/php-sdk/deps/series';
         mkdir($seriesBase, 0755, true);
@@ -90,6 +93,9 @@ public function 
testProcessesJsonCopiesSeriesFilesAndCleansUp(string $series, st
             "packages-master-$series_vs-x64-stable.txt",
             "packages-master-$series_vs-x64-staging.txt",
         ];
+        foreach ($arm64Stabilities as $stability) {
+            $sourceNames[] = "packages-master-$series_vs-arm64-$stability.txt";
+        }
         foreach ($sourceNames as $name) {
             file_put_contents($seriesBase . '/' . $name, "content-of-$name");
         }
@@ -115,6 +121,9 @@ public function 
testProcessesJsonCopiesSeriesFilesAndCleansUp(string $series, st
             "packages-$series-$target_vs-x64-stable.txt",
             "packages-$series-$target_vs-x64-staging.txt",
         ];
+        foreach ($arm64Stabilities as $stability) {
+            $destinations[] = 
"packages-$series-$target_vs-arm64-$stability.txt";
+        }
 
         foreach ($destinations as $dest) {
             $destPath = $seriesBase . '/' . $dest;
@@ -129,6 +138,9 @@ public function 
testProcessesJsonCopiesSeriesFilesAndCleansUp(string $series, st
 
         $this->assertFileDoesNotExist($jsonPath);
         $this->assertFileDoesNotExist($jsonPath . '.lock');
+        foreach (array_diff(['stable', 'staging'], $arm64Stabilities) as 
$stability) {
+            
$this->assertFileDoesNotExist("$seriesBase/packages-$series-$target_vs-arm64-$stability.txt");
+        }
     }
 
     public function testSkipsLockedJsonFile(): void
@@ -173,6 +185,109 @@ public function testSkipsLockedJsonFile(): void
         $this->assertFileDoesNotExist($seriesBase . 
"/packages-$series-$target_vs-x64-staging.txt");
     }
 
+    public static function architectureProvider(): array
+    {
+        return [
+            'x64 to ARM64' => ['8.2', 'vs16', 'vs16', 'x64', 'arm64', []],
+            'master' => ['master', 'vs18', 'vs18', 'x64', 'arm64', []],
+            'different VS target' => ['8.4', 'vs16', 'vs17', 'x64', 'arm64', 
[]],
+            'x86 source' => ['8.2', 'vs16', 'vs16', 'x86', 'arm64', []],
+            'preserve stable' => ['8.2', 'vs16', 'vs16', 'x64', 'arm64', 
['stable']],
+            'preserve staging' => ['8.2', 'vs16', 'vs16', 'x64', 'arm64', 
['staging']],
+            'preserve both' => ['8.2', 'vs16', 'vs16', 'x64', 'arm64', 
['stable', 'staging']],
+        ];
+    }
+
+    #[DataProvider('architectureProvider')]
+    public function testInitializesArchitectureWithinSelectedSeries(
+        string $phpVersion,
+        string $sourceVs,
+        string $targetVs,
+        string $sourceArch,
+        string $targetArch,
+        array $existingStabilities
+    ): void {
+        $seriesDirectory = $this->baseDirectory . '/php-sdk/deps/series';
+        mkdir($seriesDirectory, 0755, true);
+        $sourceContents = 
"libffi-3.8.0-1-$sourceVs-$sourceArch.zip\r\nx64-helper-1.0-$sourceVs-$sourceArch.zip\r\n";
+        $expectedContents = 
"libffi-3.8.0-1-$sourceVs-$targetArch.zip\r\nx64-helper-1.0-$sourceVs-$targetArch.zip\r\n";
+        foreach (['stable', 'staging'] as $stability) {
+            
file_put_contents("$seriesDirectory/packages-$phpVersion-$sourceVs-$sourceArch-$stability.txt",
 $sourceContents);
+            if (in_array($stability, $existingStabilities, true)) {
+                
file_put_contents("$seriesDirectory/packages-$phpVersion-$targetVs-$targetArch-$stability.txt",
 "existing-$stability");
+            }
+        }
+        $task = [
+            'subject' => 'arch',
+            'php_version' => $phpVersion,
+            'source_vs' => $sourceVs,
+            'target_vs' => $targetVs,
+            'source_arch' => $sourceArch,
+            'target_arch' => $targetArch,
+        ];
+        $jsonPath = $this->createTask($task);
+        $command = new SeriesInitCommand();
+        $command->options = ['base-directory' => $this->baseDirectory, 
'builds-directory' => $this->buildsDirectory];
+
+        $this->assertSame(0, $command->handle());
+
+        foreach (['stable', 'staging'] as $stability) {
+            $expected = in_array($stability, $existingStabilities, true) ? 
"existing-$stability" : $expectedContents;
+            
$this->assertStringEqualsFile("$seriesDirectory/packages-$phpVersion-$targetVs-$targetArch-$stability.txt",
 $expected);
+            
$this->assertStringEqualsFile("$seriesDirectory/packages-$phpVersion-$sourceVs-$sourceArch-$stability.txt",
 $sourceContents);
+        }
+        $this->assertFileDoesNotExist($jsonPath);
+        $this->assertFileDoesNotExist($jsonPath . '.lock');
+
+        // Repeating initialization must preserve series that have since been 
updated.
+        $targetStable = 
"$seriesDirectory/packages-$phpVersion-$targetVs-$targetArch-stable.txt";
+        file_put_contents($targetStable, 'updated-after-init');
+        $jsonPath = $this->createTask($task);
+        $this->assertSame(0, $command->handle());
+        $this->assertStringEqualsFile($targetStable, 'updated-after-init');
+        $this->assertFileDoesNotExist($jsonPath);
+        $this->assertFileDoesNotExist($jsonPath . '.lock');
+    }
+
+    public function testMissingArchitectureSourceDoesNotCreatePartialSeries(): 
void
+    {
+        $seriesDirectory = $this->baseDirectory . '/php-sdk/deps/series';
+        mkdir($seriesDirectory, 0755, true);
+        file_put_contents("$seriesDirectory/packages-8.2-vs16-x64-stable.txt", 
'libffi-3.8.0-1-vs16-x64.zip');
+        $jsonPath = $this->createTask([
+            'subject' => 'arch',
+            'php_version' => '8.2',
+            'source_vs' => 'vs16',
+            'target_vs' => 'vs16',
+            'source_arch' => 'x64',
+            'target_arch' => 'arm64',
+        ]);
+        $command = new SeriesInitCommand();
+        $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('packages-8.2-vs16-x64-staging.txt 
not found', $output);
+        
$this->assertFileDoesNotExist("$seriesDirectory/packages-8.2-vs16-arm64-stable.txt");
+        
$this->assertFileDoesNotExist("$seriesDirectory/packages-8.2-vs16-arm64-staging.txt");
+        $this->assertFileExists($jsonPath);
+        $this->assertFileExists($jsonPath . '.lock');
+    }
+
+    private function createTask(array $data): string
+    {
+        $seriesDirectory = $this->buildsDirectory . '/series';
+        if (!is_dir($seriesDirectory)) {
+            mkdir($seriesDirectory, 0755, true);
+        }
+        $file = $seriesDirectory . '/series-init-' . uniqid() . '.json';
+        file_put_contents($file, json_encode($data));
+        return $file;
+    }
+
     public function testHandlesCorruptJson(): void
     {
         $seriesDir = $this->buildsDirectory . '/series';
diff --git a/tests/Console/Command/SeriesStabilityCommandTest.php 
b/tests/Console/Command/SeriesStabilityCommandTest.php
index ad51f9a..3e4f2e7 100644
--- a/tests/Console/Command/SeriesStabilityCommandTest.php
+++ b/tests/Console/Command/SeriesStabilityCommandTest.php
@@ -35,6 +35,9 @@ public static function versionProvider(): array
         return [
             ['8.3', 'vs17'],
             ['8.2', 'vs16'],
+            ['8.4', 'vs17', 
"libffi-3.8.0-1-vs17-arm64.zip\nzlib-1.3.2-vs17-x64.zip"],
+            ['8.4', 'vs17', null, 'existing-arm64-stable'],
+            ['8.4', 'vs17', 'new-arm64-staging', 'existing-arm64-stable'],
         ];
     }
 
@@ -78,7 +81,7 @@ public function testMissingBuildsDirectory(): void
     }
 
     #[DataProvider('versionProvider')]
-    public function testPromotesStagingToStableAndCleansTask(string 
$phpVersion, string $vsVersion): void
+    public function testPromotesStagingToStableAndCleansTask(string 
$phpVersion, string $vsVersion, ?string $arm64Staging = null, ?string 
$arm64Stable = null): void
     {
         $seriesBase = $this->baseDirectory . '/php-sdk/deps/series';
         mkdir($seriesBase, 0755, true);
@@ -87,6 +90,14 @@ public function 
testPromotesStagingToStableAndCleansTask(string $phpVersion, str
         $srcX64 = 
"$seriesBase/packages-$phpVersion-$vsVersion-x64-staging.txt";
         file_put_contents($srcX86, "x86-content");
         file_put_contents($srcX64, "x64-content");
+        $srcArm64 = 
"$seriesBase/packages-$phpVersion-$vsVersion-arm64-staging.txt";
+        $dstArm64 = 
"$seriesBase/packages-$phpVersion-$vsVersion-arm64-stable.txt";
+        if ($arm64Staging !== null) {
+            file_put_contents($srcArm64, $arm64Staging);
+        }
+        if ($arm64Stable !== null) {
+            file_put_contents($dstArm64, $arm64Stable);
+        }
 
         $seriesDir = $this->buildsDirectory . '/series';
         mkdir($seriesDir, 0755, true);
@@ -108,6 +119,11 @@ public function 
testPromotesStagingToStableAndCleansTask(string $phpVersion, str
         $this->assertFileExists($dstX64);
         $this->assertSame("x86-content", file_get_contents($dstX86));
         $this->assertSame("x64-content", file_get_contents($dstX64));
+        if ($arm64Staging !== null || $arm64Stable !== null) {
+            $this->assertStringEqualsFile($dstArm64, $arm64Staging ?? 
$arm64Stable);
+        } else {
+            $this->assertFileDoesNotExist($dstArm64);
+        }
 
         $this->assertFileDoesNotExist($jsonPath);
         $this->assertFileDoesNotExist($jsonPath . '.lock');
diff --git a/tests/Console/Command/SeriesUpdateCommandTest.php 
b/tests/Console/Command/SeriesUpdateCommandTest.php
index fb7d532..d25208f 100644
--- a/tests/Console/Command/SeriesUpdateCommandTest.php
+++ b/tests/Console/Command/SeriesUpdateCommandTest.php
@@ -5,6 +5,7 @@
 
 use App\Console\Command\SeriesUpdateCommand;
 use App\Helpers\Helpers;
+use PHPUnit\Framework\Attributes\DataProvider;
 use PHPUnit\Framework\TestCase;
 
 class SeriesUpdateCommandTest extends TestCase
@@ -244,6 +245,114 @@ public function testCreatesSeriesFileWhenMissing(): void
         $this->assertSame([
             'libpq-16.0.0-vs17-x86.zip',
         ], file($filePathX86, FILE_IGNORE_NEW_LINES));
+        $this->assertFileDoesNotExist($this->baseDirectory . 
'/php-sdk/deps/series/packages-8.3-vs17-arm64-staging.txt');
+    }
+
+    public static function arm64UpdateProvider(): array
+    {
+        return [
+            'replace x64 placeholder' => [true, 'libffi-3.7.0-vs16-x64.zip', 
true],
+            'replace old ARM64 build' => [true, 'libffi-3.7.0-vs16-arm64.zip', 
true],
+            'add available ARM64 library' => [true, null, true],
+            'keep x64 placeholder until upload' => [true, 
'libffi-3.7.0-vs16-x64.zip', false],
+            'keep previous ARM64 build until upload' => [true, 
'libffi-3.7.0-vs16-arm64.zip', false],
+            'skip unavailable ARM64 library' => [true, null, false],
+            'skip missing ARM64 series' => [false, null, true],
+            'skip absent ARM64 support' => [false, null, false],
+        ];
+    }
+
+    #[DataProvider('arm64UpdateProvider')]
+    public function 
testUpdatesArm64OnlyWhenSeriesAndRequestedArtifactExist(bool $hasSeries, 
?string $existingLibrary, bool $hasArtifact): void
+    {
+        $seriesBase = $this->baseDirectory . '/php-sdk/deps/series';
+        $artifactBase = $this->baseDirectory . '/php-sdk/deps/vs16/arm64';
+        mkdir($seriesBase, 0755, true);
+        mkdir($artifactBase, 0755, true);
+        $seriesFile = "$seriesBase/packages-8.2-vs16-arm64-staging.txt";
+        $package = 'libffi-3.8.0-1-vs16-arm64.zip';
+        $lines = ['zlib-1.3.2-vs16-x64.zip'];
+        if ($existingLibrary !== null) {
+            array_unshift($lines, $existingLibrary);
+        }
+        if ($hasSeries) {
+            file_put_contents($seriesFile, implode("\n", $lines));
+        }
+        // An older artifact must not satisfy the check for the requested 
version.
+        file_put_contents("$artifactBase/libffi-3.7.0-vs16-arm64.zip", 'old 
artifact');
+        if ($hasArtifact) {
+            file_put_contents("$artifactBase/$package", 'new artifact');
+        }
+        $taskFile = $this->createTask([
+            'php_version' => '8.2',
+            'vs_version' => 'vs16',
+            'stability' => 'staging',
+            'library' => 'libffi',
+            'ref' => '3.8.0-1',
+        ]);
+
+        $command = new SeriesUpdateCommand();
+        $command->options = ['base-directory' => $this->baseDirectory, 
'builds-directory' => $this->buildsDirectory];
+        $this->assertSame(0, $command->handle());
+
+        if ($hasSeries) {
+            $expected = $hasArtifact ? [$package, 'zlib-1.3.2-vs16-x64.zip'] : 
$lines;
+            $this->assertSame($expected, file($seriesFile, 
FILE_IGNORE_NEW_LINES));
+        } else {
+            $this->assertFileDoesNotExist($seriesFile);
+        }
+        foreach (['x86', 'x64'] as $arch) {
+            
$this->assertStringEqualsFile("$seriesBase/packages-8.2-vs16-$arch-staging.txt",
 "libffi-3.8.0-1-vs16-$arch.zip");
+        }
+        $this->assertFileDoesNotExist($taskFile);
+        $this->assertFileDoesNotExist($taskFile . '.lock');
+    }
+
+    public static function arm64RemovalProvider(): array
+    {
+        return [
+            'mixed series' => [["libffi-3.8.0-1-vs16-arm64.zip", 
"zlib-1.3.2-vs16-x64.zip"], ['zlib-1.3.2-vs16-x64.zip']],
+            'x64 placeholder' => [['libffi-3.7.0-vs16-x64.zip'], null],
+            'missing series' => [null, null],
+        ];
+    }
+
+    #[DataProvider('arm64RemovalProvider')]
+    public function testRemovesArm64LibraryAndArtifacts(?array $lines, ?array 
$remaining): void
+    {
+        $seriesBase = $this->baseDirectory . '/php-sdk/deps/series';
+        $artifactBase = $this->baseDirectory . '/php-sdk/deps/vs16/arm64';
+        mkdir($seriesBase, 0755, true);
+        mkdir($artifactBase, 0755, true);
+        $seriesFile = "$seriesBase/packages-8.2-vs16-arm64-staging.txt";
+        if ($lines !== null) {
+            file_put_contents($seriesFile, implode("\n", $lines));
+        }
+        $artifact = "$artifactBase/libffi-3.8.0-1-vs16-arm64.zip";
+        $unrelated = "$artifactBase/zlib-1.3.2-vs16-arm64.zip";
+        file_put_contents($artifact, 'remove');
+        file_put_contents($unrelated, 'keep');
+        $taskFile = $this->createTask([
+            'php_version' => '8.2',
+            'vs_version' => 'vs16',
+            'stability' => 'staging',
+            'library' => 'libffi',
+            'ref' => '',
+        ]);
+
+        $command = new SeriesUpdateCommand();
+        $command->options = ['base-directory' => $this->baseDirectory, 
'builds-directory' => $this->buildsDirectory];
+        $this->assertSame(0, $command->handle());
+
+        if ($remaining !== null) {
+            $this->assertSame($remaining, file($seriesFile, 
FILE_IGNORE_NEW_LINES));
+        } else {
+            $this->assertFileDoesNotExist($seriesFile);
+        }
+        $this->assertFileDoesNotExist($artifact);
+        $this->assertStringEqualsFile($unrelated, 'keep');
+        $this->assertFileDoesNotExist($taskFile);
+        $this->assertFileDoesNotExist($taskFile . '.lock');
     }
 
     public function testSkipsLockedTask(): void
diff --git a/tests/Http/Controllers/SeriesInitControllerTest.php 
b/tests/Http/Controllers/SeriesInitControllerTest.php
index 156d77c..ad0a538 100644
--- a/tests/Http/Controllers/SeriesInitControllerTest.php
+++ b/tests/Http/Controllers/SeriesInitControllerTest.php
@@ -3,44 +3,118 @@
 
 namespace Http\Controllers;
 
+use App\Console\Command\SeriesInitCommand;
+use App\Helpers\Helpers;
 use App\Http\Controllers\SeriesInitController;
-use JsonException;
-use Override;
+use PHPUnit\Framework\Attributes\DataProvider;
 use PHPUnit\Framework\TestCase;
 
-class MockSeriesInitController extends SeriesInitController {
-    #[Override]
-    protected function validate(array $data): bool {
-        return isset($data['key']);
+class SeriesInitControllerTest extends TestCase
+{
+    private string $directory;
+    private string|false $originalBuildsDirectory;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        $this->directory = sys_get_temp_dir() . '/series_init_controller_' . 
uniqid();
+        mkdir($this->directory . '/builds/series', 0755, true);
+        $this->originalBuildsDirectory = getenv('BUILDS_DIRECTORY');
+        putenv('BUILDS_DIRECTORY=' . $this->directory . '/builds');
+        http_response_code(200);
+    }
+
+    protected function tearDown(): void
+    {
+        putenv($this->originalBuildsDirectory === false ? 'BUILDS_DIRECTORY' : 
'BUILDS_DIRECTORY=' . $this->originalBuildsDirectory);
+        http_response_code(200);
+        Helpers::rmdirr($this->directory);
+        parent::tearDown();
     }
 
-    #[Override]
-    protected function execute(array $data): void {
-        echo "Executed";
+    public static function validPayloadProvider(): array
+    {
+        return [
+            'legacy request' => [[]],
+            'VS initialization' => [['subject' => 'vs']],
+            'VS initialization ignores architecture inputs' => [['subject' => 
'vs', 'source_arch' => 'x64', 'target_arch' => 'arm64']],
+            'missing subject defaults to VS initialization' => [['source_arch' 
=> 'x64', 'target_arch' => 'arm64']],
+            'ARM64 for existing PHP series' => [['subject' => 'arch', 
'source_arch' => 'x64', 'target_arch' => 'arm64']],
+            'ARM64 for master' => [['subject' => 'arch', 'php_version' => 
'master', 'source_arch' => 'x64', 'target_arch' => 'arm64']],
+        ];
     }
 
-    #[Override]
-    public function handle(): void
+    #[DataProvider('validPayloadProvider')]
+    public function testRequestCreatesExpectedSeries(array $fields): void
     {
-        $data = json_decode(file_get_contents($this->inputPath), true);
+        $payload = $fields + ['php_version' => '8.2', 'source_vs' => 'vs16', 
'target_vs' => 'vs16'];
+        $input = $this->directory . '/input.json';
+        file_put_contents($input, json_encode($payload));
+        (new SeriesInitController($input))->handle();
+        $this->assertSame(200, http_response_code());
+        $jobs = glob($this->directory . '/builds/series/series-init-*.json');
+        $this->assertCount(1, $jobs);
+        $this->assertSame($payload, json_decode(file_get_contents($jobs[0]), 
true));
 
-        if ($this->validate($data)) {
-            $this->execute($data);
+        $seriesDirectory = $this->directory . '/base/php-sdk/deps/series';
+        mkdir($seriesDirectory, 0755, true);
+        $architectureCopy = ($payload['subject'] ?? 'vs') === 'arch';
+        $phpVersion = $payload['php_version'];
+        $sourceVersion = $architectureCopy ? $phpVersion : 'master';
+        $sourceArchitectures = $architectureCopy ? ['x64'] : ['x86', 'x64'];
+        foreach ($sourceArchitectures as $arch) {
+            foreach (['stable', 'staging'] as $stability) {
+                
file_put_contents("$seriesDirectory/packages-$sourceVersion-vs16-$arch-$stability.txt",
 "libffi-3.8.0-1-vs16-$arch.zip\nzlib-1.3.2-vs16-$arch.zip\n");
+            }
         }
+
+        $command = new SeriesInitCommand();
+        $command->options = ['base-directory' => $this->directory . '/base', 
'builds-directory' => $this->directory . '/builds'];
+        $this->assertSame(0, $command->handle());
+
+        foreach ($architectureCopy ? ['arm64'] : ['x86', 'x64'] as $arch) {
+            foreach (['stable', 'staging'] as $stability) {
+                
$this->assertStringEqualsFile("$seriesDirectory/packages-$phpVersion-vs16-$arch-$stability.txt",
 "libffi-3.8.0-1-vs16-$arch.zip\nzlib-1.3.2-vs16-$arch.zip\n");
+            }
+        }
+        $this->assertFileDoesNotExist($jobs[0]);
+        $this->assertFileDoesNotExist($jobs[0] . '.lock');
     }
-}
 
-class SeriesInitControllerTest extends TestCase {
-    /**
-     * @throws JsonException
-     */
-    public function testHandleWithValidData() {
-        $data = json_encode(["key" => "value"]);
-        $tempFile = tempnam(sys_get_temp_dir(), 'phpunit');
-        file_put_contents($tempFile, $data);
-        $controller = new MockSeriesInitController($tempFile);
-        $this->expectOutputString("Executed");
-        $controller->handle();
-        unlink($tempFile);
+    public static function invalidPayloadProvider(): array
+    {
+        return [
+            'missing target architecture' => [['source_arch' => 'x64']],
+            'missing source architecture' => [['target_arch' => 'arm64']],
+            'all to specific architecture' => [['source_arch' => 'all', 
'target_arch' => 'arm64']],
+            'specific architecture to all' => [['source_arch' => 'x64', 
'target_arch' => 'all']],
+            'invalid architecture' => [['source_arch' => 'x64', 'target_arch' 
=> 'aarch64']],
+            'architecture path' => [['source_arch' => '../x64', 'target_arch' 
=> 'arm64']],
+            'architecture trailing newline' => [['source_arch' => 'x64', 
'target_arch' => "arm64\n"]],
+            'null architecture' => [['source_arch' => null, 'target_arch' => 
'arm64']],
+            'empty architecture' => [['source_arch' => '', 'target_arch' => 
'arm64']],
+            'array architecture' => [['source_arch' => ['x64'], 'target_arch' 
=> 'arm64']],
+            'invalid PHP series' => [['php_version' => '../8.2', 'source_arch' 
=> 'x64', 'target_arch' => 'arm64']],
+            'VS master request' => [['subject' => 'vs', 'php_version' => 
'master']],
+            'invalid subject' => [['subject' => 'all']],
+            'null subject' => [['subject' => null]],
+            'empty subject' => [['subject' => '']],
+            'array subject' => [['subject' => ['arch']]],
+            'subject trailing newline' => [['subject' => "arch\n"]],
+        ];
+    }
+
+    #[DataProvider('invalidPayloadProvider')]
+    public function testRejectsInvalidArchitectureCopy(array $fields): void
+    {
+        $input = $this->directory . '/input.json';
+        file_put_contents($input, json_encode($fields + ['subject' => 'arch', 
'php_version' => '8.2', 'source_vs' => 'vs16', 'target_vs' => 'vs16']));
+        ob_start();
+        (new SeriesInitController($input))->handle();
+        $output = ob_get_clean();
+
+        $this->assertSame(400, http_response_code());
+        $this->assertStringStartsWith('Invalid request:', $output);
+        $this->assertEmpty(glob($this->directory . 
'/builds/series/series-init-*.json'));
     }
 }

Reply via email to