From b18fd42c61902bd7bef8ddf0e938976a7670197e Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Sat, 26 Sep 2026 15:26:47 -0300 Subject: [PATCH 01/14] ci(behat): shard jobs and disable PHP server workers Split each Behat DB workflow into four feature-path shards and run php -S with workers 0 so each job gets a shorter suite and a single-process server, reducing cURL 52 / server-death cascades. Signed-off-by: Luis Amorim --- .github/workflows/behat-mariadb.yml | 21 ++++++++++++++++++--- .github/workflows/behat-mysql.yml | 21 ++++++++++++++++++--- .github/workflows/behat-pgsql.yml | 21 ++++++++++++++++++--- .github/workflows/behat-sqlite.yml | 21 ++++++++++++++++++--- tests/integration/config/behat.yml | 4 +++- 5 files changed, 75 insertions(+), 13 deletions(-) diff --git a/.github/workflows/behat-mariadb.yml b/.github/workflows/behat-mariadb.yml index 970ced3a0a..e504035cb8 100644 --- a/.github/workflows/behat-mariadb.yml +++ b/.github/workflows/behat-mariadb.yml @@ -57,12 +57,24 @@ jobs: if: needs.changes.outputs.src != 'false' strategy: + fail-fast: false matrix: php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} mariadb-versions: ['10.6', '10.11'] - - name: MariaDB ${{ matrix.mariadb-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} + # Flat shard key so `act --matrix shard:sign` can select a single cell. + shard: [account-admin, file-notify, page-policies, sign] + include: + - shard: account-admin + paths: features/account features/admin features/build + - shard: file-notify + paths: features/file features/identify-account features/notification + - shard: page-policies + paths: features/page features/policies features/settings + - shard: sign + paths: features/sign + + name: MariaDB ${{ matrix.mariadb-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} (${{ matrix.shard }}) services: mariadb: @@ -166,10 +178,13 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ + # Temporary experiment: single-process php -S (no PHP_CLI_SERVER_WORKERS). + BEHAT_WORKERS: "0" run: | export BEHAT_RUN_AS=runner export BEHAT_VERBOSE="$RUNNER_DEBUG" - vendor/bin/behat -f junit -f pretty --colors + # shellcheck disable=SC2086 + vendor/bin/behat -f junit -f pretty --colors ${{ matrix.paths }} - name: Print logs if: always() diff --git a/.github/workflows/behat-mysql.yml b/.github/workflows/behat-mysql.yml index 4e22bb4ff5..9366c6b4a0 100644 --- a/.github/workflows/behat-mysql.yml +++ b/.github/workflows/behat-mysql.yml @@ -61,12 +61,24 @@ jobs: if: needs.changes.outputs.src != 'false' strategy: + fail-fast: false matrix: php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} mysql-versions: ['8.1'] - - name: MySQL ${{ matrix.mysql-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} + # Flat shard key so `act --matrix shard:sign` can select a single cell. + shard: [account-admin, file-notify, page-policies, sign] + include: + - shard: account-admin + paths: features/account features/admin features/build + - shard: file-notify + paths: features/file features/identify-account features/notification + - shard: page-policies + paths: features/page features/policies features/settings + - shard: sign + paths: features/sign + + name: MySQL ${{ matrix.mysql-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} (${{ matrix.shard }}) services: mysql: @@ -178,10 +190,13 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ + # Temporary experiment: single-process php -S (no PHP_CLI_SERVER_WORKERS). + BEHAT_WORKERS: "0" run: | export BEHAT_RUN_AS=runner export BEHAT_VERBOSE="$RUNNER_DEBUG" - vendor/bin/behat -f junit -f pretty --colors + # shellcheck disable=SC2086 + vendor/bin/behat -f junit -f pretty --colors ${{ matrix.paths }} - name: Print logs if: always() diff --git a/.github/workflows/behat-pgsql.yml b/.github/workflows/behat-pgsql.yml index abced51068..503f3700eb 100644 --- a/.github/workflows/behat-pgsql.yml +++ b/.github/workflows/behat-pgsql.yml @@ -59,11 +59,23 @@ jobs: if: needs.changes.outputs.src != 'false' strategy: + fail-fast: false matrix: php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} - - name: PostgreSQL PHP ${{ matrix.mysql-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} + # Flat shard key so `act --matrix shard:sign` can select a single cell. + shard: [account-admin, file-notify, page-policies, sign] + include: + - shard: account-admin + paths: features/account features/admin features/build + - shard: file-notify + paths: features/file features/identify-account features/notification + - shard: page-policies + paths: features/page features/policies features/settings + - shard: sign + paths: features/sign + + name: PostgreSQL PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} (${{ matrix.shard }}) services: postgres: @@ -172,10 +184,13 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ + # Temporary experiment: single-process php -S (no PHP_CLI_SERVER_WORKERS). + BEHAT_WORKERS: "0" run: | export BEHAT_RUN_AS=runner export BEHAT_VERBOSE="$RUNNER_DEBUG" - vendor/bin/behat -f junit -f pretty --colors + # shellcheck disable=SC2086 + vendor/bin/behat -f junit -f pretty --colors ${{ matrix.paths }} - name: Print logs if: always() diff --git a/.github/workflows/behat-sqlite.yml b/.github/workflows/behat-sqlite.yml index 3bf7c3aaff..29f4f43a25 100644 --- a/.github/workflows/behat-sqlite.yml +++ b/.github/workflows/behat-sqlite.yml @@ -59,11 +59,23 @@ jobs: if: needs.changes.outputs.src != 'false' strategy: + fail-fast: false matrix: php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} - - name: SQLite PHP ${{ matrix.mysql-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} + # Flat shard key so `act --matrix shard:sign` can select a single cell. + shard: [account-admin, file-notify, page-policies, sign] + include: + - shard: account-admin + paths: features/account features/admin features/build + - shard: file-notify + paths: features/file features/identify-account features/notification + - shard: page-policies + paths: features/page features/policies features/settings + - shard: sign + paths: features/sign + + name: SQLite PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} (${{ matrix.shard }}) services: mailpit: @@ -163,10 +175,13 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ + # Temporary experiment: single-process php -S (no PHP_CLI_SERVER_WORKERS). + BEHAT_WORKERS: "0" run: | export BEHAT_RUN_AS=runner export BEHAT_VERBOSE="$RUNNER_DEBUG" - vendor/bin/behat -f junit -f pretty --colors + # shellcheck disable=SC2086 + vendor/bin/behat -f junit -f pretty --colors ${{ matrix.paths }} - name: Print logs if: always() diff --git a/tests/integration/config/behat.yml b/tests/integration/config/behat.yml index 6fb644805f..e58a210499 100644 --- a/tests/integration/config/behat.yml +++ b/tests/integration/config/behat.yml @@ -16,7 +16,9 @@ default: base_url: http://mailpit:8025 PhpBuiltin\Server: runAs: www-data - workers: 10 + # Temporary experiment: single-process php -S (no PHP_CLI_SERVER_WORKERS). + # Nested same-server HTTP may deadlock without workers >= 2. + workers: 0 jarnaiz\JUnitFormatter\JUnitFormatterExtension: filename: report.xml outputDir: '%paths.base%/../output/' From cd9d6e28962187e21db0e3c4f4db503ee2bae714 Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Sat, 26 Sep 2026 15:52:01 -0300 Subject: [PATCH 02/14] ci(behat): restore PHP built-in server workers to 2 workers 0 deadlocked nested same-server HTTP for develop/pdf, so Mailpit never received sign-notification emails. Keep shards and use two workers so those fetches can complete. Signed-off-by: Luis Amorim --- .github/workflows/behat-mariadb.yml | 4 ++-- .github/workflows/behat-mysql.yml | 4 ++-- .github/workflows/behat-pgsql.yml | 4 ++-- .github/workflows/behat-sqlite.yml | 4 ++-- tests/integration/config/behat.yml | 5 ++--- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/behat-mariadb.yml b/.github/workflows/behat-mariadb.yml index e504035cb8..cc70d49c6f 100644 --- a/.github/workflows/behat-mariadb.yml +++ b/.github/workflows/behat-mariadb.yml @@ -178,8 +178,8 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # Temporary experiment: single-process php -S (no PHP_CLI_SERVER_WORKERS). - BEHAT_WORKERS: "0" + # Keep >=2 so nested same-server HTTP (e.g. develop/pdf) cannot deadlock. + BEHAT_WORKERS: "2" run: | export BEHAT_RUN_AS=runner export BEHAT_VERBOSE="$RUNNER_DEBUG" diff --git a/.github/workflows/behat-mysql.yml b/.github/workflows/behat-mysql.yml index 9366c6b4a0..161ca9c541 100644 --- a/.github/workflows/behat-mysql.yml +++ b/.github/workflows/behat-mysql.yml @@ -190,8 +190,8 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # Temporary experiment: single-process php -S (no PHP_CLI_SERVER_WORKERS). - BEHAT_WORKERS: "0" + # Keep >=2 so nested same-server HTTP (e.g. develop/pdf) cannot deadlock. + BEHAT_WORKERS: "2" run: | export BEHAT_RUN_AS=runner export BEHAT_VERBOSE="$RUNNER_DEBUG" diff --git a/.github/workflows/behat-pgsql.yml b/.github/workflows/behat-pgsql.yml index 503f3700eb..71a4e3d5bf 100644 --- a/.github/workflows/behat-pgsql.yml +++ b/.github/workflows/behat-pgsql.yml @@ -184,8 +184,8 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # Temporary experiment: single-process php -S (no PHP_CLI_SERVER_WORKERS). - BEHAT_WORKERS: "0" + # Keep >=2 so nested same-server HTTP (e.g. develop/pdf) cannot deadlock. + BEHAT_WORKERS: "2" run: | export BEHAT_RUN_AS=runner export BEHAT_VERBOSE="$RUNNER_DEBUG" diff --git a/.github/workflows/behat-sqlite.yml b/.github/workflows/behat-sqlite.yml index 29f4f43a25..b4e90ba196 100644 --- a/.github/workflows/behat-sqlite.yml +++ b/.github/workflows/behat-sqlite.yml @@ -175,8 +175,8 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # Temporary experiment: single-process php -S (no PHP_CLI_SERVER_WORKERS). - BEHAT_WORKERS: "0" + # Keep >=2 so nested same-server HTTP (e.g. develop/pdf) cannot deadlock. + BEHAT_WORKERS: "2" run: | export BEHAT_RUN_AS=runner export BEHAT_VERBOSE="$RUNNER_DEBUG" diff --git a/tests/integration/config/behat.yml b/tests/integration/config/behat.yml index e58a210499..53dfd92ff8 100644 --- a/tests/integration/config/behat.yml +++ b/tests/integration/config/behat.yml @@ -16,9 +16,8 @@ default: base_url: http://mailpit:8025 PhpBuiltin\Server: runAs: www-data - # Temporary experiment: single-process php -S (no PHP_CLI_SERVER_WORKERS). - # Nested same-server HTTP may deadlock without workers >= 2. - workers: 0 + # Keep >=2 so nested same-server HTTP (e.g. develop/pdf) cannot deadlock. + workers: 2 jarnaiz\JUnitFormatter\JUnitFormatterExtension: filename: report.xml outputDir: '%paths.base%/../output/' From 0474cd3a93268557c917decd0659bfac1e5eb060 Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Sat, 26 Sep 2026 16:26:45 -0300 Subject: [PATCH 03/14] ci(behat): serve Nextcloud with FrankenPHP instead of php -S The built-in server still dies with cURL 52 after sharding and workers 2. Intercept PhpBuiltin's php -S in CI with FrankenPHP php-server so concurrent and nested requests share a stable process. Signed-off-by: Luis Amorim --- .github/workflows/behat-mariadb.yml | 12 ++- .github/workflows/behat-mysql.yml | 12 ++- .github/workflows/behat-pgsql.yml | 12 ++- .github/workflows/behat-sqlite.yml | 12 ++- .../bin/ci-setup-frankenphp-behat-server.sh | 93 +++++++++++++++++++ tests/integration/config/behat.yml | 3 +- 6 files changed, 135 insertions(+), 9 deletions(-) create mode 100755 tests/integration/bin/ci-setup-frankenphp-behat-server.sh diff --git a/.github/workflows/behat-mariadb.yml b/.github/workflows/behat-mariadb.yml index 8c5616d959..5e01fbb2b0 100644 --- a/.github/workflows/behat-mariadb.yml +++ b/.github/workflows/behat-mariadb.yml @@ -182,9 +182,14 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # Keep >=2 so nested same-server HTTP (e.g. develop/pdf) cannot deadlock. - BEHAT_WORKERS: "2" + # FrankenPHP php-server does not use PHP_CLI_SERVER_WORKERS. + BEHAT_WORKERS: "0" + # Force IPv4 so server bind and Guzzle/cURL agree. + BEHAT_HOST: "127.0.0.1" run: | + WRAP_BIN="$(./bin/ci-setup-frankenphp-behat-server.sh)" + export PATH="$WRAP_BIN:$PATH" + hash -r 2>/dev/null || true export BEHAT_RUN_AS=runner export BEHAT_VERBOSE="$RUNNER_DEBUG" # shellcheck disable=SC2086 @@ -193,6 +198,9 @@ jobs: - name: Print logs if: always() run: | + echo "=== Behat web server processes ===" + ps aux | grep -E '[p]hp -S|[f]rankenphp' || echo "No Behat web server process" + echo "=== nextcloud.log ===" cat data/nextcloud.log - name: List PHP core dumps diff --git a/.github/workflows/behat-mysql.yml b/.github/workflows/behat-mysql.yml index 906c786ded..41e523aa30 100644 --- a/.github/workflows/behat-mysql.yml +++ b/.github/workflows/behat-mysql.yml @@ -194,9 +194,14 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # Keep >=2 so nested same-server HTTP (e.g. develop/pdf) cannot deadlock. - BEHAT_WORKERS: "2" + # FrankenPHP php-server does not use PHP_CLI_SERVER_WORKERS. + BEHAT_WORKERS: "0" + # Force IPv4 so server bind and Guzzle/cURL agree. + BEHAT_HOST: "127.0.0.1" run: | + WRAP_BIN="$(./bin/ci-setup-frankenphp-behat-server.sh)" + export PATH="$WRAP_BIN:$PATH" + hash -r 2>/dev/null || true export BEHAT_RUN_AS=runner export BEHAT_VERBOSE="$RUNNER_DEBUG" # shellcheck disable=SC2086 @@ -205,6 +210,9 @@ jobs: - name: Print logs if: always() run: | + echo "=== Behat web server processes ===" + ps aux | grep -E '[p]hp -S|[f]rankenphp' || echo "No Behat web server process" + echo "=== nextcloud.log ===" cat data/nextcloud.log - name: List PHP core dumps diff --git a/.github/workflows/behat-pgsql.yml b/.github/workflows/behat-pgsql.yml index 763a6b9bda..b2070b08bb 100644 --- a/.github/workflows/behat-pgsql.yml +++ b/.github/workflows/behat-pgsql.yml @@ -188,9 +188,14 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # Keep >=2 so nested same-server HTTP (e.g. develop/pdf) cannot deadlock. - BEHAT_WORKERS: "2" + # FrankenPHP php-server does not use PHP_CLI_SERVER_WORKERS. + BEHAT_WORKERS: "0" + # Force IPv4 so server bind and Guzzle/cURL agree. + BEHAT_HOST: "127.0.0.1" run: | + WRAP_BIN="$(./bin/ci-setup-frankenphp-behat-server.sh)" + export PATH="$WRAP_BIN:$PATH" + hash -r 2>/dev/null || true export BEHAT_RUN_AS=runner export BEHAT_VERBOSE="$RUNNER_DEBUG" # shellcheck disable=SC2086 @@ -199,6 +204,9 @@ jobs: - name: Print logs if: always() run: | + echo "=== Behat web server processes ===" + ps aux | grep -E '[p]hp -S|[f]rankenphp' || echo "No Behat web server process" + echo "=== nextcloud.log ===" cat data/nextcloud.log - name: List PHP core dumps diff --git a/.github/workflows/behat-sqlite.yml b/.github/workflows/behat-sqlite.yml index 7c3d530550..f58fc61fab 100644 --- a/.github/workflows/behat-sqlite.yml +++ b/.github/workflows/behat-sqlite.yml @@ -179,9 +179,14 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # Keep >=2 so nested same-server HTTP (e.g. develop/pdf) cannot deadlock. - BEHAT_WORKERS: "2" + # FrankenPHP php-server does not use PHP_CLI_SERVER_WORKERS. + BEHAT_WORKERS: "0" + # Force IPv4 so server bind and Guzzle/cURL agree. + BEHAT_HOST: "127.0.0.1" run: | + WRAP_BIN="$(./bin/ci-setup-frankenphp-behat-server.sh)" + export PATH="$WRAP_BIN:$PATH" + hash -r 2>/dev/null || true export BEHAT_RUN_AS=runner export BEHAT_VERBOSE="$RUNNER_DEBUG" # shellcheck disable=SC2086 @@ -190,6 +195,9 @@ jobs: - name: Print logs if: always() run: | + echo "=== Behat web server processes ===" + ps aux | grep -E '[p]hp -S|[f]rankenphp' || echo "No Behat web server process" + echo "=== nextcloud.log ===" cat data/nextcloud.log - name: List PHP core dumps diff --git a/tests/integration/bin/ci-setup-frankenphp-behat-server.sh b/tests/integration/bin/ci-setup-frankenphp-behat-server.sh new file mode 100755 index 0000000000..34e91d1db2 --- /dev/null +++ b/tests/integration/bin/ci-setup-frankenphp-behat-server.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Install FrankenPHP and put a `php` wrapper earlier on PATH so +# PhpBuiltin\RunServerListener's `php -S` becomes FrankenPHP php-server. +# CLI php (occ, behat) still uses setup-php via php.real. +# Prints the wrapper bin directory to stdout (prepend to PATH). +set -euo pipefail + +DEST="${FRANKENPHP_DIR:-/tmp/libresign-frankenphp-behat}" +mkdir -p "$DEST/bin" + +ARCH="$(uname -m)" +case "$ARCH" in + x86_64|amd64) ASSET="frankenphp-linux-x86_64" ;; + aarch64|arm64) ASSET="frankenphp-linux-aarch64" ;; + *) + echo "ci-setup-frankenphp-behat-server: unsupported architecture: $ARCH" >&2 + exit 1 + ;; +esac + +VER="${FRANKENPHP_VERSION:-1.12.7}" +if [[ ! -x "$DEST/frankenphp" ]]; then + echo "Downloading FrankenPHP v${VER} (${ASSET})..." >&2 + curl -fsSL -o "$DEST/frankenphp" \ + "https://github.com/php/frankenphp/releases/download/v${VER}/${ASSET}" + chmod +x "$DEST/frankenphp" +fi + +REAL_PHP="$(command -v php)" +if [[ -z "$REAL_PHP" ]]; then + echo "ci-setup-frankenphp-behat-server: php not found on PATH" >&2 + exit 1 +fi +ln -sfn "$REAL_PHP" "$DEST/bin/php.real" + +cat >"$DEST/bin/php" <<'WRAP' +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +set -euo pipefail +if [[ "${1:-}" == "-S" ]]; then + LISTEN="${2:?php -S requires host:port}" + ROOT="." + shift 2 + while [[ $# -gt 0 ]]; do + case "$1" in + -t) + ROOT="${2:?}" + shift 2 + ;; + *) + shift + ;; + esac + done + # escapeshellarg() may quote the document root + ROOT="${ROOT#\'}" + ROOT="${ROOT%\'}" + FRANKENPHP="$(cd "$(dirname "$0")/.." && pwd)/frankenphp" + # Keep argv0 looking like php -S so leftover-process scans still match. + exec -a "php -S ${LISTEN}" "$FRANKENPHP" php-server --listen="$LISTEN" --root="$ROOT" +fi +exec "$(dirname "$0")/php.real" "$@" +WRAP +chmod +x "$DEST/bin/php" + +# RunServerListener prefers PHP_BINARY over PATH php. Force PATH so the wrapper +# is the binary used for `php -S` without changing committed vendor code. +LISTENER="" +if [[ -f vendor/libresign/behat-builtin-extension/src/RunServerListener.php ]]; then + LISTENER="vendor/libresign/behat-builtin-extension/src/RunServerListener.php" +elif [[ -f vendor/phpbuiltin/server/src/RunServerListener.php ]]; then + LISTENER="vendor/phpbuiltin/server/src/RunServerListener.php" +fi +if [[ -n "$LISTENER" ]]; then + python3 - "$LISTENER" <<'PY' +import sys +from pathlib import Path + +path = Path(sys.argv[1]) +text = path.read_text() +old = "$php = PHP_BINARY !== '' && is_file(PHP_BINARY) ? escapeshellarg(PHP_BINARY) : 'php';" +new = "$php = 'php';" +if old not in text: + raise SystemExit(f"ci-setup-frankenphp-behat-server: {path} no longer selects PHP_BINARY; update the wrapper") +path.write_text(text.replace(old, new, 1)) +PY +fi + +echo "$DEST/bin" diff --git a/tests/integration/config/behat.yml b/tests/integration/config/behat.yml index 53dfd92ff8..e4fb9f5194 100644 --- a/tests/integration/config/behat.yml +++ b/tests/integration/config/behat.yml @@ -16,7 +16,8 @@ default: base_url: http://mailpit:8025 PhpBuiltin\Server: runAs: www-data - # Keep >=2 so nested same-server HTTP (e.g. develop/pdf) cannot deadlock. + # Local default. CI replaces php -S with FrankenPHP and sets BEHAT_WORKERS=0. + # Keep >=2 locally so nested same-server HTTP (e.g. develop/pdf) cannot deadlock. workers: 2 jarnaiz\JUnitFormatter\JUnitFormatterExtension: filename: report.xml From f1d545853d382f4c90614f57d6d68091afc117ac Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Sat, 26 Sep 2026 16:48:56 -0300 Subject: [PATCH 04/14] fix(ci): keep TSA and fixture php -S off FrankenPHP The PATH wrapper rewrote every php -S, so the TSA mock lost its /tsr router and JSignPdf failed with 422. Only Nextcloud document roots now go through FrankenPHP. Signed-off-by: Luis Amorim --- .../bin/ci-setup-frankenphp-behat-server.sh | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/tests/integration/bin/ci-setup-frankenphp-behat-server.sh b/tests/integration/bin/ci-setup-frankenphp-behat-server.sh index 34e91d1db2..e57f16d61a 100755 --- a/tests/integration/bin/ci-setup-frankenphp-behat-server.sh +++ b/tests/integration/bin/ci-setup-frankenphp-behat-server.sh @@ -4,6 +4,8 @@ # # Install FrankenPHP and put a `php` wrapper earlier on PATH so # PhpBuiltin\RunServerListener's `php -S` becomes FrankenPHP php-server. +# Only Nextcloud document roots are rewritten. TSA mock and fixture +# servers keep the real php -S (they need a router or static files). # CLI php (occ, behat) still uses setup-php via php.real. # Prints the wrapper bin directory to stdout (prepend to PATH). set -euo pipefail @@ -41,29 +43,44 @@ cat >"$DEST/bin/php" <<'WRAP' # SPDX-FileCopyrightText: 2026 LibreCode coop and contributors # SPDX-License-Identifier: AGPL-3.0-or-later set -euo pipefail +REAL="$(dirname "$0")/php.real" if [[ "${1:-}" == "-S" ]]; then LISTEN="${2:?php -S requires host:port}" ROOT="." - shift 2 - while [[ $# -gt 0 ]]; do - case "$1" in + ROUTER="" + i=3 + while [[ $i -le $# ]]; do + arg="${!i}" + case "$arg" in -t) - ROOT="${2:?}" - shift 2 + next=$((i + 1)) + ROOT="${!next:?}" + i=$((i + 2)) + ;; + -*) + i=$((i + 1)) ;; *) - shift + ROUTER="$arg" + i=$((i + 1)) ;; esac done - # escapeshellarg() may quote the document root + # escapeshellarg() may quote the document root or router path ROOT="${ROOT#\'}" ROOT="${ROOT%\'}" + ROUTER="${ROUTER#\'}" + ROUTER="${ROUTER%\'}" + # TSA: php -S host:port -t $workDir $router.php — keep built-in server. + # Fixtures: php -S host:port -t $pdfDir — no occ, keep built-in server. + if [[ -n "$ROUTER" || ! -f "$ROOT/occ" || ! -f "$ROOT/lib/base.php" ]]; then + exec "$REAL" "$@" + fi FRANKENPHP="$(cd "$(dirname "$0")/.." && pwd)/frankenphp" # Keep argv0 looking like php -S so leftover-process scans still match. exec -a "php -S ${LISTEN}" "$FRANKENPHP" php-server --listen="$LISTEN" --root="$ROOT" fi -exec "$(dirname "$0")/php.real" "$@" +exec "$REAL" "$@" WRAP chmod +x "$DEST/bin/php" From 053305e7bf9f16a75c0c0259f293773d8485fe29 Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Sat, 26 Sep 2026 17:36:56 -0300 Subject: [PATCH 05/14] ci(behat): probe php -S flake with verbose logs Skip FrankenPHP for one CI pass so the built-in server can fail with cURL 52 under workers 2. Force BEHAT_VERBOSE=1 and keep the diagnostic files on failure. Signed-off-by: Luis Amorim --- .github/workflows/behat-mariadb.yml | 31 +++++++++++++++++++---------- .github/workflows/behat-mysql.yml | 31 +++++++++++++++++++---------- .github/workflows/behat-pgsql.yml | 31 +++++++++++++++++++---------- .github/workflows/behat-sqlite.yml | 31 +++++++++++++++++++---------- tests/integration/config/behat.yml | 4 ++-- 5 files changed, 82 insertions(+), 46 deletions(-) diff --git a/.github/workflows/behat-mariadb.yml b/.github/workflows/behat-mariadb.yml index 5e01fbb2b0..e4cdf8d890 100644 --- a/.github/workflows/behat-mariadb.yml +++ b/.github/workflows/behat-mariadb.yml @@ -182,37 +182,46 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # FrankenPHP php-server does not use PHP_CLI_SERVER_WORKERS. - BEHAT_WORKERS: "0" - # Force IPv4 so server bind and Guzzle/cURL agree. + # Probe: real php -S + workers 2. FrankenPHP hid cURL 52. + BEHAT_WORKERS: "2" BEHAT_HOST: "127.0.0.1" + # Capture worker PID, accept/close, and unexpected-death diagnostics. + BEHAT_VERBOSE: "1" run: | - WRAP_BIN="$(./bin/ci-setup-frankenphp-behat-server.sh)" - export PATH="$WRAP_BIN:$PATH" - hash -r 2>/dev/null || true export BEHAT_RUN_AS=runner - export BEHAT_VERBOSE="$RUNNER_DEBUG" # shellcheck disable=SC2086 - vendor/bin/behat -f junit -f pretty --colors ${{ matrix.paths }} + vendor/bin/behat -v -f junit -f pretty --colors ${{ matrix.paths }} - name: Print logs if: always() run: | echo "=== Behat web server processes ===" ps aux | grep -E '[p]hp -S|[f]rankenphp' || echo "No Behat web server process" + echo "=== php -S verbose diagnostics ===" + shopt -s nullglob + files=(/tmp/behat-php-server-*) + if (( ${#files[@]} )); then + ls -la "${files[@]}" + for f in "${files[@]}"; do + echo "----- $f -----" + cat "$f" + done + else + echo "No /tmp/behat-php-server-* files" + fi echo "=== nextcloud.log ===" cat data/nextcloud.log - name: List PHP core dumps - if: failure() && runner.debug == '1' + if: failure() run: | sudo coredumpctl --no-pager list || true - name: Upload PHP server diagnostics - if: failure() && runner.debug == '1' + if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: behat-mariadb-server-diagnostics-${{ matrix.mariadb-versions }}-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }} + name: behat-mariadb-server-diagnostics-${{ matrix.mariadb-versions }}-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }}-${{ matrix.shard }} path: /tmp/behat-php-server-* if-no-files-found: ignore diff --git a/.github/workflows/behat-mysql.yml b/.github/workflows/behat-mysql.yml index 41e523aa30..619f032fe1 100644 --- a/.github/workflows/behat-mysql.yml +++ b/.github/workflows/behat-mysql.yml @@ -194,37 +194,46 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # FrankenPHP php-server does not use PHP_CLI_SERVER_WORKERS. - BEHAT_WORKERS: "0" - # Force IPv4 so server bind and Guzzle/cURL agree. + # Probe: real php -S + workers 2. FrankenPHP hid cURL 52. + BEHAT_WORKERS: "2" BEHAT_HOST: "127.0.0.1" + # Capture worker PID, accept/close, and unexpected-death diagnostics. + BEHAT_VERBOSE: "1" run: | - WRAP_BIN="$(./bin/ci-setup-frankenphp-behat-server.sh)" - export PATH="$WRAP_BIN:$PATH" - hash -r 2>/dev/null || true export BEHAT_RUN_AS=runner - export BEHAT_VERBOSE="$RUNNER_DEBUG" # shellcheck disable=SC2086 - vendor/bin/behat -f junit -f pretty --colors ${{ matrix.paths }} + vendor/bin/behat -v -f junit -f pretty --colors ${{ matrix.paths }} - name: Print logs if: always() run: | echo "=== Behat web server processes ===" ps aux | grep -E '[p]hp -S|[f]rankenphp' || echo "No Behat web server process" + echo "=== php -S verbose diagnostics ===" + shopt -s nullglob + files=(/tmp/behat-php-server-*) + if (( ${#files[@]} )); then + ls -la "${files[@]}" + for f in "${files[@]}"; do + echo "----- $f -----" + cat "$f" + done + else + echo "No /tmp/behat-php-server-* files" + fi echo "=== nextcloud.log ===" cat data/nextcloud.log - name: List PHP core dumps - if: failure() && runner.debug == '1' + if: failure() run: | sudo coredumpctl --no-pager list || true - name: Upload PHP server diagnostics - if: failure() && runner.debug == '1' + if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: behat-mysql-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }} + name: behat-mysql-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }}-${{ matrix.shard }} path: /tmp/behat-php-server-* if-no-files-found: ignore diff --git a/.github/workflows/behat-pgsql.yml b/.github/workflows/behat-pgsql.yml index b2070b08bb..a2ab54c3a6 100644 --- a/.github/workflows/behat-pgsql.yml +++ b/.github/workflows/behat-pgsql.yml @@ -188,37 +188,46 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # FrankenPHP php-server does not use PHP_CLI_SERVER_WORKERS. - BEHAT_WORKERS: "0" - # Force IPv4 so server bind and Guzzle/cURL agree. + # Probe: real php -S + workers 2. FrankenPHP hid cURL 52. + BEHAT_WORKERS: "2" BEHAT_HOST: "127.0.0.1" + # Capture worker PID, accept/close, and unexpected-death diagnostics. + BEHAT_VERBOSE: "1" run: | - WRAP_BIN="$(./bin/ci-setup-frankenphp-behat-server.sh)" - export PATH="$WRAP_BIN:$PATH" - hash -r 2>/dev/null || true export BEHAT_RUN_AS=runner - export BEHAT_VERBOSE="$RUNNER_DEBUG" # shellcheck disable=SC2086 - vendor/bin/behat -f junit -f pretty --colors ${{ matrix.paths }} + vendor/bin/behat -v -f junit -f pretty --colors ${{ matrix.paths }} - name: Print logs if: always() run: | echo "=== Behat web server processes ===" ps aux | grep -E '[p]hp -S|[f]rankenphp' || echo "No Behat web server process" + echo "=== php -S verbose diagnostics ===" + shopt -s nullglob + files=(/tmp/behat-php-server-*) + if (( ${#files[@]} )); then + ls -la "${files[@]}" + for f in "${files[@]}"; do + echo "----- $f -----" + cat "$f" + done + else + echo "No /tmp/behat-php-server-* files" + fi echo "=== nextcloud.log ===" cat data/nextcloud.log - name: List PHP core dumps - if: failure() && runner.debug == '1' + if: failure() run: | sudo coredumpctl --no-pager list || true - name: Upload PHP server diagnostics - if: failure() && runner.debug == '1' + if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: behat-pgsql-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }} + name: behat-pgsql-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }}-${{ matrix.shard }} path: /tmp/behat-php-server-* if-no-files-found: ignore diff --git a/.github/workflows/behat-sqlite.yml b/.github/workflows/behat-sqlite.yml index f58fc61fab..50ecf3e439 100644 --- a/.github/workflows/behat-sqlite.yml +++ b/.github/workflows/behat-sqlite.yml @@ -179,37 +179,46 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # FrankenPHP php-server does not use PHP_CLI_SERVER_WORKERS. - BEHAT_WORKERS: "0" - # Force IPv4 so server bind and Guzzle/cURL agree. + # Probe: real php -S + workers 2. FrankenPHP hid cURL 52. + BEHAT_WORKERS: "2" BEHAT_HOST: "127.0.0.1" + # Capture worker PID, accept/close, and unexpected-death diagnostics. + BEHAT_VERBOSE: "1" run: | - WRAP_BIN="$(./bin/ci-setup-frankenphp-behat-server.sh)" - export PATH="$WRAP_BIN:$PATH" - hash -r 2>/dev/null || true export BEHAT_RUN_AS=runner - export BEHAT_VERBOSE="$RUNNER_DEBUG" # shellcheck disable=SC2086 - vendor/bin/behat -f junit -f pretty --colors ${{ matrix.paths }} + vendor/bin/behat -v -f junit -f pretty --colors ${{ matrix.paths }} - name: Print logs if: always() run: | echo "=== Behat web server processes ===" ps aux | grep -E '[p]hp -S|[f]rankenphp' || echo "No Behat web server process" + echo "=== php -S verbose diagnostics ===" + shopt -s nullglob + files=(/tmp/behat-php-server-*) + if (( ${#files[@]} )); then + ls -la "${files[@]}" + for f in "${files[@]}"; do + echo "----- $f -----" + cat "$f" + done + else + echo "No /tmp/behat-php-server-* files" + fi echo "=== nextcloud.log ===" cat data/nextcloud.log - name: List PHP core dumps - if: failure() && runner.debug == '1' + if: failure() run: | sudo coredumpctl --no-pager list || true - name: Upload PHP server diagnostics - if: failure() && runner.debug == '1' + if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: behat-sqlite-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }} + name: behat-sqlite-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }}-${{ matrix.shard }} path: /tmp/behat-php-server-* if-no-files-found: ignore diff --git a/tests/integration/config/behat.yml b/tests/integration/config/behat.yml index e4fb9f5194..c3fbd588ef 100644 --- a/tests/integration/config/behat.yml +++ b/tests/integration/config/behat.yml @@ -16,8 +16,8 @@ default: base_url: http://mailpit:8025 PhpBuiltin\Server: runAs: www-data - # Local default. CI replaces php -S with FrankenPHP and sets BEHAT_WORKERS=0. - # Keep >=2 locally so nested same-server HTTP (e.g. develop/pdf) cannot deadlock. + # Local default. CI probe uses the real php -S with BEHAT_WORKERS=2 and BEHAT_VERBOSE=1. + # Keep >=2 so nested same-server HTTP (e.g. develop/pdf) cannot deadlock. workers: 2 jarnaiz\JUnitFormatter\JUnitFormatterExtension: filename: report.xml From 93cf570cb09b07e333c24f7392ca95c126cb9dc7 Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Sat, 26 Sep 2026 17:58:06 -0300 Subject: [PATCH 06/14] ci(behat): install gdb to backtrace php -S SIGSEGV The worker dies in zend_objects_store_del. Always install gdb so the verbose listener and a failure step can dump bt full, and disable JIT so the next core has a readable PHP frame. Signed-off-by: Luis Amorim --- .github/workflows/behat-mariadb.yml | 21 +++++++++++++-------- .github/workflows/behat-mysql.yml | 21 +++++++++++++-------- .github/workflows/behat-pgsql.yml | 21 +++++++++++++-------- .github/workflows/behat-sqlite.yml | 21 +++++++++++++-------- 4 files changed, 52 insertions(+), 32 deletions(-) diff --git a/.github/workflows/behat-mariadb.yml b/.github/workflows/behat-mariadb.yml index e4cdf8d890..b44c077ce2 100644 --- a/.github/workflows/behat-mariadb.yml +++ b/.github/workflows/behat-mariadb.yml @@ -97,11 +97,7 @@ jobs: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV - name: Install system dependencies - run: sudo apt update && sudo apt install poppler-utils - - - name: Install PHP crash diagnostics - if: runner.debug == '1' - run: sudo apt install -y gdb systemd-coredump + run: sudo apt update && sudo apt install poppler-utils gdb systemd-coredump - name: Checkout server uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -128,7 +124,8 @@ jobs: coverage: none ini-file: development # Required: re-enable pcntl_* functions that may be disabled via disable_functions - ini-values: disable_functions= + # Disable JIT so a worker SIGSEGV has a readable PHP frame, not n/a. + ini-values: disable_functions=, opcache.jit=disable, opcache.jit_buffer_size=0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -212,17 +209,25 @@ jobs: echo "=== nextcloud.log ===" cat data/nextcloud.log - - name: List PHP core dumps + - name: Backtrace PHP core dumps if: failure() run: | sudo coredumpctl --no-pager list || true + { + echo "===== latest php core =====" + sudo coredumpctl --no-pager info || true + echo "===== gdb bt full =====" + sudo coredumpctl --no-pager debug --debugger=gdb --debugger-arguments="-batch -ex 'set pagination off' -ex 'thread apply all bt full' -ex 'info registers' -ex 'info sharedlibrary'" || true + } | tee /tmp/php-s-gdb-bt.txt - name: Upload PHP server diagnostics if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: behat-mariadb-server-diagnostics-${{ matrix.mariadb-versions }}-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }}-${{ matrix.shard }} - path: /tmp/behat-php-server-* + path: | + /tmp/behat-php-server-* + /tmp/php-s-gdb-bt.txt if-no-files-found: ignore summary: diff --git a/.github/workflows/behat-mysql.yml b/.github/workflows/behat-mysql.yml index 619f032fe1..9f5993b9d5 100644 --- a/.github/workflows/behat-mysql.yml +++ b/.github/workflows/behat-mysql.yml @@ -101,11 +101,7 @@ jobs: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV - name: Install system dependencies - run: sudo apt update && sudo apt install poppler-utils - - - name: Install PHP crash diagnostics - if: runner.debug == '1' - run: sudo apt install -y gdb systemd-coredump + run: sudo apt update && sudo apt install poppler-utils gdb systemd-coredump - name: Checkout server uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -132,7 +128,8 @@ jobs: coverage: none ini-file: development # Required: re-enable pcntl_* functions that may be disabled via disable_functions - ini-values: disable_functions= + # Disable JIT so a worker SIGSEGV has a readable PHP frame, not n/a. + ini-values: disable_functions=, opcache.jit=disable, opcache.jit_buffer_size=0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -224,17 +221,25 @@ jobs: echo "=== nextcloud.log ===" cat data/nextcloud.log - - name: List PHP core dumps + - name: Backtrace PHP core dumps if: failure() run: | sudo coredumpctl --no-pager list || true + { + echo "===== latest php core =====" + sudo coredumpctl --no-pager info || true + echo "===== gdb bt full =====" + sudo coredumpctl --no-pager debug --debugger=gdb --debugger-arguments="-batch -ex 'set pagination off' -ex 'thread apply all bt full' -ex 'info registers' -ex 'info sharedlibrary'" || true + } | tee /tmp/php-s-gdb-bt.txt - name: Upload PHP server diagnostics if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: behat-mysql-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }}-${{ matrix.shard }} - path: /tmp/behat-php-server-* + path: | + /tmp/behat-php-server-* + /tmp/php-s-gdb-bt.txt if-no-files-found: ignore summary: diff --git a/.github/workflows/behat-pgsql.yml b/.github/workflows/behat-pgsql.yml index a2ab54c3a6..ea7e0cbe10 100644 --- a/.github/workflows/behat-pgsql.yml +++ b/.github/workflows/behat-pgsql.yml @@ -100,11 +100,7 @@ jobs: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV - name: Install system dependencies - run: sudo apt update && sudo apt install poppler-utils - - - name: Install PHP crash diagnostics - if: runner.debug == '1' - run: sudo apt install -y gdb systemd-coredump + run: sudo apt update && sudo apt install poppler-utils gdb systemd-coredump - name: Checkout server uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -131,7 +127,8 @@ jobs: coverage: none ini-file: development # Required: re-enable pcntl_* functions that may be disabled via disable_functions - ini-values: disable_functions= + # Disable JIT so a worker SIGSEGV has a readable PHP frame, not n/a. + ini-values: disable_functions=, opcache.jit=disable, opcache.jit_buffer_size=0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -218,17 +215,25 @@ jobs: echo "=== nextcloud.log ===" cat data/nextcloud.log - - name: List PHP core dumps + - name: Backtrace PHP core dumps if: failure() run: | sudo coredumpctl --no-pager list || true + { + echo "===== latest php core =====" + sudo coredumpctl --no-pager info || true + echo "===== gdb bt full =====" + sudo coredumpctl --no-pager debug --debugger=gdb --debugger-arguments="-batch -ex 'set pagination off' -ex 'thread apply all bt full' -ex 'info registers' -ex 'info sharedlibrary'" || true + } | tee /tmp/php-s-gdb-bt.txt - name: Upload PHP server diagnostics if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: behat-pgsql-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }}-${{ matrix.shard }} - path: /tmp/behat-php-server-* + path: | + /tmp/behat-php-server-* + /tmp/php-s-gdb-bt.txt if-no-files-found: ignore summary: diff --git a/.github/workflows/behat-sqlite.yml b/.github/workflows/behat-sqlite.yml index 50ecf3e439..3bced05b59 100644 --- a/.github/workflows/behat-sqlite.yml +++ b/.github/workflows/behat-sqlite.yml @@ -91,11 +91,7 @@ jobs: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV - name: Install system dependencies - run: sudo apt update && sudo apt install poppler-utils - - - name: Install PHP crash diagnostics - if: runner.debug == '1' - run: sudo apt install -y gdb systemd-coredump + run: sudo apt update && sudo apt install poppler-utils gdb systemd-coredump - name: Checkout server uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -122,7 +118,8 @@ jobs: coverage: none ini-file: development # Required: re-enable pcntl_* functions that may be disabled via disable_functions - ini-values: disable_functions= + # Disable JIT so a worker SIGSEGV has a readable PHP frame, not n/a. + ini-values: disable_functions=, opcache.jit=disable, opcache.jit_buffer_size=0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -209,17 +206,25 @@ jobs: echo "=== nextcloud.log ===" cat data/nextcloud.log - - name: List PHP core dumps + - name: Backtrace PHP core dumps if: failure() run: | sudo coredumpctl --no-pager list || true + { + echo "===== latest php core =====" + sudo coredumpctl --no-pager info || true + echo "===== gdb bt full =====" + sudo coredumpctl --no-pager debug --debugger=gdb --debugger-arguments="-batch -ex 'set pagination off' -ex 'thread apply all bt full' -ex 'info registers' -ex 'info sharedlibrary'" || true + } | tee /tmp/php-s-gdb-bt.txt - name: Upload PHP server diagnostics if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: behat-sqlite-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }}-${{ matrix.shard }} - path: /tmp/behat-php-server-* + path: | + /tmp/behat-php-server-* + /tmp/php-s-gdb-bt.txt if-no-files-found: ignore summary: From 0b37d220defab8ad143a2aac331b3a1cb1d76c4f Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Sat, 26 Sep 2026 18:21:55 -0300 Subject: [PATCH 07/14] ci(behat): run the full suite without feature shards Keep the php -S crash probe (workers 2, JIT off, verbose, gdb) while going back to one Behat job per database so the flake can be compared against the pre-shard layout. Signed-off-by: Luis Amorim --- .github/workflows/behat-mariadb.yml | 20 ++++---------------- .github/workflows/behat-mysql.yml | 18 +++--------------- .github/workflows/behat-pgsql.yml | 20 ++++---------------- .github/workflows/behat-sqlite.yml | 20 ++++---------------- 4 files changed, 15 insertions(+), 63 deletions(-) diff --git a/.github/workflows/behat-mariadb.yml b/.github/workflows/behat-mariadb.yml index b44c077ce2..8b8ced825b 100644 --- a/.github/workflows/behat-mariadb.yml +++ b/.github/workflows/behat-mariadb.yml @@ -57,24 +57,12 @@ jobs: if: needs.changes.outputs.src != 'false' strategy: - fail-fast: false matrix: php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} mariadb-versions: ['10.6', '10.11'] - # Flat shard key so `act --matrix shard:sign` can select a single cell. - shard: [account-admin, file-notify, page-policies, sign] - include: - - shard: account-admin - paths: features/account features/admin features/build - - shard: file-notify - paths: features/file features/identify-account features/notification - - shard: page-policies - paths: features/page features/policies features/settings - - shard: sign - paths: features/sign - - name: MariaDB ${{ matrix.mariadb-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} (${{ matrix.shard }}) + + name: MariaDB ${{ matrix.mariadb-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} services: mariadb: @@ -187,7 +175,7 @@ jobs: run: | export BEHAT_RUN_AS=runner # shellcheck disable=SC2086 - vendor/bin/behat -v -f junit -f pretty --colors ${{ matrix.paths }} + vendor/bin/behat -v -f junit -f pretty --colors - name: Print logs if: always() @@ -224,7 +212,7 @@ jobs: if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: behat-mariadb-server-diagnostics-${{ matrix.mariadb-versions }}-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }}-${{ matrix.shard }} + name: behat-mariadb-server-diagnostics-${{ matrix.mariadb-versions }}-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }} path: | /tmp/behat-php-server-* /tmp/php-s-gdb-bt.txt diff --git a/.github/workflows/behat-mysql.yml b/.github/workflows/behat-mysql.yml index 9f5993b9d5..e38847ccab 100644 --- a/.github/workflows/behat-mysql.yml +++ b/.github/workflows/behat-mysql.yml @@ -61,24 +61,12 @@ jobs: if: needs.changes.outputs.src != 'false' strategy: - fail-fast: false matrix: php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} mysql-versions: ['8.1'] - # Flat shard key so `act --matrix shard:sign` can select a single cell. - shard: [account-admin, file-notify, page-policies, sign] - include: - - shard: account-admin - paths: features/account features/admin features/build - - shard: file-notify - paths: features/file features/identify-account features/notification - - shard: page-policies - paths: features/page features/policies features/settings - - shard: sign - paths: features/sign - name: MySQL ${{ matrix.mysql-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} (${{ matrix.shard }}) + name: MySQL ${{ matrix.mysql-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} services: mysql: @@ -199,7 +187,7 @@ jobs: run: | export BEHAT_RUN_AS=runner # shellcheck disable=SC2086 - vendor/bin/behat -v -f junit -f pretty --colors ${{ matrix.paths }} + vendor/bin/behat -v -f junit -f pretty --colors - name: Print logs if: always() @@ -236,7 +224,7 @@ jobs: if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: behat-mysql-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }}-${{ matrix.shard }} + name: behat-mysql-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }} path: | /tmp/behat-php-server-* /tmp/php-s-gdb-bt.txt diff --git a/.github/workflows/behat-pgsql.yml b/.github/workflows/behat-pgsql.yml index ea7e0cbe10..2502b448cc 100644 --- a/.github/workflows/behat-pgsql.yml +++ b/.github/workflows/behat-pgsql.yml @@ -59,23 +59,11 @@ jobs: if: needs.changes.outputs.src != 'false' strategy: - fail-fast: false matrix: php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} - # Flat shard key so `act --matrix shard:sign` can select a single cell. - shard: [account-admin, file-notify, page-policies, sign] - include: - - shard: account-admin - paths: features/account features/admin features/build - - shard: file-notify - paths: features/file features/identify-account features/notification - - shard: page-policies - paths: features/page features/policies features/settings - - shard: sign - paths: features/sign - - name: PostgreSQL PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} (${{ matrix.shard }}) + + name: PostgreSQL PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} services: postgres: @@ -193,7 +181,7 @@ jobs: run: | export BEHAT_RUN_AS=runner # shellcheck disable=SC2086 - vendor/bin/behat -v -f junit -f pretty --colors ${{ matrix.paths }} + vendor/bin/behat -v -f junit -f pretty --colors - name: Print logs if: always() @@ -230,7 +218,7 @@ jobs: if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: behat-pgsql-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }}-${{ matrix.shard }} + name: behat-pgsql-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }} path: | /tmp/behat-php-server-* /tmp/php-s-gdb-bt.txt diff --git a/.github/workflows/behat-sqlite.yml b/.github/workflows/behat-sqlite.yml index 3bced05b59..aae4d67dfe 100644 --- a/.github/workflows/behat-sqlite.yml +++ b/.github/workflows/behat-sqlite.yml @@ -59,23 +59,11 @@ jobs: if: needs.changes.outputs.src != 'false' strategy: - fail-fast: false matrix: php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} - # Flat shard key so `act --matrix shard:sign` can select a single cell. - shard: [account-admin, file-notify, page-policies, sign] - include: - - shard: account-admin - paths: features/account features/admin features/build - - shard: file-notify - paths: features/file features/identify-account features/notification - - shard: page-policies - paths: features/page features/policies features/settings - - shard: sign - paths: features/sign - - name: SQLite PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} (${{ matrix.shard }}) + + name: SQLite PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} services: mailpit: @@ -184,7 +172,7 @@ jobs: run: | export BEHAT_RUN_AS=runner # shellcheck disable=SC2086 - vendor/bin/behat -v -f junit -f pretty --colors ${{ matrix.paths }} + vendor/bin/behat -v -f junit -f pretty --colors - name: Print logs if: always() @@ -221,7 +209,7 @@ jobs: if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: behat-sqlite-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }}-${{ matrix.shard }} + name: behat-sqlite-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }} path: | /tmp/behat-php-server-* /tmp/php-s-gdb-bt.txt From 1c914e15a1570da9d52c8ef53c50a78cc4e3959e Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Sat, 26 Sep 2026 19:02:48 -0300 Subject: [PATCH 08/14] ci(behat): restore PHP built-in server workers to 10 Match the original PhpBuiltin config so the remaining crash probe runs against the same worker count the flake was seen with. Signed-off-by: Luis Amorim --- .github/workflows/behat-mariadb.yml | 4 ++-- .github/workflows/behat-mysql.yml | 4 ++-- .github/workflows/behat-pgsql.yml | 4 ++-- .github/workflows/behat-sqlite.yml | 4 ++-- tests/integration/config/behat.yml | 4 +--- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/.github/workflows/behat-mariadb.yml b/.github/workflows/behat-mariadb.yml index 8b8ced825b..00dc97cc03 100644 --- a/.github/workflows/behat-mariadb.yml +++ b/.github/workflows/behat-mariadb.yml @@ -167,8 +167,8 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # Probe: real php -S + workers 2. FrankenPHP hid cURL 52. - BEHAT_WORKERS: "2" + # Probe: real php -S + original workers 10. FrankenPHP hid cURL 52. + BEHAT_WORKERS: "10" BEHAT_HOST: "127.0.0.1" # Capture worker PID, accept/close, and unexpected-death diagnostics. BEHAT_VERBOSE: "1" diff --git a/.github/workflows/behat-mysql.yml b/.github/workflows/behat-mysql.yml index e38847ccab..d022551cca 100644 --- a/.github/workflows/behat-mysql.yml +++ b/.github/workflows/behat-mysql.yml @@ -179,8 +179,8 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # Probe: real php -S + workers 2. FrankenPHP hid cURL 52. - BEHAT_WORKERS: "2" + # Probe: real php -S + original workers 10. FrankenPHP hid cURL 52. + BEHAT_WORKERS: "10" BEHAT_HOST: "127.0.0.1" # Capture worker PID, accept/close, and unexpected-death diagnostics. BEHAT_VERBOSE: "1" diff --git a/.github/workflows/behat-pgsql.yml b/.github/workflows/behat-pgsql.yml index 2502b448cc..36afc7834c 100644 --- a/.github/workflows/behat-pgsql.yml +++ b/.github/workflows/behat-pgsql.yml @@ -173,8 +173,8 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # Probe: real php -S + workers 2. FrankenPHP hid cURL 52. - BEHAT_WORKERS: "2" + # Probe: real php -S + original workers 10. FrankenPHP hid cURL 52. + BEHAT_WORKERS: "10" BEHAT_HOST: "127.0.0.1" # Capture worker PID, accept/close, and unexpected-death diagnostics. BEHAT_VERBOSE: "1" diff --git a/.github/workflows/behat-sqlite.yml b/.github/workflows/behat-sqlite.yml index aae4d67dfe..5b2a694042 100644 --- a/.github/workflows/behat-sqlite.yml +++ b/.github/workflows/behat-sqlite.yml @@ -164,8 +164,8 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # Probe: real php -S + workers 2. FrankenPHP hid cURL 52. - BEHAT_WORKERS: "2" + # Probe: real php -S + original workers 10. FrankenPHP hid cURL 52. + BEHAT_WORKERS: "10" BEHAT_HOST: "127.0.0.1" # Capture worker PID, accept/close, and unexpected-death diagnostics. BEHAT_VERBOSE: "1" diff --git a/tests/integration/config/behat.yml b/tests/integration/config/behat.yml index c3fbd588ef..6fb644805f 100644 --- a/tests/integration/config/behat.yml +++ b/tests/integration/config/behat.yml @@ -16,9 +16,7 @@ default: base_url: http://mailpit:8025 PhpBuiltin\Server: runAs: www-data - # Local default. CI probe uses the real php -S with BEHAT_WORKERS=2 and BEHAT_VERBOSE=1. - # Keep >=2 so nested same-server HTTP (e.g. develop/pdf) cannot deadlock. - workers: 2 + workers: 10 jarnaiz\JUnitFormatter\JUnitFormatterExtension: filename: report.xml outputDir: '%paths.base%/../output/' From 4a23e15b95c7d7dc0bda482ff4189acccdbbe628 Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Sat, 26 Sep 2026 19:08:36 -0300 Subject: [PATCH 09/14] ci(behat): drop probe scaffolding and keep original workers The flake is a php -S worker SIGSEGV, not app code. Restore the stock Behat workflows and remove the unused FrankenPHP wrapper; workers stay at 10 from behat.yml. Signed-off-by: Luis Amorim --- .github/workflows/behat-mariadb.yml | 50 +------- .github/workflows/behat-mysql.yml | 50 +------- .github/workflows/behat-pgsql.yml | 52 +-------- .github/workflows/behat-sqlite.yml | 52 +-------- .../bin/ci-setup-frankenphp-behat-server.sh | 110 ------------------ 5 files changed, 18 insertions(+), 296 deletions(-) delete mode 100755 tests/integration/bin/ci-setup-frankenphp-behat-server.sh diff --git a/.github/workflows/behat-mariadb.yml b/.github/workflows/behat-mariadb.yml index 00dc97cc03..970ced3a0a 100644 --- a/.github/workflows/behat-mariadb.yml +++ b/.github/workflows/behat-mariadb.yml @@ -85,7 +85,7 @@ jobs: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV - name: Install system dependencies - run: sudo apt update && sudo apt install poppler-utils gdb systemd-coredump + run: sudo apt update && sudo apt install poppler-utils - name: Checkout server uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -112,8 +112,7 @@ jobs: coverage: none ini-file: development # Required: re-enable pcntl_* functions that may be disabled via disable_functions - # Disable JIT so a worker SIGSEGV has a readable PHP frame, not n/a. - ini-values: disable_functions=, opcache.jit=disable, opcache.jit_buffer_size=0 + ini-values: disable_functions= env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -167,57 +166,16 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # Probe: real php -S + original workers 10. FrankenPHP hid cURL 52. - BEHAT_WORKERS: "10" - BEHAT_HOST: "127.0.0.1" - # Capture worker PID, accept/close, and unexpected-death diagnostics. - BEHAT_VERBOSE: "1" run: | export BEHAT_RUN_AS=runner - # shellcheck disable=SC2086 - vendor/bin/behat -v -f junit -f pretty --colors + export BEHAT_VERBOSE="$RUNNER_DEBUG" + vendor/bin/behat -f junit -f pretty --colors - name: Print logs if: always() run: | - echo "=== Behat web server processes ===" - ps aux | grep -E '[p]hp -S|[f]rankenphp' || echo "No Behat web server process" - echo "=== php -S verbose diagnostics ===" - shopt -s nullglob - files=(/tmp/behat-php-server-*) - if (( ${#files[@]} )); then - ls -la "${files[@]}" - for f in "${files[@]}"; do - echo "----- $f -----" - cat "$f" - done - else - echo "No /tmp/behat-php-server-* files" - fi - echo "=== nextcloud.log ===" cat data/nextcloud.log - - name: Backtrace PHP core dumps - if: failure() - run: | - sudo coredumpctl --no-pager list || true - { - echo "===== latest php core =====" - sudo coredumpctl --no-pager info || true - echo "===== gdb bt full =====" - sudo coredumpctl --no-pager debug --debugger=gdb --debugger-arguments="-batch -ex 'set pagination off' -ex 'thread apply all bt full' -ex 'info registers' -ex 'info sharedlibrary'" || true - } | tee /tmp/php-s-gdb-bt.txt - - - name: Upload PHP server diagnostics - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: behat-mariadb-server-diagnostics-${{ matrix.mariadb-versions }}-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }} - path: | - /tmp/behat-php-server-* - /tmp/php-s-gdb-bt.txt - if-no-files-found: ignore - summary: permissions: contents: none diff --git a/.github/workflows/behat-mysql.yml b/.github/workflows/behat-mysql.yml index d022551cca..4e22bb4ff5 100644 --- a/.github/workflows/behat-mysql.yml +++ b/.github/workflows/behat-mysql.yml @@ -89,7 +89,7 @@ jobs: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV - name: Install system dependencies - run: sudo apt update && sudo apt install poppler-utils gdb systemd-coredump + run: sudo apt update && sudo apt install poppler-utils - name: Checkout server uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -116,8 +116,7 @@ jobs: coverage: none ini-file: development # Required: re-enable pcntl_* functions that may be disabled via disable_functions - # Disable JIT so a worker SIGSEGV has a readable PHP frame, not n/a. - ini-values: disable_functions=, opcache.jit=disable, opcache.jit_buffer_size=0 + ini-values: disable_functions= env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -179,57 +178,16 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # Probe: real php -S + original workers 10. FrankenPHP hid cURL 52. - BEHAT_WORKERS: "10" - BEHAT_HOST: "127.0.0.1" - # Capture worker PID, accept/close, and unexpected-death diagnostics. - BEHAT_VERBOSE: "1" run: | export BEHAT_RUN_AS=runner - # shellcheck disable=SC2086 - vendor/bin/behat -v -f junit -f pretty --colors + export BEHAT_VERBOSE="$RUNNER_DEBUG" + vendor/bin/behat -f junit -f pretty --colors - name: Print logs if: always() run: | - echo "=== Behat web server processes ===" - ps aux | grep -E '[p]hp -S|[f]rankenphp' || echo "No Behat web server process" - echo "=== php -S verbose diagnostics ===" - shopt -s nullglob - files=(/tmp/behat-php-server-*) - if (( ${#files[@]} )); then - ls -la "${files[@]}" - for f in "${files[@]}"; do - echo "----- $f -----" - cat "$f" - done - else - echo "No /tmp/behat-php-server-* files" - fi - echo "=== nextcloud.log ===" cat data/nextcloud.log - - name: Backtrace PHP core dumps - if: failure() - run: | - sudo coredumpctl --no-pager list || true - { - echo "===== latest php core =====" - sudo coredumpctl --no-pager info || true - echo "===== gdb bt full =====" - sudo coredumpctl --no-pager debug --debugger=gdb --debugger-arguments="-batch -ex 'set pagination off' -ex 'thread apply all bt full' -ex 'info registers' -ex 'info sharedlibrary'" || true - } | tee /tmp/php-s-gdb-bt.txt - - - name: Upload PHP server diagnostics - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: behat-mysql-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }} - path: | - /tmp/behat-php-server-* - /tmp/php-s-gdb-bt.txt - if-no-files-found: ignore - summary: permissions: contents: none diff --git a/.github/workflows/behat-pgsql.yml b/.github/workflows/behat-pgsql.yml index 36afc7834c..abced51068 100644 --- a/.github/workflows/behat-pgsql.yml +++ b/.github/workflows/behat-pgsql.yml @@ -63,7 +63,7 @@ jobs: php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} - name: PostgreSQL PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} + name: PostgreSQL PHP ${{ matrix.mysql-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} services: postgres: @@ -88,7 +88,7 @@ jobs: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV - name: Install system dependencies - run: sudo apt update && sudo apt install poppler-utils gdb systemd-coredump + run: sudo apt update && sudo apt install poppler-utils - name: Checkout server uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -115,8 +115,7 @@ jobs: coverage: none ini-file: development # Required: re-enable pcntl_* functions that may be disabled via disable_functions - # Disable JIT so a worker SIGSEGV has a readable PHP frame, not n/a. - ini-values: disable_functions=, opcache.jit=disable, opcache.jit_buffer_size=0 + ini-values: disable_functions= env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -173,57 +172,16 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # Probe: real php -S + original workers 10. FrankenPHP hid cURL 52. - BEHAT_WORKERS: "10" - BEHAT_HOST: "127.0.0.1" - # Capture worker PID, accept/close, and unexpected-death diagnostics. - BEHAT_VERBOSE: "1" run: | export BEHAT_RUN_AS=runner - # shellcheck disable=SC2086 - vendor/bin/behat -v -f junit -f pretty --colors + export BEHAT_VERBOSE="$RUNNER_DEBUG" + vendor/bin/behat -f junit -f pretty --colors - name: Print logs if: always() run: | - echo "=== Behat web server processes ===" - ps aux | grep -E '[p]hp -S|[f]rankenphp' || echo "No Behat web server process" - echo "=== php -S verbose diagnostics ===" - shopt -s nullglob - files=(/tmp/behat-php-server-*) - if (( ${#files[@]} )); then - ls -la "${files[@]}" - for f in "${files[@]}"; do - echo "----- $f -----" - cat "$f" - done - else - echo "No /tmp/behat-php-server-* files" - fi - echo "=== nextcloud.log ===" cat data/nextcloud.log - - name: Backtrace PHP core dumps - if: failure() - run: | - sudo coredumpctl --no-pager list || true - { - echo "===== latest php core =====" - sudo coredumpctl --no-pager info || true - echo "===== gdb bt full =====" - sudo coredumpctl --no-pager debug --debugger=gdb --debugger-arguments="-batch -ex 'set pagination off' -ex 'thread apply all bt full' -ex 'info registers' -ex 'info sharedlibrary'" || true - } | tee /tmp/php-s-gdb-bt.txt - - - name: Upload PHP server diagnostics - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: behat-pgsql-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }} - path: | - /tmp/behat-php-server-* - /tmp/php-s-gdb-bt.txt - if-no-files-found: ignore - summary: permissions: contents: none diff --git a/.github/workflows/behat-sqlite.yml b/.github/workflows/behat-sqlite.yml index 5b2a694042..3bf7c3aaff 100644 --- a/.github/workflows/behat-sqlite.yml +++ b/.github/workflows/behat-sqlite.yml @@ -63,7 +63,7 @@ jobs: php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} - name: SQLite PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} + name: SQLite PHP ${{ matrix.mysql-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} services: mailpit: @@ -79,7 +79,7 @@ jobs: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV - name: Install system dependencies - run: sudo apt update && sudo apt install poppler-utils gdb systemd-coredump + run: sudo apt update && sudo apt install poppler-utils - name: Checkout server uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -106,8 +106,7 @@ jobs: coverage: none ini-file: development # Required: re-enable pcntl_* functions that may be disabled via disable_functions - # Disable JIT so a worker SIGSEGV has a readable PHP frame, not n/a. - ini-values: disable_functions=, opcache.jit=disable, opcache.jit_buffer_size=0 + ini-values: disable_functions= env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -164,57 +163,16 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # Probe: real php -S + original workers 10. FrankenPHP hid cURL 52. - BEHAT_WORKERS: "10" - BEHAT_HOST: "127.0.0.1" - # Capture worker PID, accept/close, and unexpected-death diagnostics. - BEHAT_VERBOSE: "1" run: | export BEHAT_RUN_AS=runner - # shellcheck disable=SC2086 - vendor/bin/behat -v -f junit -f pretty --colors + export BEHAT_VERBOSE="$RUNNER_DEBUG" + vendor/bin/behat -f junit -f pretty --colors - name: Print logs if: always() run: | - echo "=== Behat web server processes ===" - ps aux | grep -E '[p]hp -S|[f]rankenphp' || echo "No Behat web server process" - echo "=== php -S verbose diagnostics ===" - shopt -s nullglob - files=(/tmp/behat-php-server-*) - if (( ${#files[@]} )); then - ls -la "${files[@]}" - for f in "${files[@]}"; do - echo "----- $f -----" - cat "$f" - done - else - echo "No /tmp/behat-php-server-* files" - fi - echo "=== nextcloud.log ===" cat data/nextcloud.log - - name: Backtrace PHP core dumps - if: failure() - run: | - sudo coredumpctl --no-pager list || true - { - echo "===== latest php core =====" - sudo coredumpctl --no-pager info || true - echo "===== gdb bt full =====" - sudo coredumpctl --no-pager debug --debugger=gdb --debugger-arguments="-batch -ex 'set pagination off' -ex 'thread apply all bt full' -ex 'info registers' -ex 'info sharedlibrary'" || true - } | tee /tmp/php-s-gdb-bt.txt - - - name: Upload PHP server diagnostics - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: behat-sqlite-server-diagnostics-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }} - path: | - /tmp/behat-php-server-* - /tmp/php-s-gdb-bt.txt - if-no-files-found: ignore - summary: permissions: contents: none diff --git a/tests/integration/bin/ci-setup-frankenphp-behat-server.sh b/tests/integration/bin/ci-setup-frankenphp-behat-server.sh deleted file mode 100755 index e57f16d61a..0000000000 --- a/tests/integration/bin/ci-setup-frankenphp-behat-server.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash -# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -# -# Install FrankenPHP and put a `php` wrapper earlier on PATH so -# PhpBuiltin\RunServerListener's `php -S` becomes FrankenPHP php-server. -# Only Nextcloud document roots are rewritten. TSA mock and fixture -# servers keep the real php -S (they need a router or static files). -# CLI php (occ, behat) still uses setup-php via php.real. -# Prints the wrapper bin directory to stdout (prepend to PATH). -set -euo pipefail - -DEST="${FRANKENPHP_DIR:-/tmp/libresign-frankenphp-behat}" -mkdir -p "$DEST/bin" - -ARCH="$(uname -m)" -case "$ARCH" in - x86_64|amd64) ASSET="frankenphp-linux-x86_64" ;; - aarch64|arm64) ASSET="frankenphp-linux-aarch64" ;; - *) - echo "ci-setup-frankenphp-behat-server: unsupported architecture: $ARCH" >&2 - exit 1 - ;; -esac - -VER="${FRANKENPHP_VERSION:-1.12.7}" -if [[ ! -x "$DEST/frankenphp" ]]; then - echo "Downloading FrankenPHP v${VER} (${ASSET})..." >&2 - curl -fsSL -o "$DEST/frankenphp" \ - "https://github.com/php/frankenphp/releases/download/v${VER}/${ASSET}" - chmod +x "$DEST/frankenphp" -fi - -REAL_PHP="$(command -v php)" -if [[ -z "$REAL_PHP" ]]; then - echo "ci-setup-frankenphp-behat-server: php not found on PATH" >&2 - exit 1 -fi -ln -sfn "$REAL_PHP" "$DEST/bin/php.real" - -cat >"$DEST/bin/php" <<'WRAP' -#!/usr/bin/env bash -# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -set -euo pipefail -REAL="$(dirname "$0")/php.real" -if [[ "${1:-}" == "-S" ]]; then - LISTEN="${2:?php -S requires host:port}" - ROOT="." - ROUTER="" - i=3 - while [[ $i -le $# ]]; do - arg="${!i}" - case "$arg" in - -t) - next=$((i + 1)) - ROOT="${!next:?}" - i=$((i + 2)) - ;; - -*) - i=$((i + 1)) - ;; - *) - ROUTER="$arg" - i=$((i + 1)) - ;; - esac - done - # escapeshellarg() may quote the document root or router path - ROOT="${ROOT#\'}" - ROOT="${ROOT%\'}" - ROUTER="${ROUTER#\'}" - ROUTER="${ROUTER%\'}" - # TSA: php -S host:port -t $workDir $router.php — keep built-in server. - # Fixtures: php -S host:port -t $pdfDir — no occ, keep built-in server. - if [[ -n "$ROUTER" || ! -f "$ROOT/occ" || ! -f "$ROOT/lib/base.php" ]]; then - exec "$REAL" "$@" - fi - FRANKENPHP="$(cd "$(dirname "$0")/.." && pwd)/frankenphp" - # Keep argv0 looking like php -S so leftover-process scans still match. - exec -a "php -S ${LISTEN}" "$FRANKENPHP" php-server --listen="$LISTEN" --root="$ROOT" -fi -exec "$REAL" "$@" -WRAP -chmod +x "$DEST/bin/php" - -# RunServerListener prefers PHP_BINARY over PATH php. Force PATH so the wrapper -# is the binary used for `php -S` without changing committed vendor code. -LISTENER="" -if [[ -f vendor/libresign/behat-builtin-extension/src/RunServerListener.php ]]; then - LISTENER="vendor/libresign/behat-builtin-extension/src/RunServerListener.php" -elif [[ -f vendor/phpbuiltin/server/src/RunServerListener.php ]]; then - LISTENER="vendor/phpbuiltin/server/src/RunServerListener.php" -fi -if [[ -n "$LISTENER" ]]; then - python3 - "$LISTENER" <<'PY' -import sys -from pathlib import Path - -path = Path(sys.argv[1]) -text = path.read_text() -old = "$php = PHP_BINARY !== '' && is_file(PHP_BINARY) ? escapeshellarg(PHP_BINARY) : 'php';" -new = "$php = 'php';" -if old not in text: - raise SystemExit(f"ci-setup-frankenphp-behat-server: {path} no longer selects PHP_BINARY; update the wrapper") -path.write_text(text.replace(old, new, 1)) -PY -fi - -echo "$DEST/bin" From 9a417dfc933376313b8c9db86690e489629c63ba Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Sat, 26 Sep 2026 20:14:09 -0300 Subject: [PATCH 10/14] ci(behat): dump PHP zbacktrace from php -S SIGSEGV cores The worker dies in zend_objects_store_del under JIT. Keep JIT on, run only features/sign, and open the core with gdb + php-src zbacktrace so the next crash names the PHP method. Signed-off-by: Luis Amorim --- .github/workflows/behat-mariadb.yml | 36 +++++++++++++++++++-- .github/workflows/behat-mysql.yml | 36 +++++++++++++++++++-- .github/workflows/behat-pgsql.yml | 36 +++++++++++++++++++-- .github/workflows/behat-sqlite.yml | 36 +++++++++++++++++++-- tests/integration/bin/ci-dump-php-s-core.sh | 19 +++++++++++ 5 files changed, 151 insertions(+), 12 deletions(-) create mode 100755 tests/integration/bin/ci-dump-php-s-core.sh diff --git a/.github/workflows/behat-mariadb.yml b/.github/workflows/behat-mariadb.yml index 970ced3a0a..53beba6deb 100644 --- a/.github/workflows/behat-mariadb.yml +++ b/.github/workflows/behat-mariadb.yml @@ -85,7 +85,7 @@ jobs: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV - name: Install system dependencies - run: sudo apt update && sudo apt install poppler-utils + run: sudo apt update && sudo apt install poppler-utils gdb systemd-coredump - name: Checkout server uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -166,16 +166,46 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ + # SIGSEGV probe: keep JIT on, raise hit rate on the crashing path. + BEHAT_VERBOSE: "1" + BEHAT_HOST: "127.0.0.1" run: | export BEHAT_RUN_AS=runner - export BEHAT_VERBOSE="$RUNNER_DEBUG" - vendor/bin/behat -f junit -f pretty --colors + vendor/bin/behat -v -f junit -f pretty --colors features/sign - name: Print logs if: always() run: | + echo "=== php -S diagnostics ===" + shopt -s nullglob + files=(/tmp/behat-php-server-*) + if (( ${#files[@]} )); then + ls -la "${files[@]}" + for f in "${files[@]}"; do + echo "----- $f -----" + cat "$f" + done + else + echo "No /tmp/behat-php-server-* files" + fi + echo "=== nextcloud.log ===" cat data/nextcloud.log + - name: Backtrace PHP core dumps + if: failure() + working-directory: apps/${{ env.APP_NAME }} + run: tests/integration/bin/ci-dump-php-s-core.sh + + - name: Upload PHP server diagnostics + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: behat-mariadb-php-s-core-${{ matrix.mariadb-versions }}-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }} + path: | + /tmp/behat-php-server-* + /tmp/php-s-gdb-bt.txt + if-no-files-found: ignore + summary: permissions: contents: none diff --git a/.github/workflows/behat-mysql.yml b/.github/workflows/behat-mysql.yml index 4e22bb4ff5..6469afb72d 100644 --- a/.github/workflows/behat-mysql.yml +++ b/.github/workflows/behat-mysql.yml @@ -89,7 +89,7 @@ jobs: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV - name: Install system dependencies - run: sudo apt update && sudo apt install poppler-utils + run: sudo apt update && sudo apt install poppler-utils gdb systemd-coredump - name: Checkout server uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -178,16 +178,46 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ + # SIGSEGV probe: keep JIT on, raise hit rate on the crashing path. + BEHAT_VERBOSE: "1" + BEHAT_HOST: "127.0.0.1" run: | export BEHAT_RUN_AS=runner - export BEHAT_VERBOSE="$RUNNER_DEBUG" - vendor/bin/behat -f junit -f pretty --colors + vendor/bin/behat -v -f junit -f pretty --colors features/sign - name: Print logs if: always() run: | + echo "=== php -S diagnostics ===" + shopt -s nullglob + files=(/tmp/behat-php-server-*) + if (( ${#files[@]} )); then + ls -la "${files[@]}" + for f in "${files[@]}"; do + echo "----- $f -----" + cat "$f" + done + else + echo "No /tmp/behat-php-server-* files" + fi + echo "=== nextcloud.log ===" cat data/nextcloud.log + - name: Backtrace PHP core dumps + if: failure() + working-directory: apps/${{ env.APP_NAME }} + run: tests/integration/bin/ci-dump-php-s-core.sh + + - name: Upload PHP server diagnostics + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: behat-mysql-php-s-core-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }} + path: | + /tmp/behat-php-server-* + /tmp/php-s-gdb-bt.txt + if-no-files-found: ignore + summary: permissions: contents: none diff --git a/.github/workflows/behat-pgsql.yml b/.github/workflows/behat-pgsql.yml index abced51068..0fe33463bb 100644 --- a/.github/workflows/behat-pgsql.yml +++ b/.github/workflows/behat-pgsql.yml @@ -88,7 +88,7 @@ jobs: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV - name: Install system dependencies - run: sudo apt update && sudo apt install poppler-utils + run: sudo apt update && sudo apt install poppler-utils gdb systemd-coredump - name: Checkout server uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -172,16 +172,46 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ + # SIGSEGV probe: keep JIT on, raise hit rate on the crashing path. + BEHAT_VERBOSE: "1" + BEHAT_HOST: "127.0.0.1" run: | export BEHAT_RUN_AS=runner - export BEHAT_VERBOSE="$RUNNER_DEBUG" - vendor/bin/behat -f junit -f pretty --colors + vendor/bin/behat -v -f junit -f pretty --colors features/sign - name: Print logs if: always() run: | + echo "=== php -S diagnostics ===" + shopt -s nullglob + files=(/tmp/behat-php-server-*) + if (( ${#files[@]} )); then + ls -la "${files[@]}" + for f in "${files[@]}"; do + echo "----- $f -----" + cat "$f" + done + else + echo "No /tmp/behat-php-server-* files" + fi + echo "=== nextcloud.log ===" cat data/nextcloud.log + - name: Backtrace PHP core dumps + if: failure() + working-directory: apps/${{ env.APP_NAME }} + run: tests/integration/bin/ci-dump-php-s-core.sh + + - name: Upload PHP server diagnostics + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: behat-pgsql-php-s-core-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }} + path: | + /tmp/behat-php-server-* + /tmp/php-s-gdb-bt.txt + if-no-files-found: ignore + summary: permissions: contents: none diff --git a/.github/workflows/behat-sqlite.yml b/.github/workflows/behat-sqlite.yml index 3bf7c3aaff..366fde11fd 100644 --- a/.github/workflows/behat-sqlite.yml +++ b/.github/workflows/behat-sqlite.yml @@ -79,7 +79,7 @@ jobs: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV - name: Install system dependencies - run: sudo apt update && sudo apt install poppler-utils + run: sudo apt update && sudo apt install poppler-utils gdb systemd-coredump - name: Checkout server uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -163,16 +163,46 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ + # SIGSEGV probe: keep JIT on, raise hit rate on the crashing path. + BEHAT_VERBOSE: "1" + BEHAT_HOST: "127.0.0.1" run: | export BEHAT_RUN_AS=runner - export BEHAT_VERBOSE="$RUNNER_DEBUG" - vendor/bin/behat -f junit -f pretty --colors + vendor/bin/behat -v -f junit -f pretty --colors features/sign - name: Print logs if: always() run: | + echo "=== php -S diagnostics ===" + shopt -s nullglob + files=(/tmp/behat-php-server-*) + if (( ${#files[@]} )); then + ls -la "${files[@]}" + for f in "${files[@]}"; do + echo "----- $f -----" + cat "$f" + done + else + echo "No /tmp/behat-php-server-* files" + fi + echo "=== nextcloud.log ===" cat data/nextcloud.log + - name: Backtrace PHP core dumps + if: failure() + working-directory: apps/${{ env.APP_NAME }} + run: tests/integration/bin/ci-dump-php-s-core.sh + + - name: Upload PHP server diagnostics + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: behat-sqlite-php-s-core-php${{ matrix.php-versions }}-nc${{ matrix.server-versions }} + path: | + /tmp/behat-php-server-* + /tmp/php-s-gdb-bt.txt + if-no-files-found: ignore + summary: permissions: contents: none diff --git a/tests/integration/bin/ci-dump-php-s-core.sh b/tests/integration/bin/ci-dump-php-s-core.sh new file mode 100755 index 0000000000..b6ca3a8556 --- /dev/null +++ b/tests/integration/bin/ci-dump-php-s-core.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Dump a readable PHP stack from a php -S worker core. +# JIT frames show as n/a in coredumpctl; zbacktrace reads EG(current_execute_data). +set -u + +sudo coredumpctl --no-pager list || true + +curl -fsSL -o /tmp/php.gdbinit \ + https://raw.githubusercontent.com/php/php-src/PHP-8.3/.gdbinit || true + +{ + echo "===== latest php core =====" + sudo coredumpctl --no-pager info || true + echo "===== gdb zbacktrace + bt full =====" + sudo coredumpctl --no-pager debug --debugger=gdb --debugger-arguments="-batch -ex 'set pagination off' -ex 'set debuginfod enabled on' -ex 'source /tmp/php.gdbinit' -ex zbacktrace -ex 'bt full' -ex 'info registers'" || true +} | tee /tmp/php-s-gdb-bt.txt From 348e835c37e39501cf45986e6e8163ffc51352ff Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Sat, 26 Sep 2026 20:26:23 -0300 Subject: [PATCH 11/14] fix(ci): serve Nextcloud Behat with FrankenPHP php -S workers SIGSEGV in zend_objects_store_del under JIT and drop request-signature with cURL 52. FrankenPHP php-server has no worker fork; TSA and fixture servers keep the real php -S. Signed-off-by: Luis Amorim --- .github/workflows/behat-mariadb.yml | 11 +- .github/workflows/behat-mysql.yml | 11 +- .github/workflows/behat-pgsql.yml | 11 +- .github/workflows/behat-sqlite.yml | 11 +- .../bin/ci-setup-frankenphp-behat-server.sh | 110 ++++++++++++++++++ 5 files changed, 142 insertions(+), 12 deletions(-) create mode 100755 tests/integration/bin/ci-setup-frankenphp-behat-server.sh diff --git a/.github/workflows/behat-mariadb.yml b/.github/workflows/behat-mariadb.yml index 53beba6deb..6981366b17 100644 --- a/.github/workflows/behat-mariadb.yml +++ b/.github/workflows/behat-mariadb.yml @@ -166,12 +166,17 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # SIGSEGV probe: keep JIT on, raise hit rate on the crashing path. - BEHAT_VERBOSE: "1" + # php -S workers SIGSEGV in zend_objects_store_del (JIT). FrankenPHP + # php-server has no worker fork; TSA/fixtures still use real php -S. + BEHAT_WORKERS: "0" BEHAT_HOST: "127.0.0.1" run: | + WRAP_BIN="$(./bin/ci-setup-frankenphp-behat-server.sh)" + export PATH="$WRAP_BIN:$PATH" + hash -r 2>/dev/null || true export BEHAT_RUN_AS=runner - vendor/bin/behat -v -f junit -f pretty --colors features/sign + export BEHAT_VERBOSE="$RUNNER_DEBUG" + vendor/bin/behat -f junit -f pretty --colors - name: Print logs if: always() diff --git a/.github/workflows/behat-mysql.yml b/.github/workflows/behat-mysql.yml index 6469afb72d..d23f1a838f 100644 --- a/.github/workflows/behat-mysql.yml +++ b/.github/workflows/behat-mysql.yml @@ -178,12 +178,17 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # SIGSEGV probe: keep JIT on, raise hit rate on the crashing path. - BEHAT_VERBOSE: "1" + # php -S workers SIGSEGV in zend_objects_store_del (JIT). FrankenPHP + # php-server has no worker fork; TSA/fixtures still use real php -S. + BEHAT_WORKERS: "0" BEHAT_HOST: "127.0.0.1" run: | + WRAP_BIN="$(./bin/ci-setup-frankenphp-behat-server.sh)" + export PATH="$WRAP_BIN:$PATH" + hash -r 2>/dev/null || true export BEHAT_RUN_AS=runner - vendor/bin/behat -v -f junit -f pretty --colors features/sign + export BEHAT_VERBOSE="$RUNNER_DEBUG" + vendor/bin/behat -f junit -f pretty --colors - name: Print logs if: always() diff --git a/.github/workflows/behat-pgsql.yml b/.github/workflows/behat-pgsql.yml index 0fe33463bb..ba72799e24 100644 --- a/.github/workflows/behat-pgsql.yml +++ b/.github/workflows/behat-pgsql.yml @@ -172,12 +172,17 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # SIGSEGV probe: keep JIT on, raise hit rate on the crashing path. - BEHAT_VERBOSE: "1" + # php -S workers SIGSEGV in zend_objects_store_del (JIT). FrankenPHP + # php-server has no worker fork; TSA/fixtures still use real php -S. + BEHAT_WORKERS: "0" BEHAT_HOST: "127.0.0.1" run: | + WRAP_BIN="$(./bin/ci-setup-frankenphp-behat-server.sh)" + export PATH="$WRAP_BIN:$PATH" + hash -r 2>/dev/null || true export BEHAT_RUN_AS=runner - vendor/bin/behat -v -f junit -f pretty --colors features/sign + export BEHAT_VERBOSE="$RUNNER_DEBUG" + vendor/bin/behat -f junit -f pretty --colors - name: Print logs if: always() diff --git a/.github/workflows/behat-sqlite.yml b/.github/workflows/behat-sqlite.yml index 366fde11fd..40cf0df379 100644 --- a/.github/workflows/behat-sqlite.yml +++ b/.github/workflows/behat-sqlite.yml @@ -163,12 +163,17 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # SIGSEGV probe: keep JIT on, raise hit rate on the crashing path. - BEHAT_VERBOSE: "1" + # php -S workers SIGSEGV in zend_objects_store_del (JIT). FrankenPHP + # php-server has no worker fork; TSA/fixtures still use real php -S. + BEHAT_WORKERS: "0" BEHAT_HOST: "127.0.0.1" run: | + WRAP_BIN="$(./bin/ci-setup-frankenphp-behat-server.sh)" + export PATH="$WRAP_BIN:$PATH" + hash -r 2>/dev/null || true export BEHAT_RUN_AS=runner - vendor/bin/behat -v -f junit -f pretty --colors features/sign + export BEHAT_VERBOSE="$RUNNER_DEBUG" + vendor/bin/behat -f junit -f pretty --colors - name: Print logs if: always() diff --git a/tests/integration/bin/ci-setup-frankenphp-behat-server.sh b/tests/integration/bin/ci-setup-frankenphp-behat-server.sh new file mode 100755 index 0000000000..e57f16d61a --- /dev/null +++ b/tests/integration/bin/ci-setup-frankenphp-behat-server.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Install FrankenPHP and put a `php` wrapper earlier on PATH so +# PhpBuiltin\RunServerListener's `php -S` becomes FrankenPHP php-server. +# Only Nextcloud document roots are rewritten. TSA mock and fixture +# servers keep the real php -S (they need a router or static files). +# CLI php (occ, behat) still uses setup-php via php.real. +# Prints the wrapper bin directory to stdout (prepend to PATH). +set -euo pipefail + +DEST="${FRANKENPHP_DIR:-/tmp/libresign-frankenphp-behat}" +mkdir -p "$DEST/bin" + +ARCH="$(uname -m)" +case "$ARCH" in + x86_64|amd64) ASSET="frankenphp-linux-x86_64" ;; + aarch64|arm64) ASSET="frankenphp-linux-aarch64" ;; + *) + echo "ci-setup-frankenphp-behat-server: unsupported architecture: $ARCH" >&2 + exit 1 + ;; +esac + +VER="${FRANKENPHP_VERSION:-1.12.7}" +if [[ ! -x "$DEST/frankenphp" ]]; then + echo "Downloading FrankenPHP v${VER} (${ASSET})..." >&2 + curl -fsSL -o "$DEST/frankenphp" \ + "https://github.com/php/frankenphp/releases/download/v${VER}/${ASSET}" + chmod +x "$DEST/frankenphp" +fi + +REAL_PHP="$(command -v php)" +if [[ -z "$REAL_PHP" ]]; then + echo "ci-setup-frankenphp-behat-server: php not found on PATH" >&2 + exit 1 +fi +ln -sfn "$REAL_PHP" "$DEST/bin/php.real" + +cat >"$DEST/bin/php" <<'WRAP' +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +set -euo pipefail +REAL="$(dirname "$0")/php.real" +if [[ "${1:-}" == "-S" ]]; then + LISTEN="${2:?php -S requires host:port}" + ROOT="." + ROUTER="" + i=3 + while [[ $i -le $# ]]; do + arg="${!i}" + case "$arg" in + -t) + next=$((i + 1)) + ROOT="${!next:?}" + i=$((i + 2)) + ;; + -*) + i=$((i + 1)) + ;; + *) + ROUTER="$arg" + i=$((i + 1)) + ;; + esac + done + # escapeshellarg() may quote the document root or router path + ROOT="${ROOT#\'}" + ROOT="${ROOT%\'}" + ROUTER="${ROUTER#\'}" + ROUTER="${ROUTER%\'}" + # TSA: php -S host:port -t $workDir $router.php — keep built-in server. + # Fixtures: php -S host:port -t $pdfDir — no occ, keep built-in server. + if [[ -n "$ROUTER" || ! -f "$ROOT/occ" || ! -f "$ROOT/lib/base.php" ]]; then + exec "$REAL" "$@" + fi + FRANKENPHP="$(cd "$(dirname "$0")/.." && pwd)/frankenphp" + # Keep argv0 looking like php -S so leftover-process scans still match. + exec -a "php -S ${LISTEN}" "$FRANKENPHP" php-server --listen="$LISTEN" --root="$ROOT" +fi +exec "$REAL" "$@" +WRAP +chmod +x "$DEST/bin/php" + +# RunServerListener prefers PHP_BINARY over PATH php. Force PATH so the wrapper +# is the binary used for `php -S` without changing committed vendor code. +LISTENER="" +if [[ -f vendor/libresign/behat-builtin-extension/src/RunServerListener.php ]]; then + LISTENER="vendor/libresign/behat-builtin-extension/src/RunServerListener.php" +elif [[ -f vendor/phpbuiltin/server/src/RunServerListener.php ]]; then + LISTENER="vendor/phpbuiltin/server/src/RunServerListener.php" +fi +if [[ -n "$LISTENER" ]]; then + python3 - "$LISTENER" <<'PY' +import sys +from pathlib import Path + +path = Path(sys.argv[1]) +text = path.read_text() +old = "$php = PHP_BINARY !== '' && is_file(PHP_BINARY) ? escapeshellarg(PHP_BINARY) : 'php';" +new = "$php = 'php';" +if old not in text: + raise SystemExit(f"ci-setup-frankenphp-behat-server: {path} no longer selects PHP_BINARY; update the wrapper") +path.write_text(text.replace(old, new, 1)) +PY +fi + +echo "$DEST/bin" From 498222daebe3a74b9dcd175421a9c1248b2c6513 Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Sat, 26 Sep 2026 20:50:07 -0300 Subject: [PATCH 12/14] ci(phpunit): skip Coveralls finish on forks The parallel-done webhook returns 500 for lfals/libresign because Coveralls is registered only on LibreSign/libresign. Keep the job on upstream main and stop failing green PHPUnit runs on the fork. Signed-off-by: Luis Amorim --- .github/workflows/phpunit-sqlite.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/phpunit-sqlite.yml b/.github/workflows/phpunit-sqlite.yml index b9d6989b5b..72aee1c4f9 100644 --- a/.github/workflows/phpunit-sqlite.yml +++ b/.github/workflows/phpunit-sqlite.yml @@ -208,7 +208,9 @@ jobs: run: if ${{ needs.changes.outputs.src != 'false' && needs.phpunit-sqlite.result != 'success' }}; then exit 1; fi finish-coverage: - if: ${{ github.event.pull_request.base.ref == 'main' }} + # Coveralls is wired to LibreSign/libresign. On forks the parallel-done + # webhook returns 500 and would fail an otherwise green PHPUnit run. + if: ${{ github.event.pull_request.base.ref == 'main' && github.repository == 'LibreSign/libresign' }} needs: phpunit-sqlite runs-on: ubuntu-latest steps: From b3630c815240eff59b119cebfe87dbd806f4ba9f Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Sat, 26 Sep 2026 20:51:45 -0300 Subject: [PATCH 13/14] revert: stop touching Coveralls finish-coverage Leave the upstream PHPUnit coverage job as-is. Coveralls 500 on this fork is ignored. Signed-off-by: Luis Amorim --- .github/workflows/phpunit-sqlite.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/phpunit-sqlite.yml b/.github/workflows/phpunit-sqlite.yml index 72aee1c4f9..b9d6989b5b 100644 --- a/.github/workflows/phpunit-sqlite.yml +++ b/.github/workflows/phpunit-sqlite.yml @@ -208,9 +208,7 @@ jobs: run: if ${{ needs.changes.outputs.src != 'false' && needs.phpunit-sqlite.result != 'success' }}; then exit 1; fi finish-coverage: - # Coveralls is wired to LibreSign/libresign. On forks the parallel-done - # webhook returns 500 and would fail an otherwise green PHPUnit run. - if: ${{ github.event.pull_request.base.ref == 'main' && github.repository == 'LibreSign/libresign' }} + if: ${{ github.event.pull_request.base.ref == 'main' }} needs: phpunit-sqlite runs-on: ubuntu-latest steps: From 0acd4ad86b510888ea1fc2eeae0c63740b3abee9 Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Sat, 26 Sep 2026 20:52:53 -0300 Subject: [PATCH 14/14] fix(ci): disable PHP JIT to stop php -S worker SIGSEGV Keep the built-in server. Cores die in zend_objects_store_del under a JIT frame; turning JIT off removes that path instead of swapping servers. Signed-off-by: Luis Amorim --- .github/workflows/behat-mariadb.yml | 10 +- .github/workflows/behat-mysql.yml | 10 +- .github/workflows/behat-pgsql.yml | 10 +- .github/workflows/behat-sqlite.yml | 10 +- .../bin/ci-setup-frankenphp-behat-server.sh | 110 ------------------ 5 files changed, 8 insertions(+), 142 deletions(-) delete mode 100755 tests/integration/bin/ci-setup-frankenphp-behat-server.sh diff --git a/.github/workflows/behat-mariadb.yml b/.github/workflows/behat-mariadb.yml index 6981366b17..788ab2e483 100644 --- a/.github/workflows/behat-mariadb.yml +++ b/.github/workflows/behat-mariadb.yml @@ -112,7 +112,8 @@ jobs: coverage: none ini-file: development # Required: re-enable pcntl_* functions that may be disabled via disable_functions - ini-values: disable_functions= + # php -S workers SIGSEGV in zend_objects_store_del under a JIT frame. + ini-values: disable_functions=, opcache.jit=disable, opcache.jit_buffer_size=0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -166,14 +167,7 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # php -S workers SIGSEGV in zend_objects_store_del (JIT). FrankenPHP - # php-server has no worker fork; TSA/fixtures still use real php -S. - BEHAT_WORKERS: "0" - BEHAT_HOST: "127.0.0.1" run: | - WRAP_BIN="$(./bin/ci-setup-frankenphp-behat-server.sh)" - export PATH="$WRAP_BIN:$PATH" - hash -r 2>/dev/null || true export BEHAT_RUN_AS=runner export BEHAT_VERBOSE="$RUNNER_DEBUG" vendor/bin/behat -f junit -f pretty --colors diff --git a/.github/workflows/behat-mysql.yml b/.github/workflows/behat-mysql.yml index d23f1a838f..ea67afe117 100644 --- a/.github/workflows/behat-mysql.yml +++ b/.github/workflows/behat-mysql.yml @@ -116,7 +116,8 @@ jobs: coverage: none ini-file: development # Required: re-enable pcntl_* functions that may be disabled via disable_functions - ini-values: disable_functions= + # php -S workers SIGSEGV in zend_objects_store_del under a JIT frame. + ini-values: disable_functions=, opcache.jit=disable, opcache.jit_buffer_size=0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -178,14 +179,7 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # php -S workers SIGSEGV in zend_objects_store_del (JIT). FrankenPHP - # php-server has no worker fork; TSA/fixtures still use real php -S. - BEHAT_WORKERS: "0" - BEHAT_HOST: "127.0.0.1" run: | - WRAP_BIN="$(./bin/ci-setup-frankenphp-behat-server.sh)" - export PATH="$WRAP_BIN:$PATH" - hash -r 2>/dev/null || true export BEHAT_RUN_AS=runner export BEHAT_VERBOSE="$RUNNER_DEBUG" vendor/bin/behat -f junit -f pretty --colors diff --git a/.github/workflows/behat-pgsql.yml b/.github/workflows/behat-pgsql.yml index ba72799e24..ffe9f86a18 100644 --- a/.github/workflows/behat-pgsql.yml +++ b/.github/workflows/behat-pgsql.yml @@ -115,7 +115,8 @@ jobs: coverage: none ini-file: development # Required: re-enable pcntl_* functions that may be disabled via disable_functions - ini-values: disable_functions= + # php -S workers SIGSEGV in zend_objects_store_del under a JIT frame. + ini-values: disable_functions=, opcache.jit=disable, opcache.jit_buffer_size=0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -172,14 +173,7 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # php -S workers SIGSEGV in zend_objects_store_del (JIT). FrankenPHP - # php-server has no worker fork; TSA/fixtures still use real php -S. - BEHAT_WORKERS: "0" - BEHAT_HOST: "127.0.0.1" run: | - WRAP_BIN="$(./bin/ci-setup-frankenphp-behat-server.sh)" - export PATH="$WRAP_BIN:$PATH" - hash -r 2>/dev/null || true export BEHAT_RUN_AS=runner export BEHAT_VERBOSE="$RUNNER_DEBUG" vendor/bin/behat -f junit -f pretty --colors diff --git a/.github/workflows/behat-sqlite.yml b/.github/workflows/behat-sqlite.yml index 40cf0df379..5707352279 100644 --- a/.github/workflows/behat-sqlite.yml +++ b/.github/workflows/behat-sqlite.yml @@ -106,7 +106,8 @@ jobs: coverage: none ini-file: development # Required: re-enable pcntl_* functions that may be disabled via disable_functions - ini-values: disable_functions= + # php -S workers SIGSEGV in zend_objects_store_del under a JIT frame. + ini-values: disable_functions=, opcache.jit=disable, opcache.jit_buffer_size=0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -163,14 +164,7 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration env: BEHAT_ROOT_DIR: ../../../../ - # php -S workers SIGSEGV in zend_objects_store_del (JIT). FrankenPHP - # php-server has no worker fork; TSA/fixtures still use real php -S. - BEHAT_WORKERS: "0" - BEHAT_HOST: "127.0.0.1" run: | - WRAP_BIN="$(./bin/ci-setup-frankenphp-behat-server.sh)" - export PATH="$WRAP_BIN:$PATH" - hash -r 2>/dev/null || true export BEHAT_RUN_AS=runner export BEHAT_VERBOSE="$RUNNER_DEBUG" vendor/bin/behat -f junit -f pretty --colors diff --git a/tests/integration/bin/ci-setup-frankenphp-behat-server.sh b/tests/integration/bin/ci-setup-frankenphp-behat-server.sh deleted file mode 100755 index e57f16d61a..0000000000 --- a/tests/integration/bin/ci-setup-frankenphp-behat-server.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash -# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -# -# Install FrankenPHP and put a `php` wrapper earlier on PATH so -# PhpBuiltin\RunServerListener's `php -S` becomes FrankenPHP php-server. -# Only Nextcloud document roots are rewritten. TSA mock and fixture -# servers keep the real php -S (they need a router or static files). -# CLI php (occ, behat) still uses setup-php via php.real. -# Prints the wrapper bin directory to stdout (prepend to PATH). -set -euo pipefail - -DEST="${FRANKENPHP_DIR:-/tmp/libresign-frankenphp-behat}" -mkdir -p "$DEST/bin" - -ARCH="$(uname -m)" -case "$ARCH" in - x86_64|amd64) ASSET="frankenphp-linux-x86_64" ;; - aarch64|arm64) ASSET="frankenphp-linux-aarch64" ;; - *) - echo "ci-setup-frankenphp-behat-server: unsupported architecture: $ARCH" >&2 - exit 1 - ;; -esac - -VER="${FRANKENPHP_VERSION:-1.12.7}" -if [[ ! -x "$DEST/frankenphp" ]]; then - echo "Downloading FrankenPHP v${VER} (${ASSET})..." >&2 - curl -fsSL -o "$DEST/frankenphp" \ - "https://github.com/php/frankenphp/releases/download/v${VER}/${ASSET}" - chmod +x "$DEST/frankenphp" -fi - -REAL_PHP="$(command -v php)" -if [[ -z "$REAL_PHP" ]]; then - echo "ci-setup-frankenphp-behat-server: php not found on PATH" >&2 - exit 1 -fi -ln -sfn "$REAL_PHP" "$DEST/bin/php.real" - -cat >"$DEST/bin/php" <<'WRAP' -#!/usr/bin/env bash -# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -set -euo pipefail -REAL="$(dirname "$0")/php.real" -if [[ "${1:-}" == "-S" ]]; then - LISTEN="${2:?php -S requires host:port}" - ROOT="." - ROUTER="" - i=3 - while [[ $i -le $# ]]; do - arg="${!i}" - case "$arg" in - -t) - next=$((i + 1)) - ROOT="${!next:?}" - i=$((i + 2)) - ;; - -*) - i=$((i + 1)) - ;; - *) - ROUTER="$arg" - i=$((i + 1)) - ;; - esac - done - # escapeshellarg() may quote the document root or router path - ROOT="${ROOT#\'}" - ROOT="${ROOT%\'}" - ROUTER="${ROUTER#\'}" - ROUTER="${ROUTER%\'}" - # TSA: php -S host:port -t $workDir $router.php — keep built-in server. - # Fixtures: php -S host:port -t $pdfDir — no occ, keep built-in server. - if [[ -n "$ROUTER" || ! -f "$ROOT/occ" || ! -f "$ROOT/lib/base.php" ]]; then - exec "$REAL" "$@" - fi - FRANKENPHP="$(cd "$(dirname "$0")/.." && pwd)/frankenphp" - # Keep argv0 looking like php -S so leftover-process scans still match. - exec -a "php -S ${LISTEN}" "$FRANKENPHP" php-server --listen="$LISTEN" --root="$ROOT" -fi -exec "$REAL" "$@" -WRAP -chmod +x "$DEST/bin/php" - -# RunServerListener prefers PHP_BINARY over PATH php. Force PATH so the wrapper -# is the binary used for `php -S` without changing committed vendor code. -LISTENER="" -if [[ -f vendor/libresign/behat-builtin-extension/src/RunServerListener.php ]]; then - LISTENER="vendor/libresign/behat-builtin-extension/src/RunServerListener.php" -elif [[ -f vendor/phpbuiltin/server/src/RunServerListener.php ]]; then - LISTENER="vendor/phpbuiltin/server/src/RunServerListener.php" -fi -if [[ -n "$LISTENER" ]]; then - python3 - "$LISTENER" <<'PY' -import sys -from pathlib import Path - -path = Path(sys.argv[1]) -text = path.read_text() -old = "$php = PHP_BINARY !== '' && is_file(PHP_BINARY) ? escapeshellarg(PHP_BINARY) : 'php';" -new = "$php = 'php';" -if old not in text: - raise SystemExit(f"ci-setup-frankenphp-behat-server: {path} no longer selects PHP_BINARY; update the wrapper") -path.write_text(text.replace(old, new, 1)) -PY -fi - -echo "$DEST/bin"