From 0a5359e7f22863187ac4e7ee5ad72578c13b4c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 2 Sep 2026 16:58:09 +0200 Subject: [PATCH 01/11] Switch back to ext-mongodb latest stable after the release of 2.5.0 --- .github/workflows/coding-standards.yml | 4 +--- .github/workflows/generator.yml | 4 +--- .github/workflows/static-analysis.yml | 4 +--- .github/workflows/tests.yml | 4 +--- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 900b66ce1..6589a87eb 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -13,9 +13,7 @@ on: env: PHP_VERSION: "8.2" - # TODO: change to "stable" once 2.5.0 is released - # DRIVER_VERSION: "stable" - DRIVER_VERSION: "mongodb/mongo-php-driver@v2.x" + DRIVER_VERSION: "stable" jobs: phpcs: diff --git a/.github/workflows/generator.yml b/.github/workflows/generator.yml index 9707aa804..f9894f259 100644 --- a/.github/workflows/generator.yml +++ b/.github/workflows/generator.yml @@ -13,9 +13,7 @@ on: env: PHP_VERSION: "8.2" - # TODO: change to "stable" once 2.5.0 is released - # DRIVER_VERSION: "stable" - DRIVER_VERSION: "mongodb/mongo-php-driver@v2.x" + DRIVER_VERSION: "stable" jobs: diff: diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 34a798915..f55c0c6ec 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -19,9 +19,7 @@ on: env: PHP_VERSION: "8.5" - # TODO: change to "stable" once 2.5.0 is released - # DRIVER_VERSION: "stable" - DRIVER_VERSION: "mongodb/mongo-php-driver@v2.x" + DRIVER_VERSION: "stable" jobs: psalm: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 084acb434..99189f2c2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,9 +12,7 @@ on: - "feature/*" env: - # TODO: change to "stable" once 2.5.0 is released - # DRIVER_VERSION: "stable" - DRIVER_VERSION: "mongodb/mongo-php-driver@v2.x" + DRIVER_VERSION: "stable" jobs: phpunit: From 92fa68c97c377bd4c40e27a6771977ffde10d7af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 2 Sep 2026 17:11:22 +0200 Subject: [PATCH 02/11] Pin extension 2.5.0 for lowest and v2.5 for next-stable builds --- .../generated/build/build-extension.yml | 45 +++++-------------- .../templates/build/build-extension.yml | 9 +--- 2 files changed, 12 insertions(+), 42 deletions(-) diff --git a/.evergreen/config/generated/build/build-extension.yml b/.evergreen/config/generated/build/build-extension.yml index 0cb05718f..e51880c58 100644 --- a/.evergreen/config/generated/build/build-extension.yml +++ b/.evergreen/config/generated/build/build-extension.yml @@ -7,9 +7,6 @@ tasks: vars: PHP_VERSION: "8.5" - func: "compile extension" - # TODO: Remove vars to switch to latest stable version when 2.5.0 is released - vars: - EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.5-lowest" tags: ["build", "php8.5", "lowest", "pr", "tag"] @@ -18,9 +15,8 @@ tasks: vars: PHP_VERSION: "8.5" - func: "compile extension" - # TODO: Switch to EXTENSION_VERSION: "2.5.0" when 2.5.0 is released on PECL vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_VERSION: "2.5.0" - func: "upload extension" - name: "build-php-8.5-next-stable" tags: ["build", "php8.5", "next-stable", "pr", "tag"] @@ -29,9 +25,8 @@ tasks: vars: PHP_VERSION: "8.5" - func: "compile extension" - # TODO: Switch to EXTENSION_VERSION: "2.5.0" when 2.5.0 is released on PECL vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_BRANCH: "v2.5" - func: "upload extension" - name: "build-php-8.5-next-minor" tags: ["build", "php8.5", "next-minor"] @@ -50,9 +45,6 @@ tasks: vars: PHP_VERSION: "8.4" - func: "compile extension" - # TODO: Remove vars to switch to latest stable version when 2.5.0 is released - vars: - EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.4-lowest" tags: ["build", "php8.4", "lowest", "pr", "tag"] @@ -61,9 +53,8 @@ tasks: vars: PHP_VERSION: "8.4" - func: "compile extension" - # TODO: Switch to EXTENSION_VERSION: "2.5.0" when 2.5.0 is released on PECL vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_VERSION: "2.5.0" - func: "upload extension" - name: "build-php-8.4-next-stable" tags: ["build", "php8.4", "next-stable", "pr", "tag"] @@ -72,9 +63,8 @@ tasks: vars: PHP_VERSION: "8.4" - func: "compile extension" - # TODO: Switch to EXTENSION_VERSION: "2.5.0" when 2.5.0 is released on PECL vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_BRANCH: "v2.5" - func: "upload extension" - name: "build-php-8.4-next-minor" tags: ["build", "php8.4", "next-minor"] @@ -93,9 +83,6 @@ tasks: vars: PHP_VERSION: "8.3" - func: "compile extension" - # TODO: Remove vars to switch to latest stable version when 2.5.0 is released - vars: - EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.3-lowest" tags: ["build", "php8.3", "lowest", "pr", "tag"] @@ -104,9 +91,8 @@ tasks: vars: PHP_VERSION: "8.3" - func: "compile extension" - # TODO: Switch to EXTENSION_VERSION: "2.5.0" when 2.5.0 is released on PECL vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_VERSION: "2.5.0" - func: "upload extension" - name: "build-php-8.3-next-stable" tags: ["build", "php8.3", "next-stable", "pr", "tag"] @@ -115,9 +101,8 @@ tasks: vars: PHP_VERSION: "8.3" - func: "compile extension" - # TODO: Switch to EXTENSION_VERSION: "2.5.0" when 2.5.0 is released on PECL vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_BRANCH: "v2.5" - func: "upload extension" - name: "build-php-8.3-next-minor" tags: ["build", "php8.3", "next-minor"] @@ -136,9 +121,6 @@ tasks: vars: PHP_VERSION: "8.2" - func: "compile extension" - # TODO: Remove vars to switch to latest stable version when 2.5.0 is released - vars: - EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.2-lowest" tags: ["build", "php8.2", "lowest", "pr", "tag"] @@ -147,9 +129,8 @@ tasks: vars: PHP_VERSION: "8.2" - func: "compile extension" - # TODO: Switch to EXTENSION_VERSION: "2.5.0" when 2.5.0 is released on PECL vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_VERSION: "2.5.0" - func: "upload extension" - name: "build-php-8.2-next-stable" tags: ["build", "php8.2", "next-stable", "pr", "tag"] @@ -158,9 +139,8 @@ tasks: vars: PHP_VERSION: "8.2" - func: "compile extension" - # TODO: Switch to EXTENSION_VERSION: "2.5.0" when 2.5.0 is released on PECL vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_BRANCH: "v2.5" - func: "upload extension" - name: "build-php-8.2-next-minor" tags: ["build", "php8.2", "next-minor"] @@ -179,9 +159,6 @@ tasks: vars: PHP_VERSION: "8.1" - func: "compile extension" - # TODO: Remove vars to switch to latest stable version when 2.5.0 is released - vars: - EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-8.1-lowest" tags: ["build", "php8.1", "lowest", "pr", "tag"] @@ -190,9 +167,8 @@ tasks: vars: PHP_VERSION: "8.1" - func: "compile extension" - # TODO: Switch to EXTENSION_VERSION: "2.5.0" when 2.5.0 is released on PECL vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_VERSION: "2.5.0" - func: "upload extension" - name: "build-php-8.1-next-stable" tags: ["build", "php8.1", "next-stable", "pr", "tag"] @@ -201,9 +177,8 @@ tasks: vars: PHP_VERSION: "8.1" - func: "compile extension" - # TODO: Switch to EXTENSION_VERSION: "2.5.0" when 2.5.0 is released on PECL vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_BRANCH: "v2.5" - func: "upload extension" - name: "build-php-8.1-next-minor" tags: ["build", "php8.1", "next-minor"] diff --git a/.evergreen/config/templates/build/build-extension.yml b/.evergreen/config/templates/build/build-extension.yml index 0ab0872ea..58d616c5b 100644 --- a/.evergreen/config/templates/build/build-extension.yml +++ b/.evergreen/config/templates/build/build-extension.yml @@ -5,9 +5,6 @@ vars: PHP_VERSION: "%phpVersion%" - func: "compile extension" - # TODO: Remove vars to switch to latest stable version when 2.5.0 is released - vars: - EXTENSION_BRANCH: "v2.x" - func: "upload extension" - name: "build-php-%phpVersion%-lowest" tags: ["build", "php%phpVersion%", "lowest", "pr", "tag"] @@ -16,9 +13,8 @@ vars: PHP_VERSION: "%phpVersion%" - func: "compile extension" - # TODO: Switch to EXTENSION_VERSION: "2.5.0" when 2.5.0 is released on PECL vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_VERSION: "2.5.0" - func: "upload extension" - name: "build-php-%phpVersion%-next-stable" tags: ["build", "php%phpVersion%", "next-stable", "pr", "tag"] @@ -27,9 +23,8 @@ vars: PHP_VERSION: "%phpVersion%" - func: "compile extension" - # TODO: Switch to EXTENSION_VERSION: "2.5.0" when 2.5.0 is released on PECL vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_BRANCH: "v2.5" - func: "upload extension" - name: "build-php-%phpVersion%-next-minor" tags: ["build", "php%phpVersion%", "next-minor"] From 790b42816b78471b1894429cce4c888d094ee857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 2 Sep 2026 17:14:48 +0200 Subject: [PATCH 03/11] Test the lowest supported extension version on GitHub Actions --- .github/workflows/tests.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 99189f2c2..d7ba55d4e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,6 +13,8 @@ on: env: DRIVER_VERSION: "stable" + # Lowest version of the extension required in composer.json + DRIVER_VERSION_LOWEST: "2.5.0" jobs: phpunit: @@ -44,19 +46,22 @@ jobs: php-version: "8.4" mongodb-version: "9.0" topology: "sharded_cluster" - # Test lowest server/php versions + # Test lowest server/php/extension versions - os: "ubuntu-22.04" php-version: "8.1" mongodb-version: "6.0" topology: "server" + driver-lowest: true - os: "ubuntu-22.04" php-version: "8.1" mongodb-version: "6.0" topology: "replica_set" + driver-lowest: true - os: "ubuntu-22.04" php-version: "8.1" mongodb-version: "6.0" topology: "sharded_cluster" + driver-lowest: true steps: - name: "Checkout" @@ -80,7 +85,7 @@ jobs: uses: "./.github/actions/setup" with: php-version: ${{ matrix.php-version }} - driver-version: ${{ env.DRIVER_VERSION }} + driver-version: ${{ matrix.driver-lowest && env.DRIVER_VERSION_LOWEST || env.DRIVER_VERSION }} php-ini-values: "zend.assertions=1" ignore-platform-req: ${{ matrix.php-version == '8.5' && 'true' || 'false' }} From b7a096902fde844788fa9afd7a70bc57fb49af47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 2 Sep 2026 17:59:38 +0200 Subject: [PATCH 04/11] Resolve the extension version from composer.json in CI Evergreen build tasks now select a symbolic EXTENSION_TARGET, resolved by compile-extension.sh, so no extension version number is left in the CI configuration. GitHub Actions gets a dedicated job for the lowest supported extension version. Switching to an unreleased extension is now a single variable per CI system, and a new check fails if it is left enabled. --- .evergreen/compile-extension.sh | 64 ++++++++++++++++++ .../generated/build/build-extension.yml | 40 ++++++----- .../templates/build/build-extension.yml | 8 ++- .github/actions/setup/action.yml | 55 +++++++++++++-- .github/workflows/coding-standards.yml | 3 - .github/workflows/generator.yml | 31 ++++++++- .github/workflows/static-analysis.yml | 2 - .github/workflows/tests.yml | 23 ++++--- CONTRIBUTING.md | 67 +++++++++++++++++++ RELEASING.md | 21 ++---- 10 files changed, 260 insertions(+), 54 deletions(-) diff --git a/.evergreen/compile-extension.sh b/.evergreen/compile-extension.sh index c15baaf1d..ee80bfc4b 100644 --- a/.evergreen/compile-extension.sh +++ b/.evergreen/compile-extension.sh @@ -3,6 +3,69 @@ set -o errexit # Exit the script with error if any of the commands fail PATH="$PHP_PATH/bin:$PATH" +# Branch of the latest stable minor version of the extension. +EXTENSION_STABLE_BRANCH="v2.5" + +# Branch of the next minor version of the extension, still in development. +EXTENSION_NEXT_MINOR_BRANCH="v2.x" + +# Set this to a branch name (e.g. "v2.x") to compile every task from source +# while the library depends on an unreleased version of the extension. This +# also overrides the "lowest" target, as the lowest released version cannot run +# the library code in that case. Reset to an empty value once the extension is +# released. See the "Continuous integration" section of CONTRIBUTING.md. +EXTENSION_DEV_BRANCH="" + +# Lowest version of the extension allowed by the composer.json constraint. +lowest_extension_version () +{ + php -r ' + $constraint = json_decode(file_get_contents($argv[1]), true)["require"]["ext-mongodb"]; + + if (!preg_match("/^\^(\d+)\.(\d+)(?:\.(\d+))?$/", $constraint, $matches)) { + fwrite(STDERR, sprintf("Unsupported ext-mongodb constraint: %s\n", $constraint)); + exit(1); + } + + printf("%d.%d.%d", $matches[1], $matches[2], $matches[3] ?? 0); + ' ${PROJECT_DIRECTORY}/composer.json +} + +# Turn EXTENSION_TARGET into the EXTENSION_BRANCH or EXTENSION_VERSION expected +# by install_extension. Both variables can also be set explicitly, e.g. in a +# patch build, in which case they take precedence over EXTENSION_TARGET. +resolve_extension_target () +{ + if [ "x${EXTENSION_BRANCH}" != "x" ] || [ "x${EXTENSION_VERSION}" != "x" ]; then + return + fi + + if [ "x${EXTENSION_DEV_BRANCH}" != "x" ]; then + EXTENSION_BRANCH="${EXTENSION_DEV_BRANCH}" + + return + fi + + case "${EXTENSION_TARGET:-stable}" in + stable) + # Latest release from PECL, i.e. the highest version allowed by composer.json + ;; + lowest) + EXTENSION_VERSION=$(lowest_extension_version) + ;; + next-stable) + EXTENSION_BRANCH="${EXTENSION_STABLE_BRANCH}" + ;; + next-minor) + EXTENSION_BRANCH="${EXTENSION_NEXT_MINOR_BRANCH}" + ;; + *) + echo "Unknown EXTENSION_TARGET: ${EXTENSION_TARGET}" >&2 + exit 1 + ;; + esac +} + install_extension () { rm -f ${PHP_PATH}/lib/php.ini @@ -45,4 +108,5 @@ install_extension () php --ri mongodb } +resolve_extension_target install_extension diff --git a/.evergreen/config/generated/build/build-extension.yml b/.evergreen/config/generated/build/build-extension.yml index e51880c58..fdc85c836 100644 --- a/.evergreen/config/generated/build/build-extension.yml +++ b/.evergreen/config/generated/build/build-extension.yml @@ -7,6 +7,8 @@ tasks: vars: PHP_VERSION: "8.5" - func: "compile extension" + vars: + EXTENSION_TARGET: "stable" - func: "upload extension" - name: "build-php-8.5-lowest" tags: ["build", "php8.5", "lowest", "pr", "tag"] @@ -16,7 +18,7 @@ tasks: PHP_VERSION: "8.5" - func: "compile extension" vars: - EXTENSION_VERSION: "2.5.0" + EXTENSION_TARGET: "lowest" - func: "upload extension" - name: "build-php-8.5-next-stable" tags: ["build", "php8.5", "next-stable", "pr", "tag"] @@ -26,7 +28,7 @@ tasks: PHP_VERSION: "8.5" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.5" + EXTENSION_TARGET: "next-stable" - func: "upload extension" - name: "build-php-8.5-next-minor" tags: ["build", "php8.5", "next-minor"] @@ -36,7 +38,7 @@ tasks: PHP_VERSION: "8.5" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_TARGET: "next-minor" - func: "upload extension" - name: "build-php-8.4" tags: ["build", "php8.4", "stable", "pr", "tag"] @@ -45,6 +47,8 @@ tasks: vars: PHP_VERSION: "8.4" - func: "compile extension" + vars: + EXTENSION_TARGET: "stable" - func: "upload extension" - name: "build-php-8.4-lowest" tags: ["build", "php8.4", "lowest", "pr", "tag"] @@ -54,7 +58,7 @@ tasks: PHP_VERSION: "8.4" - func: "compile extension" vars: - EXTENSION_VERSION: "2.5.0" + EXTENSION_TARGET: "lowest" - func: "upload extension" - name: "build-php-8.4-next-stable" tags: ["build", "php8.4", "next-stable", "pr", "tag"] @@ -64,7 +68,7 @@ tasks: PHP_VERSION: "8.4" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.5" + EXTENSION_TARGET: "next-stable" - func: "upload extension" - name: "build-php-8.4-next-minor" tags: ["build", "php8.4", "next-minor"] @@ -74,7 +78,7 @@ tasks: PHP_VERSION: "8.4" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_TARGET: "next-minor" - func: "upload extension" - name: "build-php-8.3" tags: ["build", "php8.3", "stable", "pr", "tag"] @@ -83,6 +87,8 @@ tasks: vars: PHP_VERSION: "8.3" - func: "compile extension" + vars: + EXTENSION_TARGET: "stable" - func: "upload extension" - name: "build-php-8.3-lowest" tags: ["build", "php8.3", "lowest", "pr", "tag"] @@ -92,7 +98,7 @@ tasks: PHP_VERSION: "8.3" - func: "compile extension" vars: - EXTENSION_VERSION: "2.5.0" + EXTENSION_TARGET: "lowest" - func: "upload extension" - name: "build-php-8.3-next-stable" tags: ["build", "php8.3", "next-stable", "pr", "tag"] @@ -102,7 +108,7 @@ tasks: PHP_VERSION: "8.3" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.5" + EXTENSION_TARGET: "next-stable" - func: "upload extension" - name: "build-php-8.3-next-minor" tags: ["build", "php8.3", "next-minor"] @@ -112,7 +118,7 @@ tasks: PHP_VERSION: "8.3" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_TARGET: "next-minor" - func: "upload extension" - name: "build-php-8.2" tags: ["build", "php8.2", "stable", "pr", "tag"] @@ -121,6 +127,8 @@ tasks: vars: PHP_VERSION: "8.2" - func: "compile extension" + vars: + EXTENSION_TARGET: "stable" - func: "upload extension" - name: "build-php-8.2-lowest" tags: ["build", "php8.2", "lowest", "pr", "tag"] @@ -130,7 +138,7 @@ tasks: PHP_VERSION: "8.2" - func: "compile extension" vars: - EXTENSION_VERSION: "2.5.0" + EXTENSION_TARGET: "lowest" - func: "upload extension" - name: "build-php-8.2-next-stable" tags: ["build", "php8.2", "next-stable", "pr", "tag"] @@ -140,7 +148,7 @@ tasks: PHP_VERSION: "8.2" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.5" + EXTENSION_TARGET: "next-stable" - func: "upload extension" - name: "build-php-8.2-next-minor" tags: ["build", "php8.2", "next-minor"] @@ -150,7 +158,7 @@ tasks: PHP_VERSION: "8.2" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_TARGET: "next-minor" - func: "upload extension" - name: "build-php-8.1" tags: ["build", "php8.1", "stable", "pr", "tag"] @@ -159,6 +167,8 @@ tasks: vars: PHP_VERSION: "8.1" - func: "compile extension" + vars: + EXTENSION_TARGET: "stable" - func: "upload extension" - name: "build-php-8.1-lowest" tags: ["build", "php8.1", "lowest", "pr", "tag"] @@ -168,7 +178,7 @@ tasks: PHP_VERSION: "8.1" - func: "compile extension" vars: - EXTENSION_VERSION: "2.5.0" + EXTENSION_TARGET: "lowest" - func: "upload extension" - name: "build-php-8.1-next-stable" tags: ["build", "php8.1", "next-stable", "pr", "tag"] @@ -178,7 +188,7 @@ tasks: PHP_VERSION: "8.1" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.5" + EXTENSION_TARGET: "next-stable" - func: "upload extension" - name: "build-php-8.1-next-minor" tags: ["build", "php8.1", "next-minor"] @@ -188,5 +198,5 @@ tasks: PHP_VERSION: "8.1" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_TARGET: "next-minor" - func: "upload extension" diff --git a/.evergreen/config/templates/build/build-extension.yml b/.evergreen/config/templates/build/build-extension.yml index 58d616c5b..26ca0d5b4 100644 --- a/.evergreen/config/templates/build/build-extension.yml +++ b/.evergreen/config/templates/build/build-extension.yml @@ -5,6 +5,8 @@ vars: PHP_VERSION: "%phpVersion%" - func: "compile extension" + vars: + EXTENSION_TARGET: "stable" - func: "upload extension" - name: "build-php-%phpVersion%-lowest" tags: ["build", "php%phpVersion%", "lowest", "pr", "tag"] @@ -14,7 +16,7 @@ PHP_VERSION: "%phpVersion%" - func: "compile extension" vars: - EXTENSION_VERSION: "2.5.0" + EXTENSION_TARGET: "lowest" - func: "upload extension" - name: "build-php-%phpVersion%-next-stable" tags: ["build", "php%phpVersion%", "next-stable", "pr", "tag"] @@ -24,7 +26,7 @@ PHP_VERSION: "%phpVersion%" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.5" + EXTENSION_TARGET: "next-stable" - func: "upload extension" - name: "build-php-%phpVersion%-next-minor" tags: ["build", "php%phpVersion%", "next-minor"] @@ -34,5 +36,5 @@ PHP_VERSION: "%phpVersion%" - func: "compile extension" vars: - EXTENSION_BRANCH: "v2.x" + EXTENSION_TARGET: "next-minor" - func: "upload extension" diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index dfb380a09..a366c3394 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -5,8 +5,9 @@ inputs: description: "PHP version to install" required: true driver-version: - description: "MongoDB extension version to install" - required: true + description: "MongoDB extension version to install: \"stable\" for the highest version allowed by composer.json, \"lowest\" for the minimum required version" + required: false + default: "stable" php-ini-values: description: "INI values to pass along to setup-php action" required: false @@ -22,15 +23,47 @@ inputs: runs: using: composite steps: + - name: Resolve extension version + id: driver + shell: bash + # Set DEV_BRANCH to a branch name (e.g. "v2.x") to build the extension + # from source while the library depends on an unreleased version. This + # also overrides the "lowest" version. Reset it to an empty value once the + # extension is released. See the "Continuous integration" section of + # CONTRIBUTING.md. + run: | + DEV_BRANCH="" + + if [ -n "$DEV_BRANCH" ]; then + # Source builds cannot be cached, and caching a moving branch under a + # stable key would freeze an outdated extension. + echo "version=mongodb/mongo-php-driver@${DEV_BRANCH}" >> "$GITHUB_OUTPUT" + echo "cacheable=false" >> "$GITHUB_OUTPUT" + elif [ "${{ inputs.driver-version }}" = "lowest" ]; then + CONSTRAINT=$(jq -er '.require["ext-mongodb"]' composer.json) + VERSION=${CONSTRAINT#^} + case "$VERSION" in + *.*.*) ;; + *) VERSION="${VERSION}.0" ;; + esac + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "cacheable=true" >> "$GITHUB_OUTPUT" + else + echo "version=${{ inputs.driver-version }}" >> "$GITHUB_OUTPUT" + echo "cacheable=true" >> "$GITHUB_OUTPUT" + fi + - name: Setup cache environment id: extcache + if: steps.driver.outputs.cacheable == 'true' uses: shivammathur/cache-extensions@v1 with: php-version: ${{ inputs.php-version }} - extensions: "mongodb-${{ inputs.driver-version }}" - key: "extcache-${{ inputs.driver-version }}-${{ hashFiles('composer.json') }}" + extensions: "mongodb-${{ steps.driver.outputs.version }}" + key: "extcache-${{ steps.driver.outputs.version }}-${{ hashFiles('composer.json') }}" - name: Cache extensions + if: steps.driver.outputs.cacheable == 'true' uses: actions/cache@v4 with: path: ${{ steps.extcache.outputs.dir }} @@ -41,7 +74,7 @@ runs: uses: shivammathur/setup-php@v2 with: coverage: xdebug - extensions: "mongodb-${{ inputs.driver-version }}" + extensions: "mongodb-${{ steps.driver.outputs.version }}" php-version: "${{ inputs.php-version }}" tools: cs2pr ini-values: "${{ inputs.php-ini-values }}" @@ -50,6 +83,18 @@ runs: run: "php --ri mongodb" shell: bash + - name: Check the expected driver version is installed + if: inputs.driver-version == 'lowest' + shell: bash + run: | + INSTALLED=$(php -r 'echo phpversion("mongodb");') + EXPECTED="${{ steps.driver.outputs.version }}" + + if [ "$INSTALLED" != "$EXPECTED" ]; then + echo "::error::Expected extension version $EXPECTED, got $INSTALLED" + exit 1 + fi + - name: Install dependencies with Composer uses: ramsey/composer-install@3.0.0 with: diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 6589a87eb..a5d4d7ae3 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -13,7 +13,6 @@ on: env: PHP_VERSION: "8.2" - DRIVER_VERSION: "stable" jobs: phpcs: @@ -28,7 +27,6 @@ jobs: uses: "./.github/actions/setup" with: php-version: ${{ env.PHP_VERSION }} - driver-version: ${{ env.DRIVER_VERSION }} # The -q option is required until phpcs v4 is released - name: "Run PHP_CodeSniffer" @@ -46,7 +44,6 @@ jobs: uses: "./.github/actions/setup" with: php-version: ${{ env.PHP_VERSION }} - driver-version: ${{ env.DRIVER_VERSION }} - name: "Run Rector" run: "vendor/bin/rector --ansi --dry-run" diff --git a/.github/workflows/generator.yml b/.github/workflows/generator.yml index f9894f259..e99f5720e 100644 --- a/.github/workflows/generator.yml +++ b/.github/workflows/generator.yml @@ -13,7 +13,6 @@ on: env: PHP_VERSION: "8.2" - DRIVER_VERSION: "stable" jobs: diff: @@ -30,7 +29,6 @@ jobs: uses: "./.github/actions/setup" with: php-version: ${{ env.PHP_VERSION }} - driver-version: ${{ env.DRIVER_VERSION }} working-directory: "generator" - name: "Run Generator" @@ -38,3 +36,32 @@ jobs: - name: "Check file diff" run: git add . -N && git diff --exit-code + + evergreen-config: + name: "Evergreen config diff check" + runs-on: "ubuntu-24.04" + + steps: + - name: "Checkout" + uses: "actions/checkout@v7" + + - name: "Setup PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: ${{ env.PHP_VERSION }} + + - name: "Generate Evergreen config" + run: "php .evergreen/config/generate-config.php" + + - name: "Check file diff" + run: git diff --exit-code .evergreen/config/generated + + # Building the extension from source is only acceptable while a feature + # depends on an unreleased version. It must be reverted before a release. + - name: "Check the extension dev branch is not enabled" + if: github.base_ref != '' && github.base_ref != 'v2.x' + run: | + if ! grep -q '^EXTENSION_DEV_BRANCH=""$' .evergreen/compile-extension.sh || ! grep -q '^ DEV_BRANCH=""$' .github/actions/setup/action.yml; then + echo "::error::The extension dev branch is enabled. Bump ext-mongodb in composer.json and reset the dev branch, see CONTRIBUTING.md." + exit 1 + fi diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index f55c0c6ec..9f1fb89e8 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -19,7 +19,6 @@ on: env: PHP_VERSION: "8.5" - DRIVER_VERSION: "stable" jobs: psalm: @@ -41,7 +40,6 @@ jobs: uses: "./.github/actions/setup" with: php-version: ${{ env.PHP_VERSION }} - driver-version: ${{ env.DRIVER_VERSION }} - name: "Run Psalm" run: "vendor/bin/psalm --show-info=false --stats --output-format=github --threads=$(nproc) --report=psalm.sarif" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d7ba55d4e..4d23e37df 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,11 +11,6 @@ on: - "v*.*" - "feature/*" -env: - DRIVER_VERSION: "stable" - # Lowest version of the extension required in composer.json - DRIVER_VERSION_LOWEST: "2.5.0" - jobs: phpunit: name: "PHPUnit tests" @@ -46,22 +41,30 @@ jobs: php-version: "8.4" mongodb-version: "9.0" topology: "sharded_cluster" - # Test lowest server/php/extension versions + # Test the lowest supported extension version on a recent PHP version. + # This combination must differ from the ones above, otherwise GitHub + # merges it into an existing job instead of adding one. + - os: "ubuntu-24.04" + php-version: "8.3" + mongodb-version: "9.0" + topology: "server" + driver-version: "lowest" + # Test lowest server/php versions - os: "ubuntu-22.04" php-version: "8.1" mongodb-version: "6.0" topology: "server" - driver-lowest: true + driver-version: "lowest" - os: "ubuntu-22.04" php-version: "8.1" mongodb-version: "6.0" topology: "replica_set" - driver-lowest: true + driver-version: "lowest" - os: "ubuntu-22.04" php-version: "8.1" mongodb-version: "6.0" topology: "sharded_cluster" - driver-lowest: true + driver-version: "lowest" steps: - name: "Checkout" @@ -85,7 +88,7 @@ jobs: uses: "./.github/actions/setup" with: php-version: ${{ matrix.php-version }} - driver-version: ${{ matrix.driver-lowest && env.DRIVER_VERSION_LOWEST || env.DRIVER_VERSION }} + driver-version: ${{ matrix.driver-version || 'stable' }} php-ini-values: "zend.assertions=1" ignore-platform-req: ${{ matrix.php-version == '8.5' && 'true' || 'false' }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c412e8ec..10fd5def8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -123,6 +123,73 @@ The goal is that the library passes tests with the latest spec version at all times, either by implementing small changes quickly, or by skipping tests as necessary. +## Continuous integration + +Two systems run the test suite: + + * GitHub Actions runs the fast checks on every pull request: unit tests + against a small matrix, coding standards, static analysis and the + aggregation builder generator. The workflows live in `.github/workflows/` + and share the `.github/actions/setup` action, which installs PHP, the + extension and the Composer dependencies. + * Evergreen runs the full matrix: every supported PHP version, every supported + server version, several topologies, CSFLE and load balanced tests. The + configuration lives in `.evergreen/config/`. + +Part of the Evergreen configuration is generated from templates to avoid +repeating a block for each PHP or server version. Edit the files in +`.evergreen/config/templates/`, never the ones in +`.evergreen/config/generated/`, then regenerate and commit the result: + +```console +$ php .evergreen/config/generate-config.php +``` + +### Extension version + +The `ext-mongodb` constraint in `composer.json` is the only place where the +required extension version is declared. CI derives everything else from it, so +bumping the constraint is enough to move the whole matrix. + +Evergreen builds the extension in four flavours, selected with the +`EXTENSION_TARGET` variable of the build tasks and resolved by +`.evergreen/compile-extension.sh`: + + * `stable`: latest release from PECL, that is the highest version allowed by + the `composer.json` constraint. + * `lowest`: the lowest version allowed by the constraint, installed from PECL. + This variant also installs the lowest Composer dependencies. + * `next-stable`: the maintenance branch of the current extension minor + version, to catch regressions before the next patch release. + * `next-minor`: the development branch of the next extension minor version, + to catch incompatibilities before the next minor release. + +GitHub Actions covers the two ends of the constraint only. The +`driver-version` input of the setup action accepts `stable`, which is the +default, and `lowest`, which resolves the minimum version from +`composer.json`. + +### Testing against an unreleased extension + +A feature can require an extension version that is not released yet. In that +case CI must build the extension from source, which is a temporary state +enabled in two places: + + * `EXTENSION_DEV_BRANCH` in `.evergreen/compile-extension.sh` + * `DEV_BRANCH` in the "Resolve extension version" step of + `.github/actions/setup/action.yml` + +Set both to the extension branch to build, usually `v2.x`, and mention in the +pull request that this has to be reverted. While the dev branch is enabled, the +lowest extension version is no longer tested, because the library code cannot +run on it. + +Once the extension is released, bump the `ext-mongodb` constraint in +`composer.json`, reset both variables to an empty value, and update +`EXTENSION_STABLE_BRANCH` in `.evergreen/compile-extension.sh` to the branch of +the newly released minor version. A check in the `Generator` workflow fails if +the dev branch is still enabled on a maintenance branch. + ## Backward compatibility When submitting a PR, be mindful of our backward compatibility guarantees. Our diff --git a/RELEASING.md b/RELEASING.md index c09b218e1..6095934a9 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -15,20 +15,13 @@ the extension version matches the library version number. For example: } ``` -After bumping the extension version in `composer.json`, the `vars` for calling -`compile extension` from `.evergreen/config/templates/build/build-extension.yml` -in the Evergreen configuration must be updated. Edit the template file and -regenerate the config: - -* The `stable` task should specify no vars. -* The `lowest` task should specify `EXTENSION_VERSION` with the version that - was just released. -* The `next-stable` task should specify `EXTENSION_BRANCH` with the branch that - was just created. -* The `next-minor` task should specify `EXTENSION_BRANCH: v2.x`. - -The `DRIVER_VERSION` environment variable for any GitHub Actions should also be -set to `stable`. +This constraint drives the extension versions used in CI, so nothing else has +to be edited unless the release was developed against an unreleased extension. +In that case, follow the revert steps of the "Testing against an unreleased +extension" section of [CONTRIBUTING.md](CONTRIBUTING.md): reset the dev branch +variables and update `EXTENSION_STABLE_BRANCH` in +`.evergreen/compile-extension.sh` to the branch of the newly released extension +minor version. After making changes, create a pull request targeting the default branch and wait for this PR to be merged before proceeding with the release. From d66dff10200709df67668ef4219bce25e0af0289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 2 Sep 2026 18:34:13 +0200 Subject: [PATCH 05/11] Block a release when CI builds the extension from a development branch The release workflow now fails before creating the tag if the extension dev branch is still enabled. This check replaces the one added to the generator workflow, which was based on the pull request base branch. RELEASING.md no longer asks to bump the extension requirement, as composer.json is updated during development. --- .evergreen/compile-extension.sh | 1 + .github/actions/setup/action.yml | 17 +++++++------ .github/workflows/generator.yml | 10 -------- .github/workflows/release.yml | 10 ++++++++ CONTRIBUTING.md | 12 ++++----- RELEASING.md | 43 ++++++++++++++++---------------- 6 files changed, 47 insertions(+), 46 deletions(-) diff --git a/.evergreen/compile-extension.sh b/.evergreen/compile-extension.sh index ee80bfc4b..32ea67456 100644 --- a/.evergreen/compile-extension.sh +++ b/.evergreen/compile-extension.sh @@ -15,6 +15,7 @@ EXTENSION_NEXT_MINOR_BRANCH="v2.x" # the library code in that case. Reset to an empty value once the extension is # released. See the "Continuous integration" section of CONTRIBUTING.md. EXTENSION_DEV_BRANCH="" +# EXTENSION_DEV_BRANCH="v2.x" # Lowest version of the extension allowed by the composer.json constraint. lowest_extension_version () diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index a366c3394..e7463a3f5 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -26,18 +26,19 @@ runs: - name: Resolve extension version id: driver shell: bash - # Set DEV_BRANCH to a branch name (e.g. "v2.x") to build the extension - # from source while the library depends on an unreleased version. This - # also overrides the "lowest" version. Reset it to an empty value once the - # extension is released. See the "Continuous integration" section of - # CONTRIBUTING.md. + # Set EXTENSION_DEV_BRANCH to a branch name (e.g. "v2.x") to build the + # extension from source while the library depends on an unreleased + # version. This also overrides the "lowest" version. Reset it to an empty + # value once the extension is released. See the "Continuous integration" + # section of CONTRIBUTING.md. run: | - DEV_BRANCH="" + EXTENSION_DEV_BRANCH="" + # EXTENSION_DEV_BRANCH="v2.x" - if [ -n "$DEV_BRANCH" ]; then + if [ -n "$EXTENSION_DEV_BRANCH" ]; then # Source builds cannot be cached, and caching a moving branch under a # stable key would freeze an outdated extension. - echo "version=mongodb/mongo-php-driver@${DEV_BRANCH}" >> "$GITHUB_OUTPUT" + echo "version=mongodb/mongo-php-driver@${EXTENSION_DEV_BRANCH}" >> "$GITHUB_OUTPUT" echo "cacheable=false" >> "$GITHUB_OUTPUT" elif [ "${{ inputs.driver-version }}" = "lowest" ]; then CONSTRAINT=$(jq -er '.require["ext-mongodb"]' composer.json) diff --git a/.github/workflows/generator.yml b/.github/workflows/generator.yml index e99f5720e..852386e87 100644 --- a/.github/workflows/generator.yml +++ b/.github/workflows/generator.yml @@ -55,13 +55,3 @@ jobs: - name: "Check file diff" run: git diff --exit-code .evergreen/config/generated - - # Building the extension from source is only acceptable while a feature - # depends on an unreleased version. It must be reverted before a release. - - name: "Check the extension dev branch is not enabled" - if: github.base_ref != '' && github.base_ref != 'v2.x' - run: | - if ! grep -q '^EXTENSION_DEV_BRANCH=""$' .evergreen/compile-extension.sh || ! grep -q '^ DEV_BRANCH=""$' .github/actions/setup/action.yml; then - echo "::error::The extension dev branch is enabled. Bump ext-mongodb in composer.json and reset the dev branch, see CONTRIBUTING.md." - exit 1 - fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 37bb2ed3f..aab4ccf37 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,6 +93,16 @@ jobs: git checkout -b ${RELEASE_BRANCH} git push origin ${RELEASE_BRANCH} + # The extension is always released before the library, so CI must no + # longer build it from a development branch. See the "Continuous + # integration" section of CONTRIBUTING.md. + - name: "Fail if CI builds the extension from a development branch" + run: | + if grep -qE '^[[:space:]]*EXTENSION_DEV_BRANCH="[^"]+"' .evergreen/compile-extension.sh .github/actions/setup/action.yml; then + echo '❌ Release failed: CI builds the extension from a development branch instead of a released version' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + # # Preliminary checks done - commence the release process # diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 10fd5def8..a985b0752 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -176,7 +176,7 @@ case CI must build the extension from source, which is a temporary state enabled in two places: * `EXTENSION_DEV_BRANCH` in `.evergreen/compile-extension.sh` - * `DEV_BRANCH` in the "Resolve extension version" step of + * `EXTENSION_DEV_BRANCH` in the "Resolve extension version" step of `.github/actions/setup/action.yml` Set both to the extension branch to build, usually `v2.x`, and mention in the @@ -184,11 +184,11 @@ pull request that this has to be reverted. While the dev branch is enabled, the lowest extension version is no longer tested, because the library code cannot run on it. -Once the extension is released, bump the `ext-mongodb` constraint in -`composer.json`, reset both variables to an empty value, and update -`EXTENSION_STABLE_BRANCH` in `.evergreen/compile-extension.sh` to the branch of -the newly released minor version. A check in the `Generator` workflow fails if -the dev branch is still enabled on a maintenance branch. +Once the extension is released, reset both variables to an empty value and +update `EXTENSION_STABLE_BRANCH` in `.evergreen/compile-extension.sh` to the +branch of the newly released minor version. The release workflow fails if the +dev branch is still enabled, so a release cannot ship with CI building the +extension from source. ## Backward compatibility diff --git a/RELEASING.md b/RELEASING.md index 6095934a9..124e30f35 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,27 +1,26 @@ # Releasing -## Update extension requirement (new minor versions only) - -In `composer.json`, ensure that the version of `ext-mongodb` is correct for -the library version being released. Set the constraint to the minimum -extension/driver version required for that library release; do not assume that -the extension version matches the library version number. For example: - -```json -{ - "require": { - "ext-mongodb": "^2.3" - } -} -``` - -This constraint drives the extension versions used in CI, so nothing else has -to be edited unless the release was developed against an unreleased extension. -In that case, follow the revert steps of the "Testing against an unreleased -extension" section of [CONTRIBUTING.md](CONTRIBUTING.md): reset the dev branch -variables and update `EXTENSION_STABLE_BRANCH` in -`.evergreen/compile-extension.sh` to the branch of the newly released extension -minor version. +## Check that CI uses released versions of the extension + +A feature may have been developed against an extension version that was not +released yet. In that case the `ext-mongodb` constraint in `composer.json` was +updated during development, and CI was configured to compile the extension from +a development branch of the `mongodb/mongo-php-driver` repository. A library +release must not ship in that state, because the extension it requires is +released first. + +Two variables control this, both named `EXTENSION_DEV_BRANCH`: + + * in `.evergreen/compile-extension.sh`, for the Evergreen build tasks + * in `.github/actions/setup/action.yml`, for the GitHub Actions workflows + +Both must hold an empty value. If either one holds a branch name, reset it, and +set `EXTENSION_STABLE_BRANCH` in `.evergreen/compile-extension.sh` to the branch +of the latest released extension minor version, for example `v2.5`. The +"Continuous integration" section of [CONTRIBUTING.md](CONTRIBUTING.md) explains +how these variables are used. + +If this is missed, the release workflow fails before creating the release tag. After making changes, create a pull request targeting the default branch and wait for this PR to be merged before proceeding with the release. From 93a4c39be310f72582a51a057be989cb64f238e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 2 Sep 2026 18:51:08 +0200 Subject: [PATCH 06/11] Address review comments on the extension version resolution Read composer.json from the working directory when the input is set, validate the constraint format before parsing it, and detect new generated files in the Evergreen config diff check. --- .github/actions/setup/action.yml | 9 ++++++++- .github/workflows/generator.yml | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index e7463a3f5..6560bffee 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -41,7 +41,14 @@ runs: echo "version=mongodb/mongo-php-driver@${EXTENSION_DEV_BRANCH}" >> "$GITHUB_OUTPUT" echo "cacheable=false" >> "$GITHUB_OUTPUT" elif [ "${{ inputs.driver-version }}" = "lowest" ]; then - CONSTRAINT=$(jq -er '.require["ext-mongodb"]' composer.json) + WORKING_DIRECTORY="${{ inputs.working-directory }}" + CONSTRAINT=$(jq -er '.require["ext-mongodb"]' "${WORKING_DIRECTORY:-.}/composer.json") + + if [[ ! "$CONSTRAINT" =~ ^\^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then + echo "::error::Unsupported ext-mongodb constraint: $CONSTRAINT" + exit 1 + fi + VERSION=${CONSTRAINT#^} case "$VERSION" in *.*.*) ;; diff --git a/.github/workflows/generator.yml b/.github/workflows/generator.yml index 852386e87..2f1f18e96 100644 --- a/.github/workflows/generator.yml +++ b/.github/workflows/generator.yml @@ -54,4 +54,4 @@ jobs: run: "php .evergreen/config/generate-config.php" - name: "Check file diff" - run: git diff --exit-code .evergreen/config/generated + run: git add .evergreen/config/generated -N && git diff --exit-code .evergreen/config/generated From 98e508c8e746d423b0ea31cf99942af575b9c5f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 3 Sep 2026 11:42:21 +0200 Subject: [PATCH 07/11] Declare the extension versions used by CI in one file .extension-version is sourced by the Evergreen build script and by the GitHub setup action. It holds the extension branches and the EXTENSION_REQUIRE_NEXT_MINOR flag, so depending on an unreleased extension is a single line to change for both CI systems. The release workflow fails while the flag is enabled. --- .evergreen/compile-extension.sh | 22 ++------ .extension-version | 16 ++++++ .github/actions/setup/action.yml | 73 ++++++++++++++------------ .github/workflows/release.yml | 4 +- CONTRIBUTING.md | 90 ++++++++++++-------------------- RELEASING.md | 30 ++++------- 6 files changed, 108 insertions(+), 127 deletions(-) create mode 100644 .extension-version diff --git a/.evergreen/compile-extension.sh b/.evergreen/compile-extension.sh index 32ea67456..022d35140 100644 --- a/.evergreen/compile-extension.sh +++ b/.evergreen/compile-extension.sh @@ -3,19 +3,7 @@ set -o errexit # Exit the script with error if any of the commands fail PATH="$PHP_PATH/bin:$PATH" -# Branch of the latest stable minor version of the extension. -EXTENSION_STABLE_BRANCH="v2.5" - -# Branch of the next minor version of the extension, still in development. -EXTENSION_NEXT_MINOR_BRANCH="v2.x" - -# Set this to a branch name (e.g. "v2.x") to compile every task from source -# while the library depends on an unreleased version of the extension. This -# also overrides the "lowest" target, as the lowest released version cannot run -# the library code in that case. Reset to an empty value once the extension is -# released. See the "Continuous integration" section of CONTRIBUTING.md. -EXTENSION_DEV_BRANCH="" -# EXTENSION_DEV_BRANCH="v2.x" +source ${PROJECT_DIRECTORY}/.extension-version # Lowest version of the extension allowed by the composer.json constraint. lowest_extension_version () @@ -41,10 +29,8 @@ resolve_extension_target () return fi - if [ "x${EXTENSION_DEV_BRANCH}" != "x" ]; then - EXTENSION_BRANCH="${EXTENSION_DEV_BRANCH}" - - return + if [ "${EXTENSION_REQUIRE_NEXT_MINOR}" = "true" ]; then + EXTENSION_TARGET="next-minor" fi case "${EXTENSION_TARGET:-stable}" in @@ -61,7 +47,7 @@ resolve_extension_target () EXTENSION_BRANCH="${EXTENSION_NEXT_MINOR_BRANCH}" ;; *) - echo "Unknown EXTENSION_TARGET: ${EXTENSION_TARGET}" >&2 + echo "Unknown extension target: ${EXTENSION_TARGET}" >&2 exit 1 ;; esac diff --git a/.extension-version b/.extension-version new file mode 100644 index 000000000..ed9f83c07 --- /dev/null +++ b/.extension-version @@ -0,0 +1,16 @@ +# Versions of the mongodb extension used by CI. This file is sourced by +# .evergreen/compile-extension.sh and by .github/actions/setup/action.yml. +# The lowest supported version is read from the composer.json constraint. + +# Branch of the latest stable minor version of the extension. +EXTENSION_STABLE_BRANCH="v2.5" + +# Branch of the next minor version of the extension, still in development. +EXTENSION_NEXT_MINOR_BRANCH="v2.x" + +# Set to true while the library depends on a version of the extension that is +# not released yet. Every CI job then builds the extension from +# EXTENSION_NEXT_MINOR_BRANCH, and the lowest supported version is no longer tested, as +# it cannot run the library code. Set it back to false once that version is +# released. See the "Continuous integration" section of CONTRIBUTING.md. +EXTENSION_REQUIRE_NEXT_MINOR=false diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 6560bffee..18f8c749b 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -26,41 +26,50 @@ runs: - name: Resolve extension version id: driver shell: bash - # Set EXTENSION_DEV_BRANCH to a branch name (e.g. "v2.x") to build the - # extension from source while the library depends on an unreleased - # version. This also overrides the "lowest" version. Reset it to an empty - # value once the extension is released. See the "Continuous integration" - # section of CONTRIBUTING.md. run: | - EXTENSION_DEV_BRANCH="" - # EXTENSION_DEV_BRANCH="v2.x" + source .extension-version - if [ -n "$EXTENSION_DEV_BRANCH" ]; then - # Source builds cannot be cached, and caching a moving branch under a - # stable key would freeze an outdated extension. - echo "version=mongodb/mongo-php-driver@${EXTENSION_DEV_BRANCH}" >> "$GITHUB_OUTPUT" - echo "cacheable=false" >> "$GITHUB_OUTPUT" - elif [ "${{ inputs.driver-version }}" = "lowest" ]; then - WORKING_DIRECTORY="${{ inputs.working-directory }}" - CONSTRAINT=$(jq -er '.require["ext-mongodb"]' "${WORKING_DIRECTORY:-.}/composer.json") + TARGET="${{ inputs.driver-version }}" - if [[ ! "$CONSTRAINT" =~ ^\^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then - echo "::error::Unsupported ext-mongodb constraint: $CONSTRAINT" - exit 1 - fi - - VERSION=${CONSTRAINT#^} - case "$VERSION" in - *.*.*) ;; - *) VERSION="${VERSION}.0" ;; - esac - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "cacheable=true" >> "$GITHUB_OUTPUT" - else - echo "version=${{ inputs.driver-version }}" >> "$GITHUB_OUTPUT" - echo "cacheable=true" >> "$GITHUB_OUTPUT" + if [ "$EXTENSION_REQUIRE_NEXT_MINOR" = "true" ]; then + TARGET="next-minor" fi + case "$TARGET" in + next-minor) + # Source builds cannot be cached, and caching a moving branch under + # a stable key would freeze an outdated extension. + echo "version=mongodb/mongo-php-driver@${EXTENSION_NEXT_MINOR_BRANCH}" >> "$GITHUB_OUTPUT" + echo "cacheable=false" >> "$GITHUB_OUTPUT" + ;; + lowest) + WORKING_DIRECTORY="${{ inputs.working-directory }}" + CONSTRAINT=$(jq -er '.require["ext-mongodb"]' "${WORKING_DIRECTORY:-.}/composer.json") + + if [[ ! "$CONSTRAINT" =~ ^\^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then + echo "::error::Unsupported ext-mongodb constraint: $CONSTRAINT" + exit 1 + fi + + VERSION=${CONSTRAINT#^} + case "$VERSION" in + *.*.*) ;; + *) VERSION="${VERSION}.0" ;; + esac + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "expected=${VERSION}" >> "$GITHUB_OUTPUT" + echo "cacheable=true" >> "$GITHUB_OUTPUT" + ;; + stable) + echo "version=stable" >> "$GITHUB_OUTPUT" + echo "cacheable=true" >> "$GITHUB_OUTPUT" + ;; + *) + echo "::error::Unsupported extension target: $TARGET" + exit 1 + ;; + esac + - name: Setup cache environment id: extcache if: steps.driver.outputs.cacheable == 'true' @@ -92,11 +101,11 @@ runs: shell: bash - name: Check the expected driver version is installed - if: inputs.driver-version == 'lowest' + if: steps.driver.outputs.expected != '' shell: bash run: | INSTALLED=$(php -r 'echo phpversion("mongodb");') - EXPECTED="${{ steps.driver.outputs.version }}" + EXPECTED="${{ steps.driver.outputs.expected }}" if [ "$INSTALLED" != "$EXPECTED" ]; then echo "::error::Expected extension version $EXPECTED, got $INSTALLED" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aab4ccf37..e19eb9c7d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -98,7 +98,9 @@ jobs: # integration" section of CONTRIBUTING.md. - name: "Fail if CI builds the extension from a development branch" run: | - if grep -qE '^[[:space:]]*EXTENSION_DEV_BRANCH="[^"]+"' .evergreen/compile-extension.sh .github/actions/setup/action.yml; then + source .extension-version + + if [ "$EXTENSION_REQUIRE_NEXT_MINOR" = "true" ]; then echo '❌ Release failed: CI builds the extension from a development branch instead of a released version' >> $GITHUB_STEP_SUMMARY exit 1 fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a985b0752..1cede9b01 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -125,21 +125,15 @@ necessary. ## Continuous integration -Two systems run the test suite: - - * GitHub Actions runs the fast checks on every pull request: unit tests - against a small matrix, coding standards, static analysis and the - aggregation builder generator. The workflows live in `.github/workflows/` - and share the `.github/actions/setup` action, which installs PHP, the - extension and the Composer dependencies. - * Evergreen runs the full matrix: every supported PHP version, every supported - server version, several topologies, CSFLE and load balanced tests. The - configuration lives in `.evergreen/config/`. - -Part of the Evergreen configuration is generated from templates to avoid -repeating a block for each PHP or server version. Edit the files in -`.evergreen/config/templates/`, never the ones in -`.evergreen/config/generated/`, then regenerate and commit the result: + * GitHub Actions (`.github/workflows/`) runs the fast checks on pull requests: + tests on a small matrix of local servers, coding standards, static analysis + and the generator diff. + * Evergreen (`.evergreen/config/`) runs the full matrix: all supported PHP and + server versions, all topologies, CSFLE, load balancer, and the `atlas` group + on a provisioned Atlas cluster. + +When you edit a file in `.evergreen/config/templates/`, never edit +`.evergreen/config/generated/` by hand: regenerate it and commit the result. ```console $ php .evergreen/config/generate-config.php @@ -147,48 +141,30 @@ $ php .evergreen/config/generate-config.php ### Extension version -The `ext-mongodb` constraint in `composer.json` is the only place where the -required extension version is declared. CI derives everything else from it, so -bumping the constraint is enough to move the whole matrix. - -Evergreen builds the extension in four flavours, selected with the -`EXTENSION_TARGET` variable of the build tasks and resolved by -`.evergreen/compile-extension.sh`: - - * `stable`: latest release from PECL, that is the highest version allowed by - the `composer.json` constraint. - * `lowest`: the lowest version allowed by the constraint, installed from PECL. - This variant also installs the lowest Composer dependencies. - * `next-stable`: the maintenance branch of the current extension minor - version, to catch regressions before the next patch release. - * `next-minor`: the development branch of the next extension minor version, - to catch incompatibilities before the next minor release. - -GitHub Actions covers the two ends of the constraint only. The -`driver-version` input of the setup action accepts `stable`, which is the -default, and `lowest`, which resolves the minimum version from -`composer.json`. - -### Testing against an unreleased extension - -A feature can require an extension version that is not released yet. In that -case CI must build the extension from source, which is a temporary state -enabled in two places: - - * `EXTENSION_DEV_BRANCH` in `.evergreen/compile-extension.sh` - * `EXTENSION_DEV_BRANCH` in the "Resolve extension version" step of - `.github/actions/setup/action.yml` - -Set both to the extension branch to build, usually `v2.x`, and mention in the -pull request that this has to be reverted. While the dev branch is enabled, the -lowest extension version is no longer tested, because the library code cannot -run on it. - -Once the extension is released, reset both variables to an empty value and -update `EXTENSION_STABLE_BRANCH` in `.evergreen/compile-extension.sh` to the -branch of the newly released minor version. The release workflow fails if the -dev branch is still enabled, so a release cannot ship with CI building the -extension from source. +When you change the `ext-mongodb` constraint in `composer.json`, there is +nothing else to update: both CI systems resolve the extension version from that +constraint at build time. + +Evergreen builds four targets, resolved by `.evergreen/compile-extension.sh` +from the branches declared in `.extension-version`: + + * `stable`: latest PECL release, the highest version the constraint allows + * `lowest`: lowest version the constraint allows, with lowest Composer + dependencies + * `next-stable`: maintenance branch of the current extension minor version + * `next-minor`: development branch of the next extension minor version + +GitHub Actions only covers `stable` and `lowest`, through the `driver-version` +input of the setup action. + +### If you need an extension version that is not released yet + +Set `EXTENSION_REQUIRE_NEXT_MINOR=true` in `.extension-version`. Every job of both CI +systems then builds the extension from its development branch. + +The lowest version is no longer tested while this is enabled, and the release +workflow refuses to run. Once the extension is released, set it back to `false` +and point `EXTENSION_STABLE_BRANCH` to the branch of the new minor version. ## Backward compatibility diff --git a/RELEASING.md b/RELEASING.md index 124e30f35..4b9de7b8b 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,28 +2,20 @@ ## Check that CI uses released versions of the extension -A feature may have been developed against an extension version that was not -released yet. In that case the `ext-mongodb` constraint in `composer.json` was -updated during development, and CI was configured to compile the extension from -a development branch of the `mongodb/mongo-php-driver` repository. A library -release must not ship in that state, because the extension it requires is -released first. +Check that the version required by the `ext-mongodb` constraint in +`composer.json` is [released on PECL](https://pecl.php.net/package/mongodb). +The extension is always released before the library that requires it. -Two variables control this, both named `EXTENSION_DEV_BRANCH`: +Then, in `.extension-version`, check that `EXTENSION_REQUIRE_NEXT_MINOR` is `false`, +and set it back if it is not. Set `EXTENSION_STABLE_BRANCH` to the branch of the +latest released extension minor version, for example `v2.5`. - * in `.evergreen/compile-extension.sh`, for the Evergreen build tasks - * in `.github/actions/setup/action.yml`, for the GitHub Actions workflows +Nothing to bump in `composer.json`: the `ext-mongodb` constraint is updated +during development. The release workflow fails while `EXTENSION_REQUIRE_NEXT_MINOR` is +enabled. -Both must hold an empty value. If either one holds a branch name, reset it, and -set `EXTENSION_STABLE_BRANCH` in `.evergreen/compile-extension.sh` to the branch -of the latest released extension minor version, for example `v2.5`. The -"Continuous integration" section of [CONTRIBUTING.md](CONTRIBUTING.md) explains -how these variables are used. - -If this is missed, the release workflow fails before creating the release tag. - -After making changes, create a pull request targeting the default branch and -wait for this PR to be merged before proceeding with the release. +If you changed something, create a pull request targeting the default branch and +wait for it to be merged before proceeding with the release. ## Transition JIRA issues and version From 9f87f8b88bcaaa8b71151f279c6e8cad6e8dbf60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 3 Sep 2026 12:39:12 +0200 Subject: [PATCH 08/11] Resolve the stable extension target to a released version The stable target used to install whatever PECL serves, ignoring the ext-mongodb constraint, and no concrete version was part of the GitHub Actions cache key. Both CI systems now resolve the highest release allowed by the constraint, and the cache key goes back to a plain invalidation marker, the requested version being already part of the key computed by cache-extensions. --- .evergreen/compile-extension.sh | 45 ++++++++++--- .github/actions/setup/action.yml | 22 ++----- .github/actions/setup/extension-version.py | 73 ++++++++++++++++++++++ CONTRIBUTING.md | 23 ++++--- 4 files changed, 132 insertions(+), 31 deletions(-) create mode 100644 .github/actions/setup/extension-version.py diff --git a/.evergreen/compile-extension.sh b/.evergreen/compile-extension.sh index 022d35140..2f4eba8c2 100644 --- a/.evergreen/compile-extension.sh +++ b/.evergreen/compile-extension.sh @@ -5,19 +5,50 @@ PATH="$PHP_PATH/bin:$PATH" source ${PROJECT_DIRECTORY}/.extension-version -# Lowest version of the extension allowed by the composer.json constraint. -lowest_extension_version () +# Lowest or highest version of the extension allowed by the composer.json +# constraint. The lowest version is the lower bound of the constraint, the +# highest one is the latest matching release published on PECL. +extension_version () { php -r ' - $constraint = json_decode(file_get_contents($argv[1]), true)["require"]["ext-mongodb"]; + $constraint = json_decode(file_get_contents($argv[2]), true)["require"]["ext-mongodb"]; if (!preg_match("/^\^(\d+)\.(\d+)(?:\.(\d+))?$/", $constraint, $matches)) { fwrite(STDERR, sprintf("Unsupported ext-mongodb constraint: %s\n", $constraint)); exit(1); } - printf("%d.%d.%d", $matches[1], $matches[2], $matches[3] ?? 0); - ' ${PROJECT_DIRECTORY}/composer.json + $lowest = sprintf("%d.%d.%d", $matches[1], $matches[2], $matches[3] ?? 0); + + if ($argv[1] === "lowest") { + echo $lowest; + exit(0); + } + + $releases = @simplexml_load_file("https://pecl.php.net/rest/r/mongodb/allreleases.xml"); + + if ($releases === false) { + fwrite(STDERR, "Cannot read the list of mongodb releases from PECL\n"); + exit(1); + } + + // Releases are listed from the most recent one + foreach ($releases->r as $release) { + $version = (string) $release->v; + + if ((string) $release->s !== "stable") { + continue; + } + + if (version_compare($version, $lowest, ">=") && version_compare($version, ($matches[1] + 1) . ".0.0", "<")) { + echo $version; + exit(0); + } + } + + fwrite(STDERR, sprintf("No release matching %s found on PECL\n", $constraint)); + exit(1); + ' "$1" ${PROJECT_DIRECTORY}/composer.json } # Turn EXTENSION_TARGET into the EXTENSION_BRANCH or EXTENSION_VERSION expected @@ -35,10 +66,10 @@ resolve_extension_target () case "${EXTENSION_TARGET:-stable}" in stable) - # Latest release from PECL, i.e. the highest version allowed by composer.json + EXTENSION_VERSION=$(extension_version highest) ;; lowest) - EXTENSION_VERSION=$(lowest_extension_version) + EXTENSION_VERSION=$(extension_version lowest) ;; next-stable) EXTENSION_BRANCH="${EXTENSION_STABLE_BRANCH}" diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 18f8c749b..4c7793747 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -42,28 +42,14 @@ runs: echo "version=mongodb/mongo-php-driver@${EXTENSION_NEXT_MINOR_BRANCH}" >> "$GITHUB_OUTPUT" echo "cacheable=false" >> "$GITHUB_OUTPUT" ;; - lowest) + lowest | stable) WORKING_DIRECTORY="${{ inputs.working-directory }}" - CONSTRAINT=$(jq -er '.require["ext-mongodb"]' "${WORKING_DIRECTORY:-.}/composer.json") + VERSION=$(python3 "$GITHUB_ACTION_PATH/extension-version.py" "$TARGET" "${WORKING_DIRECTORY:-.}/composer.json") - if [[ ! "$CONSTRAINT" =~ ^\^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then - echo "::error::Unsupported ext-mongodb constraint: $CONSTRAINT" - exit 1 - fi - - VERSION=${CONSTRAINT#^} - case "$VERSION" in - *.*.*) ;; - *) VERSION="${VERSION}.0" ;; - esac echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "expected=${VERSION}" >> "$GITHUB_OUTPUT" echo "cacheable=true" >> "$GITHUB_OUTPUT" ;; - stable) - echo "version=stable" >> "$GITHUB_OUTPUT" - echo "cacheable=true" >> "$GITHUB_OUTPUT" - ;; *) echo "::error::Unsupported extension target: $TARGET" exit 1 @@ -77,7 +63,9 @@ runs: with: php-version: ${{ inputs.php-version }} extensions: "mongodb-${{ steps.driver.outputs.version }}" - key: "extcache-${{ steps.driver.outputs.version }}-${{ hashFiles('composer.json') }}" + # The requested extension version is part of the key computed by the + # action. This value is only there to invalidate the cache by hand. + key: "extcache-v1" - name: Cache extensions if: steps.driver.outputs.cacheable == 'true' diff --git a/.github/actions/setup/extension-version.py b/.github/actions/setup/extension-version.py new file mode 100644 index 000000000..47b103ffd --- /dev/null +++ b/.github/actions/setup/extension-version.py @@ -0,0 +1,73 @@ +"""Resolve the extension version allowed by the ext-mongodb constraint. + +The lowest version is the lower bound of the constraint. The highest version is +the latest matching release published on PECL. This mirrors the resolution done +by .evergreen/compile-extension.sh for the Evergreen build tasks. + +Usage: extension-version.py lowest|stable path/to/composer.json +""" + +import json +import re +import sys +import urllib.request +import xml.etree.ElementTree as ElementTree + +ALL_RELEASES_URL = "https://pecl.php.net/rest/r/mongodb/allreleases.xml" +NAMESPACE = {"r": "http://pear.php.net/dtd/rest.allreleases"} + + +def fail(message): + print(f"::error::{message}", file=sys.stderr) + sys.exit(1) + + +def parse_constraint(path): + with open(path) as file: + constraint = json.load(file)["require"]["ext-mongodb"] + + match = re.fullmatch(r"\^(\d+)\.(\d+)(?:\.(\d+))?", constraint) + + if not match: + fail(f"Unsupported ext-mongodb constraint: {constraint}") + + major, minor, patch = match.group(1), match.group(2), match.group(3) or "0" + + return constraint, int(major), (int(major), int(minor), int(patch)) + + +def released_versions(): + try: + with urllib.request.urlopen(ALL_RELEASES_URL, timeout=30) as response: + document = ElementTree.parse(response) + except Exception as error: + fail(f"Cannot read the list of mongodb releases from PECL: {error}") + + for release in document.findall("r:r", NAMESPACE): + version = release.find("r:v", NAMESPACE).text + stability = release.find("r:s", NAMESPACE).text + + if stability == "stable": + yield version, tuple(int(part) for part in version.split(".")) + + +def main(): + target, composer_json = sys.argv[1], sys.argv[2] + constraint, major, lowest = parse_constraint(composer_json) + + if target == "lowest": + print("%d.%d.%d" % lowest) + + return + + # Releases are listed from the most recent one + for version, parsed in released_versions(): + if lowest <= parsed and parsed[0] == major: + print(version) + + return + + fail(f"No release matching {constraint} found on PECL") + + +main() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1cede9b01..8d0966ced 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,14 +141,23 @@ $ php .evergreen/config/generate-config.php ### Extension version -When you change the `ext-mongodb` constraint in `composer.json`, there is -nothing else to update: both CI systems resolve the extension version from that -constraint at build time. +The lowest version tested by CI is read from the `ext-mongodb` constraint in +`composer.json`. When you change that constraint, review the branches declared +in `.extension-version`: + + * `EXTENSION_STABLE_BRANCH`: maintenance branch of the latest released + extension minor version, to update when a new minor version is released + * `EXTENSION_NEXT_MINOR_BRANCH`: development branch of the next minor version, + which only changes for a new major version + +Bumping the constraint to a version that is not released yet also requires +`EXTENSION_REQUIRE_NEXT_MINOR=true`, see below. Evergreen builds four targets, resolved by `.evergreen/compile-extension.sh` -from the branches declared in `.extension-version`: +from these branches: - * `stable`: latest PECL release, the highest version the constraint allows + * `stable`: highest version the constraint allows, taken from the releases + published on PECL * `lowest`: lowest version the constraint allows, with lowest Composer dependencies * `next-stable`: maintenance branch of the current extension minor version @@ -159,8 +168,8 @@ input of the setup action. ### If you need an extension version that is not released yet -Set `EXTENSION_REQUIRE_NEXT_MINOR=true` in `.extension-version`. Every job of both CI -systems then builds the extension from its development branch. +Set `EXTENSION_REQUIRE_NEXT_MINOR=true` in `.extension-version`. Every job of +both CI systems then builds the extension from its development branch. The lowest version is no longer tested while this is enabled, and the release workflow refuses to run. Once the extension is released, set it back to `false` From 29a6cfb6baf1564a7f5fc783459146ba108541d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 3 Sep 2026 13:22:31 +0200 Subject: [PATCH 09/11] Resolve the extension version in PHP, without a configuration file The ext-mongodb constraint in composer.json and the versions published on PECL are now the only inputs. tools/extension-version.php resolves the target for both CI systems, so the .extension-version file and its three hand maintained variables are gone, as is the Python resolver. A constraint that no published version satisfies makes every job build the extension from its development branch, which replaces the manual switch. The release workflow checks that a published version matches the constraint. The resolution now always reads the composer.json of the repository root, which fixes the generator job, as generator/composer.json does not declare the extension. --- .evergreen/compile-extension.sh | 72 +----------- .extension-version | 16 --- .github/actions/setup/action.yml | 60 +++++----- .github/actions/setup/extension-version.py | 73 ------------ .github/workflows/release.yml | 22 +++- CONTRIBUTING.md | 43 +++---- RELEASING.md | 23 ++-- tools/extension-version.php | 127 +++++++++++++++++++++ 8 files changed, 202 insertions(+), 234 deletions(-) delete mode 100644 .extension-version delete mode 100644 .github/actions/setup/extension-version.py create mode 100644 tools/extension-version.php diff --git a/.evergreen/compile-extension.sh b/.evergreen/compile-extension.sh index 2f4eba8c2..fea7d5185 100644 --- a/.evergreen/compile-extension.sh +++ b/.evergreen/compile-extension.sh @@ -3,54 +3,6 @@ set -o errexit # Exit the script with error if any of the commands fail PATH="$PHP_PATH/bin:$PATH" -source ${PROJECT_DIRECTORY}/.extension-version - -# Lowest or highest version of the extension allowed by the composer.json -# constraint. The lowest version is the lower bound of the constraint, the -# highest one is the latest matching release published on PECL. -extension_version () -{ - php -r ' - $constraint = json_decode(file_get_contents($argv[2]), true)["require"]["ext-mongodb"]; - - if (!preg_match("/^\^(\d+)\.(\d+)(?:\.(\d+))?$/", $constraint, $matches)) { - fwrite(STDERR, sprintf("Unsupported ext-mongodb constraint: %s\n", $constraint)); - exit(1); - } - - $lowest = sprintf("%d.%d.%d", $matches[1], $matches[2], $matches[3] ?? 0); - - if ($argv[1] === "lowest") { - echo $lowest; - exit(0); - } - - $releases = @simplexml_load_file("https://pecl.php.net/rest/r/mongodb/allreleases.xml"); - - if ($releases === false) { - fwrite(STDERR, "Cannot read the list of mongodb releases from PECL\n"); - exit(1); - } - - // Releases are listed from the most recent one - foreach ($releases->r as $release) { - $version = (string) $release->v; - - if ((string) $release->s !== "stable") { - continue; - } - - if (version_compare($version, $lowest, ">=") && version_compare($version, ($matches[1] + 1) . ".0.0", "<")) { - echo $version; - exit(0); - } - } - - fwrite(STDERR, sprintf("No release matching %s found on PECL\n", $constraint)); - exit(1); - ' "$1" ${PROJECT_DIRECTORY}/composer.json -} - # Turn EXTENSION_TARGET into the EXTENSION_BRANCH or EXTENSION_VERSION expected # by install_extension. Both variables can also be set explicitly, e.g. in a # patch build, in which case they take precedence over EXTENSION_TARGET. @@ -60,28 +12,10 @@ resolve_extension_target () return fi - if [ "${EXTENSION_REQUIRE_NEXT_MINOR}" = "true" ]; then - EXTENSION_TARGET="next-minor" - fi + # Assign before eval, so that a failure of the script stops the build + RESOLVED=$(php ${PROJECT_DIRECTORY}/tools/extension-version.php ${EXTENSION_TARGET:-stable}) - case "${EXTENSION_TARGET:-stable}" in - stable) - EXTENSION_VERSION=$(extension_version highest) - ;; - lowest) - EXTENSION_VERSION=$(extension_version lowest) - ;; - next-stable) - EXTENSION_BRANCH="${EXTENSION_STABLE_BRANCH}" - ;; - next-minor) - EXTENSION_BRANCH="${EXTENSION_NEXT_MINOR_BRANCH}" - ;; - *) - echo "Unknown extension target: ${EXTENSION_TARGET}" >&2 - exit 1 - ;; - esac + eval "${RESOLVED}" } install_extension () diff --git a/.extension-version b/.extension-version deleted file mode 100644 index ed9f83c07..000000000 --- a/.extension-version +++ /dev/null @@ -1,16 +0,0 @@ -# Versions of the mongodb extension used by CI. This file is sourced by -# .evergreen/compile-extension.sh and by .github/actions/setup/action.yml. -# The lowest supported version is read from the composer.json constraint. - -# Branch of the latest stable minor version of the extension. -EXTENSION_STABLE_BRANCH="v2.5" - -# Branch of the next minor version of the extension, still in development. -EXTENSION_NEXT_MINOR_BRANCH="v2.x" - -# Set to true while the library depends on a version of the extension that is -# not released yet. Every CI job then builds the extension from -# EXTENSION_NEXT_MINOR_BRANCH, and the lowest supported version is no longer tested, as -# it cannot run the library code. Set it back to false once that version is -# released. See the "Continuous integration" section of CONTRIBUTING.md. -EXTENSION_REQUIRE_NEXT_MINOR=false diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 4c7793747..ee471195a 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -23,63 +23,59 @@ inputs: runs: using: composite steps: + # The extension version is resolved by a PHP script, so PHP is installed + # first, without the extension. The second call below adds it. + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + coverage: none + php-version: "${{ inputs.php-version }}" + - name: Resolve extension version id: driver shell: bash run: | - source .extension-version + # The ext-mongodb constraint is only declared in the composer.json of + # the library, at the root of the repository. + RESOLVED=$(php tools/extension-version.php "${{ inputs.driver-version }}") - TARGET="${{ inputs.driver-version }}" + eval "$RESOLVED" - if [ "$EXTENSION_REQUIRE_NEXT_MINOR" = "true" ]; then - TARGET="next-minor" + if [ "$EXTENSION_VERSION" != "" ]; then + echo "ext-spec=mongodb-${EXTENSION_VERSION}" >> "$GITHUB_OUTPUT" + echo "ext-expected=${EXTENSION_VERSION}" >> "$GITHUB_OUTPUT" + echo "ext-cachable=true" >> "$GITHUB_OUTPUT" + else + # Source builds cannot be cached, and caching a moving branch under + # a stable key would freeze an outdated extension. + echo "ext-spec=mongodb/mongo-php-driver@${EXTENSION_BRANCH}" >> "$GITHUB_OUTPUT" + echo "ext-cachable=false" >> "$GITHUB_OUTPUT" fi - case "$TARGET" in - next-minor) - # Source builds cannot be cached, and caching a moving branch under - # a stable key would freeze an outdated extension. - echo "version=mongodb/mongo-php-driver@${EXTENSION_NEXT_MINOR_BRANCH}" >> "$GITHUB_OUTPUT" - echo "cacheable=false" >> "$GITHUB_OUTPUT" - ;; - lowest | stable) - WORKING_DIRECTORY="${{ inputs.working-directory }}" - VERSION=$(python3 "$GITHUB_ACTION_PATH/extension-version.py" "$TARGET" "${WORKING_DIRECTORY:-.}/composer.json") - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "expected=${VERSION}" >> "$GITHUB_OUTPUT" - echo "cacheable=true" >> "$GITHUB_OUTPUT" - ;; - *) - echo "::error::Unsupported extension target: $TARGET" - exit 1 - ;; - esac - - name: Setup cache environment id: extcache - if: steps.driver.outputs.cacheable == 'true' + if: steps.driver.outputs.ext-cachable == 'true' uses: shivammathur/cache-extensions@v1 with: php-version: ${{ inputs.php-version }} - extensions: "mongodb-${{ steps.driver.outputs.version }}" + extensions: "${{ steps.driver.outputs.ext-spec }}" # The requested extension version is part of the key computed by the # action. This value is only there to invalidate the cache by hand. key: "extcache-v1" - name: Cache extensions - if: steps.driver.outputs.cacheable == 'true' + if: steps.driver.outputs.ext-cachable == 'true' uses: actions/cache@v4 with: path: ${{ steps.extcache.outputs.dir }} key: ${{ steps.extcache.outputs.key }} restore-keys: ${{ steps.extcache.outputs.key }} - - name: Install PHP + - name: Install the mongodb extension uses: shivammathur/setup-php@v2 with: coverage: xdebug - extensions: "mongodb-${{ steps.driver.outputs.version }}" + extensions: "${{ steps.driver.outputs.ext-spec }}" php-version: "${{ inputs.php-version }}" tools: cs2pr ini-values: "${{ inputs.php-ini-values }}" @@ -89,11 +85,11 @@ runs: shell: bash - name: Check the expected driver version is installed - if: steps.driver.outputs.expected != '' + if: steps.driver.outputs.ext-expected != '' shell: bash run: | INSTALLED=$(php -r 'echo phpversion("mongodb");') - EXPECTED="${{ steps.driver.outputs.expected }}" + EXPECTED="${{ steps.driver.outputs.ext-expected }}" if [ "$INSTALLED" != "$EXPECTED" ]; then echo "::error::Expected extension version $EXPECTED, got $INSTALLED" diff --git a/.github/actions/setup/extension-version.py b/.github/actions/setup/extension-version.py deleted file mode 100644 index 47b103ffd..000000000 --- a/.github/actions/setup/extension-version.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Resolve the extension version allowed by the ext-mongodb constraint. - -The lowest version is the lower bound of the constraint. The highest version is -the latest matching release published on PECL. This mirrors the resolution done -by .evergreen/compile-extension.sh for the Evergreen build tasks. - -Usage: extension-version.py lowest|stable path/to/composer.json -""" - -import json -import re -import sys -import urllib.request -import xml.etree.ElementTree as ElementTree - -ALL_RELEASES_URL = "https://pecl.php.net/rest/r/mongodb/allreleases.xml" -NAMESPACE = {"r": "http://pear.php.net/dtd/rest.allreleases"} - - -def fail(message): - print(f"::error::{message}", file=sys.stderr) - sys.exit(1) - - -def parse_constraint(path): - with open(path) as file: - constraint = json.load(file)["require"]["ext-mongodb"] - - match = re.fullmatch(r"\^(\d+)\.(\d+)(?:\.(\d+))?", constraint) - - if not match: - fail(f"Unsupported ext-mongodb constraint: {constraint}") - - major, minor, patch = match.group(1), match.group(2), match.group(3) or "0" - - return constraint, int(major), (int(major), int(minor), int(patch)) - - -def released_versions(): - try: - with urllib.request.urlopen(ALL_RELEASES_URL, timeout=30) as response: - document = ElementTree.parse(response) - except Exception as error: - fail(f"Cannot read the list of mongodb releases from PECL: {error}") - - for release in document.findall("r:r", NAMESPACE): - version = release.find("r:v", NAMESPACE).text - stability = release.find("r:s", NAMESPACE).text - - if stability == "stable": - yield version, tuple(int(part) for part in version.split(".")) - - -def main(): - target, composer_json = sys.argv[1], sys.argv[2] - constraint, major, lowest = parse_constraint(composer_json) - - if target == "lowest": - print("%d.%d.%d" % lowest) - - return - - # Releases are listed from the most recent one - for version, parsed in released_versions(): - if lowest <= parsed and parsed[0] == major: - print(version) - - return - - fail(f"No release matching {constraint} found on PECL") - - -main() diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e19eb9c7d..a5aaa967e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,18 +93,28 @@ jobs: git checkout -b ${RELEASE_BRANCH} git push origin ${RELEASE_BRANCH} - # The extension is always released before the library, so CI must no - # longer build it from a development branch. See the "Continuous + # The extension is always released before the library that requires it. + # Until then, CI builds it from a development branch. See the "Continuous # integration" section of CONTRIBUTING.md. - - name: "Fail if CI builds the extension from a development branch" + - name: "Setup PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: "8.4" + coverage: none + + - name: "Fail if the required extension version is not released" run: | - source .extension-version + RESOLVED=$(php tools/extension-version.php lowest) - if [ "$EXTENSION_REQUIRE_NEXT_MINOR" = "true" ]; then - echo '❌ Release failed: CI builds the extension from a development branch instead of a released version' >> $GITHUB_STEP_SUMMARY + eval "$RESOLVED" + + if [ "$EXTENSION_VERSION" = "" ]; then + echo '❌ Release failed: no version of the extension published on PECL matches the ext-mongodb constraint of composer.json' >> $GITHUB_STEP_SUMMARY exit 1 fi + echo "✅ Extension version $EXTENSION_VERSION is published on PECL" >> $GITHUB_STEP_SUMMARY + # # Preliminary checks done - commence the release process # diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8d0966ced..d3c1c211a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,39 +141,32 @@ $ php .evergreen/config/generate-config.php ### Extension version -The lowest version tested by CI is read from the `ext-mongodb` constraint in -`composer.json`. When you change that constraint, review the branches declared -in `.extension-version`: +The `ext-mongodb` constraint in `composer.json` is the only thing to maintain. +Everything else is deduced from it and from the versions published on PECL by +`tools/extension-version.php`, used by both CI systems: - * `EXTENSION_STABLE_BRANCH`: maintenance branch of the latest released - extension minor version, to update when a new minor version is released - * `EXTENSION_NEXT_MINOR_BRANCH`: development branch of the next minor version, - which only changes for a new major version - -Bumping the constraint to a version that is not released yet also requires -`EXTENSION_REQUIRE_NEXT_MINOR=true`, see below. +```console +$ php tools/extension-version.php stable +``` -Evergreen builds four targets, resolved by `.evergreen/compile-extension.sh` -from these branches: +Four targets are available: - * `stable`: highest version the constraint allows, taken from the releases - published on PECL + * `stable`: highest version the constraint allows * `lowest`: lowest version the constraint allows, with lowest Composer dependencies - * `next-stable`: maintenance branch of the current extension minor version + * `next-stable`: maintenance branch of the latest released extension minor + version * `next-minor`: development branch of the next extension minor version -GitHub Actions only covers `stable` and `lowest`, through the `driver-version` -input of the setup action. - -### If you need an extension version that is not released yet - -Set `EXTENSION_REQUIRE_NEXT_MINOR=true` in `.extension-version`. Every job of -both CI systems then builds the extension from its development branch. +Evergreen builds all four, selected with `EXTENSION_TARGET` in +`.evergreen/config/templates/build/build-extension.yml`. GitHub Actions covers +`stable` and `lowest`, through the `driver-version` input of the setup action. -The lowest version is no longer tested while this is enabled, and the release -workflow refuses to run. Once the extension is released, set it back to `false` -and point `EXTENSION_STABLE_BRANCH` to the branch of the new minor version. +When you need an extension version that is not released yet, bump the +`ext-mongodb` constraint to it. No version published on PECL then satisfies the +constraint, so every job of both CI systems builds the extension from its +development branch, and the release workflow refuses to run. Both go back to +released versions on their own once the extension is published. ## Backward compatibility diff --git a/RELEASING.md b/RELEASING.md index 4b9de7b8b..047e4312f 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,21 +1,18 @@ # Releasing -## Check that CI uses released versions of the extension +## Check that the required extension version is released -Check that the version required by the `ext-mongodb` constraint in -`composer.json` is [released on PECL](https://pecl.php.net/package/mongodb). -The extension is always released before the library that requires it. +The version required by the `ext-mongodb` constraint in `composer.json` must be +[released on PECL](https://pecl.php.net/package/mongodb). The extension is +always released before the library that requires it. Until then, CI builds the +extension from its development branch, and the release workflow refuses to run. -Then, in `.extension-version`, check that `EXTENSION_REQUIRE_NEXT_MINOR` is `false`, -and set it back if it is not. Set `EXTENSION_STABLE_BRANCH` to the branch of the -latest released extension minor version, for example `v2.5`. +```console +$ php tools/extension-version.php lowest +``` -Nothing to bump in `composer.json`: the `ext-mongodb` constraint is updated -during development. The release workflow fails while `EXTENSION_REQUIRE_NEXT_MINOR` is -enabled. - -If you changed something, create a pull request targeting the default branch and -wait for it to be merged before proceeding with the release. +Nothing to bump here: the `ext-mongodb` constraint is updated during +development, never at release time. ## Transition JIRA issues and version diff --git a/tools/extension-version.php b/tools/extension-version.php new file mode 100644 index 000000000..a1dfa99a1 --- /dev/null +++ b/tools/extension-version.php @@ -0,0 +1,127 @@ + + */ +function releasedVersions(int $major, string $lowest): array +{ + $document = @file_get_contents(ALL_RELEASES_URL, false, stream_context_create(['http' => ['timeout' => 30]])); + + if ($document === false) { + fail(sprintf('Cannot read the list of mongodb releases from %s', ALL_RELEASES_URL)); + } + + if (! preg_match_all('#([^<]+)\s*([^<]+)#', $document, $matches, PREG_SET_ORDER)) { + fail(sprintf('No release found in %s', ALL_RELEASES_URL)); + } + + $versions = []; + + foreach ($matches as [, $version, $stability]) { + if ($stability !== 'stable') { + continue; + } + + if (version_compare($version, $lowest, '>=') && version_compare($version, ($major + 1) . '.0.0', '<')) { + $versions[] = $version; + } + } + + // The document lists the most recent release first, but sort explicitly + // instead of relying on that order + usort($versions, fn (string $a, string $b) => version_compare($b, $a)); + + return $versions; +} + +$targets = ['lowest', 'stable', 'next-stable', 'next-minor']; +$target = $argv[1] ?? ''; +$composerJson = $argv[2] ?? dirname(__DIR__) . '/composer.json'; + +if (! in_array($target, $targets, true)) { + fail(sprintf('Unknown target "%s", expected one of: %s', $target, implode(', ', $targets))); +} + +[$major, $lowest] = parseConstraint($composerJson); +$released = releasedVersions($major, $lowest); + +$version = ''; +$branch = ''; + +if ($released === []) { + // The required version is not released yet, so the extension can only be + // compiled from the development branch of the next minor version. + $branch = sprintf('v%d.x', $major); + + fwrite(STDERR, sprintf( + 'No version published on PECL satisfies the ext-mongodb constraint of %s, falling back to branch %s' . PHP_EOL, + $composerJson, + $branch, + )); +} else { + switch ($target) { + case 'lowest': + if (! in_array($lowest, $released, true)) { + fail(sprintf('Version %s is not published on PECL', $lowest)); + } + + $version = $lowest; + break; + + case 'stable': + $version = $released[0]; + break; + + case 'next-stable': + // Maintenance branch of the latest released minor version + $branch = 'v' . implode('.', array_slice(explode('.', $released[0]), 0, 2)); + break; + + case 'next-minor': + $branch = sprintf('v%d.x', $major); + break; + } +} + +echo << Date: Thu, 3 Sep 2026 13:31:09 +0200 Subject: [PATCH 10/11] Harden the extension version resolution Only version numbers are matched in the PECL document, so the output stays safe to evaluate in a shell. An unreadable or invalid composer.json now fails with an explicit message, and the expansions passed to the resolver are quoted. --- .evergreen/compile-extension.sh | 2 +- tools/extension-version.php | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.evergreen/compile-extension.sh b/.evergreen/compile-extension.sh index fea7d5185..46694b0db 100644 --- a/.evergreen/compile-extension.sh +++ b/.evergreen/compile-extension.sh @@ -13,7 +13,7 @@ resolve_extension_target () fi # Assign before eval, so that a failure of the script stops the build - RESOLVED=$(php ${PROJECT_DIRECTORY}/tools/extension-version.php ${EXTENSION_TARGET:-stable}) + RESOLVED=$(php "${PROJECT_DIRECTORY}/tools/extension-version.php" "${EXTENSION_TARGET:-stable}") eval "${RESOLVED}" } diff --git a/tools/extension-version.php b/tools/extension-version.php index a1dfa99a1..b9924933d 100644 --- a/tools/extension-version.php +++ b/tools/extension-version.php @@ -23,7 +23,18 @@ function fail(string $message): never /** @return array{0: int, 1: string} Major version and lowest version allowed by the constraint */ function parseConstraint(string $composerJson): array { - $composer = json_decode(file_get_contents($composerJson), true); + $contents = @file_get_contents($composerJson); + + if ($contents === false) { + fail(sprintf('Cannot read %s', $composerJson)); + } + + try { + $composer = json_decode($contents, true, flags: JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + fail(sprintf('Cannot parse %s: %s', $composerJson, $exception->getMessage())); + } + $constraint = $composer['require']['ext-mongodb'] ?? fail(sprintf('No ext-mongodb constraint in %s', $composerJson)); if (! preg_match('/^\^(\d+)\.(\d+)(?:\.(\d+))?$/', $constraint, $matches)) { @@ -48,7 +59,9 @@ function releasedVersions(int $major, string $lowest): array fail(sprintf('Cannot read the list of mongodb releases from %s', ALL_RELEASES_URL)); } - if (! preg_match_all('#([^<]+)\s*([^<]+)#', $document, $matches, PREG_SET_ORDER)) { + // Only version numbers are matched, so that the output of this script stays + // safe to evaluate in a shell + if (! preg_match_all('#([\d.]+)\s*(\w+)#', $document, $matches, PREG_SET_ORDER)) { fail(sprintf('No release found in %s', ALL_RELEASES_URL)); } From 9a08856f838c00f85346c9d869720179e65a612d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 3 Sep 2026 13:40:00 +0200 Subject: [PATCH 11/11] Keep the extension name in the setup-php spec for branch builds --- .github/actions/setup/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index ee471195a..43615550f 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -48,7 +48,7 @@ runs: else # Source builds cannot be cached, and caching a moving branch under # a stable key would freeze an outdated extension. - echo "ext-spec=mongodb/mongo-php-driver@${EXTENSION_BRANCH}" >> "$GITHUB_OUTPUT" + echo "ext-spec=mongodb-mongodb/mongo-php-driver@${EXTENSION_BRANCH}" >> "$GITHUB_OUTPUT" echo "ext-cachable=false" >> "$GITHUB_OUTPUT" fi