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
32 changes: 20 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ jobs:
--health-retries=10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
Expand All @@ -57,13 +59,15 @@ jobs:
- name: Disable APT download retries (CVE-2026-109518)
run: echo 'Acquire::Retries "0";' | sudo tee /etc/apt/apt.conf.d/99-no-retries >/dev/null

- name: Set up PHP
uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # v2.40.0
- name: Check out the accepted PHP container build source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
php-version: ${{ matrix.php }}
extensions: mbstring, intl, pdo_mysql, redis, bcmath, gd, zip
coverage: none
tools: composer:2.10.3
repository: durable-workflow/.github
ref: 18783be6bef737cc778e4c7282a8797e39daa61a
path: .central-apt-source
persist-credentials: false
- name: Build the pinned PHP runtime and install launchers
run: bash "$GITHUB_WORKSPACE/scripts/ci/setup-php-container.sh" "${{ matrix.php }}"

- name: Verify actual Composer trust before dependency requests
uses: durable-workflow/.github/.github/actions/verify-composer-trust@52283e2e8ea9a2cf7e3865ee30f2a72a38b098a2 # clean-trust
Expand Down Expand Up @@ -157,6 +161,8 @@ jobs:
--health-retries=20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
Expand All @@ -169,13 +175,15 @@ jobs:
- name: Disable APT download retries (CVE-2026-109518)
run: echo 'Acquire::Retries "0";' | sudo tee /etc/apt/apt.conf.d/99-no-retries >/dev/null

- name: Set up PHP
uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # v2.40.0
- name: Check out the accepted PHP container build source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
php-version: ${{ matrix.php }}
extensions: mbstring, intl, pdo_mysql, bcmath, gd, zip
coverage: none
tools: composer:2.10.3
repository: durable-workflow/.github
ref: 18783be6bef737cc778e4c7282a8797e39daa61a
path: .central-apt-source
persist-credentials: false
- name: Build the pinned PHP runtime and install launchers
run: bash "$GITHUB_WORKSPACE/scripts/ci/setup-php-container.sh" "${{ matrix.php }}"

- name: Verify actual Composer trust before dependency requests
uses: durable-workflow/.github/.github/actions/verify-composer-trust@52283e2e8ea9a2cf7e3865ee30f2a72a38b098a2 # clean-trust
Expand Down
13 changes: 8 additions & 5 deletions .github/workflows/composer-lock-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,15 @@ jobs:
- name: Disable APT download retries (CVE-2026-109518)
run: echo 'Acquire::Retries "0";' | sudo tee /etc/apt/apt.conf.d/99-no-retries >/dev/null

- name: Set up PHP
uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # v2.40.0
- name: Check out the accepted PHP container build source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
php-version: '8.4'
coverage: none
tools: composer:2.10.3
repository: durable-workflow/.github
ref: 18783be6bef737cc778e4c7282a8797e39daa61a
path: .central-apt-source
persist-credentials: false
- name: Build the pinned PHP runtime and install launchers
run: bash "$GITHUB_WORKSPACE/scripts/ci/setup-php-container.sh" "8.4"

- name: Verify actual Composer trust before advisory requests
uses: durable-workflow/.github/.github/actions/verify-composer-trust@52283e2e8ea9a2cf7e3865ee30f2a72a38b098a2 # clean-trust
Expand Down
16 changes: 9 additions & 7 deletions .github/workflows/playground-journeys.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ jobs:
- name: Disable APT download retries (CVE-2026-109518)
run: echo 'Acquire::Retries "0";' | sudo tee /etc/apt/apt.conf.d/99-no-retries >/dev/null

- name: Set up PHP
uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # v2.40.0
- name: Check out the accepted PHP container build source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
php-version: '8.4'
extensions: mbstring, intl, pdo_mysql, redis, bcmath, gd, zip, curl, pcntl
coverage: none
tools: composer:2.10.3
repository: durable-workflow/.github
ref: 18783be6bef737cc778e4c7282a8797e39daa61a
path: .central-apt-source
persist-credentials: false
- name: Build the pinned PHP runtime and install launchers
run: bash "$GITHUB_WORKSPACE/scripts/ci/setup-php-container.sh" "8.4"

