diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a4b288b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +version: 2 + +updates: + - package-ecosystem: composer + directory: / + target-branch: develop + schedule: + interval: weekly + day: monday + time: '08:00' + timezone: Africa/Lusaka + open-pull-requests-limit: 5 + + - package-ecosystem: github-actions + directory: / + target-branch: develop + schedule: + interval: weekly + day: monday + time: '08:00' + timezone: Africa/Lusaka + open-pull-requests-limit: 5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe8b41c..f93aa30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,15 +1,30 @@ name: CI on: + workflow_dispatch: push: + branches: + - main + - develop pull_request: + branches: + - main + - develop + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true permissions: contents: read +env: + COMPOSER_NO_INTERACTION: '1' + jobs: test: name: PHP 8.4 / ${{ matrix.os }} + timeout-minutes: 30 strategy: fail-fast: false matrix: @@ -30,6 +45,14 @@ jobs: tools: composer:v2 coverage: none + - name: Cache Composer downloads + uses: actions/cache@v4 + with: + path: ~/.composer/cache/files + key: composer-${{ runner.os }}-php-8.4-${{ hashFiles('composer.lock') }} + restore-keys: | + composer-${{ runner.os }}-php-8.4- + - name: Validate Composer metadata run: composer validate --strict --no-check-publish @@ -50,3 +73,16 @@ jobs: - name: Smoke-test Stage 33 Slice 2 through the production PHAR run: composer smoke:phar:stage33-slice2 + + - name: Upload PHAR smoke artifact + if: runner.os == 'Linux' + uses: actions/upload-artifact@v4 + with: + name: baton-php-phar-${{ github.sha }} + path: | + build/baton.phar + build/baton.phar.sha256 + build/baton-dependencies.json + build/LICENSES/ + if-no-files-found: error + retention-days: 7 diff --git a/.github/workflows/private-runtime.yml b/.github/workflows/private-runtime.yml index b19853c..c4b80a3 100644 --- a/.github/workflows/private-runtime.yml +++ b/.github/workflows/private-runtime.yml @@ -3,6 +3,9 @@ name: Private PHP runtime on: workflow_dispatch: pull_request: + branches: + - main + - develop paths: - '.github/workflows/private-runtime.yml' - 'composer.json' @@ -14,6 +17,7 @@ on: - 'tests/Distribution/**' push: branches: + - main - develop paths: - '.github/workflows/private-runtime.yml' @@ -25,6 +29,10 @@ on: - 'templates/**' - 'tests/Distribution/**' +concurrency: + group: private-runtime-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read diff --git a/packaging/php-runtime/build.php b/packaging/php-runtime/build.php index 526b0e4..7dcb9e4 100644 --- a/packaging/php-runtime/build.php +++ b/packaging/php-runtime/build.php @@ -78,10 +78,6 @@ $builderPath = installBuilder($assetPath, $workDirectory, str_starts_with($target, 'windows-')); $extensionArgument = implode(',', $extensions); -$sourceArguments = []; -foreach ($sources as $name => $source) { - $sourceArguments[] = "--custom-url={$name}:{$source['url']}"; -} if (isset($options['prepare'])) { run([ $builderPath, @@ -91,15 +87,29 @@ ], $workDirectory); } -run([ - $builderPath, - 'download', - "--for-extensions={$extensionArgument}", - "--with-php={$spec['php']['version']}", - '--without-suggestions', - ...$sourceArguments, - '--no-interaction', -], $workDirectory); +$downloaded = false; +$sourceUrlAttempts = sourceUrlAttempts($sources); +foreach ($sourceUrlAttempts as $index => $sourceArguments) { + $exitCode = runForExitCode([ + $builderPath, + 'download', + "--for-extensions={$extensionArgument}", + "--with-php={$spec['php']['version']}", + '--without-suggestions', + ...$sourceArguments, + '--no-interaction', + ], $workDirectory); + if ($exitCode === 0) { + $downloaded = true; + break; + } + if (isset($sourceUrlAttempts[$index + 1])) { + fwrite(STDERR, "Source download failed; retrying with fallback URLs.\n"); + } +} +if (!$downloaded) { + fail('Runtime source download failed for every pinned URL set.'); +} foreach ($sources as $name => $source) { verifyDownloadedSource($workDirectory . '/downloads', $name, $source['sha256']); @@ -177,8 +187,8 @@ fwrite(STDOUT, "Runtime manifest: {$manifestPath}" . PHP_EOL); /** - * @param array}> $sources - * @return array}> + * @param array, sha256: string, runtime: bool, targets?: list}> $sources + * @return array, sha256: string, runtime: bool, targets?: list}> */ function sourcesForTarget(array $sources, string $target): array { @@ -189,6 +199,31 @@ function sourcesForTarget(array $sources, string $target): array ); } +/** + * @param array}> $sources + * @return list> + */ +function sourceUrlAttempts(array $sources): array +{ + $attemptCount = 1; + foreach ($sources as $source) { + $attemptCount = max($attemptCount, 1 + count($source['fallbackUrls'] ?? [])); + } + + $attempts = []; + for ($attempt = 0; $attempt < $attemptCount; $attempt++) { + $arguments = []; + foreach ($sources as $name => $source) { + $urls = [$source['url'], ...($source['fallbackUrls'] ?? [])]; + $url = $urls[min($attempt, count($urls) - 1)]; + $arguments[] = "--custom-url={$name}:{$url}"; + } + $attempts[] = $arguments; + } + + return $attempts; +} + /** @param list $arguments * @return array */ @@ -407,6 +442,15 @@ function installBuilder(string $assetPath, string $workDirectory, bool $windows) /** @param list $command */ function run(array $command, string $workingDirectory): void +{ + $exitCode = runForExitCode($command, $workingDirectory); + if ($exitCode !== 0) { + fail("Process exited with status {$exitCode}: {$command[0]}"); + } +} + +/** @param list $command */ +function runForExitCode(array $command, string $workingDirectory): int { fwrite(STDOUT, '> ' . implode(' ', array_map( static fn (string $argument): string => escapeshellarg($argument), @@ -416,10 +460,7 @@ function run(array $command, string $workingDirectory): void if (!is_resource($process)) { fail("Could not start process: {$command[0]}"); } - $exitCode = proc_close($process); - if ($exitCode !== 0) { - fail("Process exited with status {$exitCode}: {$command[0]}"); - } + return proc_close($process); } function copyFile(string $source, string $destination): void diff --git a/packaging/php-runtime/spec.json b/packaging/php-runtime/spec.json index c05de46..32beb6a 100644 --- a/packaging/php-runtime/spec.json +++ b/packaging/php-runtime/spec.json @@ -69,7 +69,10 @@ "runtime": true }, "libiconv": { - "url": "https://mirrors.kernel.org/gnu/libiconv/libiconv-1.19.tar.gz", + "url": "https://ftp.gnu.org/gnu/libiconv/libiconv-1.19.tar.gz", + "fallbackUrls": [ + "https://mirrors.kernel.org/gnu/libiconv/libiconv-1.19.tar.gz" + ], "sha256": "88dd96a8c0464eca144fc791ae60cd31cd8ee78321e67397e25fc095c4a19aa6", "runtime": true, "targets": [ diff --git a/tests/Distribution/RuntimeSpecTest.php b/tests/Distribution/RuntimeSpecTest.php index 1356129..f2cb0ca 100644 --- a/tests/Distribution/RuntimeSpecTest.php +++ b/tests/Distribution/RuntimeSpecTest.php @@ -22,7 +22,7 @@ public function testRuntimeInputsAndSupportedTargetsArePinned(): void * assets: array * }, * extensions: array{common: list, unix: list}, - * sources: array}>, + * sources: array, sha256: string, runtime: bool, targets?: list}>, * capabilities: list * } $spec */ @@ -66,11 +66,13 @@ public function testRuntimeInputsAndSupportedTargetsArePinned(): void array_keys($spec['sources']), ); foreach ($spec['sources'] as $source) { - self::assertPinnedUrlAndHash($source['url'], $source['sha256']); - self::assertMatchesRegularExpression( - '/\.(?:tar\.(?:gz|xz)|tgz|zip)$/', - parse_url($source['url'], PHP_URL_PATH) ?: '', - ); + foreach ([$source['url'], ...($source['fallbackUrls'] ?? [])] as $url) { + self::assertPinnedUrlAndHash($url, $source['sha256']); + self::assertMatchesRegularExpression( + '/\.(?:tar\.(?:gz|xz)|tgz|zip)$/', + parse_url($url, PHP_URL_PATH) ?: '', + ); + } } self::assertTrue($spec['sources']['zlib']['runtime']); self::assertFalse($spec['sources']['micro']['runtime']); @@ -79,9 +81,13 @@ public function testRuntimeInputsAndSupportedTargetsArePinned(): void self::assertTrue($spec['sources']['libiconv']['runtime']); self::assertTrue($spec['sources']['libiconv-win']['runtime']); self::assertStringStartsWith( - 'https://mirrors.kernel.org/gnu/', + 'https://ftp.gnu.org/gnu/', $spec['sources']['libiconv']['url'], ); + self::assertSame( + ['https://mirrors.kernel.org/gnu/libiconv/libiconv-1.19.tar.gz'], + $spec['sources']['libiconv']['fallbackUrls'] ?? null, + ); self::assertSame( ['linux-x86_64', 'linux-aarch64', 'macos-x86_64', 'macos-aarch64'], $spec['sources']['libiconv']['targets'] ?? null,