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
111 changes: 111 additions & 0 deletions qualification/composer-container-trust.json
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,117 @@
"mode": 420
}
}
},
"cli-php-reference": {
"target": "cli",
"workflows": {
"build.yml": [
"rust"
]
},
"commands": [
"bash \"$GITHUB_WORKSPACE/scripts/ci/setup-php-reference-container.sh\""
],
"environment": [
"CLI_PHP_CI_IMAGE",
"CLI_PHP_CI_ROOT",
"CLI_PHP_CI_STATE",
"DW_PARITY_PHP"
],
"absent_files": [
"docker/php-ci/Dockerfile.dockerignore",
"docker/php-ci/cli.Dockerfile.dockerignore"
],
"files": {
".dockerignore": {
"sha256": "19f259d2473129b500ed107780da5b5bc4a7dea47fde4500539bff4b919d4aee",
"mode": 420
},
"docker/php-ci/Dockerfile": {
"sha256": "81d23fbbee7e4ee97ca14df9d17982aa664335b94499c5019662259fe0387eaa",
"mode": 420
},
"docker/php-ci/cli.Dockerfile": {
"sha256": "c2b64bd82040083fbd812a470b266861f04303ea800ba58908f1e4601fe4179f",
"mode": 420
},
"docker/php-ci/verify-php-trust.php": {
"sha256": "529c282e7422202bf25565431a6391de388f86dfe5bfd6c75cf21daa56ffa75e",
"mode": 420
},
"docker/brotli/install-vendor.sh": {
"sha256": "d7eb8b2a3e0af1c3072a56bcb81632b22f061f211d8fe5e57ee901c8bf94c92f",
"mode": 420
},
"docker/brotli/verify-runtime.php": {
"sha256": "580cc465e349f3872eca592355294e7320fe7f1a4560ff5d4c83d5900921c00e",
"mode": 420
},
"docker/brotli/verify-system-brotli.c": {
"sha256": "3353927d206aaf5e63157e46be715c6cbee46bac777e29f8f9e25c4c1f656667",
"mode": 420
},
"docker/brotli/version": {
"sha256": "e064953e075ddb99fb7b48ad29e215f2dfa075812c127eb1d6ab680e9920fba2",
"mode": 420
},
"scripts/ci/php-container-images.json": {
"sha256": "85c87a2dff21225109581876befc00177c62824e44c044765dc921879ca41a4d",
"mode": 420
},
"scripts/ci/setup-php-container.sh": {
"sha256": "a7007c0fbef886b7e29b62d20b76404839746620361a9dd9e6024f1ff546d49b",
"mode": 493
},
"scripts/ci/run-php-container.sh": {
"sha256": "baef1e07ac53ce31fed01eac37ced2f98e363fa677e129952f6e15e47213db0e",
"mode": 493
},
"scripts/ci/probe-php-test-runtime.php": {
"sha256": "a0bbef217128b8ffe81de5c126dfafbfd93ebed28244c2a69204a8f4991be264",
"mode": 420
},
"scripts/ci/test-php-container-tls.py": {
"sha256": "da11950ff46810ba425fea6bb6a453869995204388570d1f764a160154f3e7b0",
"mode": 420
},
"scripts/ci/test-php-container-boundary.py": {
"sha256": "8d4db79b20598e5e9f1fc64f6a730b574d6f2161cc42a01442ac0095dc8684e9",
"mode": 420
},
"docker/php-ci/reference.ini": {
"sha256": "94b40d9558a170b09785010b8016b19c32fadc2c43cd2b7518b2c74aac1d6b55",
"mode": 420
},
"scripts/ci/setup-php-reference-container.sh": {
"sha256": "7daf89ec02bed806218c4c33d8746db921cb5d1a4ad415c03fdc1072c52f0480",
"mode": 493
},
"scripts/ci/prepare-php-reference-container.sh": {
"sha256": "f82fb1434c1c0efbf198474c9174574c0690a946ae19a85925d7109bde1b9c5d",
"mode": 493
},
"scripts/ci/run-php-reference-container.sh": {
"sha256": "b1ab4dc87b55bb6693f3e39dbd1b0ba8152c0f950984d70d62c9ab123693f8dd",
"mode": 493
},
"scripts/ci/probe-php-reference-runtime.php": {
"sha256": "855e21c5d444038f116ea5523494d8b28c2cb59053e932cf1fff2e61c7efd1c2",
"mode": 420
},
"scripts/ci/test_php_reference_container.py": {
"sha256": "a6c2f0c1bd06d386f94876c0c5dcfe1278a1a13281270af7d43223730cc7f99c",
"mode": 420
},
"rust/parity/test_php_reference_container.py": {
"sha256": "71b1e8207bc3e3df28e7b9210ed23226139e6cd896d2baf64592f1b0dc05017c",
"mode": 420
},
"tests/fixtures/php-reference-workflow.yml": {
"sha256": "68131102fff3bb1fda11cc63c58454d1788e3bbe69482231fbad332247f8befe",
"mode": 420
}
}
}
}
}
69 changes: 69 additions & 0 deletions tests/fixtures/cli-php-reference.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# 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
93 changes: 93 additions & 0 deletions tests/test_cli_php_reference_runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""The PHP reference recipe admits one caller without weakening whole-target trust."""
from copy import deepcopy
import hashlib
import json
from pathlib import Path
import tempfile
import unittest
from unittest import mock

import yaml

from scripts.qualification_policy import PolicyError, verify_composer_network_sinks


class CliPhpReferenceRuntimeTests(unittest.TestCase):
def setUp(self):
repository = Path(__file__).resolve().parents[1]
self.policy = json.loads((repository/'qualification/policy.json').read_text())
self.catalog = json.loads((repository/'qualification/composer-container-trust.json').read_text())
self.document = yaml.safe_load((repository/'tests/fixtures/cli-php-reference.yml').read_text())
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name)
self.recipe = self.catalog['runtime_setups']['cli-php-reference']
# Inert bodies test the actual catalog selectors/refusals. Independent
# source review binds the public consumer's real blobs and Git modes.
for name, expected in self.recipe['files'].items():
path = self.root/name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text('# inert reviewed input\n')
path.chmod(expected['mode'])
expected['sha256'] = hashlib.sha256(path.read_bytes()).hexdigest()

def check(self, document=None, filename='build.yml'):
sources = {filename:yaml.safe_dump(self.document if document is None else document)}
with mock.patch('scripts.qualification_policy._composer_container_catalog', return_value=self.catalog):
return verify_composer_network_sinks(self.policy, 'cli', sources, self.root)

def test_exact_proposed_caller_uses_the_registered_setup_and_actual_guard(self):
self.assertEqual('ubuntu-latest', self.document['jobs']['rust']['runs-on'])
self.assertEqual(1, len(self.check()))

def test_wrong_job_workflow_or_unguarded_extra_job_cannot_borrow_admission(self):
moved = deepcopy(self.document)
moved['jobs']['other'] = moved['jobs'].pop('rust')
with self.assertRaises(PolicyError):
self.check(moved)
with self.assertRaises(PolicyError):
self.check(filename='other.yml')
extra = deepcopy(self.document)
extra['jobs']['unguarded'] = {'steps':[{'run':'composer install --no-interaction'}]}
with self.assertRaises(PolicyError):
self.check(extra)

def test_every_bound_file_and_absence_is_required(self):
for name in self.recipe['files']:
with self.subTest(file=name):
path = self.root/name
before = path.read_bytes()
path.write_bytes(before+b'# changed\n')
with self.assertRaises(PolicyError):
self.check()
path.write_bytes(before)
for name in self.recipe['absent_files']:
with self.subTest(absence=name):
path = self.root/name
path.parent.mkdir(parents=True, exist_ok=True)
path.touch()
with self.assertRaises(PolicyError):
self.check()
path.unlink()

def test_setup_overrides_or_all_verifier_deletion_fail_closed(self):
for field, value in (('if','false'), ('continue-on-error',True), ('env',{'CLI_PHP_CI_IMAGE':'unreviewed'})):
altered = deepcopy(self.document)
setup = next(step for step in altered['jobs']['rust']['steps']
if step.get('run') == self.recipe['commands'][0])
setup[field] = value
with self.subTest(field=field), self.assertRaises(PolicyError):
self.check(altered)
altered = deepcopy(self.document)
steps = altered['jobs']['rust']['steps']
steps[:] = [step for step in steps if 'verify-composer-trust@' not in step.get('uses','')]
with self.assertRaises(PolicyError):
self.check(altered)
altered = deepcopy(self.document)
altered['jobs']['rust']['env'] = {'DW_PARITY_PHP':'unreviewed'}
with self.assertRaises(PolicyError):
self.check(altered)


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