- name: Verify actual Composer trust before dependency requests
uses: durable-workflow/.github/.github/actions/verify-composer-trust@52283e2e8ea9a2cf7e3865ee30f2a72a38b098a2 # clean-trust
Expand Down Expand Up @@ -76,7 +78,7 @@ jobs:

- name: Run each SDK journey against the published native Server
env:
PLAYGROUND_SOURCE_ROOT: ${{ runner.temp }}/playground-source
PLAYGROUND_SOURCE_ROOT: ${{ env.SAMPLE_APP_PHP_CI_STATE }}/source
run: |
mkdir -p "$RUNNER_TEMP/playground-evidence"
for language in php python rust; do
Expand Down
29 changes: 29 additions & 0 deletions docker/php-ci/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
ARG PHP_IMAGE
FROM node:22.23.4-bookworm-slim@sha256:4b150b48b4116e2a22f2c39cc27025fbf4f153e2078fd3b14d159fdb5c7279da AS node

Check warning on line 2 in docker/php-ci/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use either the version tag or the digest for the image instead of both.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaHUOQzFxgIGZ2MAdapA&open=AaHUOQzFxgIGZ2MAdapA&pullRequest=341
FROM composer:2.10.3@sha256:2cf275428757e5435a667bfc0d038b14520ca02be2b1b70d44a05c8c19e2de48 AS composer

Check warning on line 3 in docker/php-ci/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use either the version tag or the digest for the image instead of both.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaHUOQzFxgIGZ2MAdapB&open=AaHUOQzFxgIGZ2MAdapB&pullRequest=341

FROM ${PHP_IMAGE}

COPY .central-apt-source/docker/apt/require-fixed-bookworm-apt.sh /usr/local/share/sample-app/require-fixed-bookworm-apt.sh
COPY --from=composer /usr/bin/composer /usr/local/bin/composer
COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY .devcontainer/docker/verify-php-trust.php /usr/local/share/sample-app/verify-php-trust.php
COPY docker/brotli /usr/local/share/sample-app/brotli
COPY docker/install-phpredis.sh /usr/local/bin/install-phpredis

RUN /bin/sh /usr/local/share/sample-app/require-fixed-bookworm-apt.sh \
&& printf 'Acquire::Retries "0";\n' > /etc/apt/apt.conf.d/99-no-retries \
&& php /usr/local/share/sample-app/verify-php-trust.php /usr/local/bin/composer \
&& sh /usr/local/share/sample-app/brotli/install-vendor.sh /usr/local/share/sample-app/brotli \
git unzip python3 gcc libc6-dev libicu-dev libonig-dev libzip-dev libpng-dev libjpeg62-turbo-dev libfreetype6-dev \

Check warning on line 18 in docker/php-ci/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Line is too long. Split it into multiple lines using backslash continuations.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaHUOQzFxgIGZ2MAdapC&open=AaHUOQzFxgIGZ2MAdapC&pullRequest=341
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j2 bcmath gd intl mbstring pcntl pdo_mysql zip \
&& sh /usr/local/bin/install-phpredis \
&& cc -O2 /usr/local/share/sample-app/brotli/verify-system-brotli.c -lbrotlidec -lbrotlienc -ldl \
-o /usr/local/share/sample-app/brotli/verify-system-brotli \
&& cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \
&& php /usr/local/share/sample-app/verify-php-trust.php /usr/local/bin/composer \
&& php /usr/local/share/sample-app/brotli/verify-runtime.php \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app
4 changes: 4 additions & 0 deletions scripts/ci/php-container-images.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"8.4": "php:8.4.27-cli-bookworm@sha256:b3e907a1108629c562c4bb313ce3449e34d49991f6bb2487913f6146f47200fe",
"8.5": "php:8.5.12-cli-bookworm@sha256:050713ce008e2360ea9dc96e1f9a075690132388d917fc3552806f7a7be6f2a0"
}
67 changes: 67 additions & 0 deletions scripts/ci/run-php-container.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
set -euo pipefail

command="${0##*/}"
case "$command" in php|composer) ;; *) echo 'Invoke this launcher as php or composer.' >&2; exit 2 ;; esac
root="${SAMPLE_APP_PHP_CI_ROOT:?PHP CI repository root is required}"
state="${SAMPLE_APP_PHP_CI_STATE:?PHP CI state directory is required}"
image="${SAMPLE_APP_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 "sample-app-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"
--mount "type=bind,source=$root,target=$root"
--mount "type=bind,source=$state,target=$state")

