diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca9be203..c9a1f884 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -336,20 +336,6 @@ jobs: exercised_package: 'laravel/framework:12.69.0' bootstrap: fresh_supported_install script: laravel.php - - framework: Laravel - version: '12' - php: '8.3' - package: 'laravel/framework:^12.69.0' - exercised_package: 'laravel/framework:12.69.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' - bootstrap: fresh_supported_install - script: laravel.php - framework: Laravel version: '12' php: '8.4' @@ -517,6 +503,184 @@ jobs: DURABLE_WORKFLOW_WORKER_TOKEN=worker-secret \ php "tests/compat/${COMPAT_SCRIPT}" "$consumer/vendor/autoload.php" + framework-compat83: + name: ${{ matrix.framework }} ${{ matrix.version }} + runs-on: ubuntu-24.04 + timeout-minutes: 10 + needs: action-policy + strategy: + fail-fast: false + matrix: + include: + - framework: Laravel + version: '12' + php: '8.3' + package: 'laravel/framework:^12.69.0' + exercised_package: 'laravel/framework:12.69.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' + bootstrap: fresh_supported_install + script: laravel.php + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + - name: Select stock-profile system Python + run: | + /usr/bin/python3 -I -S -c 'import sys; assert sys.version_info[:2] == (3, 12)' + echo /usr/bin >> "$GITHUB_PATH" + - name: Create isolated framework Composer state + run: | + state="$(mktemp -d "$RUNNER_TEMP/framework-composer.XXXXXX")" + mkdir -m 700 "$state/home" "$state/cache" "$state/tmp" + { + echo "COMPOSER_HOME=$state/home" + echo "COMPOSER_CACHE_DIR=$state/cache" + echo "TMPDIR=$state/tmp" + } >> "$GITHUB_ENV" + - name: Select authenticated dependency trust + uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@242c2a7fb23459db2e1ada5f84dbee6d61d9b1e5 # clean-trust + with: + clients: pip,python,composer,curl,git + - name: Disable APT download retries (CVE-2026-109518) + run: echo 'Acquire::Retries "0";' | sudo tee /etc/apt/apt.conf.d/99-no-retries >/dev/null + - name: Set up the qualified stock PHP 8.3 framework profile + uses: durable-workflow/.github/.github/actions/setup-php-linux@411a14ac07d32073eed6ab4977f2825eba4739a7 # bound-linux-php + with: + php-version: '8.3' + ini-file: production + tools: composer:2.10.3,phpunit:11.5.57 + coverage: none + - name: Require the actual stock framework modules + run: | + php -r 'if (PHP_MAJOR_VERSION !== 8 || PHP_MINOR_VERSION !== 3) { throw new RuntimeException("Expected PHP 8.3"); } + foreach (["curl", "mbstring", "pdo_sqlite", "sqlite3", "xml"] as $extension) { + if (!extension_loaded($extension)) { throw new RuntimeException("Missing ".$extension); } + }' + - name: Verify Composer trust before requests + uses: durable-workflow/.github/.github/actions/verify-composer-trust@242c2a7fb23459db2e1ada5f84dbee6d61d9b1e5 # clean-trust + - name: Install SDK into a clean framework consumer + env: + FRAMEWORK_BOOTSTRAP: ${{ matrix.bootstrap }} + FRAMEWORK_PACKAGE: ${{ matrix.package }} + FRAMEWORK_EXERCISED_PACKAGE: ${{ matrix.exercised_package }} + FRAMEWORK_CONSOLE_PACKAGE: ${{ matrix.console_package }} + COMPAT_SCRIPT: ${{ matrix.script }} + run: | + consumer="$(mktemp -d)" + printf '%s\n' "$consumer" > "$RUNNER_TEMP/framework-consumer" + cd "$consumer" + composer init --name=durable-workflow/framework-compat --no-interaction + composer config repositories.sdk path "$GITHUB_WORKSPACE" + if [ "$COMPAT_SCRIPT" = laravel.php ]; then + bootstrap_arguments=() + if [ "$FRAMEWORK_BOOTSTRAP" = existing_supported_lock ]; then + # Model the established application named by the adoption contract. + # Composer may refuse a new installation of an end-of-life Laravel + # line, but SDK installation still runs with normal policy against + # the resulting existing lock file. + bootstrap_arguments+=(--no-blocking) + fi + # matrix.package is the published caret support range. Install + # exactly its minimum release, and prove the SDK does not move it. + exercised_version="${FRAMEWORK_EXERCISED_PACKAGE#laravel/framework:}" + composer require "$FRAMEWORK_EXERCISED_PACKAGE" \ + --no-interaction --prefer-dist --no-progress "${bootstrap_arguments[@]}" + php "$GITHUB_WORKSPACE/scripts/ci/assert-installed-version.php" "$consumer" laravel/framework "$exercised_version" + composer require durable-workflow/sdk:@dev \ + --no-interaction --prefer-dist --no-progress + php "$GITHUB_WORKSPACE/scripts/ci/assert-installed-version.php" "$consumer" laravel/framework "$exercised_version" + else + framework_packages=("$FRAMEWORK_PACKAGE") + if [ -n "$FRAMEWORK_CONSOLE_PACKAGE" ]; then + framework_packages+=("$FRAMEWORK_CONSOLE_PACKAGE") + fi + composer require durable-workflow/sdk:@dev "${framework_packages[@]}" \ + --no-interaction --prefer-dist --with-all-dependencies + fi + composer require --dev phpstan/phpstan:^2.0 'monolog/monolog:^2.9|^3.0' --no-interaction --prefer-dist + if [ "$COMPAT_SCRIPT" = laravel.php ]; then + php "$GITHUB_WORKSPACE/scripts/ci/assert-installed-version.php" "$consumer" laravel/framework "$exercised_version" + fi + - name: Verify Composer trust before requests + uses: durable-workflow/.github/.github/actions/verify-composer-trust@242c2a7fb23459db2e1ada5f84dbee6d61d9b1e5 # clean-trust + - name: Install SDK into a fresh Laravel application for cached role isolation + if: matrix.framework == 'Laravel' && matrix.version == '13' && matrix.php == '8.4' + env: + FRAMEWORK_EXERCISED_PACKAGE: ${{ matrix.exercised_package }} + run: | + application="$RUNNER_TEMP/laravel-fresh-config-cache" + exercised_version="${FRAMEWORK_EXERCISED_PACKAGE#laravel/framework:}" + composer create-project laravel/laravel:^13.0 "$application" \ + --no-interaction --prefer-dist --no-progress + composer --working-dir="$application" require "$FRAMEWORK_EXERCISED_PACKAGE" \ + --with-all-dependencies --no-interaction --prefer-dist --no-progress + php scripts/ci/assert-installed-version.php "$application" laravel/framework "$exercised_version" + composer --working-dir="$application" config repositories.sdk path "$GITHUB_WORKSPACE" + composer --working-dir="$application" require durable-workflow/sdk:@dev \ + --no-interaction --prefer-dist --no-progress + php scripts/ci/assert-installed-version.php "$application" laravel/framework "$exercised_version" + - name: Exercise fresh Laravel cached role isolation + if: matrix.framework == 'Laravel' && matrix.version == '13' && matrix.php == '8.4' + run: | + bash tests/compat/laravel-fresh-config-cache.sh "$RUNNER_TEMP/laravel-fresh-config-cache" + - name: Exercise native container, diagnostics, and testing integration + env: + COMPAT_SCRIPT: ${{ matrix.script }} + run: | + consumer="$(cat "$RUNNER_TEMP/framework-consumer")" + if [ "$COMPAT_SCRIPT" = laravel.php ]; then + DURABLE_WORKFLOW_CLIENT_TOKEN=client-secret \ + DURABLE_WORKFLOW_WORKER_TOKEN=worker-secret \ + php "tests/compat/${COMPAT_SCRIPT}" "$consumer/vendor/autoload.php" + php tests/compat/laravel-config-cache.php "$consumer/vendor/autoload.php" + bridge_path=src/Bridge/Laravel + compatibility_scripts=( + "tests/compat/${COMPAT_SCRIPT}" + tests/compat/laravel-config-cache.php + tests/compat/laravel-fresh-config-cache.php + tests/compat/laravel-fresh-role-launch.php + ) + else + php "tests/compat/${COMPAT_SCRIPT}" "$consumer/vendor/autoload.php" + bridge_path=src/Bridge/Symfony + compatibility_scripts=("tests/compat/${COMPAT_SCRIPT}") + fi + compatibility_scripts+=(tests/compat/history-cache.php tests/compat/execution-telemetry.php) + php tests/compat/static-credentials.php "$consumer/vendor/autoload.php" "$COMPAT_SCRIPT" + "$consumer/vendor/bin/phpstan" analyse -c phpstan-framework.neon \ + --autoload-file="$consumer/vendor/autoload.php" \ + src/Bridge/FrameworkRuntimeException.php \ + src/Bridge/ServiceConfiguration.php src/Bridge/Event \ + "$bridge_path" "${compatibility_scripts[@]}" + - name: Verify Composer trust before requests + uses: durable-workflow/.github/.github/actions/verify-composer-trust@242c2a7fb23459db2e1ada5f84dbee6d61d9b1e5 # clean-trust + - name: Exercise optional tracing in framework workers + if: >- + (matrix.framework == 'Laravel' && matrix.version == '12' && matrix.php == '8.2') || + (matrix.framework == 'Laravel' && matrix.version == '13' && matrix.php == '8.4') || + (matrix.framework == 'Symfony' && matrix.version == '6.4') + env: + COMPAT_SCRIPT: ${{ matrix.script }} + FRAMEWORK_EXERCISED_PACKAGE: ${{ matrix.exercised_package }} + DURABLE_WORKFLOW_TEST_TRACING: '1' + run: | + consumer="$(cat "$RUNNER_TEMP/framework-consumer")" + composer --working-dir="$consumer" require --dev open-telemetry/api:1.10.1 'monolog/monolog:^3.0' \ + --no-interaction --prefer-dist --no-progress + if [ "$COMPAT_SCRIPT" = laravel.php ]; then + php scripts/ci/assert-installed-version.php "$consumer" laravel/framework \ + "${FRAMEWORK_EXERCISED_PACKAGE#laravel/framework:}" + fi + DURABLE_WORKFLOW_CLIENT_TOKEN=client-secret \ + DURABLE_WORKFLOW_WORKER_TOKEN=worker-secret \ + php "tests/compat/${COMPAT_SCRIPT}" "$consumer/vendor/autoload.php" + laravel-transition-compat: name: Laravel ${{ matrix.source }} to service mode runs-on: ubuntu-24.04 @@ -1103,6 +1267,7 @@ jobs: - test-php83 - carbon-2 - framework-compat + - framework-compat83 - laravel-transition-compat - analyse - docs @@ -1124,6 +1289,7 @@ jobs: PHP_83_RESULT: ${{ needs.test-php83.result }} CARBON_2_RESULT: ${{ needs.carbon-2.result }} FRAMEWORK_RESULT: ${{ needs.framework-compat.result }} + FRAMEWORK_83_RESULT: ${{ needs.framework-compat83.result }} LARAVEL_TRANSITION_RESULT: ${{ needs.laravel-transition-compat.result }} ANALYSIS_RESULT: ${{ needs.analyse.result }} DOCS_RESULT: ${{ needs.docs.result }} @@ -1135,6 +1301,7 @@ jobs: test "$PHP_83_RESULT" = success test "$CARBON_2_RESULT" = success test "$FRAMEWORK_RESULT" = success + test "$FRAMEWORK_83_RESULT" = success test "$LARAVEL_TRANSITION_RESULT" = success test "$ANALYSIS_RESULT" = success test "$DOCS_RESULT" = success diff --git a/tests/LaravelAdoptionContractTest.php b/tests/LaravelAdoptionContractTest.php index 2452526f..ccd1da52 100644 --- a/tests/LaravelAdoptionContractTest.php +++ b/tests/LaravelAdoptionContractTest.php @@ -113,6 +113,17 @@ static function (string $workflow) use ($search, $replace): string { 'composer require "$FRAMEWORK_EXERCISED_PACKAGE" \\', 'composer require "$FRAMEWORK_PACKAGE" \\', ), 'The Laravel consumer must install exactly $FRAMEWORK_EXERCISED_PACKAGE.']; + yield 'stock 8.3 job installs a caret instead of the exact minimum' => [static function (string $workflow): string { + $offset = strpos($workflow, ' framework-compat83:'); + if ($offset === false) { + return $workflow; + } + return substr($workflow, 0, $offset).str_replace( + 'composer require "$FRAMEWORK_EXERCISED_PACKAGE" \\', + 'composer require "$FRAMEWORK_PACKAGE" \\', + substr($workflow, $offset), + ); + }, 'The Laravel consumer must install exactly $FRAMEWORK_EXERCISED_PACKAGE.']; yield 'exact minimum removed from a cell' => [$first( " exercised_package: 'laravel/framework:12.69.0'\n", '', @@ -158,10 +169,29 @@ private static function ciMinimumQualificationErrors(string $workflow, array $ma ); $cells = array_map(static fn (array $match): array => ['laravel' => $match[1], 'php' => $match[2], 'package' => $match[3], 'exercised_package' => $match[4], 'bootstrap' => $match[5]], $matches); + $order = static fn (array $left, array $right): int => + [$left['laravel'], $left['php']] <=> [$right['laravel'], $right['php']]; + usort($cells, $order); + usort($matrix, $order); if ($cells !== $matrix || count($matches) !== substr_count($workflow, "- framework: Laravel\n")) { $errors[] = 'The CI Laravel cells differ from the adoption contract.'; } + preg_match_all('/^ framework-compat(?:83)?:\n.*?(?=^ [a-zA-Z0-9_-]+:\n|\z)/ms', $workflow, $jobs); + if (count($jobs[0]) !== 2) { + $errors[] = 'Both framework jobs must qualify their Laravel cells.'; + } + foreach ($jobs[0] as $job) { + $errors = array_merge($errors, self::ciFrameworkJobMinimumErrors($job)); + } + + return $errors; + } + + /** @return list */ + private static function ciFrameworkJobMinimumErrors(string $workflow): array + { + $errors = []; $install = self::between($workflow, ' - name: Install SDK into a clean framework consumer', ' else'); $assert = 'php "$GITHUB_WORKSPACE/scripts/ci/assert-installed-version.php" "$consumer" laravel/framework "$exercised_version"'; $require = strpos($install, 'composer require "$FRAMEWORK_EXERCISED_PACKAGE" \\');