diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f02f532..d1b52318 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -333,8 +333,8 @@ jobs: - framework: Laravel version: '12' php: '8.2' - package: 'laravel/framework:^12.69.0' - exercised_package: 'laravel/framework:12.69.0' + package: 'laravel/framework:^12.72.0' + exercised_package: 'laravel/framework:12.72.0' bootstrap: fresh_supported_install script: laravel.php - framework: Symfony @@ -656,15 +656,15 @@ jobs: - framework: Laravel version: '12' php: '8.4' - package: 'laravel/framework:^12.69.0' - exercised_package: 'laravel/framework:12.69.0' + package: 'laravel/framework:^12.72.0' + exercised_package: 'laravel/framework:12.72.0' bootstrap: fresh_supported_install script: laravel.php - framework: Laravel version: '13' php: '8.4' - package: 'laravel/framework:^13.30.0' - exercised_package: 'laravel/framework:13.30.0' + package: 'laravel/framework:^13.41.0' + exercised_package: 'laravel/framework:13.41.0' bootstrap: fresh_supported_install script: laravel.php - framework: Symfony @@ -828,15 +828,15 @@ jobs: - framework: Laravel version: '12' php: '8.3' - package: 'laravel/framework:^12.69.0' - exercised_package: 'laravel/framework:12.69.0' + package: 'laravel/framework:^12.72.0' + exercised_package: 'laravel/framework:12.72.0' bootstrap: fresh_supported_install script: laravel.php - framework: Laravel version: '13' php: '8.3' - package: 'laravel/framework:^13.30.0' - exercised_package: 'laravel/framework:13.30.0' + package: 'laravel/framework:^13.41.0' + exercised_package: 'laravel/framework:13.41.0' bootstrap: fresh_supported_install script: laravel.php steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 19bb239d..90f74eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ project follows [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Security + +- Raise the optional Laravel bridge floors to 12.72.0 and 13.41.0 for + the November concurrency and signed-URL advisories, reject affected installations before provider bindings, + and update the current qualification matrix and HTTP example dependency + graph. Historical qualification reports retain their original tuples (#421). + ## [3.18.2] - 2026-11-13 ### Fixed diff --git a/composer.json b/composer.json index 52cb788f..7f3dee69 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ }, "suggest": { "ext-pcntl": "Required by framework console workers for graceful SIGINT and SIGTERM handling.", - "laravel/framework": "Enables the auto-discovered service-mode bridge on Laravel 12.69.0 or 13.30.0 and later releases of each major (the patched minimums for GHSA-f9x3-7vq9-cph6).", + "laravel/framework": "Enables the auto-discovered service-mode bridge on Laravel 12.72.0 or 13.41.0 and later releases of each major (the patched November 2026 security minimums).", "open-telemetry/api": "Optional worker tracing with the application's provider; supports API 1.10.1 and later in the 1.x line.", "symfony/framework-bundle": "Enables the Symfony 6.4, 7, and 8 service-mode bundle.", "symfony/console": "Required with symfony/framework-bundle to run the Symfony 6.4, 7, and 8 worker command." @@ -97,7 +97,7 @@ } }, "durable-workflow": { - "product-train": "3.18.2", + "product-train": "3.18.3", "supported-server-versions": "2.5.0", "supported-server-release-baselines": { "2": "2.5.0", @@ -122,7 +122,7 @@ "external-payload-references": "opaque-reference" }, "frameworks": { - "laravel": "^12.69.0|^13.30.0", + "laravel": "^12.72.0|^13.41.0", "symfony": "^6.4|^7.0|^8.0" } } diff --git a/docs/laravel-adoption-contract.json b/docs/laravel-adoption-contract.json index 9674692b..e3d1b4e4 100644 --- a/docs/laravel-adoption-contract.json +++ b/docs/laravel-adoption-contract.json @@ -3,14 +3,14 @@ "schema_version": 1, "framework": { "package": "laravel/framework", - "constraint": "^12.69.0|^13.30.0", + "constraint": "^12.72.0|^13.41.0", "php_constraint": "^8.2", "qualification_matrix": [ - {"laravel": "12", "php": "8.2", "package": "laravel/framework:^12.69.0", "exercised_package": "laravel/framework:12.69.0", "bootstrap": "fresh_supported_install"}, - {"laravel": "12", "php": "8.3", "package": "laravel/framework:^12.69.0", "exercised_package": "laravel/framework:12.69.0", "bootstrap": "fresh_supported_install"}, - {"laravel": "13", "php": "8.3", "package": "laravel/framework:^13.30.0", "exercised_package": "laravel/framework:13.30.0", "bootstrap": "fresh_supported_install"}, - {"laravel": "12", "php": "8.4", "package": "laravel/framework:^12.69.0", "exercised_package": "laravel/framework:12.69.0", "bootstrap": "fresh_supported_install"}, - {"laravel": "13", "php": "8.4", "package": "laravel/framework:^13.30.0", "exercised_package": "laravel/framework:13.30.0", "bootstrap": "fresh_supported_install"} + {"laravel": "12", "php": "8.2", "package": "laravel/framework:^12.72.0", "exercised_package": "laravel/framework:12.72.0", "bootstrap": "fresh_supported_install"}, + {"laravel": "12", "php": "8.3", "package": "laravel/framework:^12.72.0", "exercised_package": "laravel/framework:12.72.0", "bootstrap": "fresh_supported_install"}, + {"laravel": "13", "php": "8.3", "package": "laravel/framework:^13.41.0", "exercised_package": "laravel/framework:13.41.0", "bootstrap": "fresh_supported_install"}, + {"laravel": "12", "php": "8.4", "package": "laravel/framework:^12.72.0", "exercised_package": "laravel/framework:12.72.0", "bootstrap": "fresh_supported_install"}, + {"laravel": "13", "php": "8.4", "package": "laravel/framework:^13.41.0", "exercised_package": "laravel/framework:13.41.0", "bootstrap": "fresh_supported_install"} ] }, "embedded_transition_authority": { diff --git a/docs/portal/frameworks/laravel.md b/docs/portal/frameworks/laravel.md index e5018a0f..105540b2 100644 --- a/docs/portal/frameworks/laravel.md +++ b/docs/portal/frameworks/laravel.md @@ -17,11 +17,18 @@ next:

Cloud or Server

durable-workflow/sdk connects the same Laravel application structure to a separately operated namespace. The Laravel bridge keeps container resolution, configuration, Artisan, logging, events, and testing.

-The service-mode bridge supports Laravel 12.69.0 and 13.30.0, and later -releases of each major (`^12.69.0|^13.30.0`). The Laravel bridge requires PHP -8.2 or newer, with PHP 8.3 or newer for Laravel 13. These are the same patched -framework and PHP floors published by Workflow 2.21.0 for embedded v2. The -bare HTTP SDK continues to support PHP 8.1. +The service-mode bridge supports Laravel 12.72.0 and 13.41.0, and later +releases of each major (`^12.72.0|^13.41.0`). The Laravel bridge requires PHP +8.2 or newer, with PHP 8.3 or newer for Laravel 13. These Laravel floors include +the November security fixes: + +- [Task output injection](https://github.com/advisories/GHSA-63hm-g96r-2m2q) +- [Signed URL query limits](https://github.com/laravel/framework/security/advisories/GHSA-v5ch-g375-82r6) +- [Signed URL route parameters](https://github.com/laravel/framework/security/advisories/GHSA-6rv8-qj5v-p8ch) + +Embedded-to-service transitions require the installed Workflow package to +publish the same Laravel and PHP support contract. The bare HTTP SDK continues +to support PHP 8.1. SDK 3.10 removes Laravel 10 and 11 from service-mode framework support. Those majors are past their upstream end of life and have published advisories with diff --git a/docs/portal/operate/upgrading.md b/docs/portal/operate/upgrading.md index 92dbe867..54b445ac 100644 --- a/docs/portal/operate/upgrading.md +++ b/docs/portal/operate/upgrading.md @@ -13,14 +13,14 @@ next: This guide covers PHP SDK {{ release.sdkVersion }} and its changes listed in the [changelog](https://github.com/durable-workflow/sdk-php/blob/main/CHANGELOG.md). The bare SDK retains PHP `^8.1`, and Symfony 6.4/7/8 support is unchanged. -The Laravel bridge follows Workflow 2.21.0: PHP `^8.2` and patched Laravel -`^12.69.0|^13.30.0`, with PHP 8.3 or newer for Laravel 13. SDK 3.10 removes +The Laravel bridge requires PHP `^8.2` and patched Laravel +`^12.72.0|^13.41.0`, with PHP 8.3 or newer for Laravel 13. SDK 3.10 removes Laravel 10/11 bridge support. Its provider checks the installed framework and PHP before registering application bindings, including during auto-discovery. Upgrade the platform before adopting this bridge; existing SDK 3.9.x and -Workflow 2.20.x applications retain their own support contracts while upgrading. -Stable-v1 authority remains unchanged. SDK 3.11 keeps these platform contracts -and adds the occurrence-deadline rollout below. +Workflow 2.20.x applications must also apply Laravel security updates while upgrading. +Stable-v1 authority remains unchanged. SDK 3.11 adds the occurrence-deadline +rollout below. ## Condition-wait occurrence deadlines diff --git a/docs/quickstart-contract.json b/docs/quickstart-contract.json index dc8da1d7..a49992b2 100644 --- a/docs/quickstart-contract.json +++ b/docs/quickstart-contract.json @@ -3,8 +3,8 @@ "schema_version": 2, "package": { "name": "durable-workflow/sdk", - "published_version": "3.18.2", - "composer_requirement": "3.18.2", + "published_version": "3.18.3", + "composer_requirement": "3.18.3", "onboarding_requirement": "^3.18" }, "runtime_targets": { diff --git a/docs/sdk-reference.md b/docs/sdk-reference.md index 7c952af1..0c32a303 100644 --- a/docs/sdk-reference.md +++ b/docs/sdk-reference.md @@ -10,11 +10,12 @@ runnable path. - **Laravel adoption:** start with the [ownership-first transition guide](https://php.durable-workflow.com/frameworks/laravel/) when moving a Laravel Workflow v1 application to v2 embedded or service mode, or when - moving embedded v2 to service mode. The SDK 3.10 bridge and Workflow 2.21.0 - support PHP `^8.2` and patched Laravel `^12.69.0|^13.30.0`; Laravel 13 needs - PHP 8.3 or newer. The bare SDK retains PHP `^8.1`. Existing SDK 3.9.x and - Workflow 2.20.x applications keep their release's support contract while - upgrading PHP and Laravel. Stable-v1 authority is unchanged. + moving embedded v2 to service mode. The current bridge supports PHP `^8.2` + and patched Laravel `^12.72.0|^13.41.0`; Laravel 13 needs PHP 8.3 or newer. + Embedded-to-service transitions require the installed Workflow package to + publish that same support contract. The bare SDK retains PHP `^8.1`. + Existing applications must apply Laravel security updates while upgrading. + Stable-v1 authority is unchanged. - **Plain PHP service mode:** follow the quickstart below when a framework-neutral application and remote worker connect to Durable Workflow Cloud or a self-hosted Server. @@ -878,12 +879,11 @@ and the shipped [client](../examples/message-stream-client.php) and ## Laravel service mode -Patched Laravel `^12.69.0|^13.30.0` applications auto-discover the provider from +Patched Laravel `^12.72.0|^13.41.0` applications auto-discover the provider from the SDK package. The bridge requires PHP `^8.2`, with PHP 8.3 or newer for Laravel 13. The provider refuses unsupported or unknown installed framework/PHP versions before registering config, clients or workers. Upgrade PHP and Laravel -before adopting this bridge, or retain the existing SDK 3.9.x release while -upgrading the platform. The bare SDK's PHP `^8.1` support remains independent. +before adopting this bridge. The bare SDK's PHP `^8.1` support remains independent. Publish the environment-backed configuration, add attributed handler services, and start the supervised Artisan command. The worker process needs `ext-pcntl`; the command refuses to start without it so `SIGINT` and `SIGTERM` always drain diff --git a/examples/laravel-http/composer.json b/examples/laravel-http/composer.json index 584845b7..c6253e5e 100644 --- a/examples/laravel-http/composer.json +++ b/examples/laravel-http/composer.json @@ -6,7 +6,7 @@ "require": { "php": "^8.3", "durable-workflow/sdk": "3.9.1", - "laravel/framework": "12.69.3", + "laravel/framework": "12.72.0", "laravel/octane": "2.21.0", "spiral/roadrunner-cli": "^2.6.0", "spiral/roadrunner-http": "^3.3.0" diff --git a/examples/laravel-http/composer.lock b/examples/laravel-http/composer.lock index 73da04a7..0d0f2d62 100644 --- a/examples/laravel-http/composer.lock +++ b/examples/laravel-http/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "59bf5010338cae5204f26885ef9f5b62", + "content-hash": "841dce9888a700b2c79988a01e6fb4eb", "packages": [ { "name": "apache/avro", @@ -1470,16 +1470,16 @@ }, { "name": "laravel/framework", - "version": "v12.69.3", + "version": "v12.72.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "58ea544a2a80dc03c168e13a5dc9a1d176a88717" + "reference": "b089d3020e508f49b3625726ab4a2c897f1b78d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/58ea544a2a80dc03c168e13a5dc9a1d176a88717", - "reference": "58ea544a2a80dc03c168e13a5dc9a1d176a88717", + "url": "https://api.github.com/repos/laravel/framework/zipball/b089d3020e508f49b3625726ab4a2c897f1b78d5", + "reference": "b089d3020e508f49b3625726ab4a2c897f1b78d5", "shasum": "" }, "require": { @@ -1592,10 +1592,10 @@ "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", "opis/json-schema": "^2.4.1", - "orchestra/testbench-core": "^10.9.0", + "orchestra/testbench-core": "^10.15.0", "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", - "phpstan/phpstan": "^2.1.41", + "phpstan/phpstan": "2.2.13", "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", "predis/predis": "^2.3|^3.0", "resend/resend-php": "^0.10.0|^1.0", @@ -1688,7 +1688,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-09-29T12:53:25+00:00" + "time": "2026-11-10T15:45:11+00:00" }, { "name": "laravel/octane", diff --git a/src/Bridge/Laravel/LaravelPlatform.php b/src/Bridge/Laravel/LaravelPlatform.php index 16ac30ca..c217b309 100644 --- a/src/Bridge/Laravel/LaravelPlatform.php +++ b/src/Bridge/Laravel/LaravelPlatform.php @@ -10,8 +10,8 @@ final class LaravelPlatform { private const PLATFORMS = [ - 12 => ['floor' => '12.69.0', 'php' => 80200], - 13 => ['floor' => '13.30.0', 'php' => 80300], + 12 => ['floor' => '12.72.0', 'php' => 80200], + 13 => ['floor' => '13.41.0', 'php' => 80300], ]; public static function laravelConstraint(): string @@ -41,7 +41,7 @@ public static function assertSupported(?string $laravelVersion, int $phpVersionI throw new RuntimeException( 'The Durable Workflow Laravel bridge requires PHP ^8.2 and Laravel '.self::laravelConstraint() .'; Laravel 13 requires PHP ^8.3. Upgrade the application platform before activating the bridge' - .' or retain the existing SDK 3.9.x release while upgrading.', + .'. The bare HTTP client remains available independently of this optional bridge.', ); } } diff --git a/tests/FrameworkBridgeTest.php b/tests/FrameworkBridgeTest.php index a2605399..f05ebd16 100644 --- a/tests/FrameworkBridgeTest.php +++ b/tests/FrameworkBridgeTest.php @@ -277,7 +277,7 @@ public function testComposerMetadataAdvertisesOptionalSupportedFrameworks(): voi $manifest['extra']['laravel']['providers'], ); self::assertSame( - '^12.69.0|^13.30.0', + '^12.72.0|^13.41.0', $manifest['extra']['durable-workflow']['frameworks']['laravel'], ); self::assertSame('^6.4|^7.0|^8.0', $manifest['extra']['durable-workflow']['frameworks']['symfony']); diff --git a/tests/LaravelAdoptionContractTest.php b/tests/LaravelAdoptionContractTest.php index 74bbc733..7d401fc6 100644 --- a/tests/LaravelAdoptionContractTest.php +++ b/tests/LaravelAdoptionContractTest.php @@ -32,11 +32,11 @@ public function testBridgeCompatibilityMatchesTheEmbeddedLaravelSurface(): void 'The optional Laravel bridge floor must not raise the bare HTTP SDK floor.'); self::assertSame( [ - ['laravel' => '12', 'php' => '8.2', 'package' => 'laravel/framework:^12.69.0', 'exercised_package' => 'laravel/framework:12.69.0', 'bootstrap' => 'fresh_supported_install'], - ['laravel' => '12', 'php' => '8.3', 'package' => 'laravel/framework:^12.69.0', 'exercised_package' => 'laravel/framework:12.69.0', 'bootstrap' => 'fresh_supported_install'], - ['laravel' => '13', 'php' => '8.3', 'package' => 'laravel/framework:^13.30.0', 'exercised_package' => 'laravel/framework:13.30.0', 'bootstrap' => 'fresh_supported_install'], - ['laravel' => '12', 'php' => '8.4', 'package' => 'laravel/framework:^12.69.0', 'exercised_package' => 'laravel/framework:12.69.0', 'bootstrap' => 'fresh_supported_install'], - ['laravel' => '13', 'php' => '8.4', 'package' => 'laravel/framework:^13.30.0', 'exercised_package' => 'laravel/framework:13.30.0', 'bootstrap' => 'fresh_supported_install'], + ['laravel' => '12', 'php' => '8.2', 'package' => 'laravel/framework:^12.72.0', 'exercised_package' => 'laravel/framework:12.72.0', 'bootstrap' => 'fresh_supported_install'], + ['laravel' => '12', 'php' => '8.3', 'package' => 'laravel/framework:^12.72.0', 'exercised_package' => 'laravel/framework:12.72.0', 'bootstrap' => 'fresh_supported_install'], + ['laravel' => '13', 'php' => '8.3', 'package' => 'laravel/framework:^13.41.0', 'exercised_package' => 'laravel/framework:13.41.0', 'bootstrap' => 'fresh_supported_install'], + ['laravel' => '12', 'php' => '8.4', 'package' => 'laravel/framework:^12.72.0', 'exercised_package' => 'laravel/framework:12.72.0', 'bootstrap' => 'fresh_supported_install'], + ['laravel' => '13', 'php' => '8.4', 'package' => 'laravel/framework:^13.41.0', 'exercised_package' => 'laravel/framework:13.41.0', 'bootstrap' => 'fresh_supported_install'], ], $this->contract['framework']['qualification_matrix'], ); @@ -48,13 +48,13 @@ public function testBridgeCompatibilityMatchesTheEmbeddedLaravelSurface(): void ], $this->contract['embedded_transition_authority']); } - public function testServiceModePublishesThePatchedLaravelFloorsForGhsaF9x37vq9Cph6(): void + public function testServiceModePublishesThePatchedLaravelFloorsForGhsa63hmG96r2m2q(): void { - // The maintained majors and patched floors published by Workflow 2.21.0. - $floors = '^12.69.0|^13.30.0'; + // Current maintained majors and the CVE-2026-128083 patched floors. + $floors = '^12.72.0|^13.41.0'; self::assertSame($floors, $this->contract['framework']['constraint']); self::assertSame($floors, $this->composer['extra']['durable-workflow']['frameworks']['laravel']); - foreach (['12.69.0', '13.30.0'] as $floor) { + foreach (['12.72.0', '13.41.0'] as $floor) { self::assertStringContainsString($floor, $this->composer['suggest']['laravel/framework']); } self::assertArrayNotHasKey('laravel/framework', $this->composer['conflict'] ?? [], @@ -133,16 +133,16 @@ static function (string $workflow) use ($search, $replace): string { " php: '8.4'\n", ), 'A framework cell must use its job\'s fixed PHP minor.']; yield 'exact minimum removed from a cell' => [$first( - " exercised_package: 'laravel/framework:12.69.0'\n", + " exercised_package: 'laravel/framework:12.72.0'\n", '', ), 'The CI Laravel cells differ from the adoption contract.']; yield 'cell exercises a later 12.x release' => [$first( - "exercised_package: 'laravel/framework:12.69.0'", - "exercised_package: 'laravel/framework:12.70.0'", + "exercised_package: 'laravel/framework:12.72.0'", + "exercised_package: 'laravel/framework:12.73.0'", ), 'The CI Laravel cells differ from the adoption contract.']; yield 'cell exercises a caret range' => [$first( - "exercised_package: 'laravel/framework:12.69.0'", - "exercised_package: 'laravel/framework:^12.69.0'", + "exercised_package: 'laravel/framework:12.72.0'", + "exercised_package: 'laravel/framework:^12.72.0'", ), 'The CI Laravel cells differ from the adoption contract.']; yield 'no installed-version assertion after the SDK' => [static fn (string $workflow): string => str_replace( " --no-interaction --prefer-dist --no-progress\n" diff --git a/tests/LaravelPlatformTest.php b/tests/LaravelPlatformTest.php index 7f585275..19735423 100644 --- a/tests/LaravelPlatformTest.php +++ b/tests/LaravelPlatformTest.php @@ -44,25 +44,32 @@ public function testPlatformAdmissionIsClosed(?string $version, int $php, bool $ /** @return iterable */ public static function platforms(): iterable { - yield 'Laravel 12 floor' => ['12.69.0.0', 80200, true]; - yield 'Laravel 12 later patch' => ['12.69.3.0', 80400, true]; - yield 'Laravel 12 stable version' => ['12.69.0', 80300, true]; - yield 'Laravel 13 floor' => ['13.30.0.0', 80300, true]; - yield 'Laravel 13 later release' => ['13.31.0.0', 80400, true]; + yield 'Laravel 12 floor' => ['12.72.0.0', 80200, true]; + yield 'Laravel 12 later patch' => ['12.72.1.0', 80400, true]; + yield 'Laravel 12 stable version' => ['12.72.0', 80300, true]; + yield 'Laravel 13 floor' => ['13.41.0.0', 80300, true]; + yield 'Laravel 13 later release' => ['13.42.0.0', 80400, true]; yield 'removed Laravel 10' => ['10.50.4.0', 80400, false]; yield 'removed Laravel 11' => ['11.56.1.0', 80400, false]; yield 'unqualified future major' => ['14.0.0.0', 80400, false]; - yield 'Laravel 12 before patch' => ['12.68.99.0', 80400, false]; - yield 'Laravel 13 before patch' => ['13.29.99.0', 80400, false]; - yield 'bridge PHP before floor' => ['12.69.0.0', 80199, false]; - yield 'Laravel 13 PHP before floor' => ['13.30.0.0', 80299, false]; - yield 'unsupported PHP major' => ['13.30.0.0', 90000, false]; + yield 'Laravel 12 before patch' => ['12.71.99.0', 80400, false]; + yield 'Laravel 13 before patch' => ['13.40.99.0', 80400, false]; + yield 'previous Laravel 12 floor is affected' => ['12.69.0.0', 80400, false]; + yield 'previous example Laravel is affected' => ['12.69.3.0', 80400, false]; + yield 'last Laravel 12 release before fix' => ['12.70.2.0', 80400, false]; + yield 'previous Laravel 13 floor is affected' => ['13.30.0.0', 80400, false]; + yield 'last Laravel 13 release before fix' => ['13.39.0.0', 80400, false]; + yield 'concurrency-only Laravel 12 fix lacks signed-URL fixes' => ['12.71.0.0', 80400, false]; + yield 'concurrency-only Laravel 13 fix lacks signed-URL fixes' => ['13.40.0.0', 80400, false]; + yield 'bridge PHP before floor' => ['12.72.0.0', 80199, false]; + yield 'Laravel 13 PHP before floor' => ['13.41.0.0', 80299, false]; + yield 'unsupported PHP major' => ['13.41.0.0', 90000, false]; yield 'missing framework version' => [null, 80400, false]; yield 'empty framework version' => ['', 80400, false]; yield 'unresolved development version' => ['12.x-dev', 80400, false]; - yield 'unqualified prerelease' => ['13.30.0.0-beta', 80400, false]; - yield 'unrecognized version spelling' => ['v12.69.0', 80400, false]; - yield 'version controls' => ["12.69.0\n", 80400, false]; + yield 'unqualified prerelease' => ['13.41.0.0-beta', 80400, false]; + yield 'unrecognized version spelling' => ['v12.72.0', 80400, false]; + yield 'version controls' => ["12.72.0\n", 80400, false]; } #[RunInSeparateProcess] @@ -100,7 +107,9 @@ public function testAutoDiscoveryRefusesBeforeConfigOrBindings(?string $version) public static function rejectedProviderVersions(): iterable { yield 'old supported major removed' => ['11.56.1.0']; - yield 'supported major below patch' => ['12.68.99.0']; + yield 'supported major below patch' => ['12.70.99.0']; + yield 'previous example below security floor' => ['12.69.3.0']; + yield 'previous Laravel 13 floor' => ['13.30.0.0']; yield 'unknown installed framework' => [null]; } } diff --git a/tests/LaravelTransitionAuthorityTest.php b/tests/LaravelTransitionAuthorityTest.php index 7bc7c7a2..95a134c9 100644 --- a/tests/LaravelTransitionAuthorityTest.php +++ b/tests/LaravelTransitionAuthorityTest.php @@ -9,9 +9,9 @@ /** * The embedded-v2 to service-mode transition check CI runs against the - * installed Workflow package. The positive fixture is published Workflow 2.21.0's - * resources/laravel-embedded-upgrade-contract.json, byte for byte - * (SHA-256 1e65a6ffa6f2a1feba53ff0b5f7782a9ac8d7a3560f31c44c040767c592308c6). + * installed Workflow package. Versioned fixtures retain the exact published + * resources/laravel-embedded-upgrade-contract.json bytes. Historical authorities + * cannot qualify a newer floor; Workflow 2.28.1 supplies the patched boundary. */ final class LaravelTransitionAuthorityTest extends TestCase { @@ -19,6 +19,8 @@ final class LaravelTransitionAuthorityTest extends TestCase private const WORKFLOW_2_21_0 = __DIR__.'/fixtures/laravel-transition/workflow-2.21.0'; + private const WORKFLOW_2_28_1 = __DIR__.'/fixtures/laravel-transition/workflow-2.28.1'; + private string $directory; protected function setUp(): void @@ -35,10 +37,28 @@ protected function tearDown(): void rmdir($this->directory); } - public function testPublishedServiceAuthorityEqualsWorkflowEmbeddedAuthority(): void + public function testHistoricalPublishedAuthorityCannotAuthorizeTheNewPatchedServiceFloor(): void { - [$status, $errors] = $this->check(dirname(__DIR__).'/docs/laravel-adoption-contract.json'); + [$status, $errors] = $this->check(dirname(__DIR__).'/docs/laravel-adoption-contract.json', self::WORKFLOW_2_21_0); + self::assertSame(1, $status); + self::assertStringContainsString('Embedded and service modes publish different Laravel constraints.', $errors); + } + + public function testHistoricalMatchingSupportStillPassesTheUnchangedChecker(): void + { + // This is an inert historical service-contract copy. It cannot qualify + // the current SDK floor or replace the installed corrected producer gate. + $contract = $this->contract(); + $contract['framework']['constraint'] = '^12.69.0|^13.30.0'; + foreach ($contract['framework']['qualification_matrix'] as $index => $cell) { + $floor = $cell['laravel'] === '12' ? '12.69.0' : '13.30.0'; + $contract['framework']['qualification_matrix'][$index]['package'] = 'laravel/framework:^'.$floor; + $contract['framework']['qualification_matrix'][$index]['exercised_package'] = 'laravel/framework:'.$floor; + } + $path = $this->directory.'/historical-service.json'; + file_put_contents($path, json_encode($contract, JSON_THROW_ON_ERROR)); + [$status, $errors] = $this->check($path, self::WORKFLOW_2_21_0); self::assertSame('', $errors); self::assertSame(0, $status); } @@ -54,6 +74,21 @@ public function testPublishedAuthorityFixtureRetainsItsExactPackageBytesAndStabl $published['supported_intersection']['authority']['stable_v1']); } + public function testCurrentPublishedAuthorityMatchesThePatchedServiceBoundary(): void + { + $path = self::WORKFLOW_2_28_1.'/resources/laravel-embedded-upgrade-contract.json'; + self::assertSame('153a261371272e0fa8e0a6e79dbe425b9adc311cbe596ce0237575a78b0828a7', hash_file('sha256', $path)); + $published = json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR); + $historical = json_decode((string) file_get_contents(self::WORKFLOW_2_21_0.'/resources/laravel-embedded-upgrade-contract.json'), + true, 512, JSON_THROW_ON_ERROR); + self::assertSame($historical['supported_intersection']['authority']['stable_v1'], + $published['supported_intersection']['authority']['stable_v1']); + + [$status, $errors] = $this->check(dirname(__DIR__).'/docs/laravel-adoption-contract.json'); + self::assertSame('', $errors); + self::assertSame(0, $status); + } + public function testPreviousEmbeddedSupportBoundaryDoesNotAuthorizeTheNewServiceFloor(): void { [$status, $errors] = $this->check(dirname(__DIR__).'/docs/laravel-adoption-contract.json', self::WORKFLOW_2_17_1); @@ -101,17 +136,17 @@ public static function counterfactuals(): iterable return $contract; }, 'Laravel 12 on PHP 8.2 does not qualify the published floor.']; yield 'cell exercises a later release than its floor' => [static function (array $contract): array { - $contract['framework']['qualification_matrix'][0]['exercised_package'] = 'laravel/framework:12.70.0'; + $contract['framework']['qualification_matrix'][0]['exercised_package'] = 'laravel/framework:12.73.0'; return $contract; }, 'Laravel 12 on PHP 8.2 does not exercise exactly the published floor.']; yield 'cell exercises its caret range' => [static function (array $contract): array { - $contract['framework']['qualification_matrix'][0]['exercised_package'] = 'laravel/framework:^12.69.0'; + $contract['framework']['qualification_matrix'][0]['exercised_package'] = 'laravel/framework:^12.72.0'; return $contract; }, 'Laravel 12 on PHP 8.2 does not exercise exactly the published floor.']; yield 'pre-fix floor' => [static function (array $contract): array { - $contract['framework']['constraint'] = '^12.68.0|^13.30.0'; + $contract['framework']['constraint'] = '^12.68.0|^13.41.0'; return $contract; }, 'Embedded and service modes publish different Laravel constraints.']; @@ -141,7 +176,7 @@ private function contract(): array } /** @return array{int, string} */ - private function check(string $contract, string $workflow = self::WORKFLOW_2_21_0): array + private function check(string $contract, string $workflow = self::WORKFLOW_2_28_1): array { $process = proc_open([PHP_BINARY, dirname(__DIR__).'/scripts/ci/check-laravel-transition-authority.php', $contract, $workflow], [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes); diff --git a/tests/StaticCredentialSubjectTest.php b/tests/StaticCredentialSubjectTest.php index c82eb98f..f73d92b1 100644 --- a/tests/StaticCredentialSubjectTest.php +++ b/tests/StaticCredentialSubjectTest.php @@ -23,9 +23,9 @@ public function testDependencyIntentHasExactVersionsAndFutureInputs(): void self::assertSame('3.10.0', $intent['requirements']['durable-workflow/sdk']); // The selected #307 graph: FrameworkBundle 7.4.21 is refused by advisory // GHSA-xg2q-4rrq-657q, so the intent names the patched 7.4.23 release. - self::assertSame(['php' => '8.4.27', 'durable-workflow/sdk' => '3.10.0', 'laravel/framework' => '12.69.3', + self::assertSame(['php' => '8.4.27', 'durable-workflow/sdk' => '3.10.0', 'laravel/framework' => '12.72.0', 'symfony/framework-bundle' => '7.4.23'], $intent['requirements']); - self::assertSame(['laravel/framework' => '58ea544a2a80dc03c168e13a5dc9a1d176a88717', + self::assertSame(['laravel/framework' => 'b089d3020e508f49b3625726ab4a2c897f1b78d5', 'symfony/framework-bundle' => '6a749f2c37cb9def53793633ec72958aaad4b359'], $intent['framework_source_references']); self::assertCount(3, $intent['future_inputs']); self::assertSame(['plugins' => false, 'scripts' => false, 'advisory_policy' => 'normal'], $intent['dependency_policy']); diff --git a/tests/fixtures/laravel-transition/workflow-2.28.1/resources/laravel-embedded-upgrade-contract.json b/tests/fixtures/laravel-transition/workflow-2.28.1/resources/laravel-embedded-upgrade-contract.json new file mode 100644 index 00000000..b999b330 --- /dev/null +++ b/tests/fixtures/laravel-transition/workflow-2.28.1/resources/laravel-embedded-upgrade-contract.json @@ -0,0 +1,128 @@ +{ + "schema": "durable-workflow.laravel-embedded-upgrade.contract", + "version": 1, + "transition": { + "from": { + "package": "durable-workflow/workflow", + "channel": "^1.0", + "engine": "v1" + }, + "to": { + "package": "durable-workflow/workflow", + "channel": "^2.0@rc", + "engine": "embedded_v2" + }, + "legacy_package_alias": { + "package": "laravel-workflow/laravel-workflow", + "role": "composer_replace_only" + }, + "deployment_owner": "laravel_application", + "requires_service_mode": false + }, + "supported_intersection": { + "authority": { + "stable_v1": { + "php": "^8.1", + "laravel": "^9.0|^10.0|^11.0|^12.0|^13.0" + }, + "embedded_v2": { + "php": "^8.2", + "laravel": "^12.72.0|^13.41.0" + }, + "qualified_php_minors": ["8.2", "8.3", "8.4"], + "laravel_minimum_php": { + "12.*": "8.2", + "13.*": "8.3" + } + }, + "cells": [ + {"php": "8.2", "laravel": "12.*"}, + {"php": "8.3", "laravel": "12.*"}, + {"php": "8.3", "laravel": "13.*"}, + {"php": "8.4", "laravel": "12.*"}, + {"php": "8.4", "laravel": "13.*"} + ] + }, + "state_ownership": { + "v1": { + "engine": "v1", + "tables": [ + "workflows", + "workflow_logs", + "workflow_signals", + "workflow_timers", + "workflow_exceptions", + "workflow_relationships" + ], + "history_policy": "finish_on_v1", + "reinterpret_as_v2": false + }, + "embedded_v2": { + "engine": "embedded_v2", + "root_tables": [ + "workflow_instances", + "workflow_runs", + "workflow_history_events", + "workflow_tasks" + ], + "accepts_v1_history": false + }, + "composer_change_migrates_history": false + }, + "strategies": { + "drain": { + "status_command": "php artisan workflow:v2:upgrade-status --strategy=drain --json", + "new_v1_starts": "stop", + "new_v2_starts": "after_v1_open_runs_reach_zero" + }, + "coexist": { + "status_command": "php artisan workflow:v2:upgrade-status --strategy=coexist --json", + "v2_queue_config": "workflows.v2.queue", + "v2_queue_env": "DW_V2_QUEUE", + "queue_isolation_required": true, + "new_starts": "embedded_v2" + } + }, + "continuity": { + "workflow_type": "explicit_v2_type_key", + "workflow_id": "application_owned_logical_identity", + "payloads": "adapt_application_inputs_to_v2_avro_values", + "queues": "route_by_engine_during_coexistence", + "memo_and_search_metadata": "set_on_v2_start_options", + "signals": "route_to_the_engine_that_owns_the_run", + "updates": "v2_runs_only", + "rollback": { + "composer_command": "composer require durable-workflow/workflow:^1.0 --with-all-dependencies", + "state_policy": "restore_v1_code_and_workers_for_v1_runs_without_replaying_v2_history" + }, + "durable_state_transfer": "none" + }, + "laravel_experience": { + "straight_line_authoring": true, + "constructor_injection": true, + "handle_method_injection": true, + "application_config": "workflows", + "queue_runtime": "laravel_queue", + "logging": "laravel_log", + "events": "laravel_events", + "testing_fake": "Workflow\\V2\\WorkflowStub::fake", + "testing_assertions": [ + "Workflow\\V2\\WorkflowStub::assertDispatched", + "Workflow\\V2\\WorkflowStub::assertSignalSent", + "Workflow\\V2\\WorkflowStub::assertUpdateSent" + ] + }, + "journey": { + "stable_docs": "/docs/migration", + "v2_docs": "/docs/2.0/migration", + "php_sdk_laravel": "https://github.com/durable-workflow/sdk-php#laravel-service-mode" + }, + "qualification": { + "source_script": "scripts/ci/laravel-embedded-upgrade-smoke.sh", + "dependency_security_policy": "resources/laravel-dependency-security-policy.json", + "published_artifacts_required": true, + "clean_application_required": true, + "application_bootstrap_patch_allowed": false, + "eol_framework_fixture_policy": "composer_no_blocking_only_for_disposable_compatibility_qualification" + } +} diff --git a/tests/fixtures/static-credential-consumer/dependency-intent.json b/tests/fixtures/static-credential-consumer/dependency-intent.json index 401da276..aa770764 100644 --- a/tests/fixtures/static-credential-consumer/dependency-intent.json +++ b/tests/fixtures/static-credential-consumer/dependency-intent.json @@ -3,11 +3,11 @@ "requirements": { "php": "8.4.27", "durable-workflow/sdk": "3.10.0", - "laravel/framework": "12.69.3", + "laravel/framework": "12.72.0", "symfony/framework-bundle": "7.4.23" }, "framework_source_references": { - "laravel/framework": "58ea544a2a80dc03c168e13a5dc9a1d176a88717", + "laravel/framework": "b089d3020e508f49b3625726ab4a2c897f1b78d5", "symfony/framework-bundle": "6a749f2c37cb9def53793633ec72958aaad4b359" }, "fixture": {