# Composer's guard is a composite action outside the repository checkout.
if [[ -n "${GITHUB_ACTION_PATH:-}" ]]; then
options+=(--mount "type=bind,source=$GITHUB_ACTION_PATH,target=$GITHUB_ACTION_PATH,readonly")
fi
# A local worktree has its Git metadata outside the source mount.
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

# Pass the CI configuration and the playground's separate synthetic role credentials.
while IFS= read -r variable; do
case "$variable" in

Check failure on line 47 in scripts/ci/run-php-container.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a default case (*) to handle unexpected values.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaHUOQvtxgIGZ2MAdao_&open=AaHUOQvtxgIGZ2MAdao_&pullRequest=341
APP_*|DB_*|SHARED_DB_*|REDIS_*|CACHE_*|QUEUE_*|SESSION_*|MAIL_*|BCRYPT_ROUNDS|\
COMPOSER_HOME|COMPOSER_CACHE_DIR|COMPOSER_MAX_PARALLEL_*|COMPOSER_IPRESOLVE|\
SAMPLE_APP_ROOT|SAMPLE_APP_PLAYGROUND_SCENARIO|SAMPLE_APP_PLAYGROUND_WORKFLOW_ID_PREFIX|\
DURABLE_WORKFLOW_RUNTIME_URL|DURABLE_WORKFLOW_SERVER_URL|DURABLE_WORKFLOW_NAMESPACE|\
DURABLE_WORKFLOW_TASK_QUEUE|DURABLE_WORKFLOW_WORKER_ID|\
DURABLE_WORKFLOW_WORKER_TOKEN|DURABLE_WORKFLOW_CLIENT_TOKEN|\
DW_CI_CA_SHA256|GITHUB_ACTIONS|CI|TMPDIR|TZ)
options+=(--env "$variable") ;;
esac
done < <(compgen -e)

# The host verifier discovers the launcher, but must inspect the real 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[@]}"
48 changes: 48 additions & 0 deletions scripts/ci/setup-php-container.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail

root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
minor="${1:?Usage: setup-php-container.sh 8.4|8.5}"
image="$(python3 - "$root/scripts/ci/php-container-images.json" "$minor" <<'PY'
import json,re,sys
images=json.load(open(sys.argv[1]))
image=images[sys.argv[2]]
assert re.fullmatch(r'php:8\.[45]\.\d+-cli-bookworm@sha256:[0-9a-f]{64}', image)
print(image)
PY
)"
test -f "$root/.central-apt-source/docker/apt/require-fixed-bookworm-apt.sh"

Check failure on line 14 in scripts/ci/setup-php-container.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of 'test' command for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaHUOQrPxgIGZ2MAdao9&open=AaHUOQrPxgIGZ2MAdao9&pullRequest=341
test "$(git -C "$root/.central-apt-source" rev-parse HEAD)" = 18783be6bef737cc778e4c7282a8797e39daa61a

Check failure on line 15 in scripts/ci/setup-php-container.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of 'test' command for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaHUOQrPxgIGZ2MAdao-&open=AaHUOQrPxgIGZ2MAdao-&pullRequest=341
echo '2a36a2417e8947357ea6f6cbc2f6c53a834f3c069f8013a34d6d861b7123d576 '"$root/.central-apt-source/docker/apt/require-fixed-bookworm-apt.sh" | sha256sum --check --strict

