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": {