From 8d85f0ac7f1e29000f75069cfd1e1ce68aa69bc1 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Sat, 14 Nov 2026 00:35:23 +0000 Subject: [PATCH 1/3] Prepare a restricted PHP profile for Rust source parity --- docker/php-ci/README.md | 10 ++ docker/php-ci/reference.ini | 4 + rust/parity/test_php_reference_container.py | 11 ++ scripts/ci/prepare-php-reference-container.sh | 28 +++ scripts/ci/probe-php-reference-runtime.php | 14 ++ scripts/ci/run-php-reference-container.sh | 93 ++++++++++ scripts/ci/setup-php-reference-container.sh | 35 ++++ scripts/ci/test_php_reference_container.py | 163 ++++++++++++++++++ tests/fixtures/php-reference-workflow.yml | 70 ++++++++ 9 files changed, 428 insertions(+) create mode 100644 docker/php-ci/reference.ini create mode 100644 rust/parity/test_php_reference_container.py create mode 100755 scripts/ci/prepare-php-reference-container.sh create mode 100644 scripts/ci/probe-php-reference-runtime.php create mode 100755 scripts/ci/run-php-reference-container.sh create mode 100755 scripts/ci/setup-php-reference-container.sh create mode 100644 scripts/ci/test_php_reference_container.py create mode 100644 tests/fixtures/php-reference-workflow.yml diff --git a/docker/php-ci/README.md b/docker/php-ci/README.md index 681ad12..7956c56 100644 --- a/docker/php-ci/README.md +++ b/docker/php-ci/README.md @@ -32,3 +32,13 @@ through those mounts. A disposable-source control attempts direct launcher writes, wrapper unlink/replacement and rename, then invokes the next host PHP entry. The container test cells depend on the accepted central policy before executing a changed runtime source. + +The Rust source-reference caller has a separate `reference.ini` profile with +unlimited PHP memory, UTC, PHAR writes disabled and CLI OPcache disabled. Its +launcher exposes only `rust/target/php-vectors.json` as writable under the Rust +build tree. Compiled binaries, source and host controls stay read-only. Parity +config directories live in private temporary data; only `DW_CONFIG_HOME` and +build identity fields needed by the source reference cross that boundary. +Run `prepare-php-reference-container.sh CONFIG_ID PRIVATE_STATE` to check an +already-inspected image, or the setup script to build the pinned runtime. The +proposed caller fixture preserves the original vector, Cargo and parity commands. diff --git a/docker/php-ci/reference.ini b/docker/php-ci/reference.ini new file mode 100644 index 0000000..f458cbc --- /dev/null +++ b/docker/php-ci/reference.ini @@ -0,0 +1,4 @@ +date.timezone=UTC +memory_limit=-1 +phar.readonly=1 +opcache.enable_cli=0 diff --git a/rust/parity/test_php_reference_container.py b/rust/parity/test_php_reference_container.py new file mode 100644 index 0000000..894fb9e --- /dev/null +++ b/rust/parity/test_php_reference_container.py @@ -0,0 +1,11 @@ +"""Discover PHP reference launcher controls in the existing host-Python test lane.""" +import importlib.util +from pathlib import Path + + +def load_tests(loader, _tests, _pattern): + source = Path(__file__).resolve().parents[2]/'scripts/ci/test_php_reference_container.py' + spec = importlib.util.spec_from_file_location('php_reference_controls', source) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return loader.loadTestsFromModule(module) diff --git a/scripts/ci/prepare-php-reference-container.sh b/scripts/ci/prepare-php-reference-container.sh new file mode 100755 index 0000000..c8d2b31 --- /dev/null +++ b/scripts/ci/prepare-php-reference-container.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Reuse the inspected base, then prepare only the Rust PHP-reference data. +set -euo pipefail +umask 077 +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" +image="${1:?Usage: prepare-php-reference-container.sh CONFIG_ID PRIVATE_STATE}" +state="${2:?Usage: prepare-php-reference-container.sh CONFIG_ID PRIVATE_STATE}" +[[ "$image" =~ ^sha256:[0-9a-f]{64}$ ]] +[[ -d "$state" && ! -L "$state" && -O "$state" && "$(realpath "$state")" = "$state" ]] +case "$state/" in "$root/"*) exit 1 ;; *) ;; esac +case "$root/" in "$state/"*) exit 1 ;; *) ;; esac +for directory in "$root/vendor" "$root/rust/target" "$state/bin" "$state/home" \ + "$state/tmp" "$state/composer-home/cache"; do + [[ ! -L "$directory" ]] + mkdir -p "$directory" + [[ "$(realpath "$directory")" = "$directory" && -O "$directory" ]] +done +vectors="$root/rust/target/php-vectors.json" +[[ ! -L "$vectors" ]] +if [[ ! -e "$vectors" ]]; then (set -o noclobber; : > "$vectors"); fi +[[ -f "$vectors" && -O "$vectors" && -n "$(find "$vectors" -prune -type f -links 1)" ]] +for command in php composer; do + ln -sf "$root/scripts/ci/run-php-reference-container.sh" "$state/bin/$command" +done +export CLI_PHP_CI_ROOT="$root" CLI_PHP_CI_IMAGE="$image" CLI_PHP_CI_STATE="$state" +"$state/bin/php" "$root/scripts/ci/probe-php-reference-runtime.php" 8.4 +CLI_PHP_CI_TEST_IMAGE="$image" python3 -I -S -B "$root/scripts/ci/test-php-container-tls.py" -v +CLI_PHP_CI_TEST_IMAGE="$image" python3 -I -S -B "$root/scripts/ci/test_php_reference_container.py" -v diff --git a/scripts/ci/probe-php-reference-runtime.php b/scripts/ci/probe-php-reference-runtime.php new file mode 100644 index 0000000..5f87a94 --- /dev/null +++ b/scripts/ci/probe-php-reference-runtime.php @@ -0,0 +1,14 @@ + ini_get('phar.readonly'), 'memory_limit' => ini_get('memory_limit'), + 'opcache_enable_cli' => ini_get('opcache.enable_cli'), 'timezone' => date_default_timezone_get()], JSON_THROW_ON_ERROR).PHP_EOL; diff --git a/scripts/ci/run-php-reference-container.sh b/scripts/ci/run-php-reference-container.sh new file mode 100755 index 0000000..bd19a13 --- /dev/null +++ b/scripts/ci/run-php-reference-container.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -euo pipefail + +command="${0##*/}" +case "$command" in php|composer) ;; *) echo 'Invoke this reference launcher as php or composer.' >&2; exit 2 ;; esac +root="${CLI_PHP_CI_ROOT:?PHP CI repository root is required}" +state="${CLI_PHP_CI_STATE:?PHP CI state directory is required}" +image="${CLI_PHP_CI_IMAGE:?PHP CI image is required}" +if [[ ! "$image" =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo 'PHP CI must use the inspected immutable image config ID.' >&2 + exit 1 +fi + +options=(--rm --init --interactive --name "cli-php-ci-${command}-${BASHPID}" + --cpus 2 --memory 2g --pids-limit 512 --network host + --cap-drop ALL --security-opt no-new-privileges + --user "$(id -u):$(id -g)" --workdir "$PWD" + --env "HOME=$state/home" + --mount "type=bind,source=$root,target=$root,readonly" + --mount "type=bind,source=$state,target=$state,readonly") + +# Keep host-executed launchers and controls read-only. Only dependency data, +# test output and private runtime caches can be changed by application code. +for directory in "$root/vendor" "$state/home" "$state/tmp" "$state/composer-home"; do + if [[ ! -d "$directory" || -L "$directory" || "$(realpath "$directory")" != "$directory" ]]; then + echo 'PHP CI writable directories must be existing real paths.' >&2 + exit 1 + fi + options+=(--mount "type=bind,source=$directory,target=$directory") +done +# The host executes other files under target. Grant write access to the +# generated JSON alone, never to the compiled Rust binaries or their parents. +vectors="$root/rust/target/php-vectors.json" +[[ -f "$vectors" && ! -L "$vectors" && -O "$vectors" && "$(realpath "$vectors")" = "$vectors" \ + && -n "$(find "$vectors" -prune -type f -links 1)" ]] || { + echo 'The compatibility vector output must be a real owned single-link file.' >&2; exit 1; +} +options+=(--mount "type=bind,source=$vectors,target=$vectors" + --mount "type=bind,source=$root/docker/php-ci/reference.ini,target=/usr/local/etc/php/conf.d/zz-cli-reference.ini,readonly") + +# Each source-reference case receives only its disposable config directory. +if [[ -n "${DW_CONFIG_HOME:-}" ]]; then + [[ -d "$DW_CONFIG_HOME" && ! -L "$DW_CONFIG_HOME" && -O "$DW_CONFIG_HOME" \ + && "$(realpath "$DW_CONFIG_HOME")" = "$DW_CONFIG_HOME" ]] || exit 1 + case "$DW_CONFIG_HOME/" in "$state/tmp/"dw-parity-*/ ) ;; *) + echo 'PHP reference config must belong to the private parity temporary tree.' >&2; exit 1 ;; + esac + options+=(--env DW_CONFIG_HOME) +fi + +# The Composer verifier is a composite action outside the checkout. +if [[ -n "${GITHUB_ACTION_PATH:-}" ]]; then + options+=(--mount "type=bind,source=$GITHUB_ACTION_PATH,target=$GITHUB_ACTION_PATH,readonly") +fi +git_directory="$(git -C "$root" rev-parse --path-format=absolute --git-common-dir)" +if [[ "$git_directory" != "$root/"* ]]; then + options+=(--mount "type=bind,source=$git_directory,target=$git_directory,readonly") +fi + +declare -A mounted=() +for variable in SSL_CERT_FILE CURL_CA_BUNDLE GIT_SSL_CAINFO COMPOSER_CAFILE \ + REQUESTS_CA_BUNDLE PIP_CERT NPM_CONFIG_CAFILE CARGO_HTTP_CAINFO CARGO_HTTP_PROXY_CAINFO \ + SSL_CERT_DIR GIT_SSL_CAPATH COMPOSER_CAPATH; do + if [[ -n "${!variable:-}" ]]; then + selected="${!variable}" + if [[ -z "${mounted[$selected]:-}" ]]; then + options+=(--mount "type=bind,source=$selected,target=$selected,readonly") + mounted[$selected]=1 + fi + options+=(--env "$variable") + fi +done + +# This caller uses synthetic loopback fixtures and no role credentials. +while IFS= read -r variable; do + case "$variable" in + COMPOSER_HOME|COMPOSER_CACHE_DIR|COMPOSER_MAX_PARALLEL_*|COMPOSER_IPRESOLVE|\ + DW_CLI_VERSION|DW_CLI_COMMIT|DW_CLI_BUILD_DATE|\ + DW_CI_CA_SHA256|GITHUB_ACTIONS|CI|TMPDIR|TZ) + options+=(--env "$variable") ;; + *) ;; + esac +done < <(compgen -e) + +# The host verifier must inspect the actual container Composer PHAR. +arguments=() +for argument in "$@"; do + if [[ "$argument" == "$state/bin/composer" ]]; then + argument=/usr/local/bin/composer + fi + arguments+=("$argument") +done +exec docker run "${options[@]}" "$image" "$command" "${arguments[@]}" diff --git a/scripts/ci/setup-php-reference-container.sh b/scripts/ci/setup-php-reference-container.sh new file mode 100755 index 0000000..8154f00 --- /dev/null +++ b/scripts/ci/setup-php-reference-container.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail +umask 077 +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" +metadata="$(mktemp -d "${RUNNER_TEMP:?RUNNER_TEMP is required}/cli-reference-setup.XXXXXX")" +trap 'rm -rf -- "$metadata"' EXIT +: > "$metadata/environment" +: > "$metadata/path" +# Reuse the accepted build/ancestry source without changing the admitted test +# profile. Its private outputs are parsed as data, never sourced as shell code. +GITHUB_ENV="$metadata/environment" GITHUB_PATH="$metadata/path" \ + bash "$root/scripts/ci/setup-php-container.sh" 8.4 +bindings="$(python3 -I -S -B - "$metadata/environment" "$metadata/path" "$root" <<'PY' +from pathlib import Path +import re,sys +lines = Path(sys.argv[1]).read_text().splitlines() +assert len(lines) == 6 +values = dict(line.split('=', 1) for line in lines) +assert set(values) == {'CLI_PHP_CI_IMAGE','CLI_PHP_CI_ROOT','CLI_PHP_CI_STATE','COMPOSER_HOME','COMPOSER_CACHE_DIR','TMPDIR'} +assert values['CLI_PHP_CI_ROOT'] == sys.argv[3] +assert re.fullmatch(r'sha256:[0-9a-f]{64}', values['CLI_PHP_CI_IMAGE']) +state = values['CLI_PHP_CI_STATE'] +assert values['COMPOSER_HOME'] == state+'/composer-home' +assert values['COMPOSER_CACHE_DIR'] == state+'/composer-home/cache' +assert values['TMPDIR'] == state+'/tmp' +assert Path(sys.argv[2]).read_text() == state+'/bin\n' +print(values['CLI_PHP_CI_IMAGE']) +print(state) +PY +)" +mapfile -t fields <<< "$bindings" +bash "$root/scripts/ci/prepare-php-reference-container.sh" "${fields[0]}" "${fields[1]}" +cat "$metadata/environment" >> "${GITHUB_ENV:?GITHUB_ENV is required}" +echo "DW_PARITY_PHP=${fields[1]}/bin/php" >> "${GITHUB_ENV:?GITHUB_ENV is required}" +cat "$metadata/path" >> "${GITHUB_PATH:?GITHUB_PATH is required}" diff --git a/scripts/ci/test_php_reference_container.py b/scripts/ci/test_php_reference_container.py new file mode 100644 index 0000000..2295401 --- /dev/null +++ b/scripts/ci/test_php_reference_container.py @@ -0,0 +1,163 @@ +"""Rust PHP-reference mounts and control-replacement regressions.""" +import hashlib +import json +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[2] +LAUNCHER = ROOT / 'scripts/ci/run-php-reference-container.sh' + + +def fixture(parent, predecessor=False): + root, state = parent/'source', parent/'state' + for name in ('vendor', 'rust/target/debug', 'scripts/ci', 'docker/php-ci'): + (root/name).mkdir(parents=True, exist_ok=True) + for name in ('bin', 'home', 'tmp/dw-parity-fixture', 'composer-home/cache'): + (state/name).mkdir(parents=True, exist_ok=True) + (root/'rust/target/php-vectors.json').write_text('{}') + (root/'rust/target/debug/dw').write_text('synthetic compiled binary') + (root/'docker/php-ci/reference.ini').write_bytes((ROOT/'docker/php-ci/reference.ini').read_bytes()) + launcher = root/'scripts/ci/run-php-reference-container.sh' + source = LAUNCHER.read_text() + if predecessor: + source = source.replace('target=$root,readonly', 'target=$root').replace('target=$state,readonly', 'target=$state') + launcher.write_text(source) + launcher.chmod(0o755) + for name in ('php', 'composer'): + (state/'bin'/name).symlink_to(launcher) + subprocess.run(['git', 'init', '--quiet', str(root)], check=True) + return root, state, launcher + + +class ReferenceLauncherTests(unittest.TestCase): + def invoke(self, arguments=(), mutate=None): + with tempfile.TemporaryDirectory() as directory: + root, state, _ = fixture(Path(directory)) + docker = state/'bin/docker' + docker.write_text('#!/usr/bin/env python3\nimport json,sys\nprint(json.dumps(sys.argv[1:]))\n') + docker.chmod(0o755) + environment = {'PATH':str(state/'bin')+os.pathsep+os.defpath, 'CLI_PHP_CI_ROOT':str(root), + 'CLI_PHP_CI_STATE':str(state), 'CLI_PHP_CI_IMAGE':'sha256:'+'f'*64, + 'COMPOSER_AUTH':'synthetic-secret', 'GITHUB_TOKEN':'synthetic-secret', + 'PHP_INI_SCAN_DIR':'untrusted', 'DW_TOKEN':'synthetic-secret', + 'DURABLE_WORKFLOW_AUTH_TOKEN':'synthetic-secret', + 'DW_CONFIG_HOME':str(state/'tmp/dw-parity-fixture'), 'DW_CLI_VERSION':'synthetic-version'} + if mutate: + mutate(root, state, environment) + args = [str(state/'bin/composer') if x == '{composer}' else x for x in arguments] + result = subprocess.run([str(state/'bin/php'), *args], cwd=root, env=environment, + capture_output=True, text=True, timeout=10) + return result, str(root), str(state) + + def test_only_vector_file_and_private_data_are_writable(self): + result, root, state = self.invoke() + self.assertEqual(0, result.returncode, result.stderr) + options = json.loads(result.stdout) + mounts = [options[i+1] for i, item in enumerate(options[:-1]) if item == '--mount'] + self.assertIn(f'type=bind,source={root},target={root},readonly', mounts) + self.assertIn(f'type=bind,source={state},target={state},readonly', mounts) + for path in (root+'/vendor', root+'/rust/target/php-vectors.json', state+'/home', state+'/tmp', state+'/composer-home'): + self.assertIn(f'type=bind,source={path},target={path}', mounts) + self.assertNotIn(f'type=bind,source={root}/rust/target,target={root}/rust/target', mounts) + self.assertIn(f'type=bind,source={root}/docker/php-ci/reference.ini,target=/usr/local/etc/php/conf.d/zz-cli-reference.ini,readonly', mounts) + environment = [options[i+1] for i, item in enumerate(options[:-1]) if item == '--env'] + for name in ('COMPOSER_AUTH', 'GITHUB_TOKEN', 'PHP_INI_SCAN_DIR', 'DW_TOKEN', 'DURABLE_WORKFLOW_AUTH_TOKEN'): + self.assertNotIn(name, environment) + for name in ('DW_CONFIG_HOME', 'DW_CLI_VERSION'): + self.assertIn(name, environment) + self.assertIn('no-new-privileges', options) + self.assertIn('--cap-drop', options) + + def test_composer_guard_sees_actual_container_phar(self): + result, _, _ = self.invoke(('/guard.php', '{composer}')) + self.assertEqual(0, result.returncode, result.stderr) + self.assertEqual(['php', '/guard.php', '/usr/local/bin/composer'], json.loads(result.stdout)[-3:]) + + def test_mutable_image_and_linked_vector_refuse_before_docker(self): + def image(_root, _state, environment): + environment['CLI_PHP_CI_IMAGE'] = 'mutable:tag' + def linked(root, _state, _environment): + vector = root/'rust/target/php-vectors.json' + vector.unlink() + vector.symlink_to(root/'rust/target/debug/dw') + def hardlinked(root, _state, _environment): + vector = root/'rust/target/php-vectors.json' + vector.unlink() + os.link(root/'rust/target/debug/dw', vector) + def parent_link(root, _state, _environment): + (root/'rust').rename(root/'elsewhere') + (root/'rust').symlink_to(root/'elsewhere') + for mutation in (image, linked, hardlinked, parent_link): + result, _, _ = self.invoke(mutate=mutation) + self.assertNotEqual(0, result.returncode) + self.assertEqual('', result.stdout) + + def test_config_outside_private_parity_tree_or_linked_refuses(self): + def outside(root, _state, environment): + environment['DW_CONFIG_HOME'] = str(root/'vendor') + def linked(root, state, environment): + path = state/'tmp/dw-parity-link' + path.symlink_to(root/'vendor') + environment['DW_CONFIG_HOME'] = str(path) + for mutation in (outside, linked): + result, _, _ = self.invoke(mutate=mutation) + self.assertNotEqual(0, result.returncode) + self.assertEqual('', result.stdout) + + +@unittest.skipUnless(os.environ.get('CLI_PHP_CI_TEST_IMAGE'), 'requires inspected PHP image') +class ReferenceRuntimeBoundaryTests(unittest.TestCase): + def test_predecessor_replaces_controls_but_candidate_preserves_controls_and_writes(self): + for predecessor in (True, False): + for attack in ('launcher', 'wrapper', 'native'): + with self.subTest(predecessor=predecessor, attack=attack), tempfile.TemporaryDirectory( + dir=Path(os.environ['CLI_PHP_CI_STATE'])/'tmp') as directory: + root, state, launcher = fixture(Path(directory), predecessor) + wrapper, native = state/'bin/php', root/'rust/target/debug/dw' + digest = hashlib.sha256(launcher.read_bytes()).hexdigest() + environment = {'PATH':os.defpath, 'CLI_PHP_CI_ROOT':str(root), 'CLI_PHP_CI_STATE':str(state), + 'CLI_PHP_CI_IMAGE':os.environ['CLI_PHP_CI_TEST_IMAGE'], 'TMPDIR':str(state/'tmp'), + 'DW_CONFIG_HOME':str(state/'tmp/dw-parity-fixture')} + code = r''' +[$unused, $attack, $launcher, $wrapper, $native, $root, $state] = $argv; +$payload = "#!/bin/sh\necho CLI_REFERENCE_CONTROL_REPLACED\n"; +if ($attack === 'launcher') { + $changed = @file_put_contents($launcher, $payload) !== false; +} elseif ($attack === 'wrapper') { + $changed = @unlink($wrapper) && @file_put_contents($wrapper, $payload) !== false; + if ($changed) { chmod($wrapper, 0755); } +} else { + $changed = @file_put_contents($native, $payload) !== false; +} +$writable = true; +foreach ([$root.'/vendor/output', $root.'/rust/target/php-vectors.json', + $state.'/tmp/dw-parity-fixture/config.json', $state.'/home/output', + $state.'/tmp/output', $state.'/composer-home/output'] as $path) { + $writable = $writable && file_put_contents($path, '{}') !== false; +} +echo json_encode(['changed'=>$changed, 'writable'=>$writable]); +''' + result = subprocess.run([str(wrapper), '-r', code, '--', attack, str(launcher), str(wrapper), str(native), str(root), str(state)], + cwd=root, env=environment, capture_output=True, text=True, timeout=30) + self.assertEqual(0, result.returncode, result.stderr) + observation = json.loads(result.stdout) + self.assertEqual(predecessor, observation['changed']) + self.assertTrue(observation['writable']) + following = subprocess.run([str(wrapper), '-r', 'echo "CLI_REAL_CONTAINER_PHP";'], cwd=root, + env=environment, capture_output=True, text=True, timeout=30) + self.assertEqual(0, following.returncode, following.stderr) + if predecessor and attack != 'native': + self.assertEqual('CLI_REFERENCE_CONTROL_REPLACED', following.stdout.strip()) + else: + self.assertEqual('CLI_REAL_CONTAINER_PHP', following.stdout) + if not predecessor: + self.assertEqual(digest, hashlib.sha256(launcher.read_bytes()).hexdigest()) + self.assertTrue(wrapper.is_symlink()) + self.assertEqual('synthetic compiled binary', native.read_text()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/fixtures/php-reference-workflow.yml b/tests/fixtures/php-reference-workflow.yml new file mode 100644 index 0000000..a09f38d --- /dev/null +++ b/tests/fixtures/php-reference-workflow.yml @@ -0,0 +1,70 @@ +# Proposed caller. Activate only after exact central source admission. +jobs: + rust: + name: Rust CLI (unit tests and 2.x parity) + needs: [action-policy] + if: ${{ github.server_url == 'https://github.com' }} + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + working-directory: rust + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: '3.13' + - name: Bootstrap verified dependency trust + uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust + with: + clients: pip,python,composer,cargo,curl,git + - name: Install Rust 1.86 + uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable + with: + toolchain: 1.86.0 + components: rustfmt, clippy + - run: cargo fmt --check + - run: cargo clippy --locked --all-targets -- -D warnings + - name: Install completion test shells + # CVE-2026-109518 needs APT to retry a redirected download; hosted + # runner images still ship affected apt (durable-workflow/.github#295). + run: | + echo 'Acquire::Retries "0";' | sudo tee /etc/apt/apt.conf.d/99-no-retries >/dev/null + sudo apt-get update -qq && sudo apt-get install -y -qq --no-install-recommends bash zsh fish + - run: cargo test --locked + env: + DW_REQUIRE_COMPLETION_SHELLS: '1' + - run: cargo build --locked + - name: Check out the accepted PHP container build source + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + repository: durable-workflow/.github + ref: 18783be6bef737cc778e4c7282a8797e39daa61a + path: .central-apt-source + persist-credentials: false + - name: Build and prepare the pinned PHP source-reference profile + run: bash "$GITHUB_WORKSPACE/scripts/ci/setup-php-reference-container.sh" + - name: Compare compatibility checks with the 2.x PHP classes + run: | + php parity/compat-vectors.php ../src ../tests/fixtures src/compat/fixtures target/php-vectors.json + DW_COMPAT_PHP_VECTORS=target/php-vectors.json cargo test --locked -- --ignored + - name: Fetch the pinned 2.x reference CLI + run: parity/fetch-reference.sh + - name: Verify actual Composer dependency trust + uses: durable-workflow/.github/.github/actions/verify-composer-trust@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust + with: + working-directory: ${{ github.workspace }} + - name: Install PHP main for the source reference + # Cases marked "reference": "php-main" compare against this + # checkout's PHP CLI (cli#61 behaviour 2.3.5 lacks). + working-directory: ${{ github.workspace }} + run: composer install --no-dev --prefer-dist --no-interaction + - name: Compare against the 2.x CLI + env: + DW_PARITY_PHP_MAIN: ${{ github.workspace }} + run: | + python3 -m unittest discover -s parity -p 'test_*.py' + python3 parity/parity.py --verbose + From 032400bf1a4ed3457b138f4ccb11d5afcf5c4855 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Sat, 14 Nov 2026 00:40:20 +0000 Subject: [PATCH 2/3] Trim the proposed PHP reference caller fixture --- tests/fixtures/php-reference-workflow.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/fixtures/php-reference-workflow.yml b/tests/fixtures/php-reference-workflow.yml index a09f38d..21babd1 100644 --- a/tests/fixtures/php-reference-workflow.yml +++ b/tests/fixtures/php-reference-workflow.yml @@ -67,4 +67,3 @@ jobs: run: | python3 -m unittest discover -s parity -p 'test_*.py' python3 parity/parity.py --verbose - From 3dbda322d3c07d0ae678988cf83472ede0352d98 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Sat, 14 Nov 2026 00:49:09 +0000 Subject: [PATCH 3/3] Use the admitted PHP profile in Rust source parity --- .github/workflows/build.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 689f6ed..944dd1a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -282,6 +282,7 @@ jobs: rust: name: Rust CLI (unit tests and 2.x parity) + needs: [action-policy] if: ${{ github.server_url == 'https://github.com' }} runs-on: ubuntu-latest timeout-minutes: 20 @@ -316,12 +317,15 @@ jobs: env: DW_REQUIRE_COMPLETION_SHELLS: '1' - run: cargo build --locked - - name: Set up PHP for the 2.x compatibility vectors and PHP main - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 + - name: Check out the accepted PHP container build source + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: - php-version: '8.4' - tools: composer:2.10.3 - coverage: none + repository: durable-workflow/.github + ref: 18783be6bef737cc778e4c7282a8797e39daa61a + path: .central-apt-source + persist-credentials: false + - name: Build and prepare the pinned PHP source-reference profile + run: bash "$GITHUB_WORKSPACE/scripts/ci/setup-php-reference-container.sh" - name: Compare compatibility checks with the 2.x PHP classes run: | php parity/compat-vectors.php ../src ../tests/fixtures src/compat/fixtures target/php-vectors.json