runtime="sample-app-php-ci:${minor}-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}"
docker build --file "$root/docker/php-ci/Dockerfile" --build-arg "PHP_IMAGE=$image" --tag "$runtime" "$root"
runtime_id="$(docker image inspect "$runtime" --format '{{.Id}}')"
docker run --rm --init --name "sample-app-php-ci-probe-${minor}-${GITHUB_RUN_ID:-local}" \
--cpus 1 --memory 512m --pids-limit 64 --network none "$runtime_id" \
php -r '
if (PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION !== $argv[1]) { throw new RuntimeException("The PHP matrix minor does not match the image."); }
foreach (["mbstring", "intl", "pdo_mysql", "redis", "bcmath", "gd", "zip", "curl", "pcntl"] as $extension) {
if (!extension_loaded($extension)) { throw new RuntimeException("Missing extension: ".$extension); }
}
if (extension_loaded("xdebug") || extension_loaded("pcov")) { throw new RuntimeException("Coverage must remain none."); }
if (php_ini_loaded_file() !== "/usr/local/etc/php/php.ini") { throw new RuntimeException("Production INI must be loaded."); }
echo json_encode(["php" => PHP_VERSION, "extensions" => get_loaded_extensions(), "ini" => php_ini_loaded_file(), "coverage" => "none"]), PHP_EOL;
' "$minor"
printf 'php_ci_image=%s\n' "$runtime_id"

