From 9d44e5002c330d659fe18aa89f5167d2ecb31578 Mon Sep 17 00:00:00 2001 From: "david.yapfest.club" Date: Tue, 8 Sep 2026 06:29:15 -0400 Subject: [PATCH] ci: verify compiler contracts and dependency provenance Add negative-control coverage for metadata, lock stability, immutable fixtures, target coverage, and workflow requirements. Keep generated Python bytecode out of source control. --- .github/workflows/ci.yml | 65 ++++- .gitignore | 2 + scripts/tests/test_verify_msrv.py | 266 +++++++++++++++++++++ scripts/verify_msrv.py | 380 ++++++++++++++++++++++++++++++ 4 files changed, 700 insertions(+), 13 deletions(-) create mode 100644 scripts/tests/test_verify_msrv.py create mode 100644 scripts/verify_msrv.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 966e680..14b6ce5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,10 @@ on: [push, pull_request] jobs: build: runs-on: ${{ matrix.os }} + timeout-minutes: 180 + env: + RUSTUP_TOOLCHAIN: stable + CARGO_TARGET_DIR: target/ci-${{ matrix.os }}-stable strategy: matrix: os: [ubuntu-latest, macos-latest] @@ -11,20 +15,55 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - - run: cargo fmt --check - - run: cargo clippy --workspace -- -D warnings + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: python3 -m pip install PyYAML==6.0.2 + - run: python3 vendor/verify.py --root vendor + - run: mkdir -p target + - run: cargo metadata --locked --format-version 1 > target/msrv-metadata.json + - run: python3 scripts/verify_msrv.py metadata --root . --metadata target/msrv-metadata.json + - run: python3 scripts/verify_msrv.py fixtures --root . + - run: python3 scripts/verify_msrv.py ci --workflow .github/workflows/ci.yml + - run: python3 -m unittest discover -s scripts/tests -p 'test_verify_msrv.py' + - run: python3 vendor/verify.py --root vendor --self-test + - run: cargo fmt --all -- --check + - run: cargo clippy --locked --workspace --all-targets -- -D warnings - uses: taiki-e/install-action@v2 with: tool: cargo-deny - run: cargo deny check - - run: cargo test --workspace - # Determinism contract (docs/04 §4.2) is exercised here on 2 OSes via - # the core soak/hash tests. Benches compile and smoke-run here - # (`--test` = single quick pass, not a measurement). - # Quantitative >10% bench-regression gating lands with the Phase 13 - # perf harness, which commits criterion baselines and compares them; - # until then, timing budgets live next to the code that owns them: - # tick ≤ 8ms p99 (crates/hexwork-core/src/state.rs) and frame budgets - # (crates/hexwork-render/src/lib.rs, docs/05 §5.2). - - run: cargo bench --workspace -- --test - - run: cargo build --release + - run: cargo test --locked --workspace --all-targets + # Bench smoke is a single quick pass, not a measurement or a claim + # that Phase 13 quantitative regression budgets have been certified. + - run: cargo bench --locked --workspace -- --test + - run: cargo build --locked --workspace --release + msrv: + runs-on: ${{ matrix.os }} + timeout-minutes: 180 + env: + RUSTUP_TOOLCHAIN: 1.75.0 + CARGO_TARGET_DIR: target/ci-${{ matrix.os }}-1.75.0 + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.75.0 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: python3 -m pip install PyYAML==6.0.2 + - run: python3 vendor/verify.py --root vendor + - run: mkdir -p target + - run: cargo metadata --locked --format-version 1 > target/msrv-metadata.json + - run: python3 scripts/verify_msrv.py metadata --root . --metadata target/msrv-metadata.json + - run: python3 scripts/verify_msrv.py fixtures --root . + - run: python3 scripts/verify_msrv.py ci --workflow .github/workflows/ci.yml + - run: python3 -m unittest discover -s scripts/tests -p 'test_verify_msrv.py' + - run: python3 vendor/verify.py --root vendor --self-test + - run: cargo check --locked --workspace --all-targets + - run: cargo test --locked --workspace --all-targets + - run: cargo build --locked --workspace --release diff --git a/.gitignore b/.gitignore index bd09dc4..8ac6fe7 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ # Benchmark baselines are local-only until the Phase 13 perf harness # commits canonical ones. criterion/ +# Python verification-tool bytecode is generated, not source. +__pycache__/ diff --git a/scripts/tests/test_verify_msrv.py b/scripts/tests/test_verify_msrv.py new file mode 100644 index 0000000..9e93ccc --- /dev/null +++ b/scripts/tests/test_verify_msrv.py @@ -0,0 +1,266 @@ +"""Isolated acceptance controls; mutations only affect TemporaryDirectory data.""" +import copy +import importlib.util +from pathlib import Path +import shutil +import tempfile +import unittest + +import yaml + +SCRIPT = Path(__file__).resolve().parents[1] / 'verify_msrv.py' +SPEC = importlib.util.spec_from_file_location('verify_msrv', SCRIPT) +v = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(v) +ROOT = SCRIPT.parents[1] + + +class Contracts(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + + def put(self, name, content): + path = self.root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) + return path + + def metadata(self): + self.put('Cargo.toml', '''[workspace] +members = ["crates/*"] +exclude = ["vendor/bincode-2.0.1", "vendor/bincode_derive-2.0.1"] +[workspace.package] +rust-version = "1.75" +[workspace.dependencies] +bincode = { version = "=2.0.1", features = ["derive"] } +[patch.crates-io] +bincode = { path = "vendor/bincode-2.0.1" } +bincode_derive = { path = "vendor/bincode_derive-2.0.1" } +''') + packages = [] + for name in ('hexwork-core', 'hexwork-cli', 'bincode', 'bincode_derive'): + local = name.startswith('hexwork-') + path = self.put(f'crates/{name}/Cargo.toml' if local else f'vendor/{name}-2.0.1/Cargo.toml', + f'[package]\nname = "{name}"\nrust-version.workspace = true\n') + targets = [{'name': name, 'kind': ['lib']}] + if name == 'hexwork-cli': + targets += [{'name': 'hexwork', 'kind': ['bin']}, {'name': 'gate', 'kind': ['test']}, + {'name': 'speed', 'kind': ['bench']}, {'name': 'sample', 'kind': ['example']}, + {'name': 'build-script-build', 'kind': ['custom-build']}] + packages.append({'name': name, 'id': name, 'version': '0.1.0' if local else '2.0.1', + 'manifest_path': str(path), 'source': None, 'rust_version': '1.75', 'targets': targets}) + self.put('Cargo.lock', 'version = 3\n' + ''.join( + f'[[package]]\nname = "{name}"\nversion = "2.0.1"\n' for name in ('bincode', 'bincode_derive'))) + return {'workspace_root': str(self.root), 'workspace_members': ['hexwork-core', 'hexwork-cli'], + 'packages': packages, 'resolve': {'nodes': [ + {'id': 'hexwork-core', 'dependencies': ['bincode']}, + {'id': 'hexwork-cli', 'dependencies': ['hexwork-core']}, + {'id': 'bincode', 'dependencies': ['bincode_derive']}, + {'id': 'bincode_derive', 'dependencies': []}]}} + + def workflow(self): + jobs = {} + for name, channel in [('build', 'stable'), ('msrv', '1.75.0')]: + steps = [{'uses': 'actions/checkout@v4'}, + {'uses': 'dtolnay/rust-toolchain@' + channel, + 'with': {'components': 'rustfmt, clippy'} if name == 'build' else {}}, + {'uses': 'actions/setup-python@v5', 'with': {'python-version': '3.11'}}, + {'run': v.COMMON_RUNS[0]}, {'run': 'mkdir -p target\n' + v.CAPTURE}] + steps += [{'run': c} for c in v.COMMON_RUNS[1:]] + if name == 'build': + steps.append({'uses': 'taiki-e/install-action@v2', 'with': {'tool': 'cargo-deny'}}) + steps += [{'run': c} for c in v.QUALITY[name]] + jobs[name] = {'runs-on': '${{ matrix.os }}', 'timeout-minutes': 180, + 'strategy': {'matrix': {'os': ['ubuntu-latest', 'macos-latest']}}, + 'env': {'RUSTUP_TOOLCHAIN': channel, + 'CARGO_TARGET_DIR': f'target/ci-${{{{ matrix.os }}}}-{channel}'}, 'steps': steps} + return {'name': 'synthetic', 'on': ['push', 'pull_request'], 'jobs': jobs} + + def check_workflow(self, data): + path = self.put('ci.yml', yaml.safe_dump(data, sort_keys=False)) + return v.verify_ci_msrv_contract(path) + + def test_metadata_pristine(self): + self.assertEqual(len(v.verify_msrv_metadata(self.root, self.metadata())), 4) + + def test_metadata_negative_controls(self): + for mutation in ('inheritance', 'integer_inheritance', 'metadata_floor', 'root_floor', 'inactive', 'registry', + 'membership', 'exclusion', 'patch', 'lock_checksum', 'truncated'): + with self.subTest(mutation=mutation): + data = self.metadata() + if mutation == 'inheritance': + self.put('crates/hexwork-cli/Cargo.toml', '[package]\nname="hexwork-cli"\n') + elif mutation == 'integer_inheritance': + self.put('crates/hexwork-cli/Cargo.toml', '[package]\nname="hexwork-cli"\nrust-version.workspace = 1\n') + elif mutation == 'metadata_floor': + data['packages'][0]['rust_version'] = None + elif mutation == 'root_floor': + p = self.root / 'Cargo.toml' + p.write_text(p.read_text().replace('"1.75"', '"1.85"')) + elif mutation == 'inactive': + data['resolve']['nodes'][0]['dependencies'] = [] + elif mutation == 'registry': + data['packages'][2]['source'] = 'registry+https://example.com' + elif mutation == 'membership': + data['workspace_members'].append('bincode') + elif mutation == 'exclusion': + p = self.root / 'Cargo.toml' + p.write_text(p.read_text().replace('exclude = ["vendor/bincode-2.0.1", "vendor/bincode_derive-2.0.1"]', 'exclude = []')) + elif mutation == 'patch': + data['packages'][2]['manifest_path'] = '/cache/bincode/Cargo.toml' + elif mutation == 'lock_checksum': + p = self.root / 'Cargo.lock' + p.write_text(p.read_text() + 'checksum = "bad"\n') + else: + data['workspace_members'].remove('hexwork-core') + with self.assertRaises(v.ContractError): + v.verify_msrv_metadata(self.root, data) + + def test_lock_conversion_and_stability(self): + original = '# generated\nversion = 4\n[[package]]\nname="a"\nversion="1.0.0"\nchecksum="abc"\ndependencies=["b"]\n' + before = self.put('before', original) + after = self.put('after', original.replace('version = 4', 'version = 3')) + v.verify_lock_v3_conversion(before, after) + v.verify_lock_stability(after, after) + for change in ('checksum="xyz"', 'dependencies=["c"]', '# extra comment\n', 'version = 3 '): + with self.subTest(change=change): + text = original.replace('version = 4', 'version = 3') + if change.startswith('checksum'): + text = text.replace('checksum="abc"', change) + elif change.startswith('dependencies'): + text = text.replace('dependencies=["b"]', change) + elif change.startswith('version'): + text = text.replace('version = 3', change) + else: + text += change + after.write_text(text) + with self.assertRaises(v.ContractError): + v.verify_lock_v3_conversion(before, after) + with self.assertRaises(v.ContractError): + v.verify_lock_stability(before, after) + + def test_fixture_pristine_and_each_tampered_copy(self): + destination = self.root / 'crates/hexwork-save/testfixtures' + destination.mkdir(parents=True) + for name in v.FIXTURES: + shutil.copyfile(ROOT / 'crates/hexwork-save/testfixtures' / name, destination / name) + v.verify_fixture_integrity(self.root) + for name in v.FIXTURES: + path = destination / name + original = path.read_bytes() + path.write_bytes(original + b'tampered') + with self.subTest(fixture=name), self.assertRaises(v.ContractError): + v.verify_fixture_integrity(self.root) + path.write_bytes(original) + + def test_partitions_complete_and_ci_exhaustive(self): + data = self.metadata() + plan = v.make_partitions(data) + self.assertEqual(len(plan['commands']), 6) + self.assertEqual(len(v.verify_target_partition_coverage(data, plan)), 6) + v.verify_target_partition_coverage(data, {'version': 1, 'commands': [v.CI_ALL]}) + data['packages'][1]['targets'].append({'name': 'future', 'kind': ['example']}) + with self.assertRaises(v.ContractError): + v.verify_target_partition_coverage(data, plan) + v.verify_target_partition_coverage(data, v.make_partitions(data)) + + def test_partitions_negative_controls(self): + data = self.metadata() + for mutation in ('missing', 'all-targets', 'duplicate', 'unknown', 'unlocked'): + plan = v.make_partitions(data) + if mutation == 'missing': + plan['commands'].pop() + elif mutation == 'all-targets': + plan['commands'][0].remove('--all-targets') + elif mutation == 'duplicate': + plan['commands'].append(plan['commands'][-1]) + elif mutation == 'unknown': + plan['commands'][-1][-1] = 'not-a-target' + else: + plan['commands'][0].remove('--locked') + with self.subTest(mutation=mutation), self.assertRaises(v.ContractError): + v.verify_target_partition_coverage(data, plan) + data['packages'][1]['targets'].append({'name': 'future', 'kind': ['unknown-future-kind']}) + with self.assertRaises(v.ContractError): + v.make_partitions(data) + + def test_ci_pristine(self): + self.check_workflow(self.workflow()) + + def test_ci_malformed_yaml(self): + path = self.put('invalid.yml', 'jobs: [unterminated') + with self.assertRaises(v.ContractError): + v.verify_ci_msrv_contract(path) + + def test_ci_every_required_step_is_required(self): + base = self.workflow() + for name in ('build', 'msrv'): + for index, step in enumerate(base['jobs'][name]['steps']): + changed = copy.deepcopy(base) + del changed['jobs'][name]['steps'][index] + with self.subTest(job=name, step=step), self.assertRaises(v.ContractError): + self.check_workflow(changed) + + def test_ci_checkout_and_action_negative_controls(self): + for mutation in ('remove', 'duplicate', 'late', 'unknown', 'null-action'): + changed = self.workflow() + for name in ('build', 'msrv'): + steps = changed['jobs'][name]['steps'] + checkout = next(i for i, step in enumerate(steps) + if step.get('uses') == 'actions/checkout@v4') + if mutation == 'remove': + del steps[checkout] + elif mutation == 'duplicate': + steps.insert(checkout + 1, {'uses': 'actions/checkout@v4'}) + elif mutation == 'late': + step = steps.pop(checkout) + steps.append(step) + elif mutation == 'null-action': + steps.insert(checkout + 1, {'uses': None}) + else: + steps.insert(checkout + 1, {'uses': 'example/unrecognized-action@v1'}) + with self.subTest(mutation=mutation), self.assertRaises(v.ContractError): + self.check_workflow(changed) + + def test_ci_negative_controls(self): + for mutation in ('locked', 'targets', 'toolchain', 'env', 'budget', 'matrix', 'mask', + 'wrapper', 'continue', 'step_env', 'conditional', 'ignore', 'lint', 'python'): + changed = self.workflow() + job = changed['jobs']['msrv'] + test = next(s for s in job['steps'] if s.get('run') == v.QUALITY['msrv'][1]) + if mutation == 'locked': + test['run'] = test['run'].replace(' --locked', '') + elif mutation == 'targets': + test['run'] = test['run'].replace(' --all-targets', '') + elif mutation == 'toolchain': + job['steps'][1]['uses'] = 'dtolnay/rust-toolchain@stable' + elif mutation == 'env': + job['env']['RUSTUP_TOOLCHAIN'] = 'stable' + elif mutation == 'budget': + job['timeout-minutes'] = 60 + elif mutation == 'matrix': + job['strategy']['matrix']['os'].pop() + elif mutation == 'mask': + test['run'] += ' || true' + elif mutation == 'wrapper': + test['run'] = 'bash -c ' + repr(test['run']) + elif mutation == 'continue': + test['continue-on-error'] = True + elif mutation == 'step_env': + test['env'] = {'RUSTUP_TOOLCHAIN': 'stable'} + elif mutation == 'conditional': + test['if'] = 'false' + elif mutation == 'ignore': + test['run'] += ' --ignore-rust-version' + elif mutation == 'lint': + changed['env'] = {'RUSTFLAGS': '-A warnings'} + else: + job['steps'][2]['with']['python-version'] = '3.10' + with self.subTest(mutation=mutation), self.assertRaises(v.ContractError): + self.check_workflow(changed) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/verify_msrv.py b/scripts/verify_msrv.py new file mode 100644 index 0000000..18b21cd --- /dev/null +++ b/scripts/verify_msrv.py @@ -0,0 +1,380 @@ +#!/usr/bin/env python3 +"""Read-only Rust 1.75 contracts. Python 3.11+, PyYAML==6.0.2 for `ci`. + +Partitions JSON contains a version and commands (argv lists); it is an audit +plan, not an execution log. Compiler/feature/platform gates remain separate. +""" +from __future__ import annotations + +import argparse +from collections import Counter +import hashlib +import json +from pathlib import Path +import re +import shlex +import sys +import tomllib + + +class ContractError(ValueError): + """A named, fail-closed acceptance contract was violated.""" + + +def require(condition, message): + if not condition: + raise ContractError(message) + + +def read_json(path): + return json.loads(Path(path).read_text()) + + +def read_toml(path): + return tomllib.loads(Path(path).read_text()) + + +def minimum(value): + return value in ('1.75', '1.75.0') + + +def verify_msrv_metadata(root, metadata): + root = Path(root).resolve() + manifest = read_toml(root / 'Cargo.toml') + workspace = manifest['workspace'] + require(minimum(workspace['package'].get('rust-version')), 'root minimum must be 1.75') + dep = workspace['dependencies']['bincode'] + require((dep if isinstance(dep, str) else dep.get('version')) == '=2.0.1', + 'workspace bincode must be exactly =2.0.1') + packages = {p['id']: p for p in metadata['packages']} + members = metadata['workspace_members'] + require(members and len(set(members)) == len(members), 'invalid workspace members') + require(Path(metadata['workspace_root']).resolve() == root, 'metadata workspace root mismatch') + reports = [] + for member in members: + package = packages[member] + path = Path(package['manifest_path']).resolve() + require(path.is_relative_to(root), 'member outside root') + declaration = read_toml(path)['package'].get('rust-version') + require(isinstance(declaration, dict) and set(declaration) == {'workspace'} + and declaration['workspace'] is True, f'{package["name"]}: missing explicit minimum inheritance') + require(minimum(package.get('rust_version')), f'{package["name"]}: metadata minimum mismatch') + reports.append(f'{package["name"]}: inherits Rust 1.75') + # Check declared members too, so a truncated JSON workspace cannot hide a member. + declared = set() + excluded = workspace.get('exclude', []) + for pattern in workspace['members']: + for directory in root.glob(pattern): + if directory.is_dir() and (directory / 'Cargo.toml').exists(): + if not any(directory.match(p) for p in excluded): + declared.add((directory / 'Cargo.toml').resolve()) + require(declared == {Path(packages[m]['manifest_path']).resolve() for m in members}, + 'declared and metadata workspace members differ') + resolve = metadata.get('resolve') + require(isinstance(resolve, dict), 'full resolved metadata required (not --no-deps)') + nodes = {n['id']: n for n in resolve['nodes']} + reachable = set() + pending = list(members) + while pending: + node_id = pending.pop() + if node_id in reachable: + continue + require(node_id in nodes, 'missing resolve node') + reachable.add(node_id) + pending.extend(nodes[node_id].get('dependencies', [])) + lock = read_toml(root / 'Cargo.lock') + require(lock.get('version') == 3, 'Cargo.lock must be format 3') + for name in ('bincode', 'bincode_derive'): + relative = f'vendor/{name}-2.0.1' + patch = manifest.get('patch', {}).get('crates-io', {}).get(name, {}) + require(patch == {'path': relative}, f'{name}: exact relative vendor patch required') + require(relative in excluded, f'{name}: explicit workspace exclusion required') + candidates = [p for p in packages.values() if p['name'] == name] + require(len(candidates) == 1, f'{name}: missing or ambiguous resolved package') + package = candidates[0] + require(package['version'] == '2.0.1' and package.get('source') is None, + f'{name}: registry/version substitute') + expected = root / relative / 'Cargo.toml' + require(not (root / relative).is_symlink() and expected.resolve().is_relative_to(root / 'vendor'), + f'{name}: vendor path escapes repository') + require(Path(package['manifest_path']).resolve() == expected.resolve(), f'{name}: wrong resolved path') + require(package['id'] not in members and package['id'] in reachable, + f'{name}: inactive patch or vendor workspace member') + entries = [p for p in lock['package'] if p['name'] == name] + require(len(entries) == 1 and entries[0]['version'] == '2.0.1' + and 'source' not in entries[0] and 'checksum' not in entries[0], + f'{name}: lock does not activate local package') + reports.append(f'{name}: resolved local 2.0.1 patch (not a compilation claim)') + return reports + + +def verify_lock_v3_conversion(before, after): + old, new = Path(before).read_bytes(), Path(after).read_bytes() + a, b = tomllib.loads(old.decode()), tomllib.loads(new.decode()) + require(a.get('version') == 4 and b.get('version') == 3, 'expected top-level 4 to 3 conversion') + a['version'] = 3 + require(a == b, 'lock package/source/checksum/dependency metadata changed') + match = re.search(rb'(?m)^version[ \t]*=[ \t]*(4)(?:[ \t]*(?:#[^\r\n]*)?\r?$)', old) + require(match is not None and old[:match.start(1)] + b'3' + old[match.end(1):] == new, + 'lock conversion must change only the format integer byte') + for package in b.get('package', []): + source = package.get('source', '') + require(not source or source == 'registry+https://github.com/rust-lang/crates.io-index', + 'nonstandard/git source requires format-semantics review') + + +def verify_lock_stability(before, after): + require(Path(before).read_bytes() == Path(after).read_bytes(), 'locked operation rewrote Cargo.lock') + + +# Frozen from e4cfd4c: crates/hexwork-cli/tests/phase12/v4_compat.rs. +FIXTURES = { + 'run-v4-7834d52-classic.hexrun': '467e582827b6f09a05aabaee60ba5bd4e85c373e5345a6a7aeee56dbf5c340c3', + 'replay-v4-7834d52-classic.hexdemo': 'af28de1eaebcd9b66ec0bb72596f8bca22e2c5726219066e9e8b8d43ba997115', + 'run-v4-7834d52-story.hexrun': '1dc214719fecca479a4e868d0da1ea04b974a297dec1370505d1126263fd5af5', +} + + +def verify_fixture_integrity(root): + for name, expected in FIXTURES.items(): + path = Path(root) / 'crates/hexwork-save/testfixtures' / name + require(hashlib.sha256(path.read_bytes()).hexdigest() == expected, f'fixture SHA256 mismatch: {name}') + + +LIB_KINDS = {'lib', 'rlib', 'dylib', 'cdylib', 'staticlib', 'proc-macro'} + + +def target_inventory(metadata): + packages = {p['id']: p for p in metadata['packages']} + inventory = {} + cli_ids = [] + for member in metadata['workspace_members']: + package = packages[member] + if package['name'] == 'hexwork-cli': + cli_ids.append(member) + for target in package['targets']: + kinds = tuple(sorted(target['kind'])) + key = (member, target['name'], kinds) + require(key not in inventory, 'duplicate metadata target') + if set(kinds) <= LIB_KINDS and kinds: + selector = ['--lib'] + elif len(kinds) == 1 and kinds[0] in ('bin', 'test', 'bench', 'example'): + selector = ['--' + kinds[0], target['name']] + elif kinds == ('custom-build',): + selector = None # Cargo builds this implicitly with package targets. + else: + raise ContractError(f'unsupported metadata target kind: {kinds}') + inventory[key] = (package['name'], selector) + require(len(cli_ids) == 1, 'exactly one hexwork-cli workspace member required') + return inventory + + +NON_CLI = ['cargo', 'test', '--locked', '--workspace', '--exclude', 'hexwork-cli', '--all-targets'] +CLI_BASE = ['cargo', 'test', '--locked', '-p', 'hexwork-cli'] +CI_ALL = ['cargo', 'test', '--locked', '--workspace', '--all-targets'] + + +def make_partitions(metadata): + inventory = target_inventory(metadata) + commands = [NON_CLI.copy()] + for _, (name, selector) in sorted(inventory.items()): + if name == 'hexwork-cli' and selector is not None: + commands.append(CLI_BASE + selector) + plan = {'version': 1, 'commands': commands} + verify_target_partition_coverage(metadata, plan) + return plan + + +def verify_target_partition_coverage(metadata, plan): + inventory = target_inventory(metadata) + require(set(plan) == {'version', 'commands'} and plan['version'] == 1, 'invalid partition schema') + commands = plan['commands'] + require(isinstance(commands, list) and commands, 'empty partition plan') + coverage = Counter() + explicit = {key for key, (_, selector) in inventory.items() if selector is not None} + for argv in commands: + require(isinstance(argv, list) and all(isinstance(x, str) for x in argv), 'commands must be argv arrays') + if argv == CI_ALL: + selected = explicit + elif argv == NON_CLI: + selected = {key for key in explicit if inventory[key][0] != 'hexwork-cli'} + else: + selected = {key for key in explicit if inventory[key][0] == 'hexwork-cli' + and argv == CLI_BASE + inventory[key][1]} + require(len(selected) == 1, f'unsupported or ambiguous selector: {argv}') + coverage.update(selected) + require(set(coverage) == explicit and all(count == 1 for count in coverage.values()), + 'missing or duplicate target coverage') + require(len({tuple(c) for c in commands}) == len(commands), 'duplicate partition command') + return [f'{key[0]}::{key[1]} [{",".join(key[2])}]' for key in sorted(explicit)] + + +COMMON_RUNS = [ + 'python3 -m pip install PyYAML==6.0.2', + 'python3 vendor/verify.py --root vendor', + 'python3 scripts/verify_msrv.py metadata --root . --metadata target/msrv-metadata.json', + 'python3 scripts/verify_msrv.py fixtures --root .', + 'python3 scripts/verify_msrv.py ci --workflow .github/workflows/ci.yml', + "python3 -m unittest discover -s scripts/tests -p 'test_verify_msrv.py'", + 'python3 vendor/verify.py --root vendor --self-test', +] +QUALITY = { + 'build': ['cargo fmt --all -- --check', 'cargo clippy --locked --workspace --all-targets -- -D warnings', + 'cargo deny check', 'cargo test --locked --workspace --all-targets', + 'cargo bench --locked --workspace -- --test', 'cargo build --locked --workspace --release'], + 'msrv': ['cargo check --locked --workspace --all-targets', 'cargo test --locked --workspace --all-targets', + 'cargo build --locked --workspace --release'], +} +CAPTURE = 'cargo metadata --locked --format-version 1 > target/msrv-metadata.json' + + +def verify_ci_msrv_contract(workflow): + import yaml + require(yaml.__version__ == '6.0.2', 'verification requires PyYAML==6.0.2') + try: + data = yaml.safe_load(Path(workflow).read_text()) + except yaml.YAMLError as error: + raise ContractError(f'invalid workflow YAML: {error}') from error + require(isinstance(data, dict), 'workflow must be a mapping') + # Reject bypasses anywhere, including hidden shell/env settings and extra jobs. + text = json.dumps(data) + for token in ('--ignore-rust-version', 'RUSTFLAGS', 'CARGO_ENCODED_RUSTFLAGS', 'continue-on-error'): + require(token not in text, f'forbidden workflow bypass: {token}') + require('env' not in data and 'defaults' not in data, 'global env/shell overrides are not permitted') + for job_name, toolchain in [('build', 'stable'), ('msrv', '1.75.0')]: + job = data.get('jobs', {}).get(job_name) + require(isinstance(job, dict), f'missing {job_name} job') + require('if' not in job and 'defaults' not in job, f'{job_name}: conditional/shell override') + require(job.get('timeout-minutes') == 180, f'{job_name}: expected 180-minute budget') + require(job.get('runs-on') == '${{ matrix.os }}', f'{job_name}: wrong runner') + strategy = job.get('strategy', {}) + require(strategy.get('matrix') == {'os': ['ubuntu-latest', 'macos-latest']}, f'{job_name}: wrong OS matrix') + require(job.get('env') == {'RUSTUP_TOOLCHAIN': toolchain, + 'CARGO_TARGET_DIR': f'target/ci-${{{{ matrix.os }}}}-{toolchain}'}, f'{job_name}: wrong job toolchain/target env') + steps = job.get('steps', []) + require(isinstance(steps, list), 'steps must be a list') + runs = [] + checkout_actions = [] + rust_actions = [] + python_actions = [] + deny_installed = False + for index, step in enumerate(steps): + require(isinstance(step, dict), 'step must be a mapping') + require(not any(k in step for k in ('if', 'env', 'shell', 'working-directory', 'timeout-minutes')), + f'{job_name}: step can bypass frozen execution') + uses = step.get('uses') + require(('uses' in step) ^ ('run' in step), + f'{job_name}: each step must be exactly one action or run command') + if 'uses' in step: + require('run' not in step, f'{job_name}: action step cannot also run a command') + if uses == 'actions/checkout@v4': + require(set(step) == {'uses'}, f'{job_name}: checkout must use default action settings') + checkout_actions.append(index) + elif isinstance(uses, str) and uses.startswith('dtolnay/rust-toolchain@'): + require(set(step) <= {'uses', 'with'}, f'{job_name}: invalid rust-toolchain action shape') + with_args = step.get('with', {}) + require(isinstance(with_args, dict) and set(with_args) <= {'toolchain', 'components'}, + f'{job_name}: invalid rust-toolchain action options') + rust_actions.append(step) + elif uses == 'actions/setup-python@v5': + require(set(step) == {'uses', 'with'} and isinstance(step.get('with'), dict) + and set(step['with']) == {'python-version'}, + f'{job_name}: invalid setup-python action shape') + python_actions.append(step) + elif uses == 'taiki-e/install-action@v2': + require(set(step) == {'uses', 'with'} and step.get('with') == {'tool': 'cargo-deny'}, + f'{job_name}: invalid cargo-deny action shape') + deny_installed = True + else: + raise ContractError(f'{job_name}: unrecognized action: {uses}') + if 'run' in step: + command = step['run'].strip() + require(command and not any(s in command for s in ('||', '&&', ';', '`', '$(', '\\\n')), + f'{job_name}: wrapper/failure-masking shell') + runs.append((index, command)) + require(len(checkout_actions) == 1, f'{job_name}: expected exactly one actions/checkout@v4') + require(all(checkout_actions[0] < index for index, _ in runs), + f'{job_name}: checkout must precede every run step') + require(len(rust_actions) == 1, f'{job_name}: expected one toolchain action') + action = rust_actions[0] + installed = action.get('with', {}).get('toolchain', action['uses'].split('@', 1)[1]) + require(installed == toolchain, f'{job_name}: wrong installed toolchain') + if job_name == 'build': + components = action.get('with', {}).get('components', '') + require(set(x.strip() for x in components.split(',')) >= {'rustfmt', 'clippy'}, 'stable lint components missing') + require(deny_installed, 'cargo-deny installation missing') + require(len(python_actions) == 1 and str(python_actions[0].get('with', {}).get('python-version')) == '3.11', + f'{job_name}: Python 3.11 setup missing') + required = COMMON_RUNS + QUALITY[job_name] + positions = {} + for expected in required: + matches = [i for i, command in runs if '\n' not in command and shlex.split(command) == shlex.split(expected)] + require(len(matches) == 1, f'{job_name}: missing/duplicate exact step: {expected}') + positions[expected] = matches[0] + captures = [i for i, command in runs if command == CAPTURE or command == 'mkdir -p target\n' + CAPTURE] + require(len(captures) == 1, f'{job_name}: exact metadata capture required') + capture = captures[0] + require(any(i <= capture and (command == 'mkdir -p target' or command == 'mkdir -p target\n' + CAPTURE) + for i, command in runs), 'target directory creation missing') + require(capture < positions[COMMON_RUNS[2]], 'metadata verification must follow capture') + setup = steps.index(python_actions[0]) + require(setup < positions[COMMON_RUNS[0]] < min(positions[c] for c in COMMON_RUNS[1:]), 'Python prerequisite ordering') + require(steps.index(action) < capture and all(steps.index(action) < positions[c] for c in QUALITY[job_name]), + 'Rust setup must precede Cargo commands') + allowed = required + [CAPTURE, 'mkdir -p target', 'mkdir -p target\n' + CAPTURE] + for _, command in runs: + require(any(command == a or ('\n' not in command and '\n' not in a and shlex.split(command) == shlex.split(a)) + for a in allowed), f'{job_name}: unsupported extra shell step: {command}') + return ['build: stable exhaustive all-target CI', 'msrv: 1.75.0 exhaustive all-target CI'] + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + subs = parser.add_subparsers(dest='command', required=True) + for name in ('metadata', 'fixtures'): + sub = subs.add_parser(name) + sub.add_argument('--root', required=True, type=Path) + if name == 'metadata': + sub.add_argument('--metadata', required=True, type=Path) + for name in ('lock-conversion', 'lock-stable'): + sub = subs.add_parser(name) + sub.add_argument('--before', required=True, type=Path) + sub.add_argument('--after', required=True, type=Path) + for name, flag in [('partitions', '--output'), ('verify-partitions', '--plan')]: + sub = subs.add_parser(name) + sub.add_argument('--metadata', required=True, type=Path) + sub.add_argument(flag, required=True, type=Path) + subs.add_parser('ci').add_argument('--workflow', required=True, type=Path) + args = parser.parse_args(argv) + names = {'metadata': 'verify_msrv_metadata', 'fixtures': 'verify_fixture_integrity', + 'lock-conversion': 'verify_lock_v3_conversion', 'lock-stable': 'verify_lock_stability', + 'partitions': 'verify_target_partition_coverage', 'verify-partitions': 'verify_target_partition_coverage', + 'ci': 'verify_ci_msrv_contract'} + check = names[args.command] + try: + if args.command == 'metadata': + result = verify_msrv_metadata(args.root, read_json(args.metadata)) + elif args.command == 'fixtures': + result = verify_fixture_integrity(args.root) + elif args.command in ('lock-conversion', 'lock-stable'): + result = globals()[check](args.before, args.after) + elif args.command == 'partitions': + plan = make_partitions(read_json(args.metadata)) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(plan, indent=2) + '\n') + result = [f'wrote {len(plan["commands"])} audited command partitions: {args.output}'] + elif args.command == 'verify-partitions': + result = verify_target_partition_coverage(read_json(args.metadata), read_json(args.plan)) + else: + result = verify_ci_msrv_contract(args.workflow) + print(f'PASS {check}') + for line in result or []: + print(line) + return 0 + except (ContractError, OSError, ValueError, KeyError, TypeError, ImportError) as error: + print(f'FAIL {check}: {error}', file=sys.stderr) + return 1 + + +if __name__ == '__main__': + sys.exit(main()) -- 2.54.0