fix(pool): dispose per-worker completion futures on shutdown/reload — no leaked cross-thread triggers (#93) #304
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Linux x64 Build & Test | |
| # CI for php-http-server. Modeled after ext/async build-linux.yml | |
| # (php-src/ext/async/.github/workflows/build-linux.yml) — same | |
| # "build the world from source with aggressive caching" pattern. | |
| # | |
| # Two-stage layout: a single `deps` job builds the native stack | |
| # (OpenSSL, nghttp2, nghttp3, ngtcp2, libuv) and saves caches once; | |
| # the matrix `build` job restores them in parallel. This removes the | |
| # race where two matrix jobs would simultaneously try to reserve the | |
| # same cache key and one would lose with "another job may be creating | |
| # this cache". | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| env: | |
| # Pinned versions match the locally-known-good stack on the | |
| # maintainer's box (pkg-config --modversion). Don't guess — | |
| # keep these in sync with reality. | |
| OPENSSL_VERSION: 3.5.0 | |
| CURL_VERSION: 8.12.1 | |
| NGTCP2_VERSION: 1.22.1 | |
| NGHTTP3_VERSION: 1.15.0 | |
| NGHTTP2_VERSION: 1.59.0 | |
| LIBUV_VERSION: 1.52.1 | |
| # `true-async` (not -stable) is the branch that has the H3 lifecycle | |
| # fixes. Required configure form is the maintainer's config.nice: | |
| # --disable-all + a fixed allowlist (matches the modules the H3 tests | |
| # expect to be present at runtime). | |
| PHP_SRC_BRANCH: true-async | |
| PHP_ASYNC_BRANCH: main | |
| jobs: | |
| # -------------------------------------------------------------------- | |
| # Stage 1: build the native dependency stack once and save caches. | |
| # -------------------------------------------------------------------- | |
| deps: | |
| name: Native deps | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 30 | |
| steps: | |
| # actions/cache extracts as the runner user; ensure /usr/local | |
| # subdirs aren't root-owned (they are on stock GH images for | |
| # /usr/local/include). | |
| - name: Make /usr/local writable | |
| run: | | |
| sudo mkdir -p /usr/local/lib/pkgconfig /usr/local/include /usr/local/bin | |
| sudo chown -R "$(id -u):$(id -g)" /usr/local/lib /usr/local/include /usr/local/bin | |
| # OpenSSL 3.5 ---------------------------------------------------- | |
| - name: Restore OpenSSL cache | |
| id: cache-openssl | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libssl* | |
| /usr/local/lib/libcrypto* | |
| /usr/local/include/openssl | |
| /usr/local/lib/pkgconfig/openssl.pc | |
| /usr/local/lib/pkgconfig/libssl.pc | |
| /usr/local/lib/pkgconfig/libcrypto.pc | |
| /usr/local/bin/openssl | |
| key: ${{ runner.os }}-openssl-${{ env.OPENSSL_VERSION }}-r4 | |
| - name: Build OpenSSL from source | |
| if: steps.cache-openssl.outputs.cache-hit != 'true' | |
| run: | | |
| set -eux | |
| wget -q https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz | |
| tar -xzf openssl-${OPENSSL_VERSION}.tar.gz | |
| cd openssl-${OPENSSL_VERSION} | |
| # --openssldir=/etc/ssl makes OpenSSL reuse Ubuntu's CA bundle | |
| # so wget/git keep verifying TLS once /usr/local/lib lands in | |
| # ldconfig and shadows the system libssl.so.3. | |
| ./Configure --prefix=/usr/local --libdir=lib --openssldir=/etc/ssl \ | |
| enable-tls1_3 shared | |
| make -j"$(nproc)" | |
| sudo make install_sw | |
| - name: Save OpenSSL cache | |
| if: steps.cache-openssl.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libssl* | |
| /usr/local/lib/libcrypto* | |
| /usr/local/include/openssl | |
| /usr/local/lib/pkgconfig/openssl.pc | |
| /usr/local/lib/pkgconfig/libssl.pc | |
| /usr/local/lib/pkgconfig/libcrypto.pc | |
| /usr/local/bin/openssl | |
| key: ${{ runner.os }}-openssl-${{ env.OPENSSL_VERSION }}-r4 | |
| - name: Refresh ldconfig after OpenSSL | |
| run: sudo ldconfig | |
| # nghttp2 -------------------------------------------------------- | |
| - name: Restore nghttp2 cache | |
| id: cache-nghttp2 | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libnghttp2* | |
| /usr/local/include/nghttp2 | |
| /usr/local/lib/pkgconfig/libnghttp2.pc | |
| key: ${{ runner.os }}-nghttp2-${{ env.NGHTTP2_VERSION }}-r4 | |
| - name: Build nghttp2 from source | |
| if: steps.cache-nghttp2.outputs.cache-hit != 'true' | |
| run: | | |
| set -eux | |
| git clone --depth=1 --branch=v${NGHTTP2_VERSION} \ | |
| https://github.com/nghttp2/nghttp2.git | |
| cd nghttp2 | |
| autoreconf -i | |
| ./configure --prefix=/usr/local --enable-lib-only | |
| make -j"$(nproc)" | |
| sudo make install | |
| - name: Save nghttp2 cache | |
| if: steps.cache-nghttp2.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libnghttp2* | |
| /usr/local/include/nghttp2 | |
| /usr/local/lib/pkgconfig/libnghttp2.pc | |
| key: ${{ runner.os }}-nghttp2-${{ env.NGHTTP2_VERSION }}-r4 | |
| # nghttp3 -------------------------------------------------------- | |
| - name: Restore nghttp3 cache | |
| id: cache-nghttp3 | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libnghttp3* | |
| /usr/local/include/nghttp3 | |
| /usr/local/lib/pkgconfig/libnghttp3.pc | |
| key: ${{ runner.os }}-nghttp3-${{ env.NGHTTP3_VERSION }}-r4 | |
| - name: Build nghttp3 from source | |
| if: steps.cache-nghttp3.outputs.cache-hit != 'true' | |
| run: | | |
| set -eux | |
| git clone --depth=1 --branch=v${NGHTTP3_VERSION} --recurse-submodules \ | |
| https://github.com/ngtcp2/nghttp3.git | |
| cd nghttp3 | |
| autoreconf -i | |
| ./configure --prefix=/usr/local --enable-lib-only | |
| make -j"$(nproc)" | |
| sudo make install | |
| - name: Save nghttp3 cache | |
| if: steps.cache-nghttp3.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libnghttp3* | |
| /usr/local/include/nghttp3 | |
| /usr/local/lib/pkgconfig/libnghttp3.pc | |
| key: ${{ runner.os }}-nghttp3-${{ env.NGHTTP3_VERSION }}-r4 | |
| # ngtcp2 (depends on OpenSSL 3.5) ------------------------------- | |
| - name: Restore ngtcp2 cache | |
| id: cache-ngtcp2 | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libngtcp2* | |
| /usr/local/include/ngtcp2 | |
| /usr/local/lib/pkgconfig/libngtcp2.pc | |
| /usr/local/lib/pkgconfig/libngtcp2_crypto_*.pc | |
| key: ${{ runner.os }}-ngtcp2-${{ env.NGTCP2_VERSION }}-ossl-${{ env.OPENSSL_VERSION }}-r5 | |
| - name: Build ngtcp2 from source | |
| if: steps.cache-ngtcp2.outputs.cache-hit != 'true' | |
| run: | | |
| set -eux | |
| git clone --depth=1 --branch=v${NGTCP2_VERSION} --recurse-submodules \ | |
| https://github.com/ngtcp2/ngtcp2.git | |
| cd ngtcp2 | |
| autoreconf -i | |
| PKG_CONFIG_PATH=/usr/local/lib/pkgconfig \ | |
| ./configure --prefix=/usr/local --with-openssl --enable-lib-only | |
| make -j"$(nproc)" | |
| sudo make install | |
| # http_server's config.m4 looks for libngtcp2_crypto_ossl | |
| # specifically (OpenSSL 3.5 native QUIC). Fail loud if the | |
| # ngtcp2 build picked a different backend. | |
| test -f /usr/local/lib/pkgconfig/libngtcp2_crypto_ossl.pc \ | |
| || { echo "ERROR: ngtcp2 built without crypto_ossl backend"; \ | |
| ls /usr/local/lib/pkgconfig/libngtcp2_crypto_*.pc; exit 1; } | |
| - name: Save ngtcp2 cache | |
| if: steps.cache-ngtcp2.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libngtcp2* | |
| /usr/local/include/ngtcp2 | |
| /usr/local/lib/pkgconfig/libngtcp2.pc | |
| /usr/local/lib/pkgconfig/libngtcp2_crypto_*.pc | |
| key: ${{ runner.os }}-ngtcp2-${{ env.NGTCP2_VERSION }}-ossl-${{ env.OPENSSL_VERSION }}-r5 | |
| # libuv ---------------------------------------------------------- | |
| - name: Restore libuv cache | |
| id: cache-libuv | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libuv* | |
| /usr/local/include/uv* | |
| /usr/local/lib/pkgconfig/libuv.pc | |
| key: ${{ runner.os }}-libuv-${{ env.LIBUV_VERSION }}-r4 | |
| - name: Build libuv from source | |
| if: steps.cache-libuv.outputs.cache-hit != 'true' | |
| run: | | |
| set -eux | |
| sudo apt-get update -y | |
| sudo apt-get install -y --no-install-recommends cmake ninja-build | |
| wget -q https://github.com/libuv/libuv/archive/v${LIBUV_VERSION}.tar.gz | |
| tar -xzf v${LIBUV_VERSION}.tar.gz | |
| cd libuv-${LIBUV_VERSION} | |
| mkdir build && cd build | |
| cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF | |
| ninja | |
| sudo ninja install | |
| - name: Save libuv cache | |
| if: steps.cache-libuv.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libuv* | |
| /usr/local/include/uv* | |
| /usr/local/lib/pkgconfig/libuv.pc | |
| key: ${{ runner.os }}-libuv-${{ env.LIBUV_VERSION }}-r4 | |
| # curl (built against OpenSSL 3.5 — Ubuntu 22.04 ships 7.81 which | |
| # is below the 7.87 floor TrueAsync requires, and its libcurl is | |
| # linked against system OpenSSL 3.0). ---------------------------- | |
| - name: Restore curl cache | |
| id: cache-curl | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libcurl* | |
| /usr/local/include/curl | |
| /usr/local/lib/pkgconfig/libcurl.pc | |
| /usr/local/bin/curl | |
| key: ${{ runner.os }}-curl-${{ env.CURL_VERSION }}-ossl-${{ env.OPENSSL_VERSION }}-r1 | |
| - name: Build curl from source | |
| if: steps.cache-curl.outputs.cache-hit != 'true' | |
| run: | | |
| set -eux | |
| ver_us=$(echo "${CURL_VERSION}" | tr '.' '_') | |
| wget -q https://github.com/curl/curl/releases/download/curl-${ver_us}/curl-${CURL_VERSION}.tar.gz | |
| tar -xzf curl-${CURL_VERSION}.tar.gz | |
| cd curl-${CURL_VERSION} | |
| PKG_CONFIG_PATH=/usr/local/lib/pkgconfig \ | |
| ./configure --prefix=/usr/local --with-openssl \ | |
| --with-nghttp2=/usr/local --enable-shared --disable-static \ | |
| --without-libpsl | |
| make -j"$(nproc)" | |
| sudo make install | |
| - name: Save curl cache | |
| if: steps.cache-curl.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libcurl* | |
| /usr/local/include/curl | |
| /usr/local/lib/pkgconfig/libcurl.pc | |
| /usr/local/bin/curl | |
| key: ${{ runner.os }}-curl-${{ env.CURL_VERSION }}-ossl-${{ env.OPENSSL_VERSION }}-r1 | |
| # -------------------------------------------------------------------- | |
| # Stage 2: build PHP + ext/async (no http_server yet — added in a | |
| # later step once the PHP base build is green). | |
| # Restores caches only — never tries to save them, so two matrix | |
| # jobs can't race for the same key. | |
| # -------------------------------------------------------------------- | |
| build: | |
| needs: deps | |
| name: "LINUX_X64_${{ matrix.debug && 'DEBUG' || 'RELEASE' }}_${{ matrix.zts && 'ZTS' || 'NTS' }}" | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 60 | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| debug: [false, true] | |
| zts: [true] | |
| steps: | |
| - name: Checkout php-http-server | |
| uses: actions/checkout@v5 | |
| with: | |
| path: http-server | |
| # actions/cache restores tarballs as the runner user, so any path | |
| # already owned by root (e.g. /usr/local/include) breaks `tar -x` | |
| # with "Cannot mkdir: Permission denied". Make /usr/local writable | |
| # by the runner before any cache restore step. | |
| - name: Make /usr/local writable | |
| run: | | |
| sudo mkdir -p /usr/local/lib/pkgconfig /usr/local/include /usr/local/bin | |
| sudo chown -R "$(id -u):$(id -g)" /usr/local/lib /usr/local/include /usr/local/bin | |
| - name: Restore OpenSSL cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libssl* | |
| /usr/local/lib/libcrypto* | |
| /usr/local/include/openssl | |
| /usr/local/lib/pkgconfig/openssl.pc | |
| /usr/local/lib/pkgconfig/libssl.pc | |
| /usr/local/lib/pkgconfig/libcrypto.pc | |
| /usr/local/bin/openssl | |
| key: ${{ runner.os }}-openssl-${{ env.OPENSSL_VERSION }}-r4 | |
| fail-on-cache-miss: true | |
| - name: Restore nghttp2 cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libnghttp2* | |
| /usr/local/include/nghttp2 | |
| /usr/local/lib/pkgconfig/libnghttp2.pc | |
| key: ${{ runner.os }}-nghttp2-${{ env.NGHTTP2_VERSION }}-r4 | |
| fail-on-cache-miss: true | |
| - name: Restore nghttp3 cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libnghttp3* | |
| /usr/local/include/nghttp3 | |
| /usr/local/lib/pkgconfig/libnghttp3.pc | |
| key: ${{ runner.os }}-nghttp3-${{ env.NGHTTP3_VERSION }}-r4 | |
| fail-on-cache-miss: true | |
| - name: Restore ngtcp2 cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libngtcp2* | |
| /usr/local/include/ngtcp2 | |
| /usr/local/lib/pkgconfig/libngtcp2.pc | |
| /usr/local/lib/pkgconfig/libngtcp2_crypto_*.pc | |
| key: ${{ runner.os }}-ngtcp2-${{ env.NGTCP2_VERSION }}-ossl-${{ env.OPENSSL_VERSION }}-r5 | |
| fail-on-cache-miss: true | |
| - name: Restore libuv cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libuv* | |
| /usr/local/include/uv* | |
| /usr/local/lib/pkgconfig/libuv.pc | |
| key: ${{ runner.os }}-libuv-${{ env.LIBUV_VERSION }}-r4 | |
| fail-on-cache-miss: true | |
| - name: Restore curl cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libcurl* | |
| /usr/local/include/curl | |
| /usr/local/lib/pkgconfig/libcurl.pc | |
| /usr/local/bin/curl | |
| key: ${{ runner.os }}-curl-${{ env.CURL_VERSION }}-ossl-${{ env.OPENSSL_VERSION }}-r1 | |
| fail-on-cache-miss: true | |
| - name: Refresh ldconfig | |
| run: sudo ldconfig | |
| - name: Install PHP build dependencies | |
| run: | | |
| set -eux | |
| sudo apt-get update -y | |
| # Mirror ext/async/.github/workflows/build-linux.yml apt list, | |
| # minus libssl-dev / libcurl4-openssl-dev (we use our own | |
| # OpenSSL 3.5 + curl 8.12.1 from /usr/local). | |
| sudo apt-get install -y \ | |
| autoconf bison build-essential re2c \ | |
| libxml2-dev pkg-config \ | |
| libargon2-dev libedit-dev \ | |
| libsodium-dev libsqlite3-dev libonig-dev libzip-dev \ | |
| libpng-dev libjpeg-dev libwebp-dev libfreetype6-dev \ | |
| libgmp-dev libldap2-dev libsasl2-dev libpq-dev \ | |
| libmysqlclient-dev libbz2-dev libenchant-2-dev libffi-dev \ | |
| libgdbm-dev liblmdb-dev libsnmp-dev libtidy-dev \ | |
| libxslt1-dev libicu-dev wget ca-certificates | |
| - name: Clone php-src + ext/async | |
| run: | | |
| set -eux | |
| git clone --depth=1 --branch=${PHP_SRC_BRANCH} \ | |
| https://github.com/true-async/php-src.git php-src | |
| git clone --depth=1 --branch=${PHP_ASYNC_BRANCH} \ | |
| https://github.com/true-async/php-async.git php-src/ext/async | |
| # Mirrors the maintainer's local config.nice (~/php-src): | |
| # --disable-all + the allowlist the true-async branch's | |
| # internal_functions_cli.c generation expects. http_server is | |
| # built separately as a shared extension after install. | |
| - name: Configure PHP | |
| working-directory: php-src | |
| run: | | |
| set -eux | |
| ./buildconf --force | |
| PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:${PKG_CONFIG_PATH:-} \ | |
| ./configure \ | |
| --prefix=/usr/local \ | |
| --disable-all \ | |
| --without-pear \ | |
| --enable-async \ | |
| --enable-pdo \ | |
| --with-pdo-mysql \ | |
| --with-pdo-sqlite \ | |
| --enable-sockets \ | |
| --enable-posix \ | |
| --enable-pcntl \ | |
| --enable-opcache \ | |
| --with-openssl \ | |
| --with-config-file-path=/etc \ | |
| --with-config-file-scan-dir=/etc/php.d \ | |
| --${{ matrix.debug && 'enable' || 'disable' }}-debug \ | |
| --${{ matrix.zts && 'enable' || 'disable' }}-zts | |
| - name: Build PHP | |
| working-directory: php-src | |
| run: make -j"$(nproc)" >/dev/null | |
| - name: Install PHP | |
| working-directory: php-src | |
| run: | | |
| sudo make install | |
| sudo mkdir -p /etc/php.d | |
| echo "opcache.enable_cli=1" | sudo tee /etc/php.d/opcache.ini | |
| - name: Show PHP info | |
| run: | | |
| /usr/local/bin/php -v | |
| /usr/local/bin/php -m | grep -iE 'async|openssl|curl' || true | |
| # ---------------------------------------------------------------- | |
| # Stage 3: build http_server as a shared extension via phpize | |
| # against the PHP we just installed. | |
| # ---------------------------------------------------------------- | |
| - name: phpize http_server | |
| working-directory: http-server | |
| run: | | |
| set -eux | |
| /usr/local/bin/phpize | |
| - name: Install lcov (release ZTS only — coverage capture) | |
| if: matrix.debug == false | |
| run: sudo apt-get install -y --no-install-recommends lcov | |
| - name: Configure http_server | |
| working-directory: http-server | |
| env: | |
| ENABLE_COVERAGE: ${{ matrix.debug == false && '--enable-coverage' || '' }} | |
| run: | | |
| set -eux | |
| PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:${PKG_CONFIG_PATH:-} \ | |
| ./configure \ | |
| --with-php-config=/usr/local/bin/php-config \ | |
| --enable-http-server \ | |
| --enable-http2 \ | |
| --enable-http3 \ | |
| --with-openssl \ | |
| --with-nghttp2=/usr/local \ | |
| --with-nghttp3=/usr/local \ | |
| --with-ngtcp2=/usr/local \ | |
| $ENABLE_COVERAGE | |
| - name: Build http_server | |
| working-directory: http-server | |
| run: make -j"$(nproc)" | |
| - name: Install http_server | |
| working-directory: http-server | |
| run: | | |
| sudo make install | |
| echo "extension=true_async_server.so" | sudo tee /etc/php.d/true_async_server.ini | |
| - name: Verify http_server loads | |
| run: | | |
| /usr/local/bin/php -m | grep -i 'true_async_server' | |
| /usr/local/bin/php --ri true_async_server || true | |
| - name: Enable core dumps | |
| run: | | |
| set -eux | |
| sudo apt-get install -y --no-install-recommends gdb | |
| sudo mkdir -p /var/coredumps | |
| sudo chmod 777 /var/coredumps | |
| # Pattern: /var/coredumps/core.<exe>.<pid>.<ts> | |
| echo '/var/coredumps/core.%e.%p.%t' | sudo tee /proc/sys/kernel/core_pattern | |
| # apport hijacks core delivery on Ubuntu; disable so the kernel writes the file. | |
| sudo systemctl stop apport.service 2>/dev/null || true | |
| sudo sysctl -w kernel.core_uses_pid=1 | |
| ulimit -c unlimited | |
| ulimit -c | |
| - name: Run http_server phpt suite | |
| working-directory: http-server | |
| run: | | |
| ulimit -c unlimited | |
| /usr/local/bin/php "$GITHUB_WORKSPACE/php-src/run-tests.php" \ | |
| -P -q -j"$(nproc)" \ | |
| -g FAIL,BORK,LEAK,XLEAK \ | |
| --no-progress \ | |
| --offline \ | |
| --show-diff \ | |
| --show-slow 4000 \ | |
| --set-timeout 120 \ | |
| tests/phpt | |
| - name: Collect core dumps + backtraces | |
| if: failure() | |
| run: | | |
| set -eux | |
| mkdir -p /tmp/cores | |
| # Pick up any cores the kernel wrote. | |
| sudo cp -a /var/coredumps/. /tmp/cores/ 2>/dev/null || true | |
| # Some runners also drop cores in cwd. | |
| find "$GITHUB_WORKSPACE/http-server" -maxdepth 4 -name 'core*' \ | |
| -size +0c -exec cp -a {} /tmp/cores/ \; 2>/dev/null || true | |
| sudo chown -R "$(id -u):$(id -g)" /tmp/cores || true | |
| ls -la /tmp/cores || true | |
| for c in /tmp/cores/core.*; do | |
| [ -f "$c" ] || continue | |
| echo "===== $c =====" | |
| # exe is in field 5 of `file <core>`: "from 'php-cgi ...'" | |
| EXE=$(file "$c" | sed -n "s/.*execfn: '\([^']*\)'.*/\1/p" | head -1) | |
| [ -z "$EXE" ] && EXE=$(file "$c" | sed -n "s/.*from '\([^ ']*\).*/\1/p" | head -1) | |
| [ -z "$EXE" ] && EXE=/usr/local/bin/php | |
| echo "exe=$EXE" | |
| gdb --batch \ | |
| -ex 'set pagination off' \ | |
| -ex 'thread apply all bt full' \ | |
| -ex 'info sharedlibrary' \ | |
| "$EXE" "$c" 2>&1 | tee "${c}.bt.txt" | tail -200 || true | |
| done | |
| - name: Upload failing test artifacts + cores | |
| if: failure() | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: phpt-failures-${{ matrix.debug && 'debug' || 'release' }}-${{ github.run_id }} | |
| path: | | |
| http-server/tests/phpt/**/*.out | |
| http-server/tests/phpt/**/*.diff | |
| http-server/tests/phpt/**/*.log | |
| /tmp/cores/** | |
| /usr/local/bin/php | |
| /usr/local/bin/php-cgi | |
| /usr/local/lib/php/extensions/**/true_async_server.so | |
| http-server/modules/true_async_server.so | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| # ---------------------------------------------------------------- | |
| # Coverage capture — Release ZTS only. Reuses the PHP install | |
| # and http_server build from this job (built with --enable-coverage | |
| # above), so we don't have to rebuild everything just to collect | |
| # gcda. Replaces the old standalone `coverage` job. | |
| # ---------------------------------------------------------------- | |
| - name: Capture lcov + extract JSON | |
| if: matrix.debug == false && success() | |
| working-directory: http-server | |
| run: | | |
| set -eux | |
| mkdir -p coverage | |
| # Initial baseline (so files never executed still appear). | |
| lcov --capture --initial \ | |
| --directory . \ | |
| --output-file coverage/baseline.info \ | |
| --rc geninfo_unexecuted_blocks=1 --quiet | |
| # Post-run counters (phpt has already run via the previous step). | |
| lcov --capture --directory . \ | |
| --output-file coverage/run.info \ | |
| --rc geninfo_unexecuted_blocks=1 --quiet | |
| lcov --add-tracefile coverage/baseline.info \ | |
| --add-tracefile coverage/run.info \ | |
| --output-file coverage/raw.info --quiet | |
| lcov --extract coverage/raw.info "$PWD/src/*" \ | |
| --output-file coverage/filtered.info --quiet | |
| python3 scripts/coverage-report.py \ | |
| coverage/filtered.info -o coverage/current.json | |
| jq '.totals' coverage/current.json | |
| # HTML report for the artifact. | |
| genhtml coverage/filtered.info \ | |
| --output-directory coverage/html \ | |
| --title "php-http-server coverage" \ | |
| --legend --quiet 2>/dev/null || true | |
| - name: Compute touched-files list | |
| if: matrix.debug == false && github.event_name == 'pull_request' | |
| working-directory: http-server | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -eux | |
| # actions/checkout for pull_request checks out the merge ref, | |
| # so neither BASE nor HEAD is guaranteed to be present locally — | |
| # fetch both as explicit objects before diffing. | |
| git fetch --no-tags --depth=200 origin "$BASE_SHA" || true | |
| git fetch --no-tags --depth=200 origin "$HEAD_SHA" || true | |
| git diff --name-only "$BASE_SHA" "$HEAD_SHA" > /tmp/touched.txt | |
| wc -l /tmp/touched.txt | |
| head -50 /tmp/touched.txt | |
| - name: Detect [coverage-drop-ok] override | |
| if: matrix.debug == false && github.event_name == 'pull_request' | |
| id: override | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| PR_TITLE: ${{ github.event.pull_request.title }} | |
| PR_BODY: ${{ github.event.pull_request.body }} | |
| run: | | |
| set -eu | |
| if git -C http-server log --format=%B "$BASE_SHA".."$HEAD_SHA" \ | |
| | grep -qF '[coverage-drop-ok]'; then | |
| echo "allow=1" >> "$GITHUB_OUTPUT" | |
| elif printf '%s\n%s\n' "$PR_TITLE" "$PR_BODY" \ | |
| | grep -qF '[coverage-drop-ok]'; then | |
| echo "allow=1" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "allow=0" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Compare vs baseline | |
| if: matrix.debug == false && github.event_name == 'pull_request' | |
| id: compare | |
| working-directory: http-server | |
| run: | | |
| ARGS=(--baseline docs/coverage-baseline.json | |
| --current coverage/current.json | |
| --touched /tmp/touched.txt | |
| --output /tmp/coverage.md) | |
| if [ "${{ steps.override.outputs.allow }}" = "1" ]; then | |
| ARGS+=(--allow-drop) | |
| fi | |
| set +e | |
| python3 scripts/coverage-compare.py "${ARGS[@]}" | |
| echo "exit=$?" >> "$GITHUB_OUTPUT" | |
| - name: Upload coverage HTML | |
| if: matrix.debug == false && success() | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: coverage-html-${{ github.run_id }} | |
| path: http-server/coverage/html | |
| if-no-files-found: warn | |
| retention-days: 7 | |
| - name: Post coverage comment | |
| if: matrix.debug == false && github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const body = fs.readFileSync('/tmp/coverage.md', 'utf8'); | |
| const marker = '<!-- coverage-gate -->'; | |
| const tagged = `${marker}\n${body}`; | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.issue.number; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner, repo, issue_number, per_page: 100, | |
| }); | |
| const existing = comments.find(c => | |
| c.user.type === 'Bot' && c.body && c.body.startsWith(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner, repo, comment_id: existing.id, body: tagged }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner, repo, issue_number, body: tagged }); | |
| } | |
| - name: Warn on coverage regression | |
| if: matrix.debug == false && github.event_name == 'pull_request' && steps.compare.outputs.exit != '0' | |
| run: | | |
| echo "::warning::Coverage regression detected (exit=${{ steps.compare.outputs.exit }}). See coverage comment for details." | |
| - name: Refresh baseline on main | |
| if: matrix.debug == false && github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| working-directory: http-server | |
| run: | | |
| set -eux | |
| cp coverage/current.json docs/coverage-baseline.json | |
| if git diff --quiet -- docs/coverage-baseline.json; then | |
| echo "Baseline unchanged." | |
| exit 0 | |
| fi | |
| git config user.name 'github-actions[bot]' | |
| git config user.email '41898282+github-actions[bot]@users.noreply.github.com' | |
| git add docs/coverage-baseline.json | |
| git commit -m "ci: refresh coverage baseline [skip ci]" | |
| git push origin HEAD:main | |
| # -------------------------------------------------------------------- | |
| # Stage 5a: standalone fuzz smoke. Builds the no-PHP multipart | |
| # harness and runs it on a 60 s budget. Independent of the deps | |
| # stack, runs in parallel with everything. | |
| # -------------------------------------------------------------------- | |
| fuzz-smoke: | |
| name: Fuzz smoke | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| - name: Install clang + libFuzzer | |
| run: | | |
| set -eux | |
| sudo apt-get update -y | |
| sudo apt-get install -y --no-install-recommends clang | |
| - name: Build fuzz_multipart | |
| working-directory: fuzz | |
| run: make fuzz_multipart | |
| - name: Run fuzz_multipart 60s | |
| working-directory: fuzz | |
| env: | |
| ASAN_OPTIONS: "abort_on_error=1:print_stacktrace=1" | |
| run: | | |
| set -eux | |
| mkdir -p corpus/multipart | |
| ./fuzz_multipart \ | |
| -max_total_time=60 \ | |
| -print_final_stats=1 \ | |
| -timeout=10 \ | |
| -rss_limit_mb=2048 \ | |
| corpus/multipart/ | |
| - name: Upload crash artifacts | |
| if: failure() | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: fuzz-crash-multipart-${{ github.run_id }} | |
| path: | | |
| fuzz/crash-* | |
| fuzz/leak-* | |
| fuzz/oom-* | |
| fuzz/timeout-* | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| # -------------------------------------------------------------------- | |
| # Stage 5b: PHP-embedded fuzz gate. Builds libphp via --enable-embed, | |
| # then builds and runs fuzz_http1_parser + fuzz_h2_session for 60 s | |
| # each on the seed corpora. ~5–10× slower than the standalone smoke | |
| # but covers the production parser wiring (emalloc/zend_string, | |
| # llhttp, nghttp2 HPACK). | |
| # -------------------------------------------------------------------- | |
| fuzz-embedded: | |
| needs: deps | |
| name: Fuzz embedded | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout php-http-server | |
| uses: actions/checkout@v5 | |
| with: | |
| path: http-server | |
| - name: Make /usr/local writable | |
| run: | | |
| sudo mkdir -p /usr/local/lib/pkgconfig /usr/local/include /usr/local/bin | |
| sudo chown -R "$(id -u):$(id -g)" /usr/local/lib /usr/local/include /usr/local/bin | |
| - name: Restore OpenSSL cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libssl* | |
| /usr/local/lib/libcrypto* | |
| /usr/local/include/openssl | |
| /usr/local/lib/pkgconfig/openssl.pc | |
| /usr/local/lib/pkgconfig/libssl.pc | |
| /usr/local/lib/pkgconfig/libcrypto.pc | |
| /usr/local/bin/openssl | |
| key: ${{ runner.os }}-openssl-${{ env.OPENSSL_VERSION }}-r4 | |
| fail-on-cache-miss: true | |
| - name: Restore nghttp2 cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libnghttp2* | |
| /usr/local/include/nghttp2 | |
| /usr/local/lib/pkgconfig/libnghttp2.pc | |
| key: ${{ runner.os }}-nghttp2-${{ env.NGHTTP2_VERSION }}-r4 | |
| fail-on-cache-miss: true | |
| - name: Restore libuv cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| /usr/local/lib/libuv* | |
| /usr/local/include/uv* | |
| /usr/local/lib/pkgconfig/libuv.pc | |
| key: ${{ runner.os }}-libuv-${{ env.LIBUV_VERSION }}-r4 | |
| fail-on-cache-miss: true | |
| - name: Refresh ldconfig | |
| run: sudo ldconfig | |
| - name: Install build deps | |
| run: | | |
| set -eux | |
| sudo apt-get update -y | |
| sudo apt-get install -y \ | |
| autoconf bison build-essential clang re2c \ | |
| libxml2-dev pkg-config libsqlite3-dev libonig-dev \ | |
| wget ca-certificates | |
| - name: Clone php-src + ext/async | |
| run: | | |
| set -eux | |
| git clone --depth=1 --branch=${PHP_SRC_BRANCH} \ | |
| https://github.com/true-async/php-src.git php-src | |
| git clone --depth=1 --branch=${PHP_ASYNC_BRANCH} \ | |
| https://github.com/true-async/php-async.git php-src/ext/async | |
| - name: Configure & build PHP with --enable-embed | |
| working-directory: php-src | |
| run: | | |
| set -eux | |
| ./buildconf --force | |
| PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:${PKG_CONFIG_PATH:-} \ | |
| ./configure \ | |
| --prefix=/usr/local \ | |
| --disable-all \ | |
| --without-pear \ | |
| --enable-async \ | |
| --enable-embed=shared \ | |
| --with-openssl \ | |
| --with-config-file-path=/etc \ | |
| --disable-debug \ | |
| --enable-zts | |
| make -j"$(nproc)" >/dev/null | |
| sudo make install | |
| sudo ldconfig | |
| - name: Build fuzz_http1_parser + fuzz_h2_session | |
| working-directory: http-server/fuzz | |
| run: | | |
| set -eux | |
| PATH="/usr/local/bin:$PATH" make fuzz_http1_parser | |
| PATH="/usr/local/bin:$PATH" make fuzz_h2_session | |
| # h2o ships much larger seed corpora than ours (1972 + 1272 | |
| # vs 3 + 3 committed). Their corpora are raw HTTP/1 & HTTP/2 | |
| # bytes, so directly compatible. Fetch via sparse shallow clone | |
| # — fast (~5 s) and no repo bloat on our side. | |
| - name: Fetch h2o seed corpora | |
| run: | | |
| set -eux | |
| git clone --depth=1 --filter=blob:none --no-checkout \ | |
| https://github.com/h2o/h2o.git /tmp/h2o | |
| cd /tmp/h2o | |
| git sparse-checkout init --cone | |
| git sparse-checkout set fuzz/http1-corpus fuzz/http2-corpus | |
| git checkout | |
| echo "h2o http1 seeds: $(ls fuzz/http1-corpus | wc -l)" | |
| echo "h2o http2 seeds: $(ls fuzz/http2-corpus | wc -l)" | |
| - name: Run fuzz_http1_parser 60s | |
| working-directory: http-server/fuzz | |
| env: | |
| ASAN_OPTIONS: "abort_on_error=1:print_stacktrace=1:detect_leaks=0" | |
| USE_ZEND_ALLOC: "0" | |
| run: | | |
| set -eux | |
| ./fuzz_http1_parser \ | |
| -max_total_time=60 \ | |
| -print_final_stats=1 \ | |
| -timeout=10 \ | |
| -rss_limit_mb=4096 \ | |
| corpus/http1/ /tmp/h2o/fuzz/http1-corpus/ | |
| - name: Run fuzz_h2_session 60s | |
| working-directory: http-server/fuzz | |
| env: | |
| ASAN_OPTIONS: "abort_on_error=1:print_stacktrace=1:detect_leaks=0" | |
| USE_ZEND_ALLOC: "0" | |
| run: | | |
| set -eux | |
| ./fuzz_h2_session \ | |
| -max_total_time=60 \ | |
| -print_final_stats=1 \ | |
| -timeout=10 \ | |
| -rss_limit_mb=4096 \ | |
| corpus/h2/ /tmp/h2o/fuzz/http2-corpus/ | |
| - name: Upload crash artifacts | |
| if: failure() | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: fuzz-crash-embedded-${{ github.run_id }} | |
| path: | | |
| http-server/fuzz/crash-* | |
| http-server/fuzz/leak-* | |
| http-server/fuzz/oom-* | |
| http-server/fuzz/timeout-* | |
| if-no-files-found: ignore | |
| retention-days: 14 |