state="${RUNNER_TEMP:?RUNNER_TEMP is required}/sample-app-php-ci"
mkdir -p "$state/bin" "$state/tmp" "$state/composer-home/cache"
ln -sf "$root/scripts/ci/run-php-container.sh" "$state/bin/php"
ln -sf "$root/scripts/ci/run-php-container.sh" "$state/bin/composer"
SAMPLE_APP_PHP_CI_TEST_IMAGE="$runtime_id" SAMPLE_APP_PHP_CI_ROOT="$root" SAMPLE_APP_PHP_CI_STATE="$state" \
python3 -m unittest discover -s "$root/tests/ci" -p test_php_container_tls.py -v
{
echo "SAMPLE_APP_PHP_CI_IMAGE=$runtime_id"
echo "SAMPLE_APP_PHP_CI_ROOT=$root"
echo "SAMPLE_APP_PHP_CI_STATE=$state"
echo "COMPOSER_HOME=$state/composer-home"
echo "COMPOSER_CACHE_DIR=$state/composer-home/cache"
echo "TMPDIR=$state/tmp"
} >> "${GITHUB_ENV:?GITHUB_ENV is required}"
echo "$state/bin" >> "${GITHUB_PATH:?GITHUB_PATH is required}"
9 changes: 7 additions & 2 deletions tests/Unit/DevcontainerImageContractTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,20 @@ public function test_composer_jobs_check_trust_before_dependency_resolution(): v
$this->assertSame('52283e2e8ea9a2cf7e3865ee30f2a72a38b098a2', explode('@', $step['uses'])[1]);
$this->assertStringContainsString('composer,curl,git', $step['with']['clients']);
}
if (str_starts_with($step['uses'] ?? '', 'shivammathur/setup-php@')) {
if (str_contains($step['run'] ?? '', 'scripts/ci/setup-php-container.sh')) {
$setup = $index;
break;
}
}
$this->assertNotNull($bootstrap);
$this->assertNotNull($setup);
$this->assertLessThan($setup, $bootstrap);
$this->assertSame('composer:2.10.3', $steps[$setup]['with']['tools']);
$this->assertStringContainsString('"${{ matrix.php }}"', $steps[$setup]['run']);
$images = json_decode((string) file_get_contents($this->path('scripts/ci/php-container-images.json')), true, flags: JSON_THROW_ON_ERROR);
$this->assertSame(['8.4', '8.5'], array_keys($images));
foreach ($images as $minor => $image) {
$this->assertMatchesRegularExpression('/^php:'.preg_quote($minor, '/').'\.[0-9]+-cli-bookworm@sha256:[0-9a-f]{64}$/', $image);
}
$this->assertSame(
'durable-workflow/.github/.github/actions/verify-composer-trust@52283e2e8ea9a2cf7e3865ee30f2a72a38b098a2',
$steps[$setup + 1]['uses'],
Expand Down
88 changes: 88 additions & 0 deletions tests/ci/test_php_container_launcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import json
import os
import pathlib
import subprocess
import tempfile
import unittest


ROOT = pathlib.Path(__file__).resolve().parents[2]
LAUNCHER = ROOT / "scripts/ci/run-php-container.sh"


class PhpContainerLauncherTests(unittest.TestCase):
def invoke(self, command="php", arguments=(), **overrides):
with tempfile.TemporaryDirectory() as directory:
state = pathlib.Path(directory)
(state / "bin").mkdir()
(state / "bin" / command).symlink_to(LAUNCHER)
docker = state / "bin/docker"
docker.write_text("#!/usr/bin/env python3\nimport json,sys\nprint(json.dumps(sys.argv[1:]))\n")
docker.chmod(0o755)
bundle = state / "ca.pem"
bundle.write_text("synthetic CA mount, never executed")
environment = {
"PATH": str(state / "bin") + os.pathsep + os.environ["PATH"],
"SAMPLE_APP_PHP_CI_ROOT": str(ROOT),
"SAMPLE_APP_PHP_CI_STATE": str(state),
"SAMPLE_APP_PHP_CI_IMAGE": "sha256:" + "f" * 64,
"SSL_CERT_FILE": str(bundle),
"COMPOSER_CAFILE": str(bundle),
"DW_CI_CA_SHA256": "1" * 64,
"DB_PORT": "12345",
"GITHUB_TOKEN": "must-not-enter-container",
"COMPOSER_AUTH": "must-not-enter-container",
"SAMPLE_APP_PLAYGROUND_SCENARIO": '{"input":["sample"]}',
"SAMPLE_APP_ROOT": str(ROOT),
"DURABLE_WORKFLOW_RUNTIME_URL": "http://127.0.0.1:8080",
"DURABLE_WORKFLOW_WORKER_TOKEN": "synthetic-worker",
**overrides,
}
arguments = [str(state / "bin/composer") if x == "{composer}" else x for x in arguments]
result = subprocess.run([str(state / "bin" / command), *arguments], cwd=ROOT,
env=environment, text=True, capture_output=True, check=True)
return json.loads(result.stdout), str(bundle)

def test_only_selected_application_and_trust_environment_reaches_runtime(self):
invocation, bundle = self.invoke(arguments=("-r", "echo PHP_VERSION;"))
passed = [invocation[i + 1] for i, part in enumerate(invocation[:-1]) if part == "--env"]
self.assertIn("DB_PORT", passed)
self.assertIn("DW_CI_CA_SHA256", passed)
self.assertNotIn("GITHUB_TOKEN", passed)
self.assertNotIn("COMPOSER_AUTH", passed)
self.assertIn("SAMPLE_APP_PLAYGROUND_SCENARIO", passed)
self.assertIn("SAMPLE_APP_ROOT", passed)
self.assertIn("DURABLE_WORKFLOW_RUNTIME_URL", passed)
self.assertIn("DURABLE_WORKFLOW_WORKER_TOKEN", passed)
mounts = [invocation[i + 1] for i, part in enumerate(invocation[:-1]) if part == "--mount"]
self.assertEqual(mounts.count(f"type=bind,source={bundle},target={bundle},readonly"), 1)
self.assertIn("host", invocation)
self.assertEqual(invocation[-3:], ["php", "-r", "echo PHP_VERSION;"])

def test_trust_verifier_inspects_real_composer_phar(self):
invocation, _ = self.invoke(arguments=("/guard/verify.php", "{composer}", str(ROOT)),
GITHUB_ACTION_PATH="/guard")
self.assertEqual(invocation[-4:], ["php", "/guard/verify.php", "/usr/local/bin/composer", str(ROOT)])
self.assertIn("type=bind,source=/guard,target=/guard,readonly", invocation)

def test_composer_uses_the_container_executable(self):
invocation, _ = self.invoke(command="composer", arguments=("audit", "--locked"))
self.assertEqual(invocation[-3:], ["composer", "audit", "--locked"])

def test_mutable_image_tag_refuses_before_docker(self):
with self.assertRaises(subprocess.CalledProcessError) as failure:
self.invoke(SAMPLE_APP_PHP_CI_IMAGE="sample-app-php-ci:mutable")
self.assertIn("inspected immutable image config ID", failure.exception.stderr)

def test_missing_runtime_identity_refuses_before_docker(self):
with tempfile.TemporaryDirectory() as directory:
command = pathlib.Path(directory) / "php"
command.symlink_to(LAUNCHER)
result = subprocess.run([str(command), "--version"], env={"PATH": os.environ["PATH"]},
text=True, capture_output=True)
self.assertNotEqual(result.returncode, 0)
self.assertIn("PHP CI repository root is required", result.stderr)


if __name__ == "__main__":
unittest.main()
Loading
Loading