Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions docker/php-ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 4 additions & 0 deletions docker/php-ci/reference.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
date.timezone=UTC
memory_limit=-1
phar.readonly=1
opcache.enable_cli=0
11 changes: 11 additions & 0 deletions rust/parity/test_php_reference_container.py
Original file line number Diff line number Diff line change
@@ -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)
28 changes: 28 additions & 0 deletions scripts/ci/prepare-php-reference-container.sh
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions scripts/ci/probe-php-reference-runtime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

final class ReferenceRuntimeRefused extends RuntimeException {}

if (PHP_VERSION !== '8.4.27' || ini_get('phar.readonly') !== '1'
|| ini_get('memory_limit') !== '-1' || ini_get('opcache.enable_cli') !== '0' || date_default_timezone_get() !== 'UTC'
|| ! in_array('/usr/local/etc/php/conf.d/zz-cli-reference.ini', explode(",\n", trim((string) php_ini_scanned_files())), true)) {
throw new ReferenceRuntimeRefused('The exact PHP source-reference profile must be active.');
}
require_once __DIR__.'/probe-php-test-runtime.php';
echo json_encode(['phar_readonly' => 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;
93 changes: 93 additions & 0 deletions scripts/ci/run-php-reference-container.sh
Original file line number Diff line number Diff line change
@@ -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[@]}"
35 changes: 35 additions & 0 deletions scripts/ci/setup-php-reference-container.sh
Original file line number Diff line number Diff line change
@@ -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}"
Loading
Loading