diff --git a/.github/workflows/bench-gate.yml b/.github/workflows/bench-gate.yml
index 1be51dd..527dfdd 100644
--- a/.github/workflows/bench-gate.yml
+++ b/.github/workflows/bench-gate.yml
@@ -101,7 +101,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- - uses: actions/checkout@v7
+ - uses: actions/checkout@v4
with:
# NOT shallow: arm S is reconstructed by `git archive` at a pinned
# historical commit, which a depth-1 clone does not have.
@@ -119,11 +119,17 @@ jobs:
./tools/bench-gate/build-arms.sh "$RUNNER_TEMP/arms" 2
cc --version | head -1 > "$RUNNER_TEMP/toolchain.txt"
+ - name: Install distro libJudy
+ uses: nick-fields/retry@v3
+ with:
+ timeout_minutes: 5
+ max_attempts: 3
+ retry_wait_seconds: 5
+ command: sudo apt-get update -qq && sudo apt-get install -y libjudy-dev
+
- name: Build arm B against the distro libJudy (reported, never gated)
run: |
set -euo pipefail
- sudo apt-get update -qq
- sudo apt-get install -y libjudy-dev
# Same source, same compiler, same PHP; only --with-judy differs.
src="$RUNNER_TEMP/src-B"
rm -rf "$src"; mkdir -p "$src"
@@ -138,6 +144,7 @@ jobs:
GATE_FLAG: ${{ (github.event.inputs.gate == 'false') && ' ' || '--gate' }}
run: |
set -euo pipefail
+ # shellcheck disable=SC2086
./tools/bench-gate/run-gate.sh \
--platform linux-glibc-x86_64 \
--arms "$RUNNER_TEMP/arms" \
@@ -169,7 +176,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- - uses: actions/checkout@v7
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -227,7 +234,7 @@ jobs:
runs-on: macos-latest
timeout-minutes: 45
steps:
- - uses: actions/checkout@v7
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -262,6 +269,7 @@ jobs:
set -euo pipefail
extra=""
[ -f "$RUNNER_TEMP/arms/judy-B-1.so" ] && extra="--with-b"
+ # shellcheck disable=SC2086
./tools/bench-gate/run-gate.sh \
--platform macos-arm64 \
--arms "$RUNNER_TEMP/arms" \
@@ -300,7 +308,7 @@ jobs:
# are the same ones. Kept off the narrow per-PR trigger.
if: github.event_name != 'pull_request'
steps:
- - uses: actions/checkout@v7
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -423,6 +431,7 @@ jobs:
cp "$BENCH_TMP/arm-s-manifest.json" "$BENCH_TMP/results/"
# Driven by the BUILDER's php.exe: it is the one the DLLs load into,
# and bench-gate.php spawns its children with PHP_BINARY.
+ # shellcheck disable=SC2086
"$BENCH_TMP/php-runtime/php.exe" scripts/bench-gate.php \
--arm "C=$BENCH_TMP/arms/judy-C-1.dll" \
--arm "S=$BENCH_TMP/arms/judy-S-1.dll" \
@@ -452,7 +461,7 @@ jobs:
contents: read
pull-requests: write
steps:
- - uses: actions/checkout@v7
+ - uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
pattern: bench-gate-*
@@ -484,7 +493,7 @@ jobs:
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
- uses: actions/github-script@v9
+ uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 763b6bc..ca4f513 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -38,11 +38,84 @@ permissions:
pull-requests: write
jobs:
+ detect-changes:
+ name: Subsystems / Change Detection
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ outputs:
+ php: ${{ steps.filter.outputs.php }}
+ c: ${{ steps.filter.outputs.c }}
+ code: ${{ steps.filter.outputs.code }}
+ docs: ${{ steps.filter.outputs.docs }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dorny/paths-filter@v3
+ id: filter
+ with:
+ filters: |
+ code:
+ - 'php_judy.*'
+ - 'judy_*.*'
+ - 'Judy.stub.php'
+ - 'Judy_arginfo.h'
+ - 'config.*'
+ - 'configure*'
+ - 'Makefile*'
+ - 'Dockerfile*'
+ - 'libjudy/**'
+ - 'tests/**'
+ - 'examples/**'
+ - 'scripts/**'
+ - 'tools/**'
+ - 'research/**'
+ - 'baselines/**'
+ - 'package.xml'
+ - 'composer.json'
+ - '.github/workflows/ci.yml'
+ - '.github/workflows/bench-gate.yml'
+ - '.github/workflows/nightly.yml'
+ php:
+ - 'php_judy.*'
+ - 'judy_*.*'
+ - 'Judy.stub.php'
+ - 'Judy_arginfo.h'
+ - 'config.*'
+ - 'configure*'
+ - 'tests/**'
+ - 'examples/**'
+ - 'scripts/**'
+ - 'package.xml'
+ - 'composer.json'
+ - '.github/workflows/ci.yml'
+ c:
+ - 'php_judy.*'
+ - 'judy_*.*'
+ - 'Judy_arginfo.h'
+ - 'libjudy/**'
+ - 'config.*'
+ - 'configure*'
+ - 'tools/**'
+ - 'research/**'
+ - '.github/workflows/ci.yml'
+ docs:
+ - 'docs/**'
+ - '*.md'
+ - 'LICENSE'
+ - 'CREDITS'
+ - 'llms.txt'
+ - 'THIRD-PARTY.md'
+
build-linux:
+ name: Core / Linux Matrix (PHP ${{ matrix.php-version }})
+ needs: [detect-changes]
+ if: needs.detect-changes.outputs.code == 'true' || needs.detect-changes.outputs.php == 'true' || needs.detect-changes.outputs.c == 'true' || github.event_name != 'pull_request'
runs-on: ubuntu-latest
+ timeout-minutes: 20
# Experimental (pre-release) PHP versions may fail without blocking CI.
continue-on-error: ${{ matrix.experimental == true }}
strategy:
+ fail-fast: false
+ max-parallel: 6
matrix:
php-version: ["8.1", "8.2", "8.3", "8.4", "8.5"]
include:
@@ -52,8 +125,11 @@ jobs:
experimental: true
steps:
+ - name: Stagger matrix runner startup jitter
+ run: sleep $(( (RANDOM % 5) + 1 ))
+
- name: Checkout code
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
@@ -97,7 +173,7 @@ jobs:
- name: Upload JUnit results
if: always()
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@v4
with:
name: junit-linux-${{ matrix.php-version }}
path: junit.xml
@@ -109,37 +185,33 @@ jobs:
- name: Install PIE
if: matrix.php-version == '8.4'
- run: |
- curl -fsSL https://github.com/php/pie/releases/latest/download/pie.phar \
- -o /usr/local/bin/pie
- chmod +x /usr/local/bin/pie
- pie --version
+ uses: nick-fields/retry@v3
+ with:
+ timeout_minutes: 5
+ max_attempts: 3
+ retry_wait_seconds: 3
+ command: |
+ curl --retry 5 --retry-delay 2 --retry-max-time 60 --retry-all-errors --retry-connrefused -fsSL https://github.com/php/pie/releases/latest/download/pie.phar \
+ -o /usr/local/bin/pie
+ chmod +x /usr/local/bin/pie
+ pie --version
- name: Install previous-release baseline via PIE
if: matrix.php-version == '8.4'
- run: |
- # Compare against the most recent stable release (release-over-
- # release regression detection). Bump this to the just-published
- # release on each release — see the "Releasing" section in README.
- #
- # Build the baseline THE SAME WAY as the arm under test, i.e.
- # bundled. Until 2.6.0 the baseline had to be `--with-judy=/usr`
- # because releases predating the vendored tree had no bundled mode
- # — but leaving that flag on once the pin reached 2.6.0 made the
- # two arms differ by LIBRARY rather than by release: system libJudy
- # (shared, unpatched) against the bundled patched tree. The row it
- # produced was the vendoring delta wearing a release comparison's
- # label — "2.6.0 -> 2.6.0, 44 faster" — which is a real measurement
- # of the wrong thing. Dropping the flag makes the source version
- # the only difference again, which is what this job exists to
- # measure. The bundled-vs-system comparison has its own instrument:
- # the S->C axis in bench-gate.yml, decomposed per key type.
- # `debug-mirror-assertions` remains the permanent system-lib guard.
- sudo pie install --skip-enable-extension orieg/judy:2.6.0
- BASELINE_SO="$(php-config --extension-dir)/judy.so"
- echo "BASELINE_SO=$BASELINE_SO" >> "$GITHUB_ENV"
- php -d "extension=$BASELINE_SO" -r \
- 'echo "Baseline loaded: judy " . judy_version() . "\n";'
+ uses: nick-fields/retry@v3
+ with:
+ timeout_minutes: 5
+ max_attempts: 3
+ retry_wait_seconds: 5
+ command: |
+ # Compare against the most recent stable release (release-over-
+ # release regression detection). Bump this to the just-published
+ # release on each release — see the "Releasing" section in README.
+ sudo pie install --skip-enable-extension orieg/judy:2.6.0
+ BASELINE_SO="$(php-config --extension-dir)/judy.so"
+ echo "BASELINE_SO=$BASELINE_SO" >> "$GITHUB_ENV"
+ php -d "extension=$BASELINE_SO" -r \
+ 'echo "Baseline loaded: judy " . judy_version() . "\n";'
- name: Run interleaved release comparison
if: matrix.php-version == '8.4'
@@ -166,7 +238,7 @@ jobs:
- name: Upload release comparison results
if: always() && matrix.php-version == '8.4'
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@v4
with:
name: benchmark-baseline-linux-php${{ matrix.php-version }}
path: |
@@ -190,7 +262,7 @@ jobs:
- name: Upload benchmark results
if: always()
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@v4
with:
name: benchmark-linux-php${{ matrix.php-version }}
path: |
@@ -199,10 +271,15 @@ jobs:
if-no-files-found: ignore
build-linux-expanse:
- name: build-linux-expanse (PHP ${{ matrix.php-version }})
+ name: Compat / Linux with Expanse Drop-in (PHP ${{ matrix.php-version }})
+ needs: [detect-changes]
+ if: needs.detect-changes.outputs.code == 'true' || needs.detect-changes.outputs.php == 'true' || needs.detect-changes.outputs.c == 'true' || github.event_name != 'pull_request'
runs-on: ubuntu-latest
+ timeout-minutes: 20
continue-on-error: ${{ matrix.experimental == true }}
strategy:
+ fail-fast: false
+ max-parallel: 6
matrix:
php-version: ["8.1", "8.2", "8.3", "8.4", "8.5"]
include:
@@ -210,14 +287,17 @@ jobs:
experimental: true
steps:
+ - name: Stagger matrix runner startup jitter
+ run: sleep $(( (RANDOM % 5) + 1 ))
+
- name: Checkout code
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Checkout Expanse
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
with:
repository: orieg/expanse
path: expanse
@@ -260,7 +340,7 @@ jobs:
- name: Upload JUnit results
if: always()
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@v4
with:
name: junit-linux-expanse-${{ matrix.php-version }}
path: junit.xml
@@ -274,10 +354,14 @@ jobs:
# Built through Docker on a glibc runner rather than as a `container:`
# job: GitHub's node runtime for JS actions (actions/checkout) is
# glibc-linked and does not execute inside an Alpine container.
+ name: Compat / Alpine musl Matrix (PHP 8.4)
+ needs: [detect-changes]
+ if: needs.detect-changes.outputs.code == 'true' || needs.detect-changes.outputs.php == 'true' || needs.detect-changes.outputs.c == 'true' || github.event_name != 'pull_request'
runs-on: ubuntu-latest
+ timeout-minutes: 15
steps:
- name: Checkout code
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
- name: Build the extension on Alpine/musl
run: docker build -f Dockerfile.alpine -t php-judy-alpine .
@@ -304,13 +388,22 @@ jobs:
# is not known to work, and a harness that silently stops checking is
# worse than none — so CI deletes one line the checker depends on and
# requires the abort to happen.
+ name: Safety / Mirror Consistency Assertions (PHP 8.4)
+ needs: [detect-changes]
+ if: needs.detect-changes.outputs.code == 'true' || needs.detect-changes.outputs.php == 'true' || needs.detect-changes.outputs.c == 'true' || github.event_name != 'pull_request'
runs-on: ubuntu-latest
+ timeout-minutes: 15
steps:
- name: Checkout code
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
- name: Install Judy library
- run: sudo apt-get update && sudo apt-get install -y libjudy-dev
+ uses: nick-fields/retry@v3
+ with:
+ timeout_minutes: 5
+ max_attempts: 3
+ retry_wait_seconds: 5
+ command: sudo apt-get update && sudo apt-get install -y libjudy-dev
- name: Setup PHP
uses: shivammathur/setup-php@v2
@@ -353,6 +446,7 @@ jobs:
make
set +e
+ # shellcheck disable=SC2016
php -d extension="$(pwd)/modules/judy.so" \
-r '$j = new Judy(Judy::STRING_TO_INT_HASH); $j["a"] = 1;' \
> /tmp/negative-control.log 2>&1
@@ -386,6 +480,7 @@ jobs:
make
set +e
+ # shellcheck disable=SC2016
php -d extension="$(pwd)/modules/judy.so" \
-r '$j = new Judy(Judy::STRING_TO_INT_HASH, true); $j["a"] = 1;' \
> /tmp/negative-control-payload.log 2>&1
@@ -405,6 +500,7 @@ jobs:
# was gated off, so nothing should fire. If this aborts, the gate is
# not a gate and "optimizeIteration defaults to off" is not true.
set +e
+ # shellcheck disable=SC2016
php -d extension="$(pwd)/modules/judy.so" \
-r '$j = new Judy(Judy::STRING_TO_INT_HASH); $j["a"] = 1; echo $j["a"];' \
> /tmp/negative-control-default.log 2>&1
@@ -433,6 +529,7 @@ jobs:
make
set +e
+ # shellcheck disable=SC2016
php -d extension="$(pwd)/modules/judy.so" \
-r '$j = new Judy(Judy::STRING_TO_INT_HASH); $j["a"] = 1;' \
> /tmp/negative-control-leak.log 2>&1
@@ -469,10 +566,14 @@ jobs:
#
# setup-php ships no --enable-debug builds, so the PHP is compiled from
# source here and cached on its version.
+ name: Safety / Debug PHP Arginfo & Invariants (PHP 8.4)
+ needs: [detect-changes]
+ if: needs.detect-changes.outputs.code == 'true' || needs.detect-changes.outputs.php == 'true' || needs.detect-changes.outputs.c == 'true' || github.event_name != 'pull_request'
runs-on: ubuntu-latest
+ timeout-minutes: 30
steps:
- name: Checkout code
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
- name: Resolve the PHP 8.4 release to build
run: |
@@ -481,7 +582,7 @@ jobs:
# shape: the patch level is not what this job is testing, so a feed
# hiccup should not turn the job red.
FALLBACK=8.4.24
- VERSION=$(curl -fsSL --max-time 30 \
+ VERSION=$(curl --retry 5 --retry-delay 2 --retry-max-time 60 --retry-all-errors --retry-connrefused -fsSL \
'https://www.php.net/releases/?json&version=8.4' \
| grep -oE '"version":"8\.4\.[0-9]+"' \
| grep -oE '8\.4\.[0-9]+' | head -1 || true)
@@ -494,7 +595,7 @@ jobs:
- name: Restore the cached debug PHP
id: php-debug-cache
- uses: actions/cache@v6
+ uses: actions/cache@v4
with:
path: ~/php-debug
# The configure line is part of the key: a build made with different
@@ -503,20 +604,25 @@ jobs:
- name: Build PHP ${{ env.PHP_DEBUG_VERSION }} with --enable-debug
if: steps.php-debug-cache.outputs.cache-hit != 'true'
- run: |
- # --disable-all keeps the build to what the suite actually needs:
- # json and pcre cannot be disabled and are the only extensions the
- # .phpt files use, and the whole suite runs under it with zero
- # skipped tests. Add an extension back here if a test starts
- # needing one, rather than enabling everything.
- curl -fsSL --retry 3 -o /tmp/php.tar.xz \
- "https://www.php.net/distributions/php-${PHP_DEBUG_VERSION}.tar.xz"
- tar -xf /tmp/php.tar.xz -C /tmp
- cd "/tmp/php-${PHP_DEBUG_VERSION}"
- ./configure --enable-debug --disable-all --enable-cli --without-pear \
- --prefix="$HOME/php-debug"
- make -j"$(nproc)"
- make install
+ uses: nick-fields/retry@v3
+ with:
+ timeout_minutes: 20
+ max_attempts: 3
+ retry_wait_seconds: 5
+ command: |
+ # --disable-all keeps the build to what the suite actually needs:
+ # json and pcre cannot be disabled and are the only extensions the
+ # .phpt files use, and the whole suite runs under it with zero
+ # skipped tests. Add an extension back here if a test starts
+ # needing one, rather than enabling everything.
+ curl --retry 5 --retry-delay 2 --retry-max-time 60 --retry-all-errors --retry-connrefused -fsSL -o /tmp/php.tar.xz \
+ "https://www.php.net/distributions/php-${PHP_DEBUG_VERSION}.tar.xz"
+ tar -xf /tmp/php.tar.xz -C /tmp
+ cd "/tmp/php-${PHP_DEBUG_VERSION}"
+ ./configure --enable-debug --disable-all --enable-cli --without-pear \
+ --prefix="$HOME/php-debug"
+ make -j"$(nproc)"
+ make install
- name: Confirm the PHP really is a debug build
run: |
@@ -628,13 +734,22 @@ jobs:
# them are reachable at the single (corpus, keylen, absent-key) point the
# harnesses were habitually run at. The last step is the negative control:
# an unfired gate is not a known-working gate.
+ name: Safety / C Harnesses & ASan/UBSan Smoke Grid
+ needs: [detect-changes]
+ if: needs.detect-changes.outputs.code == 'true' || needs.detect-changes.outputs.php == 'true' || needs.detect-changes.outputs.c == 'true' || github.event_name != 'pull_request'
runs-on: ubuntu-latest
+ timeout-minutes: 15
steps:
- name: Checkout code
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
- name: Install Judy library
- run: sudo apt-get update && sudo apt-get install -y libjudy-dev
+ uses: nick-fields/retry@v3
+ with:
+ timeout_minutes: 5
+ max_attempts: 3
+ retry_wait_seconds: 5
+ command: sudo apt-get update && sudo apt-get install -y libjudy-dev
- name: Build every harness and run the sanitized smoke grid
run: ./tools/ci-smoke.sh 1000
@@ -697,10 +812,14 @@ jobs:
# only -- the fixed-seed 48-cell smoke grid plus a short random-seed soak
# on the production build, and the sanitized smoke grid. The 300s+ soaks
# stay a local / pre-release tool.
+ name: Safety / Differential Fuzz vs C++ stdlib Oracle
+ needs: [detect-changes]
+ if: needs.detect-changes.outputs.code == 'true' || needs.detect-changes.outputs.php == 'true' || needs.detect-changes.outputs.c == 'true' || github.event_name != 'pull_request'
runs-on: ubuntu-latest
+ timeout-minutes: 15
steps:
- name: Checkout code
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
- name: Build the bundled tree at the shipped production flags
# The same isolated flag set config.m4 hands the vendored units
@@ -794,7 +913,7 @@ jobs:
sh tools/differential-fuzz/validation/build-stock.sh \
/tmp/judy-broken-src /tmp/judy-broken \
- -O2 -fno-lto -fno-unroll-loops $JUDY_POPCNT
+ -O2 -fno-lto -fno-unroll-loops "$JUDY_POPCNT"
make -C tools/differential-fuzz clean >/dev/null
make -C tools/differential-fuzz JUDY_PREFIX=/tmp/judy-broken
@@ -815,10 +934,14 @@ jobs:
grep -m1 -A1 'DIVERGENCE' /tmp/diffuzz-negctl.log
validate-pecl:
+ name: Core / PECL Packaging & Version Parity
+ needs: [detect-changes]
+ if: needs.detect-changes.outputs.code == 'true' || needs.detect-changes.outputs.php == 'true' || needs.detect-changes.outputs.c == 'true' || github.event_name != 'pull_request'
runs-on: ubuntu-latest
+ timeout-minutes: 15
steps:
- name: Checkout code
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
- name: Verify PHP_JUDY_VERSION matches package.xml
run: |
@@ -928,11 +1051,16 @@ jobs:
tests/*.phpt
build-windows:
+ name: Core / Windows MSVC Matrix (PHP ${{ matrix.php-version }}-${{ matrix.arch }}-${{ matrix.ts }})
+ needs: [detect-changes]
+ if: needs.detect-changes.outputs.code == 'true' || needs.detect-changes.outputs.php == 'true' || needs.detect-changes.outputs.c == 'true' || github.event_name != 'pull_request'
runs-on: windows-latest
+ timeout-minutes: 25
# Experimental (pre-release) PHP versions may fail without blocking CI.
continue-on-error: ${{ matrix.experimental == true }}
strategy:
fail-fast: false
+ max-parallel: 6
matrix:
php-version: ["8.1", "8.2", "8.3", "8.4", "8.5"]
arch: ["x64"]
@@ -946,8 +1074,13 @@ jobs:
experimental: true
steps:
+ - name: Stagger matrix runner startup jitter
+ shell: pwsh
+ run: |
+ Start-Sleep -Seconds (Get-Random -Minimum 1 -Maximum 6)
+
- name: Checkout code
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
- name: Setup MSVC
uses: ilammy/msvc-dev-cmd@v1
@@ -987,7 +1120,7 @@ jobs:
- name: Upload JUnit results
if: always()
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@v4
with:
name: junit-windows-${{ matrix.php-version }}-${{ matrix.arch }}-${{ matrix.ts }}
path: ${{ runner.temp }}\junit.xml
@@ -1046,7 +1179,7 @@ jobs:
- name: Upload benchmark results
if: always()
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@v4
with:
name: benchmark-windows-php${{ matrix.php-version }}-${{ matrix.arch }}-${{ matrix.ts }}
path: |
@@ -1069,11 +1202,15 @@ jobs:
Select-Object -First 1 | ForEach-Object { Get-Content $_.FullName -Tail 30 }
build-windows-expanse:
- name: build-windows-expanse (PHP ${{ matrix.php-version }}-${{ matrix.arch }}-${{ matrix.ts }})
+ name: Compat / Windows with Expanse Drop-in (PHP ${{ matrix.php-version }}-${{ matrix.arch }}-${{ matrix.ts }})
+ needs: [detect-changes]
+ if: needs.detect-changes.outputs.code == 'true' || needs.detect-changes.outputs.php == 'true' || needs.detect-changes.outputs.c == 'true' || github.event_name != 'pull_request'
runs-on: windows-latest
+ timeout-minutes: 25
continue-on-error: ${{ matrix.experimental == true }}
strategy:
fail-fast: false
+ max-parallel: 6
matrix:
php-version: ["8.1", "8.2", "8.3", "8.4", "8.5"]
arch: ["x64"]
@@ -1085,8 +1222,13 @@ jobs:
experimental: true
steps:
+ - name: Stagger matrix runner startup jitter
+ shell: pwsh
+ run: |
+ Start-Sleep -Seconds (Get-Random -Minimum 1 -Maximum 6)
+
- name: Checkout code
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
- name: Setup MSVC
uses: ilammy/msvc-dev-cmd@v1
@@ -1097,7 +1239,7 @@ jobs:
uses: dtolnay/rust-toolchain@stable
- name: Checkout Expanse
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
with:
repository: orieg/expanse
path: expanse
@@ -1142,7 +1284,7 @@ jobs:
- name: Upload JUnit results
if: always()
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@v4
with:
name: junit-windows-expanse-${{ matrix.php-version }}-${{ matrix.arch }}-${{ matrix.ts }}
path: ${{ runner.temp }}\junit.xml
@@ -1164,11 +1306,14 @@ jobs:
# Each assertion matches the guard's MESSAGE, not merely a nonzero exit, so an
# unrelated breakage cannot masquerade as the guard working.
guard-32bit:
- name: 32-bit bundled build is refused (negative control)
+ name: Safety / 32-bit Refusal Guard (Negative Control)
+ needs: [detect-changes]
+ if: needs.detect-changes.outputs.code == 'true' || needs.detect-changes.outputs.c == 'true' || github.event_name != 'pull_request'
runs-on: ubuntu-latest
+ timeout-minutes: 10
steps:
- name: Checkout code
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
# config.w32 is JScript. Drive it through the php-src Windows build API,
# stubbed, with X64 set the way php-src sets it (win32/build/config.w32:
@@ -1251,6 +1396,7 @@ jobs:
rc=0
for f in libjudy/src/Judy1/Judy1Tables.c libjudy/src/JudyL/JudyLTables.c; do
echo "--- $f: 32-bit word must be rejected ---"
+ # shellcheck disable=SC2086
if cc -fsyntax-only -include /tmp/word32.h $INC "$f" 2>/tmp/err.txt; then
echo "FAIL: $f compiled with a 4-byte Word_t and no JU_64BIT"; rc=1; continue
fi
@@ -1262,6 +1408,7 @@ jobs:
echo "OK: rejected by the JU_64BIT #error guard"
echo "--- $f: 64-bit build must still be clean ---"
+ # shellcheck disable=SC2086
if ! cc -c -Wall -Werror -O2 -DJU_64BIT $INC "$f" -o /tmp/ok.o 2>/tmp/err64.txt; then
echo "FAIL: $f no longer compiles for a normal 64-bit build"
head -20 /tmp/err64.txt; rc=1; continue
@@ -1271,27 +1418,29 @@ jobs:
exit $rc
report-results:
- needs: [build-linux, build-windows, build-linux-expanse, build-windows-expanse]
- if: ${{ !cancelled() }}
+ name: Core / Consolidated Test & Benchmark Report
+ needs: [detect-changes, build-linux, build-windows, build-linux-expanse, build-windows-expanse]
+ if: (needs.detect-changes.outputs.code == 'true' || needs.detect-changes.outputs.php == 'true' || needs.detect-changes.outputs.c == 'true' || github.event_name != 'pull_request') && !cancelled()
runs-on: ubuntu-latest
+ timeout-minutes: 10
permissions:
checks: write
pull-requests: write
steps:
- name: Checkout code
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
with:
sparse-checkout: baselines
- name: Download all test artifacts
- uses: actions/download-artifact@v8
+ uses: actions/download-artifact@v4
with:
pattern: junit-*
path: artifacts
- name: Download benchmark artifacts
- uses: actions/download-artifact@v8
+ uses: actions/download-artifact@v4
with:
pattern: benchmark-*
path: benchmarks
@@ -2436,7 +2585,7 @@ jobs:
# report-posting issues never fail the run after tests pass.
if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
continue-on-error: true
- uses: actions/github-script@v9
+ uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
@@ -2466,3 +2615,55 @@ jobs:
body: fullBody,
});
}
+
+ ci-gate:
+ name: CI Gate / All Checks Passed
+ if: always()
+ needs:
+ - detect-changes
+ - build-linux
+ - build-linux-expanse
+ - build-alpine
+ - debug-mirror-assertions
+ - debug-php-assertions
+ - build-harnesses
+ - differential-fuzz
+ - validate-pecl
+ - build-windows
+ - build-windows-expanse
+ - guard-32bit
+ - report-results
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ steps:
+ - name: Evaluate Dependency Statuses
+ shell: bash
+ run: |
+ echo "Evaluating CI matrix health..."
+ STATUSES='${{ toJson(needs) }}'
+ FAILED=()
+ SKIPPED=()
+ PASSED=()
+
+ for job in $(echo "$STATUSES" | jq -r 'keys[]'); do
+ result=$(echo "$STATUSES" | jq -r --arg j "$job" '.[$j].result')
+ echo " Job [$job]: $result"
+ if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then
+ FAILED+=("$job")
+ elif [ "$result" = "skipped" ]; then
+ SKIPPED+=("$job")
+ else
+ PASSED+=("$job")
+ fi
+ done
+
+ echo ""
+ echo "Summary: ${#PASSED[@]} passed, ${#SKIPPED[@]} cleanly skipped, ${#FAILED[@]} failed"
+
+ if [ ${#FAILED[@]} -gt 0 ]; then
+ echo "::error::The following required checks failed: ${FAILED[*]}"
+ exit 1
+ fi
+
+ echo "::notice::All applicable CI checks passed (or were cleanly skipped by path filters)."
+ exit 0
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
new file mode 100644
index 0000000..68d99a6
--- /dev/null
+++ b/.github/workflows/nightly.yml
@@ -0,0 +1,358 @@
+name: Nightly
+
+on:
+ schedule:
+ - cron: "17 3 * * *"
+ workflow_dispatch:
+ inputs:
+ only:
+ description: "Which jobs to run"
+ required: false
+ default: all
+ type: choice
+ options: [all, valgrind, php-dev, fuzz]
+
+concurrency:
+ group: nightly
+ cancel-in-progress: false
+
+permissions:
+ contents: read
+ issues: write
+
+jobs:
+ valgrind-memory-leak:
+ name: Safety / Valgrind Memory Leak Check (Linux, PHP 8.4)
+ if: inputs.only == null || inputs.only == 'all' || inputs.only == 'valgrind'
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Install dependencies (Valgrind & Judy)
+ uses: nick-fields/retry@v3
+ with:
+ timeout_minutes: 5
+ max_attempts: 3
+ retry_wait_seconds: 5
+ command: sudo apt-get update && sudo apt-get install -y valgrind libjudy-dev
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: "8.4"
+ coverage: none
+
+ - name: Build extension with debug symbols
+ run: |
+ phpize
+ ./configure --with-judy=/usr CFLAGS="-g -O0 -Wall"
+ set -o pipefail
+ make 2>&1 | tee /tmp/build.log
+
+ - name: Run Valgrind memory leak test suite
+ run: |
+ # run-tests.php -m invokes Valgrind memcheck with USE_ZEND_ALLOC=0
+ NO_INTERACTION=1 REPORT_EXIT_STATUS=1 php run-tests.php -m -q --show-diff tests/
+
+ - name: Report or Update Nightly Failure Issue
+ if: failure() && github.event_name == 'schedule'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const title = '🚨 [Nightly CI Failure] Valgrind Memory Leak Detected on main';
+ const { data: issues } = await github.rest.issues.listForRepo({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ labels: 'nightly-failure'
+ });
+
+ const existing = issues.find(i => i.title.includes('Valgrind Memory Leak'));
+ const body = [
+ '### Nightly Valgrind Leak Check Alert',
+ '',
+ `- **Commit**: [${context.sha}](${context.payload.repository?.html_url}/commit/${context.sha})`,
+ `- **Workflow Run**: [View Failed Run Logs](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`,
+ '- **Failing Target**: `php run-tests.php -m` (Valgrind memcheck)',
+ '',
+ '#### Local Reproduction Command:',
+ '```bash',
+ 'USE_ZEND_ALLOC=0 valgrind --leak-check=full php run-tests.php -m -q --show-diff tests/',
+ '```'
+ ].join('\n');
+
+ if (existing) {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: existing.number,
+ body: `⚠️ Nightly Valgrind memory leak run still failing on commit ${context.sha}. [View Run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`
+ });
+ } else {
+ await github.rest.issues.create({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ title: title,
+ body: body,
+ labels: ['bug', 'nightly-failure', 'automated-triage']
+ });
+ }
+
+ - name: Auto-Close Resolved Nightly Issue on Success
+ if: success() && github.event_name == 'schedule'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const { data: issues } = await github.rest.issues.listForRepo({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ labels: 'nightly-failure'
+ });
+ const existing = issues.find(i => i.title.includes('Valgrind Memory Leak'));
+ if (existing) {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: existing.number,
+ body: `🟢 Nightly Valgrind leak verification succeeded on commit ${context.sha}! Auto-closing this issue.`
+ });
+ await github.rest.issues.update({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: existing.number,
+ state: 'closed'
+ });
+ }
+
+ php-dev-nightly:
+ name: Compat / PHP Development Branch Nightly (PHP 8.6-dev)
+ if: inputs.only == null || inputs.only == 'all' || inputs.only == 'php-dev'
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup PHP Development Build
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: "8.6"
+ coverage: none
+
+ - name: Build the extension
+ run: |
+ phpize
+ ./configure
+ set -o pipefail
+ make 2>&1 | tee /tmp/build.log
+
+ - name: Fail on compiler warnings
+ run: |
+ if grep -E '(php_judy|judy_handlers|judy_arrayaccess|judy_iterator)\.[ch]:[0-9]+:[0-9]+: warning:' /tmp/build.log; then
+ echo "::error::Compiler warnings detected against PHP development branch"
+ exit 1
+ fi
+ echo "No compiler warnings in extension sources."
+
+ - name: Run tests
+ run: make test TESTS=tests/ NO_INTERACTION=1 REPORT_EXIT_STATUS=1
+
+ - name: Report or Update Nightly Failure Issue
+ if: failure() && github.event_name == 'schedule'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const title = '🚨 [Nightly CI Failure] PHP-dev Compatibility Failure on main';
+ const { data: issues } = await github.rest.issues.listForRepo({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ labels: 'nightly-failure'
+ });
+
+ const existing = issues.find(i => i.title.includes('PHP-dev Compatibility'));
+ const body = [
+ '### Nightly PHP Development Branch Compatibility Alert',
+ '',
+ `- **Commit**: [${context.sha}](${context.payload.repository?.html_url}/commit/${context.sha})`,
+ `- **Workflow Run**: [View Failed Run Logs](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`,
+ '- **Failing Target**: PHP 8.6-dev build and test suite',
+ '',
+ '#### Local Reproduction:',
+ 'Compile and test php-judy against the latest php-src master / 8.6 branch.',
+ ].join('\n');
+
+ if (existing) {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: existing.number,
+ body: `⚠️ Nightly PHP-dev compatibility run still failing on commit ${context.sha}. [View Run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`
+ });
+ } else {
+ await github.rest.issues.create({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ title: title,
+ body: body,
+ labels: ['bug', 'nightly-failure', 'automated-triage']
+ });
+ }
+
+ - name: Auto-Close Resolved Nightly Issue on Success
+ if: success() && github.event_name == 'schedule'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const { data: issues } = await github.rest.issues.listForRepo({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ labels: 'nightly-failure'
+ });
+ const existing = issues.find(i => i.title.includes('PHP-dev Compatibility'));
+ if (existing) {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: existing.number,
+ body: `🟢 Nightly PHP-dev verification succeeded on commit ${context.sha}! Auto-closing this issue.`
+ });
+ await github.rest.issues.update({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: existing.number,
+ state: 'closed'
+ });
+ }
+
+ diffuzz-soak:
+ name: Safety / Extended Differential Fuzz Soak
+ if: inputs.only == null || inputs.only == 'all' || inputs.only == 'fuzz'
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Build bundled tree at production flags
+ run: |
+ POPCNT=""
+ echo 'int main(void){return 0;}' > /tmp/popcnt-probe.c
+ cc -Werror -mpopcnt -c /tmp/popcnt-probe.c -o /dev/null 2>/dev/null && POPCNT="-mpopcnt"
+ sh tools/differential-fuzz/validation/build-stock.sh \
+ "$PWD/libjudy/src" /tmp/judy-prod \
+ -O2 -fno-lto -fno-unroll-loops "$POPCNT"
+
+ - name: Run extended soak (300s)
+ run: |
+ make -C tools/differential-fuzz JUDY_PREFIX=/tmp/judy-prod
+ cd tools/differential-fuzz
+ ./diffuzz soak 300
+
+ - name: Report or Update Nightly Failure Issue
+ if: failure() && github.event_name == 'schedule'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const title = '🚨 [Nightly CI Failure] Differential Fuzz Soak Divergence on main';
+ const { data: issues } = await github.rest.issues.listForRepo({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ labels: 'nightly-failure'
+ });
+
+ const existing = issues.find(i => i.title.includes('Differential Fuzz Soak'));
+ const body = [
+ '### Nightly Differential Fuzz Soak Divergence Alert',
+ '',
+ `- **Commit**: [${context.sha}](${context.payload.repository?.html_url}/commit/${context.sha})`,
+ `- **Workflow Run**: [View Failed Run Logs](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`,
+ '- **Failing Target**: `tools/differential-fuzz/diffuzz soak 300`',
+ '',
+ '#### Local Reproduction Command:',
+ '```bash',
+ 'make -C tools/differential-fuzz && (cd tools/differential-fuzz && ./diffuzz soak 300)',
+ '```'
+ ].join('\n');
+
+ if (existing) {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: existing.number,
+ body: `⚠️ Nightly fuzz soak still failing on commit ${context.sha}. [View Run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`
+ });
+ } else {
+ await github.rest.issues.create({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ title: title,
+ body: body,
+ labels: ['bug', 'nightly-failure', 'automated-triage']
+ });
+ }
+
+ - name: Auto-Close Resolved Nightly Issue on Success
+ if: success() && github.event_name == 'schedule'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const { data: issues } = await github.rest.issues.listForRepo({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ labels: 'nightly-failure'
+ });
+ const existing = issues.find(i => i.title.includes('Differential Fuzz Soak'));
+ if (existing) {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: existing.number,
+ body: `🟢 Nightly fuzz soak succeeded on commit ${context.sha}! Auto-closing this issue.`
+ });
+ await github.rest.issues.update({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: existing.number,
+ state: 'closed'
+ });
+ }
+
+ nightly-gate:
+ name: CI Gate / Nightly Rollup Gate
+ if: always()
+ needs:
+ - valgrind-memory-leak
+ - php-dev-nightly
+ - diffuzz-soak
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ steps:
+ - name: Evaluate Dependency Statuses
+ shell: bash
+ run: |
+ echo "Evaluating Nightly matrix health..."
+ STATUSES='${{ toJson(needs) }}'
+ FAILED=()
+
+ for job in $(echo "$STATUSES" | jq -r 'keys[]'); do
+ result=$(echo "$STATUSES" | jq -r --arg j "$job" '.[$j].result')
+ echo " Job [$job]: $result"
+ if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then
+ FAILED+=("$job")
+ fi
+ done
+
+ if [ ${#FAILED[@]} -gt 0 ]; then
+ echo "::error::The following nightly checks failed: ${FAILED[*]}"
+ exit 1
+ fi
+
+ echo "::notice::All nightly checks passed (or were cleanly skipped)."
+ exit 0
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4d339d8..0f70580 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,9 +11,10 @@ jobs:
# Ensure the Git release tag matches the version declared in package.xml
validate:
runs-on: ubuntu-latest
+ timeout-minutes: 10
steps:
- name: Checkout
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
- name: Validate tag matches package.xml version
run: |
@@ -34,11 +35,12 @@ jobs:
get-extension-matrix:
needs: validate
runs-on: ubuntu-latest
+ timeout-minutes: 10
outputs:
matrix: ${{ steps.extension-matrix.outputs.matrix }}
steps:
- name: Checkout
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
- name: Get the extension matrix
id: extension-matrix
uses: php/php-windows-builder/extension-matrix@v1
@@ -56,11 +58,19 @@ jobs:
build-windows:
needs: get-extension-matrix
runs-on: ${{ matrix.os }}
+ timeout-minutes: 25
strategy:
+ fail-fast: false
+ max-parallel: 6
matrix: ${{fromJson(needs.get-extension-matrix.outputs.matrix)}}
steps:
+ - name: Stagger matrix runner startup jitter
+ shell: pwsh
+ run: |
+ Start-Sleep -Seconds (Get-Random -Minimum 1 -Maximum 6)
+
- name: Checkout
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
- name: Setup MSVC
uses: ilammy/msvc-dev-cmd@v1
@@ -84,9 +94,10 @@ jobs:
pecl-package:
needs: validate
runs-on: ubuntu-latest
+ timeout-minutes: 15
steps:
- name: Checkout
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
@@ -98,7 +109,7 @@ jobs:
run: pecl package package.xml
- name: Upload PECL package artifact
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@v4
with:
name: pecl-package
path: "*.tgz"
@@ -108,6 +119,7 @@ jobs:
runs-on: ubuntu-latest
needs: [build-windows, pecl-package]
if: ${{ github.event_name == 'release' }}
+ timeout-minutes: 15
steps:
- name: Upload Windows DLLs to the release
uses: php/php-windows-builder/release@v1
@@ -116,7 +128,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Download PECL package artifact
- uses: actions/download-artifact@v8
+ uses: actions/download-artifact@v4
with:
name: pecl-package
diff --git a/docs/CI_CD_GUIDE.md b/docs/CI_CD_GUIDE.md
new file mode 100644
index 0000000..7b7566c
--- /dev/null
+++ b/docs/CI_CD_GUIDE.md
@@ -0,0 +1,309 @@
+# CI/CD & Zero-Regression Engineering Guide
+
+Welcome to the **Expanse & Sister Projects CI/CD Guide**. This document establishes mandatory engineering standards, workflow architectures, regression gating rules, and operational best practices for all autonomous AI coding agents and human engineers designing or maintaining GitHub Actions pipelines across **`expanse`**, **`php-judy`**, **`judy-cache`**, **`judy-polyfill`**, **`yaml-workflows`**, **`gws-connectors`**, and future projects.
+
+---
+
+## 1. The 5 Commandments of High-Efficiency CI/CD
+
+```mermaid
+flowchart TD
+ PR[Pull Request Push] --> Filter[1. Fast Path Filtering
dorny/paths-filter <= 2s]
+
+ Filter -->|No relevant changes| ZeroRunners[Zero Heavy Runners Provisioned
100% Compute Minutes Saved]
+ Filter -->|Code changed| Matrix[2. Scoped PR Smoke Matrix
Fast Unit & Invariant Tests]
+
+ Matrix --> CancelCheck[3. Concurrency Hygiene
Cancel superseded runs]
+ Matrix --> StepSummary[4. Rich Step Summaries
Color-coded Markdown Tables]
+
+ ZeroRunners --> Gate[5. Single Aggregated Gate
ci-gate rollup]
+ Matrix --> Gate
+
+ Gate -->|All executed passed / skipped| Green[🟢 Branch Protection Passed]
+ Gate -->|Any executed failed| Red[🔴 PR Blocked]
+```
+
+### 1. Zero-Compute on Irrelevant PRs (Two-Tiered Path Filtering)
+- Never spin up dozens of heavy compilation runners on PRs touching only documentation (`docs/**`), markdown files (`*.md`), or unrelated scripts.
+- Use a single, lightweight ($\le 2\text{s}$) initial job (`detect-changes`) powered by `dorny/paths-filter`.
+- Downstream verification jobs declare `needs: [detect-changes]` and `if: needs.detect-changes.outputs. == 'true'`.
+
+### 2. Organization-Wide Concurrency Hygiene (Cancel Superseded Runs)
+- GitHub Actions runner minutes and queue concurrency are pooled **across the entire organization**.
+- When new commits are pushed to an open pull request, immediately cancel in-flight runs from previous commits:
+ ```yaml
+ concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+ ```
+- Merges to `main` (`push`) must use unique run IDs (`github.run_id`) with `cancel-in-progress: false` to ensure every merge has a complete, persistent audit trail.
+
+### 3. Single Rollup Gate for Branch Protection (`ci-gate`)
+- **The Problem**: Requiring individual matrix jobs in branch protection causes PRs to deadlock in "Pending" when path filters skip non-applicable jobs.
+- **The Solution**: Branch protection requires **only one check**: `CI Gate / All Checks Passed` (`ci-gate`).
+- The `ci-gate` job runs `if: always()`, inspects `${{ toJson(needs) }}`, and treats cleanly skipped jobs as successful.
+
+### 4. PR Smoke vs. Release Matrix Separation
+- **Pull Request Stage**: Fast, low-latency smoke testing (compilation, linter, core unit tests, fast unsafe invariants).
+- **Release Stage (`tags: v*`)**: Full multi-architecture binary compilation (e.g. 5-platform Python wheels, cross-compiled Debian/RPM packages, Windows MSVC DLLs).
+
+### 5. Rich Color-Coded Step Summaries
+- Output structured, human-readable Markdown tables to `$GITHUB_STEP_SUMMARY` with explicit status indicators (🟢 PASS, 🟡 PARITY, 🔴 REGRESSION).
+
+---
+
+## 2. Upstream Network Resilience & Thundering-Herd Mitigation
+
+When running large matrices across dozens of runner VMs, simultaneous network requests can trigger upstream rate limits or `504 Gateway Timeout` errors (e.g., the `php-judy` v2.7.1 incident hitting `downloads.php.net`).
+
+### Resilient Network Standards
+
+#### 1. Standard Resilient Curl Wrapper
+Never use bare `curl -f`. Always configure exponential backoff, connection retries, and max execution time:
+```bash
+curl --retry 5 \
+ --retry-delay 2 \
+ --retry-max-time 60 \
+ --retry-all-errors \
+ --retry-connrefused \
+ -fsSL "$URL" -o "$OUTPUT"
+```
+
+#### 2. Startup Sleep Jitter for Matrix Jobs
+When 20+ matrix jobs start concurrently, stagger their initial network requests:
+```bash
+# Stagger simultaneous runner network requests by 1..5 seconds
+sleep $(( (RANDOM % 5) + 1 ))
+```
+
+#### 3. Matrix Concurrency Throttling (`max-parallel`)
+For heavy release matrices interacting with external CDNs or package registries, limit concurrent jobs:
+```yaml
+strategy:
+ fail-fast: false
+ max-parallel: 6
+ matrix:
+ ...
+```
+
+#### 4. Automated Step-Level Retries (`nick-fields/retry`)
+Wrap flaky setup steps (e.g. `setup-php`, `maturin-action`, `apt-get`) with automated retry logic:
+```yaml
+- name: Resilient Package Installation
+ uses: nick-fields/retry@v3
+ with:
+ timeout_minutes: 5
+ max_attempts: 3
+ retry_wait_seconds: 5
+ command: pip install --upgrade pip && pip install "maturin>=1.5,<2.0" pytest
+```
+
+---
+
+## 3. Performance & Software Regression Prevention
+
+### 3.1 Why Wall-Clock Benchmarks Fail in CI
+Wall-clock timing (`std::time::Instant`, criterion nanoseconds) on shared cloud CI runners exhibits $\pm 20\text{--}50\%$ noise due to VM multi-tenancy, hyperthread throttling, and background host load. Gating builds on raw wall-clock time causes frequent false-positive CI failures.
+
+### 3.2 Deterministic Instruction Counting (Valgrind / Callgrind / Iai)
+Expanse uses **hardware-agnostic, deterministic instruction counting**:
+1. Counts total CPU instructions retired (`Ir`).
+2. Counts L1 Data Cache accesses (`Dr`/`Dw`) and Last Level Cache (LLC) misses.
+3. **The Invariant**: For a fixed 50k operation workload with fixed seed, `Ir` is **100% deterministic down to the exact integer**.
+4. **The Gate Rule**: Any PR increasing instruction count by $>0.1\%$ vs baseline main without an approved bypass fails the build automatically.
+
+```mermaid
+flowchart TD
+ PR[PR Push / Commit] --> Runner[Linux Benchmark Runner]
+ Runner --> Callgrind[Valgrind / Callgrind Suite
50k Ops with Fixed Seed]
+ Callgrind --> Parse[Parse Instruction Counts & Cache Accesses]
+ Parse --> Compare[Compare against Baseline JSON / Thresholds]
+
+ Compare -->|Delta <= 0.0%| Green[🟢 PASS: Performance Maintained or Accelerated]
+ Compare -->|Delta > +0.1%| CheckBypass{Has 'perf-bypass-approved' Label?}
+
+ CheckBypass -->|Yes + Documented Rationale| Yellow[🟡 PASS_OVERRIDE: Approved Regression with Audit Trail]
+ CheckBypass -->|No| Red[🔴 FAIL: Instruction Count Regression Blocked]
+```
+
+### 3.3 Interleaved Dual-Arm Comparative Ratio Benchmark Pattern (`php-judy`)
+When micro-benchmarking full end-to-end execution where instruction counting is unavailable (e.g. PHP runtime, JIT):
+- Never compare absolute wall-clock durations across runs.
+- Measure two arms (**Arm S** = pristine baseline, **Arm C** = candidate PR) in **alternating interleaved rounds on the exact same runner**.
+- Gate strictly on the ratio:
+ $$\text{Ratio} = \frac{\text{Candidate Time}}{\text{Baseline Time}}$$
+- Runner slowdown slows both arms equally, keeping the ratio noise-free.
+
+### 3.4 Controlled Performance Bypass Protocol
+When an architectural change deliberately trades a minor instruction increase for a critical feature (e.g. security hardening, transaction snapshot isolation, metadata tagging):
+1. **PR Label**: PR must have the GitHub label `perf-bypass-approved` added by a repository maintainer.
+2. **PR Description Rationale**: PR body must contain an explicit section:
+ ```markdown
+ ### Performance Trade-off Disclosure
+ - **Regressed Metric**: `map_insert / random` (+1.8% instructions)
+ - **Load-Bearing Rationale**: Added 32-bit hot metadata value slots for columnar predicate pushdown.
+ - **Net System Win**: Scanning range queries is 6.8x faster with 0 cold DRAM fetches.
+ ```
+3. **Audit Trail**: The CI step summary logs `PASS_OVERRIDE (Approved by Maintainer)`.
+
+### 3.5 Memory Density & Zero-Heap-Churn Assertions
+- **Memory Density Budget**: Measure total heap bytes divided by key count; assert strict ceilings (e.g. $\le 0.50\text{ B/key}$ for sets, $\le 9.00\text{ B/key}$ for maps).
+- **Zero-Allocation Invariant (`no_heap_churn.rs`)**: Custom tracking allocator asserts `allocated_bytes == 0` during read-only lookups, contains queries, and range navigations.
+
+---
+
+## 4. 3-Tiered Miri & Undefined Behavior Prevention
+
+```mermaid
+graph TD
+ subgraph Tier 1: Pull Request CI [Fast Smoke: ~15s]
+ A[PR Touch Rust Code] --> B[Miri Core Unsafe Smoke
cargo miri test -p expanse-trie --lib -- --skip model_]
+ B -->|Checks: Pointer tags, Stacked Borrows, Leaf alignment| C[Pass: Green PR Check]
+ B -->|Fails: Retag / Provenance error| D[Block PR Before Merge]
+ end
+
+ subgraph Tier 2: Merge Gate [Branch Protection]
+ C --> E[CI Gate Rollup: All checks green]
+ E --> F[Merge Commit to main]
+ end
+
+ subgraph Tier 3: Nightly Full Matrix & Auto-Triage [Deep Sweep: ~10m]
+ G[Daily 03:17 UTC Cron] --> H[Full Miri Matrix
cargo miri test -p expanse-trie]
+ H -->|Pass| I{Open Nightly Issue Exists?}
+ I -->|Yes| J[Auto-Close Issue: Nightly Green]
+ I -->|No| K[Log Success Metric]
+ H -->|Fail| L{Open Nightly Issue Exists?}
+ L -->|Yes| M[Add Run Comment with Error Logs]
+ L -->|No| N[Open Deduplicated Bug Issue with Stack Trace & Repro Steps]
+ end
+```
+
+### Tier 1: Pull Request Fast Miri Smoke (`ci.yml`)
+- **Command**: `cargo miri test -p expanse-trie --lib -- --skip model_`
+- **Execution Time**: $\le 15\text{ seconds}$ on Linux x86_64.
+- **Coverage**: Validates 100% of raw pointer derivations in `get.rs`, `mutate.rs`, `mutate_map.rs`, `slot.rs`, `leaf.rs`, and `node.rs`. Catches Stacked Borrows / Tree Borrows invalidations *before* merging.
+
+### Tier 2: CI Gate Rollup (`ci.yml`)
+- `ci-gate` requires Tier 1 Miri to pass before any PR is mergeable.
+
+### Tier 3: Nightly Full Matrix & Automated Incident Triage (`nightly.yml`)
+- Executes full test suite under Miri including long-running randomized model sweeps (`proptest_model.rs`).
+- Automatically creates or updates deduplicated GitHub issues on failure, and auto-closes them on recovery.
+
+---
+
+## 5. Automated Nightly Failure Triage Pattern
+
+Nightly workflows run out-of-band without a human reviewing PR check results. To prevent silent test failures from rotting unnoticed:
+
+```yaml
+- name: Report or Update Nightly Failure Issue
+ if: failure() && github.event_name == 'schedule'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const title = '🚨 [Nightly CI Failure] Full Miri / Model Verification Failed on main';
+ const { data: issues } = await github.rest.issues.listForRepo({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ labels: 'nightly-failure'
+ });
+
+ const existing = issues.find(i => i.title.includes('Full Miri'));
+ const body = `### Nightly Verification Failure Alert
+
+ - **Commit**: [${context.sha}](${context.payload.repository?.html_url}/commit/${context.sha})
+ - **Workflow Run**: [View Failed Run Logs](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})
+ - **Failing Target**: \`cargo miri test -p expanse-trie\`
+
+ #### Local Reproduction Command:
+ \`\`\`bash
+ cargo miri test -p expanse-trie
+ \`\`\`
+ `;
+
+ if (existing) {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: existing.number,
+ body: `⚠️ Nightly run still failing on commit ${context.sha}. [View Run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`
+ });
+ } else {
+ await github.rest.issues.create({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ title: title,
+ body: body,
+ labels: ['bug', 'nightly-failure', 'automated-triage']
+ });
+ }
+
+- name: Auto-Close Resolved Nightly Issue on Success
+ if: success() && github.event_name == 'schedule'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const { data: issues } = await github.rest.issues.listForRepo({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ labels: 'nightly-failure'
+ });
+ const existing = issues.find(i => i.title.includes('Full Miri'));
+ if (existing) {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: existing.number,
+ body: `🟢 Nightly verification succeeded on commit ${context.sha}! Auto-closing this issue.`
+ });
+ await github.rest.issues.update({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: existing.number,
+ state: 'closed'
+ });
+ }
+```
+
+---
+
+## 6. Architectural Role of `yaml-workflows` GitHub Action
+
+When deciding between native GitHub Actions YAML and `orieg/yaml-workflow`:
+
+| Workflow Stage | Recommended Architecture | Rationale |
+| :--- | :--- | :--- |
+| **Core PR CI (`ci.yml`)** | **Native GitHub Actions** | Zero Python/pip setup overhead; sub-second runner startup; direct compiler diagnostic streaming. |
+| **Multi-Arch Release Packaging (`release.yml`)** | **`yaml-workflows` Action** | Ideal for DAG-based multi-artifact bundling, checksum calculations, templating changelogs, and cloud publishing. |
+| **Nightly Cross-Repo Sweeps (`nightly.yml`)** | **`yaml-workflows` Action** | Excellent for running comparative sweeps across `expanse`, `php-judy`, `judy-cache`, and synthesizing unified JSON/SVG reports. |
+| **Static Documentation Portals (`pages.yml`)** | **`yaml-workflows` Action** | Multi-step site compilation, broken link verification, and asset staging. |
+
+---
+
+## 7. Multi-Project Architecture & Quick-Start Checklist
+
+Use this checklist when creating a new project or updating an existing sister repository:
+
+### Standard Matrix Template
+
+| Repository | Primary Technology | Key Quality Gates | Path Filter Configuration |
+| :--- | :--- | :--- | :--- |
+| **`expanse`** | Rust 2024 / C ABI | 1. Callgrind instruction gate
2. Tier 1 Fast Miri smoke ($\le 50\text{s}$)
3. Loom atomic race model tests
4. 32-Bit Bare-Metal Cross-Compiles (`RV32IMAC` & `Cortex-M4`)
5. Memory density assertions ($\le 0.40\text{ B/key}$)
6. Differential stock-oracle verification | `crates/**`, `Cargo.toml`, `Cargo.lock`, `rust-toolchain*`, `.github/workflows/ci.yml` |
+| **`php-judy`** | C / PHP Extension | 1. Interleaved dual-arm benchmark gate (`bench-gate.php`)
2. Valgrind zero-leak check (`--leak-check=full --error-exitcode=1 php run-tests.php -P`)
3. PHP 8.1..8.5 matrix + ZTS
4. Compiler warning zero-tolerance (`set -o pipefail` + first-party filter)
5. Memory ceiling ($\le 25\text{ B/key}$) | `php_judy.*`, `judy_*.*`, `config.m4`, `tests/**`, `.github/workflows/**`, `libjudy/**`, `tools/**` |
+| **`judy-cache`** | C / PHP Extension | 1. Runtime dependency version floor check (`judy_version() >= 2.6.0`)
2. APCu & Redis YCSB comparison gate
3. Multithreaded churn thrash gate (0 deadlocks)
4. GC compaction pause ceiling ($\le 1.0\text{ ms}$) | `src/**`, `include/**`, `tests/**` |
+| **`judy-polyfill`** | Pure PHP | 1. PHPUnit across PHP 8.1..8.5
2. PHPStan Level 9 + Psalm
3. Infection Mutation Testing (MSI $\ge 90\%$) | `src/**`, `tests/**`, `composer.json` |
+| **`yaml-workflows`** | Python / GitHub Actions | 1. `actionlint` schema validation
2. ShellCheck on inline action scripts
3. Smart matrix pruning (full versions on Linux, LTS only on Windows/macOS) | `*.yml`, `actions/**`, `scripts/**` |
+| **`gws-connectors`** | TypeScript / Go | 1. `golangci-lint` / `biome`
2. Unit test suite with mock API
3. Concurrency cancellation hygiene | `src/**`, `go.mod`, `package.json` |
+
+### New Project Setup Checklist:
+- [ ] 1. Define `concurrency` with `cancel-in-progress: ${{ github.event_name == 'pull_request' }}`.
+- [ ] 2. Create `detect-changes` job with `dorny/paths-filter@v3`.
+- [ ] 3. Gate downstream test jobs on `needs: [detect-changes]` and `if: needs.detect-changes.outputs. == 'true'`.
+- [ ] 4. Create `ci-gate` rollup job evaluating `${{ toJson(needs) }}`.
+- [ ] 5. Set up deterministic regression gating (Callgrind instructions or interleaved dual-arm ratios).
+- [ ] 6. Enforce explicit `timeout-minutes: 10..20` on every job.
+- [ ] 7. Configure branch protection to require **only** `ci-gate`.
+- [ ] 8. Add automated nightly issue triage and self-healing to `nightly.yml`.