From fb50dfd79ead1b79379e3850c60d9ef5959375fa Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Tue, 1 Sep 2026 17:36:33 -0400 Subject: [PATCH 01/18] ci: rebuild delivery around bounded Cargo Rail work Plan each revision once and execute only selected feature, target, assurance, and policy work through repository-owned commands. Collapse qualification and release into exact-commit DAGs with isolated cache and publication authority. --- .config/ci-plan-variants.json | 234 ----- .config/ci-tool-archives.tsv | 1 - .config/feature-matrix.json | 264 +++++ .config/fuzz-matrix.json | 592 +++++++++++ .config/miri-matrix.json | 35 + .config/rail.toml | 209 +++- .config/target-matrix.json | 527 +++++++++- .github/actionlint.yaml | 10 + .github/actions/plan/action.yaml | 30 + .github/actions/rust/action.yaml | 99 ++ .github/actions/setup-toolchain/action.yaml | 45 - .github/actions/setup/action.yaml | 129 --- .../release-immutability.json | 3 - .github/rulesets/protect-main.json | 46 - .github/rulesets/protect-release-tags.json | 20 - .github/runs-on.yml | 14 +- .github/workflows/_ci-suite.yaml | 81 -- .github/workflows/_rust-job.yaml | 246 ----- .github/workflows/bench.yaml | 100 +- .github/workflows/ci.yaml | 486 +++++++--- .github/workflows/ct.yaml | 257 ++--- .github/workflows/qualification.yaml | 480 +++++++++ .github/workflows/release.yaml | 531 +++------- .github/workflows/riscv.yaml | 147 --- .github/workflows/rsa.yaml | 131 --- .github/workflows/scorecard.yaml | 1 + .github/workflows/weekly.yaml | 474 --------- docs/constant-time.md | 7 + docs/features.md | 9 +- docs/platforms.md | 47 +- docs/release.md | 148 +-- docs/secret-lifecycle.md | 3 + docs/test-vector-coverage.md | 2 +- justfile | 92 +- scripts/README.md | 346 +++---- scripts/check/affected.sh | 63 ++ scripts/check/check-all.sh | 177 +--- scripts/check/check-feature-matrix.sh | 40 - scripts/check/check-win.sh | 111 --- scripts/check/check-zig.sh | 96 -- scripts/check/check.sh | 200 ++-- scripts/check/feature-contracts.sh | 614 ++++++++++++ scripts/check/policy.sh | 118 +++ scripts/check/zig-cc.sh | 4 +- scripts/ci/actions-policy.sh | 40 + scripts/ci/activate-plan.sh | 44 + scripts/ci/assurance-planning-test.sh | 355 +++++++ scripts/ci/capture-cache-status-test.sh | 159 --- scripts/ci/capture-cache-status.sh | 41 - scripts/ci/changed-test-planning-test.sh | 38 +- scripts/ci/check-action-pins-test.sh | 15 +- scripts/ci/check-action-pins.sh | 45 +- scripts/ci/check-ci-ownership-test.sh | 556 ----------- scripts/ci/check-ci-ownership.sh | 918 ------------------ scripts/ci/check-worktree-test.sh | 52 +- scripts/ci/ci-check.sh | 32 - scripts/ci/cross-targets.sh | 58 +- scripts/ci/dependabot-smoke-test.sh | 34 - scripts/ci/dependabot-smoke.sh | 135 --- scripts/ci/emit-manual-matrix-test.sh | 11 +- scripts/ci/emit-manual-matrix.sh | 57 +- scripts/ci/feature-contracts-test.sh | 213 ++++ scripts/ci/feature-planning-test.sh | 88 ++ scripts/ci/install-actionlint.sh | 29 + scripts/ci/install-codecov.sh | 28 - scripts/ci/install-tools.sh | 130 +-- scripts/ci/materialize-rail-plan-test.sh | 76 -- scripts/ci/materialize-rail-plan.sh | 44 - scripts/ci/native-check.sh | 46 - scripts/ci/native-platform.sh | 116 +++ scripts/ci/package-release-ct-evidence.sh | 28 +- scripts/ci/publish-immutable-release-test.sh | 13 +- scripts/ci/publish-immutable-release.sh | 32 +- scripts/ci/release-ct-recovery-check-test.sh | 128 --- scripts/ci/release-ct-recovery-check.sh | 98 -- scripts/ci/release-evidence-check-test.sh | 320 ------ scripts/ci/release-evidence-check.sh | 161 --- scripts/ci/release-identity-test.sh | 165 +--- scripts/ci/release-preflight.sh | 7 +- scripts/ci/release-recipes-test.sh | 58 -- scripts/ci/remote-cache-recipes-test.sh | 24 +- .../ci/repository-controls-evidence-test.sh | 209 ---- scripts/ci/repository-controls-evidence.sh | 325 ------- scripts/ci/require-work.sh | 37 + scripts/ci/run-rust-job-test.sh | 318 ------ scripts/ci/run-rust-job.sh | 496 ---------- scripts/ci/target-contracts.sh | 91 ++ scripts/ci/tool-integrity-test.sh | 693 ------------- scripts/ci/write-release-manifest.sh | 238 ----- scripts/ct/ci.sh | 68 ++ scripts/ct/structural.sh | 25 + scripts/ct/validate.py | 7 +- scripts/ct/validate_release_evidence.py | 2 +- scripts/lib/ci-tool-integrity.sh | 13 +- scripts/lib/common.sh | 23 +- scripts/lib/feature-profiles.sh | 77 +- scripts/lib/rail-plan.sh | 211 +++- scripts/lib/target-matrix.sh | 47 +- scripts/lib/targets.sh | 15 +- scripts/test/fuzz-contracts.sh | 157 +++ scripts/test/miri-contracts.sh | 119 +++ scripts/test/test-feature-matrix.sh | 69 -- scripts/test/test-fuzz-asan.sh | 1 + scripts/test/test-fuzz-scheduler-test.sh | 37 + scripts/test/test-fuzz.sh | 110 ++- scripts/test/test-rsa-linux-asm.sh | 38 + scripts/test/test.sh | 6 +- src/platform/target_matrix.rs | 7 +- 108 files changed, 6128 insertions(+), 8974 deletions(-) delete mode 100644 .config/ci-plan-variants.json create mode 100644 .config/feature-matrix.json create mode 100644 .config/fuzz-matrix.json create mode 100644 .config/miri-matrix.json create mode 100644 .github/actions/plan/action.yaml create mode 100644 .github/actions/rust/action.yaml delete mode 100644 .github/actions/setup-toolchain/action.yaml delete mode 100644 .github/actions/setup/action.yaml delete mode 100644 .github/repository-settings/release-immutability.json delete mode 100644 .github/rulesets/protect-main.json delete mode 100644 .github/rulesets/protect-release-tags.json delete mode 100644 .github/workflows/_ci-suite.yaml delete mode 100644 .github/workflows/_rust-job.yaml create mode 100644 .github/workflows/qualification.yaml delete mode 100644 .github/workflows/riscv.yaml delete mode 100644 .github/workflows/rsa.yaml delete mode 100644 .github/workflows/weekly.yaml create mode 100755 scripts/check/affected.sh delete mode 100755 scripts/check/check-feature-matrix.sh delete mode 100755 scripts/check/check-win.sh delete mode 100755 scripts/check/check-zig.sh create mode 100755 scripts/check/feature-contracts.sh create mode 100755 scripts/check/policy.sh create mode 100755 scripts/ci/actions-policy.sh create mode 100755 scripts/ci/activate-plan.sh create mode 100755 scripts/ci/assurance-planning-test.sh delete mode 100755 scripts/ci/capture-cache-status-test.sh delete mode 100755 scripts/ci/capture-cache-status.sh delete mode 100755 scripts/ci/check-ci-ownership-test.sh delete mode 100755 scripts/ci/check-ci-ownership.sh delete mode 100755 scripts/ci/ci-check.sh delete mode 100755 scripts/ci/dependabot-smoke-test.sh delete mode 100755 scripts/ci/dependabot-smoke.sh create mode 100755 scripts/ci/feature-contracts-test.sh create mode 100755 scripts/ci/feature-planning-test.sh create mode 100755 scripts/ci/install-actionlint.sh delete mode 100755 scripts/ci/install-codecov.sh delete mode 100755 scripts/ci/materialize-rail-plan-test.sh delete mode 100755 scripts/ci/materialize-rail-plan.sh delete mode 100755 scripts/ci/native-check.sh create mode 100755 scripts/ci/native-platform.sh delete mode 100755 scripts/ci/release-ct-recovery-check-test.sh delete mode 100755 scripts/ci/release-ct-recovery-check.sh delete mode 100755 scripts/ci/release-evidence-check-test.sh delete mode 100755 scripts/ci/release-evidence-check.sh delete mode 100755 scripts/ci/release-recipes-test.sh delete mode 100755 scripts/ci/repository-controls-evidence-test.sh delete mode 100755 scripts/ci/repository-controls-evidence.sh create mode 100755 scripts/ci/require-work.sh delete mode 100755 scripts/ci/run-rust-job-test.sh delete mode 100755 scripts/ci/run-rust-job.sh create mode 100755 scripts/ci/target-contracts.sh delete mode 100755 scripts/ci/tool-integrity-test.sh delete mode 100755 scripts/ci/write-release-manifest.sh create mode 100755 scripts/ct/ci.sh create mode 100755 scripts/ct/structural.sh create mode 100755 scripts/test/fuzz-contracts.sh create mode 100755 scripts/test/miri-contracts.sh delete mode 100755 scripts/test/test-feature-matrix.sh create mode 100755 scripts/test/test-rsa-linux-asm.sh diff --git a/.config/ci-plan-variants.json b/.config/ci-plan-variants.json deleted file mode 100644 index 4e2420bd..00000000 --- a/.config/ci-plan-variants.json +++ /dev/null @@ -1,234 +0,0 @@ -{ - "variant_catalog_version": 1, - "work": "ci-suite", - "variants": [ - { - "id": "quality", - "dimensions": { - "display_name": "Quality", - "operation": "quality", - "runner_type": "runson", - "runner": "linux-x64-ci", - "timeout_minutes": 30, - "tools_mode": "quality", - "toolchain_contract": "development", - "toolchain_components": "clippy, rustfmt, rust-src" - }, - "paths": ["scripts/check/**", "scripts/ci/*-test.sh", "scripts/ci/ci-check.sh", "scripts/ci/check-action-pins.sh", "scripts/ct/**", "testdata/**"], - "config": [], - "cargo": ["cargo.clippy", "cargo.doc"] - }, - { - "id": "examples", - "dimensions": { - "display_name": "Examples", - "operation": "examples", - "runner_type": "runson", - "runner": "linux-x64-ci", - "timeout_minutes": 30, - "tools_mode": "minimal", - "toolchain_contract": "development", - "toolchain_components": "" - }, - "paths": ["examples/**", "scripts/test/test-examples.sh"], - "config": [], - "cargo": ["cargo.build", "cargo.test"] - }, - { - "id": "msrv", - "dimensions": { - "display_name": "MSRV", - "operation": "msrv", - "runner_type": "runson", - "runner": "linux-x64-ci", - "timeout_minutes": 30, - "tools_mode": "none", - "toolchain_contract": "msrv", - "toolchain_components": "" - }, - "paths": ["Cargo.toml", "Cargo.lock", "rust-toolchain.toml", ".config/toolchains.toml"], - "config": [], - "cargo": ["cargo.build"] - }, - { - "id": "cargo-graph", - "dimensions": { - "display_name": "Cargo Graph Assurance", - "operation": "cargo-graph", - "runner_type": "runson", - "runner": "linux-x64-ci", - "timeout_minutes": 90, - "tools_mode": "rail", - "toolchain_contract": "development", - "toolchain_components": "rust-src" - }, - "paths": [".config/rail.toml", "Cargo.toml", "Cargo.lock", "build.rs", "rust-toolchain.toml"], - "config": ["targets", "unify.include_renamed", "unify.strict_version_compat", "unify.exact_pin_handling", "unify.major_version_conflict", "unify.skip_undeclared_patterns", "unify.consumer_scope", "unify.msrv_policy.mode"], - "cargo": ["cargo.build", "cargo.test"] - }, - { - "id": "feature-contracts", - "dimensions": { - "display_name": "Feature Contracts", - "operation": "feature-contracts", - "runner_type": "runson", - "runner": "linux-x64-ci", - "timeout_minutes": 120, - "tools_mode": "minimal", - "toolchain_contract": "development", - "toolchain_components": "rust-src" - }, - "paths": ["scripts/check/check-feature-matrix.sh", "scripts/test/test-feature-matrix.sh", "scripts/lib/feature-profiles.sh"], - "config": [], - "cargo": ["cargo.build", "cargo.test"] - }, - { - "id": "native-linux-aarch64", - "dimensions": { - "display_name": "CI (aarch64-unknown-linux-gnu)", - "operation": "native", - "runner_type": "runson", - "runner": "linux-arm64-ci", - "target": "aarch64-unknown-linux-gnu", - "timeout_minutes": 180, - "tools_mode": "ci", - "toolchain_contract": "development", - "toolchain_components": "clippy, rustfmt, rust-src" - }, - "paths": ["scripts/ci/native-check.sh", "scripts/test/**"], - "config": ["targets"], - "cargo": ["cargo.clippy", "cargo.test"] - }, - { - "id": "native-linux-x86-64", - "dimensions": { - "display_name": "CI (x86_64-unknown-linux-gnu)", - "operation": "native", - "runner_type": "runson", - "runner": "linux-x64-ci", - "target": "x86_64-unknown-linux-gnu", - "timeout_minutes": 180, - "tools_mode": "ci", - "toolchain_contract": "development", - "toolchain_components": "clippy, rustfmt, rust-src" - }, - "paths": ["scripts/ci/native-check.sh", "scripts/test/**"], - "config": ["targets"], - "cargo": ["cargo.clippy", "cargo.test"] - }, - { - "id": "native-windows-aarch64", - "dimensions": { - "display_name": "CI (aarch64-pc-windows-msvc)", - "operation": "native", - "runner_type": "github", - "runner": "windows-11-arm", - "target": "aarch64-pc-windows-msvc", - "timeout_minutes": 180, - "tools_mode": "none", - "toolchain_contract": "development", - "toolchain_components": "clippy" - }, - "paths": ["scripts/ci/native-check.sh", "scripts/test/**"], - "config": ["targets"], - "cargo": ["cargo.clippy", "cargo.test"] - }, - { - "id": "native-windows-x86-64", - "dimensions": { - "display_name": "CI (x86_64-pc-windows-msvc)", - "operation": "native", - "runner_type": "github", - "runner": "windows-latest", - "target": "x86_64-pc-windows-msvc", - "timeout_minutes": 180, - "tools_mode": "none", - "toolchain_contract": "development", - "toolchain_components": "clippy" - }, - "paths": ["scripts/ci/native-check.sh", "scripts/test/**"], - "config": ["targets"], - "cargo": ["cargo.clippy", "cargo.test"] - }, - { - "id": "native-ibm-s390x", - "dimensions": { - "display_name": "CI (IBM Z s390x)", - "operation": "native-ibm", - "runner_type": "github", - "runner": "ubuntu-24.04-s390x", - "timeout_minutes": 120, - "tools_mode": "none", - "toolchain_contract": "nightly", - "toolchain_components": "clippy, rustfmt" - }, - "paths": ["scripts/ci/native-check.sh", "scripts/test/**"], - "config": ["targets"], - "cargo": ["cargo.clippy", "cargo.test"] - }, - { - "id": "native-ibm-power10", - "dimensions": { - "display_name": "CI (IBM POWER10 ppc64le)", - "operation": "native-ibm", - "runner_type": "github", - "runner": "ubuntu-24.04-ppc64le-p10", - "timeout_minutes": 120, - "tools_mode": "none", - "toolchain_contract": "nightly", - "toolchain_components": "clippy, rustfmt" - }, - "paths": ["scripts/ci/native-check.sh", "scripts/test/**"], - "config": ["targets"], - "cargo": ["cargo.clippy", "cargo.test"] - }, - { - "id": "platform-amx", - "dimensions": { - "display_name": "Platform (Linux x64 AMX permission)", - "operation": "platform-amx", - "runner_type": "runson", - "runner": "intel-spr", - "timeout_minutes": 30, - "tools_mode": "none", - "toolchain_contract": "nightly", - "toolchain_components": "rustfmt" - }, - "paths": ["src/platform/**", "tests/platform_amx_permission.rs"], - "config": [], - "cargo": [] - }, - { - "id": "cross-targets", - "dimensions": { - "display_name": "Cross Targets", - "operation": "cross-targets", - "runner_type": "runson", - "runner": "linux-x64-ci", - "timeout_minutes": 90, - "tools_mode": "none", - "toolchain_contract": "nightly", - "toolchain_components": "clippy, rust-src" - }, - "paths": [".config/target-matrix.json", "scripts/ci/cross-targets.sh", "scripts/ci/nostd-wasm-suite.sh"], - "config": ["targets"], - "cargo": ["cargo.build"] - }, - { - "id": "supply-chain", - "dimensions": { - "display_name": "Supply Chain", - "operation": "supply-chain", - "runner_type": "runson", - "runner": "linux-x64-ci", - "timeout_minutes": 45, - "tools_mode": "supply-chain", - "toolchain_contract": "development", - "toolchain_components": "clippy, rustfmt, rust-src" - }, - "paths": ["Cargo.toml", "Cargo.lock", "deny.toml", "scripts/ci/dependabot-smoke.sh"], - "config": [], - "cargo": [] - } - ] -} diff --git a/.config/ci-tool-archives.tsv b/.config/ci-tool-archives.tsv index ff718e53..eaa18299 100644 --- a/.config/ci-tool-archives.tsv +++ b/.config/ci-tool-archives.tsv @@ -7,4 +7,3 @@ wasm-tools 1.257.1 linux x86_64 wasm-tools-1.257.1-x86_64-linux.tar.gz https://g wasm-tools 1.257.1 linux aarch64 wasm-tools-1.257.1-aarch64-linux.tar.gz https://github.com/bytecodealliance/wasm-tools/releases/download/v1.257.1/wasm-tools-1.257.1-aarch64-linux.tar.gz ff23fa2ed1bf46c590ebaff9ec585c941b820dd56384080466dabc61ed829178 wasm-tools 1.257.1 macos x86_64 wasm-tools-1.257.1-x86_64-macos.tar.gz https://github.com/bytecodealliance/wasm-tools/releases/download/v1.257.1/wasm-tools-1.257.1-x86_64-macos.tar.gz e35977a05d9e04b8e20208db5d2cd67031d3f782be2da763296b6a8ba13527ce wasm-tools 1.257.1 macos aarch64 wasm-tools-1.257.1-aarch64-macos.tar.gz https://github.com/bytecodealliance/wasm-tools/releases/download/v1.257.1/wasm-tools-1.257.1-aarch64-macos.tar.gz 1d1ced29aa8320733a7e27452434e66f9bcb61ce39a00ee63229594d71f8544d -codecov v11.3.1 linux x86_64 codecov https://cli.codecov.io/v11.3.1/linux/codecov ca1d64196d2d34771084afe76ea657d581bf628e31d993ff8e52ea09cc88a56d diff --git a/.config/feature-matrix.json b/.config/feature-matrix.json new file mode 100644 index 00000000..b4464529 --- /dev/null +++ b/.config/feature-matrix.json @@ -0,0 +1,264 @@ +{ + "variant_catalog_version": 2, + "work": "contracts.features", + "variants": [ + { + "id": "feature-policy", + "dimensions": { + "group": "Feature policy", + "feature_roots": "", + "runtime_profiles": "", + "full": true + }, + "external_paths": [ + ".config/feature-matrix.json", + ".config/rail.toml", + ".config/toolchains.toml", + "Cargo.lock", + "Cargo.toml", + "build.rs", + "rust-toolchain.toml", + "scripts/check/feature-contracts.sh", + "scripts/ci/feature-contracts-test.sh", + "scripts/lib/feature-profiles.sh", + "scripts/lib/rail-plan.sh" + ] + }, + { + "id": "surface", + "dimensions": { + "group": "Shared surface", + "feature_roots": "", + "runtime_profiles": "", + "full": true + }, + "external_paths": [ + "src/hex.rs", + "src/lib.rs", + "src/macros.rs", + "src/secret.rs", + "src/traits/**", + "tests/api_consistency.rs", + "tests/common/**", + "tests/common_properties.rs", + "tests/owned_equality.rs", + "tests/root_surface.rs", + "tests/secret_redaction.rs" + ] + }, + { + "id": "platform", + "dimensions": { + "group": "Platform and dispatch", + "feature_roots": "", + "runtime_profiles": "", + "full": true + }, + "external_paths": [ + "src/backend/**", + "src/platform/**", + "tests/platform_*.rs", + "tests/portable_fallback.rs", + "tests/vectored_dispatch.rs" + ] + }, + { + "id": "checksums", + "dimensions": { + "group": "Checksums", + "feature_roots": "crc16,crc24,crc32,crc64", + "runtime_profiles": "runtime.diagnostics", + "full": false + }, + "external_paths": [ + "src/checksum/**", + "tests/crc*.rs" + ] + }, + { + "id": "crypto-hashes", + "dimensions": { + "group": "Cryptographic hashes", + "feature_roots": "sha2,sha3,blake2b,blake2s,blake3,ascon-hash", + "runtime_profiles": "runtime.diagnostics", + "full": false + }, + "external_paths": [ + "src/hashes/common/**", + "src/hashes/crypto/**", + "testdata/ascon/asconcxof128.txt", + "testdata/ascon/asconhash.blb", + "testdata/ascon/asconxof.blb", + "testdata/blake2/**", + "testdata/blake3/**", + "testdata/sha2/**", + "testdata/sha3/**", + "tests/ascon_*.rs", + "tests/blake*.rs", + "tests/cshake*.rs", + "tests/sha*.rs", + "tests/shake*.rs" + ] + }, + { + "id": "fast-hashes", + "dimensions": { + "group": "Fast hashes", + "feature_roots": "xxh3,rapidhash", + "runtime_profiles": "", + "full": false + }, + "external_paths": [ + "src/hashes/fast/**", + "tests/fast_hash_allocations.rs", + "tests/rapidhash_*.rs", + "tests/xxh3_*.rs" + ] + }, + { + "id": "websocket-sha1", + "dimensions": { + "group": "WebSocket SHA-1", + "feature_roots": "websocket-sha1", + "runtime_profiles": "runtime.websocket-sha1", + "full": false + }, + "external_paths": [ + "src/hashes/legacy/**", + "tests/websocket_*.rs" + ] + }, + { + "id": "macs-kdfs", + "dimensions": { + "group": "MACs and KDFs", + "feature_roots": "hmac,hmac-sha3,kmac,hkdf,pbkdf2,poly1305", + "runtime_profiles": "runtime.diagnostics", + "full": false + }, + "external_paths": [ + "src/auth/hkdf.rs", + "src/auth/hmac.rs", + "src/auth/hmac_sha3.rs", + "src/auth/kmac.rs", + "src/auth/pbkdf2.rs", + "src/auth/poly1305.rs", + "testdata/auth/wycheproof/hkdf_*.json", + "testdata/auth/wycheproof/hmac_*.json", + "testdata/auth/wycheproof/kmac*.json", + "testdata/auth/wycheproof/pbkdf2_*.json", + "tests/hkdf_*.rs", + "tests/hmac_*.rs", + "tests/kmac_*.rs", + "tests/pbkdf2_*.rs", + "tests/poly1305_*.rs" + ] + }, + { + "id": "signatures", + "dimensions": { + "group": "Signatures", + "feature_roots": "ecdsa-p256,ecdsa-p384,ed25519,rsa", + "runtime_profiles": "runtime.entropy,runtime.diagnostics", + "full": false + }, + "external_paths": [ + "src/auth/curve25519_edwards.rs", + "src/auth/ecdsa*.rs", + "src/auth/ed25519.rs", + "src/auth/ed25519/**", + "src/auth/rsa*.rs", + "testdata/auth/wycheproof/ecdsa_*.json", + "testdata/auth/wycheproof/ed25519_*.json", + "testdata/rsa/**", + "tests/ecdsa_*.rs", + "tests/ed25519_*.rs", + "tests/rsa_*.rs" + ] + }, + { + "id": "key-exchange", + "dimensions": { + "group": "Key exchange", + "feature_roots": "x25519,ml-kem", + "runtime_profiles": "runtime.entropy,runtime.diagnostics", + "full": false + }, + "external_paths": [ + "src/auth/curve25519_edwards.rs", + "src/auth/mlkem.rs", + "src/auth/mlkem/**", + "src/auth/x25519.rs", + "src/auth/x25519/**", + "testdata/auth/wycheproof/x25519_*.json", + "tests/mlkem_*.rs", + "tests/vectors/mlkem_*.json", + "tests/x25519_*.rs" + ] + }, + { + "id": "password-hashing", + "dimensions": { + "group": "Password hashing", + "feature_roots": "argon2,scrypt,phc-strings", + "runtime_profiles": "runtime.entropy,runtime.diagnostics,runtime.parallel", + "full": false + }, + "external_paths": [ + "src/auth/argon2/**", + "src/auth/phc.rs", + "src/auth/scrypt.rs", + "tests/argon2_*.rs", + "tests/phc_*.rs", + "tests/scrypt_*.rs" + ] + }, + { + "id": "aead", + "dimensions": { + "group": "AEAD", + "feature_roots": "aes-gcm,aes-gcm-siv,aes-siv,chacha20poly1305,xchacha20poly1305,aegis256,ascon-aead", + "runtime_profiles": "runtime.entropy,runtime.diagnostics", + "full": false + }, + "external_paths": [ + "src/aead/**", + "testdata/aead/**", + "testdata/ascon/asconaead128.txt", + "tests/aead_*.rs", + "tests/aegis256_*.rs", + "tests/aes*.rs", + "tests/ascon_aead_*.rs", + "tests/ascon_final_kats.rs", + "tests/chacha20poly1305.rs", + "tests/header_protection.rs", + "tests/xchacha20poly1305.rs" + ] + }, + { + "id": "serde", + "dimensions": { + "group": "Serialization", + "feature_roots": "serde,serde-secrets", + "runtime_profiles": "runtime.public-serde,runtime.secret-serde", + "full": false + }, + "external_paths": [ + "tests/serde_roundtrip.rs" + ] + }, + { + "id": "parallel", + "dimensions": { + "group": "Parallel execution", + "feature_roots": "parallel", + "runtime_profiles": "runtime.parallel", + "full": false + }, + "external_paths": [ + "src/hashes/crypto/blake3/parallel.rs", + "tests/argon2_parallel.rs" + ] + } + ] +} diff --git a/.config/fuzz-matrix.json b/.config/fuzz-matrix.json new file mode 100644 index 00000000..fb6dcd93 --- /dev/null +++ b/.config/fuzz-matrix.json @@ -0,0 +1,592 @@ +{ + "variant_catalog_version": 2, + "work": "assurance.fuzz", + "variants": [ + { + "id": "surface", + "dimensions": { + "name": "Public parsing and I/O", + "targets": "hex_parse,traits_io", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/hex_parse/**", + "fuzz/corpus/traits_io/**", + "fuzz/fuzz_targets/hex_parse.rs", + "fuzz/fuzz_targets/traits_io.rs", + "fuzz/dictionaries/hex_parse.dict", + "fuzz-packages/surface-hex-parse/**", + "fuzz-packages/traits-io/**", + "src/hex.rs", + "src/traits/**" + ] + }, + { + "id": "crc16", + "dimensions": { + "name": "CRC-16", + "targets": "checksum_crc16", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz-packages/checksums-crc16/**", + "src/checksum/common/**", + "src/checksum/diag.rs", + "src/checksum/dispatchers.rs", + "src/checksum/generated/**", + "src/checksum/introspect.rs", + "src/checksum/io.rs", + "src/checksum/kernel_table.rs", + "src/checksum/macros.rs", + "src/checksum/mod.rs", + "src/checksum/crc16/**" + ] + }, + { + "id": "crc24", + "dimensions": { + "name": "CRC-24", + "targets": "checksum_crc24", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz-packages/checksums-crc24/**", + "src/checksum/common/**", + "src/checksum/diag.rs", + "src/checksum/dispatchers.rs", + "src/checksum/generated/**", + "src/checksum/introspect.rs", + "src/checksum/io.rs", + "src/checksum/kernel_table.rs", + "src/checksum/macros.rs", + "src/checksum/mod.rs", + "src/checksum/crc24/**" + ] + }, + { + "id": "crc32", + "dimensions": { + "name": "CRC-32", + "targets": "checksum_crc32", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz-packages/checksums-crc32/**", + "src/checksum/common/**", + "src/checksum/diag.rs", + "src/checksum/dispatchers.rs", + "src/checksum/generated/**", + "src/checksum/introspect.rs", + "src/checksum/io.rs", + "src/checksum/kernel_table.rs", + "src/checksum/macros.rs", + "src/checksum/mod.rs", + "src/checksum/crc32/**" + ] + }, + { + "id": "crc64", + "dimensions": { + "name": "CRC-64", + "targets": "checksum_crc64", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz-packages/checksums-crc64/**", + "src/checksum/common/**", + "src/checksum/diag.rs", + "src/checksum/dispatchers.rs", + "src/checksum/generated/**", + "src/checksum/introspect.rs", + "src/checksum/io.rs", + "src/checksum/kernel_table.rs", + "src/checksum/macros.rs", + "src/checksum/mod.rs", + "src/checksum/crc64/**" + ] + }, + { + "id": "checksums-full", + "dimensions": { + "name": "Combined checksum harness", + "targets": "checksum_crc", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/checksum_crc/**", + "fuzz/fuzz_targets/checksum_crc.rs" + ] + }, + { + "id": "rapidhash", + "dimensions": { + "name": "RapidHash", + "targets": "fast_rapidhash", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/fast_rapidhash/**", + "fuzz/fuzz_targets/fast_rapidhash.rs", + "fuzz-packages/fast-rapidhash/**", + "src/hashes/fast/rapidhash.rs", + "src/hashes/fast/rapidhash/**" + ] + }, + { + "id": "xxh3", + "dimensions": { + "name": "XXH3", + "targets": "fast_xxh3", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/fast_xxh3/**", + "fuzz/fuzz_targets/fast_xxh3.rs", + "fuzz-packages/fast-xxh3/**", + "src/hashes/fast/xxh3.rs", + "src/hashes/fast/xxh3/**" + ] + }, + { + "id": "ascon-hash", + "dimensions": { + "name": "Ascon hash", + "targets": "hash_ascon,hash_ascon_cxof", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/hash_ascon/**", + "fuzz/corpus/hash_ascon_cxof/**", + "fuzz/fuzz_targets/hash_ascon*.rs", + "fuzz-packages/hash-ascon/**", + "src/backend/ascon.rs", + "src/hashes/crypto/ascon.rs", + "src/hashes/crypto/ascon/**" + ] + }, + { + "id": "blake2", + "dimensions": { + "name": "BLAKE2", + "targets": "hash_blake2b,hash_blake2s", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/hash_blake2b/**", + "fuzz/corpus/hash_blake2s/**", + "fuzz/fuzz_targets/hash_blake2*.rs", + "fuzz-packages/hash-blake2/**", + "src/hashes/crypto/blake2b/**", + "src/hashes/crypto/blake2s/**" + ] + }, + { + "id": "blake3", + "dimensions": { + "name": "BLAKE3", + "targets": "hash_blake3,hash_blake3_keyed,hash_blake3_derive", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/hash_blake3/**", + "fuzz/corpus/hash_blake3_derive/**", + "fuzz/corpus/hash_blake3_keyed/**", + "fuzz/fuzz_targets/hash_blake3*.rs", + "fuzz-packages/hash-blake3/**", + "src/hashes/crypto/blake3/**" + ] + }, + { + "id": "sha2", + "dimensions": { + "name": "SHA-2", + "targets": "hash_sha2", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/hash_sha2/**", + "fuzz/fuzz_targets/hash_sha2.rs", + "fuzz-packages/hash-sha2/**", + "src/hashes/crypto/sha*.rs", + "src/hashes/crypto/sha224/**", + "src/hashes/crypto/sha256/**", + "src/hashes/crypto/sha384/**", + "src/hashes/crypto/sha512/**", + "src/hashes/crypto/sha512_256/**" + ] + }, + { + "id": "sha3", + "dimensions": { + "name": "SHA-3 and cSHAKE", + "targets": "hash_sha3,hash_cshake256", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/hash_cshake256/**", + "fuzz/corpus/hash_sha3/**", + "fuzz/fuzz_targets/hash_sha3.rs", + "fuzz/fuzz_targets/hash_cshake256.rs", + "fuzz-packages/hash-sha3/**", + "src/hashes/crypto/cshake.rs", + "src/hashes/crypto/keccak.rs", + "src/hashes/crypto/keccak/**", + "src/hashes/crypto/sha3.rs", + "src/hashes/crypto/sp800185.rs" + ] + }, + { + "id": "websocket-sha1", + "dimensions": { + "name": "WebSocket SHA-1", + "targets": "hash_websocket_sha1", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/hash_websocket_sha1/**", + "fuzz/fuzz_targets/hash_websocket_sha1.rs", + "fuzz-packages/hash-websocket-sha1/**", + "src/hashes/legacy/**" + ] + }, + { + "id": "aes-gcm", + "dimensions": { + "name": "AES-GCM", + "targets": "aead_aes128gcm,aead_aes256gcm", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/aead_aes128gcm/**", + "fuzz/corpus/aead_aes256gcm/**", + "fuzz/fuzz_targets/aead_aes128gcm.rs", + "fuzz/fuzz_targets/aead_aes256gcm.rs", + "fuzz/dictionaries/aead_boundary.dict", + "fuzz-packages/aead-aes128gcm/**", + "fuzz-packages/aead-aes256gcm/**", + "src/aead/aes.rs", + "src/aead/aes/**", + "src/aead/aes_round.rs", + "src/aead/aes128gcm.rs", + "src/aead/aes256gcm.rs", + "src/aead/ghash.rs" + ] + }, + { + "id": "aes-gcm-siv", + "dimensions": { + "name": "AES-GCM-SIV", + "targets": "aead_aes128gcmsiv,aead_aes256gcmsiv", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/aead_aes128gcmsiv/**", + "fuzz/corpus/aead_aes256gcmsiv/**", + "fuzz/fuzz_targets/aead_aes128gcmsiv.rs", + "fuzz/fuzz_targets/aead_aes256gcmsiv.rs", + "fuzz/dictionaries/aead_boundary.dict", + "fuzz-packages/aead-aes128gcmsiv/**", + "fuzz-packages/aead-aes256gcmsiv/**", + "src/aead/aes.rs", + "src/aead/aes/**", + "src/aead/aes_round.rs", + "src/aead/aes128gcmsiv.rs", + "src/aead/aes256gcmsiv.rs", + "src/aead/polyval.rs" + ] + }, + { + "id": "aes-siv", + "dimensions": { + "name": "AES-SIV", + "targets": "aead_aes_siv_cmac256", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/aead_aes_siv_cmac256/**", + "fuzz/fuzz_targets/aead_aes_siv_cmac256.rs", + "fuzz/dictionaries/aead_boundary.dict", + "fuzz-packages/aead-aes-siv-cmac256/**", + "src/aead/aes.rs", + "src/aead/aes/**", + "src/aead/aes_round.rs", + "src/aead/aes_siv_cmac256.rs" + ] + }, + { + "id": "chacha-poly1305", + "dimensions": { + "name": "ChaCha20-Poly1305", + "targets": "aead_chacha20poly1305,aead_xchacha20poly1305", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/aead_chacha20poly1305/**", + "fuzz/corpus/aead_xchacha20poly1305/**", + "fuzz/fuzz_targets/aead_chacha20poly1305.rs", + "fuzz/fuzz_targets/aead_xchacha20poly1305.rs", + "fuzz/dictionaries/aead_boundary.dict", + "fuzz-packages/aead-chacha20poly1305/**", + "fuzz-packages/aead-xchacha20poly1305/**", + "src/aead/chacha20.rs", + "src/aead/chacha20/**", + "src/aead/chacha20poly1305.rs", + "src/aead/chacha20poly1305/**", + "src/aead/poly1305.rs", + "src/aead/poly1305/**", + "src/aead/xchacha20poly1305.rs" + ] + }, + { + "id": "aegis256", + "dimensions": { + "name": "AEGIS-256", + "targets": "aead_aegis256", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/aead_aegis256/**", + "fuzz/fuzz_targets/aead_aegis256.rs", + "fuzz/dictionaries/aead_boundary.dict", + "fuzz-packages/aead-aegis256/**", + "src/aead/aegis256.rs", + "src/aead/aegis256/**" + ] + }, + { + "id": "ascon-aead", + "dimensions": { + "name": "Ascon-AEAD", + "targets": "aead_ascon128", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/aead_ascon128/**", + "fuzz/fuzz_targets/aead_ascon128.rs", + "fuzz/dictionaries/aead_boundary.dict", + "fuzz-packages/aead-ascon128/**", + "src/aead/ascon128.rs", + "src/backend/ascon.rs" + ] + }, + { + "id": "header-protection", + "dimensions": { + "name": "Header protection", + "targets": "aead_header_protection", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/aead_header_protection/**", + "fuzz/fuzz_targets/aead_header_protection.rs", + "fuzz-packages/aead-header-protection/**", + "src/aead/aes.rs", + "src/aead/aes/**", + "src/aead/aes_round.rs", + "src/aead/chacha20.rs", + "src/aead/chacha20/**", + "src/aead/header_protection.rs" + ] + }, + { + "id": "nonce-counter", + "dimensions": { + "name": "AEAD nonce counter", + "targets": "aead_nonce_counter", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/aead_nonce_counter/**", + "fuzz/fuzz_targets/aead_nonce_counter.rs", + "fuzz-packages/aead-nonce-counter/**", + "src/aead/nonce_counter.rs" + ] + }, + { + "id": "argon2", + "dimensions": { + "name": "Argon2", + "targets": "auth_argon2id,auth_argon2d,auth_argon2i", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/auth_argon2d/**", + "fuzz/corpus/auth_argon2i/**", + "fuzz/corpus/auth_argon2id/**", + "fuzz/fuzz_targets/auth_argon2*.rs", + "fuzz-packages/auth-argon2/**", + "src/auth/argon2/**" + ] + }, + { + "id": "ed25519", + "dimensions": { + "name": "Ed25519", + "targets": "auth_ed25519,auth_ed25519_verify", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/auth_ed25519/**", + "fuzz/corpus/auth_ed25519_verify/**", + "fuzz/fuzz_targets/auth_ed25519*.rs", + "fuzz-packages/auth-ed25519/**", + "src/auth/curve25519_edwards.rs", + "src/auth/ed25519.rs", + "src/auth/ed25519/**", + "src/backend/curve25519.rs" + ] + }, + { + "id": "ecdsa", + "dimensions": { + "name": "ECDSA", + "targets": "auth_ecdsa_verify,auth_ecdsa_sign", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/auth_ecdsa_sign/**", + "fuzz/corpus/auth_ecdsa_verify/**", + "fuzz/fuzz_targets/auth_ecdsa*.rs", + "src/auth/ecdsa*.rs" + ] + }, + { + "id": "hmac", + "dimensions": { + "name": "HMAC", + "targets": "auth_hmac_sha256,auth_hmac_sha384,auth_hmac_sha512", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/auth_hmac_sha256/**", + "fuzz/corpus/auth_hmac_sha384/**", + "fuzz/corpus/auth_hmac_sha512/**", + "fuzz/fuzz_targets/auth_hmac*.rs", + "fuzz-packages/auth-hmac/**", + "src/auth/hmac.rs", + "src/auth/hmac_sha3.rs" + ] + }, + { + "id": "hkdf", + "dimensions": { + "name": "HKDF", + "targets": "auth_hkdf_sha256,auth_hkdf_sha384", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/auth_hkdf_sha256/**", + "fuzz/corpus/auth_hkdf_sha384/**", + "fuzz/fuzz_targets/auth_hkdf*.rs", + "fuzz-packages/auth-hkdf/**", + "src/auth/hkdf.rs" + ] + }, + { + "id": "kmac", + "dimensions": { + "name": "KMAC", + "targets": "auth_kmac256", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/auth_kmac256/**", + "fuzz/fuzz_targets/auth_kmac256.rs", + "fuzz-packages/auth-kmac256/**", + "src/auth/kmac.rs" + ] + }, + { + "id": "pbkdf2", + "dimensions": { + "name": "PBKDF2", + "targets": "auth_pbkdf2", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/auth_pbkdf2/**", + "fuzz/fuzz_targets/auth_pbkdf2.rs", + "fuzz-packages/auth-pbkdf2/**", + "src/auth/pbkdf2.rs" + ] + }, + { + "id": "phc", + "dimensions": { + "name": "PHC strings", + "targets": "auth_phc", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/auth_phc/**", + "fuzz/fuzz_targets/auth_phc.rs", + "fuzz/dictionaries/phc.dict", + "fuzz-packages/auth-phc/**", + "src/auth/phc.rs" + ] + }, + { + "id": "scrypt", + "dimensions": { + "name": "scrypt", + "targets": "auth_scrypt", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/auth_scrypt/**", + "fuzz/fuzz_targets/auth_scrypt.rs", + "fuzz-packages/auth-scrypt/**", + "src/auth/scrypt.rs" + ] + }, + { + "id": "x25519", + "dimensions": { + "name": "X25519", + "targets": "auth_x25519", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/auth_x25519/**", + "fuzz/fuzz_targets/auth_x25519.rs", + "fuzz-packages/auth-x25519/**", + "src/auth/x25519.rs", + "src/auth/x25519/**", + "src/backend/curve25519.rs" + ] + }, + { + "id": "mlkem", + "dimensions": { + "name": "ML-KEM", + "targets": "auth_mlkem512,auth_mlkem768,auth_mlkem1024", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/auth_mlkem1024/**", + "fuzz/corpus/auth_mlkem512/**", + "fuzz/corpus/auth_mlkem768/**", + "fuzz/fuzz_targets/auth_mlkem*.rs", + "src/auth/mlkem.rs", + "src/auth/mlkem/**" + ] + }, + { + "id": "rsa", + "dimensions": { + "name": "RSA", + "targets": "auth_rsa_public_key,auth_rsa_import,auth_rsa_verify,auth_rsa_private_ops,auth_rsa_protocol", + "duration_seconds": 15 + }, + "external_paths": [ + "fuzz/corpus/auth_rsa_import/**", + "fuzz/corpus/auth_rsa_private_ops/**", + "fuzz/corpus/auth_rsa_protocol/**", + "fuzz/corpus/auth_rsa_public_key/**", + "fuzz/corpus/auth_rsa_verify/**", + "fuzz/fuzz_targets/auth_rsa*.rs", + "src/auth/rsa*.rs" + ] + } + ] +} diff --git a/.config/miri-matrix.json b/.config/miri-matrix.json new file mode 100644 index 00000000..f4b631dd --- /dev/null +++ b/.config/miri-matrix.json @@ -0,0 +1,35 @@ +{ + "variant_catalog_version": 2, + "work": "assurance.miri", + "variants": [ + { + "id": "portable", + "dimensions": { + "name": "Portable unsafe boundaries", + "mode": "focused" + }, + "external_paths": [ + "src/auth/argon2/**", + "src/auth/mlkem.rs", + "src/auth/mlkem/**", + "src/auth/x25519.rs", + "src/auth/x25519/**", + "src/backend/**", + "src/checksum/**", + "src/miri_shadow_tests.rs", + "src/platform/**", + "tests/argon2_miri.rs" + ] + }, + { + "id": "rsa", + "dimensions": { + "name": "RSA unsafe boundaries", + "mode": "rsa" + }, + "external_paths": [ + "src/auth/rsa.rs" + ] + } + ] +} diff --git a/.config/rail.toml b/.config/rail.toml index d0f6dc1c..06844542 100644 --- a/.config/rail.toml +++ b/.config/rail.toml @@ -35,19 +35,15 @@ enabled = false consumer_scope = "open" # Published library: external consumers remain part of the visibility boundary. [release] -source = "changes" tag_format = "{prefix}{version}" sign_tags = true -require_change_files = true -require_changelog_entries = true -require_release_notes = true pre_1_breaking_bump = "minor" # Before 1.0, reviewed minor releases may deliberately replace unstable API # shapes. Re-enable compiler-backed SemVer enforcement when rscrypto adopts a # stable compatibility contract. semver_check = "off" -change_dir = ".changes" -remote_effects = "push" # The repository workflow, not Cargo Rail, owns forge release publication. +remote_effects = "push" # Push the signed tag; its workflow owns the forge release. +registry_publication = "none" # crates.io Trusted Publishing is available only inside that workflow. auxiliary_cargo_manifests = [ "fuzz-packages/aead-aegis256/Cargo.toml", "fuzz-packages/aead-aes-siv-cmac256/Cargo.toml", @@ -94,24 +90,201 @@ auxiliary_cargo_manifests = [ path = "CHANGELOG.md" relative_to = "workspace" -[plan.work.ci-policy] +[plan.work."policy.actions"] scope = "repository" paths = [ - ".cargo/**", ".config/ci-tool-archives.tsv", - ".config/toolchains.toml", + ".config/feature-matrix.json", + ".config/fuzz-matrix.json", + ".config/miri-matrix.json", + ".config/rail.toml", ".github/**", "justfile", + "scripts/check/feature-contracts.sh", + "scripts/ci/**", + "scripts/lib/ci-tool-integrity.sh", + "scripts/lib/feature-profiles.sh", + "scripts/lib/rail-plan.sh", + "scripts/test/fuzz-contracts.sh", + "scripts/test/miri-contracts.sh", + "scripts/test/test-fuzz-asan.sh", + "scripts/test/test-fuzz-scheduler-test.sh", + "scripts/test/test-fuzz.sh", + "scripts/test/test-miri.sh", +] + +[plan.work."policy.repository"] +scope = "repository" +paths = [ + ".config/benchmark-matrix.json", + ".config/nextest.toml", + ".config/target-matrix.json", + "Cargo.lock", + "Cargo.toml", + "benches/**", + "build.rs", + "ct.toml", + "deny.toml", + "examples/**", + "scripts/check/**", + "scripts/bench/benchmark_catalog.py", + "scripts/bench/benchmark_catalog_test.py", + "scripts/ct/*.py", + "src/**", + "testdata/**", + "tests/**", +] + +[plan.work."contracts.features"] +scope = "variants" +paths = [ + ".config/feature-matrix.json", + ".config/rail.toml", + ".config/toolchains.toml", + "Cargo.toml", + "build.rs", "rust-toolchain.toml", - "scripts/ci/check-ci-ownership.sh", - "scripts/ci/install-tools.sh", - "scripts/ci/materialize-rail-plan.sh", - "scripts/ci/run-rust-job.sh", - "scripts/ci/setup-toolchain.sh", - "scripts/lib/**", + "scripts/check/feature-contracts.sh", + "scripts/ci/feature-contracts-test.sh", + "scripts/lib/feature-profiles.sh", + "scripts/lib/rail-plan.sh", + "src/**", + "testdata/**", + "tests/**", +] +variant_catalog = ".config/feature-matrix.json" + +[plan.work."contracts.auxiliary"] +scope = "repository" +paths = [ + "Cargo.toml", + "fuzz/**", + "fuzz-packages/**", + "scripts/check/lint-independent-workspaces.sh", + "tools/**", ] -[plan.work.ci-suite] +[plan.work."targets.platforms"] scope = "variants" -cargo = ["cargo.build", "cargo.clippy", "cargo.doc", "cargo.doctest", "cargo.test"] -variant_catalog = ".config/ci-plan-variants.json" +config = ["targets"] +paths = [ + ".cargo/**", + ".config/toolchains.toml", + "Cargo.toml", + "rust-toolchain.toml", + "scripts/ci/target-contracts.sh", + "scripts/lib/target-matrix.sh", + "scripts/lib/targets.sh", + "src/backend/**", + "src/**/dispatch*.rs", + "src/**/kernel_table.rs", + "src/**/mod.rs", + "src/lib.rs", + "src/macros.rs", + "src/platform/caps.rs", + "src/platform/detect.rs", + "src/platform/detect/cache_override.rs", + "src/platform/detect/compile_time.rs", + "src/platform/detect/tests.rs", + "src/platform/mod.rs", + "src/platform/target_matrix.rs", +] +variant_catalog = ".config/target-matrix.json" + +[plan.work."assurance.miri"] +scope = "variants" +paths = [ + ".config/miri-matrix.json", + ".config/rail.toml", + ".config/toolchains.toml", + "Cargo.toml", + "build.rs", + "rust-toolchain.toml", + "scripts/lib/common.sh", + "scripts/lib/rail-plan.sh", + "scripts/lib/toolchain.sh", + "scripts/test/miri-contracts.sh", + "scripts/test/test-miri.sh", + "src/auth/argon2/**", + "src/auth/mlkem.rs", + "src/auth/mlkem/**", + "src/auth/rsa.rs", + "src/auth/x25519.rs", + "src/auth/x25519/**", + "src/backend/**", + "src/checksum/**", + "src/lib.rs", + "src/miri_shadow_tests.rs", + "src/platform/**", + "tests/argon2_miri.rs", +] +variant_catalog = ".config/miri-matrix.json" + +[plan.work."assurance.fuzz"] +scope = "variants" +paths = [ + ".config/fuzz-matrix.json", + ".config/rail.toml", + ".config/toolchains.toml", + "Cargo.toml", + "build.rs", + "rust-toolchain.toml", + "fuzz/**", + "fuzz-packages/**", + "scripts/lib/common.sh", + "scripts/lib/fuzz-packages.sh", + "scripts/lib/rail-plan.sh", + "scripts/lib/toolchain.sh", + "scripts/test/fuzz-contracts.sh", + "scripts/test/test-fuzz.sh", + "src/**", +] +variant_catalog = ".config/fuzz-matrix.json" + +[plan.work."assurance.ct"] +scope = "repository" +paths = [ + ".config/target-matrix.json", + ".config/toolchains.toml", + "Cargo.toml", + "build.rs", + "ct.toml", + "rust-toolchain.toml", + "scripts/ct/**", + "src/aead/**", + "src/auth/**", + "src/backend/**", + "src/hashes/crypto/**", + "src/lib.rs", + "src/secret.rs", + "src/traits/**", + "tools/ct-*/**", +] + +[plan.work."assurance.rsa"] +scope = "repository" +paths = [ + ".config/toolchains.toml", + "Cargo.toml", + "build.rs", + "rust-toolchain.toml", + "scripts/check/rsa-asm-provenance.sh", + "scripts/test/test-rsa-*.sh", + "src/auth/asm/**", + "src/auth/rsa*.rs", + "src/lib.rs", + "testdata/rsa/**", + "tests/rsa_*.rs", +] + +[plan.work."dependencies.auxiliary"] +scope = "repository" +paths = [ + "fuzz/Cargo.lock", + "fuzz/Cargo.toml", + "fuzz-packages/**/Cargo.lock", + "fuzz-packages/**/Cargo.toml", + "scripts/ci/install-tools.sh", + "tools/**/Cargo.lock", + "tools/**/Cargo.toml", +] diff --git a/.config/target-matrix.json b/.config/target-matrix.json index 0b5938c1..42c2c761 100644 --- a/.config/target-matrix.json +++ b/.config/target-matrix.json @@ -1,21 +1,510 @@ { - "groups": { - "win": ["aarch64-pc-windows-msvc", "x86_64-pc-windows-msvc"], - "macos": ["aarch64-apple-darwin", "x86_64-apple-darwin"], - "linux": [ - "aarch64-unknown-linux-gnu", - "aarch64-unknown-linux-musl", - "riscv64gc-unknown-linux-gnu", - "x86_64-unknown-linux-gnu", - "x86_64-unknown-linux-musl" - ], - "ibm": ["powerpc64le-unknown-linux-gnu", "s390x-unknown-linux-gnu"], - "no_std": [ - "aarch64-unknown-none", - "riscv32imac-unknown-none-elf", - "thumbv6m-none-eabi", - "x86_64-unknown-none" - ], - "wasm": ["wasm32-unknown-unknown", "wasm32-wasip1"] - } + "variant_catalog_version": 2, + "work": "targets.platforms", + "variants": [ + { + "id": "aarch64-apple-darwin", + "dimensions": { + "name": "macOS AArch64 runtime", + "target": "aarch64-apple-darwin", + "group": "macos", + "operation": "native", + "platform": "macos-arm64", + "runner_type": "github", + "runner": "macos-15", + "contract": "development", + "components": "", + "timeout_minutes": 45, + "verify_plan": false, + "cache": true, + "compile": "native", + "runtime": "virtual-native", + "performance": false, + "constant_time": false, + "release": true + }, + "config": ["targets"], + "external_paths": [ + "scripts/ci/native-platform.sh", + "src/**/*aarch64*", + "tests/aead_kernel_equivalence.rs", + "tests/portable_fallback.rs", + "tests/vectored_dispatch.rs" + ] + }, + { + "id": "aarch64-pc-windows-msvc", + "dimensions": { + "name": "Windows AArch64 compile", + "target": "aarch64-pc-windows-msvc", + "group": "win", + "operation": "native", + "platform": "windows-arm64", + "runner_type": "github", + "runner": "windows-11-arm", + "contract": "development", + "components": "clippy", + "timeout_minutes": 30, + "verify_plan": false, + "cache": true, + "compile": "hosted", + "runtime": "none", + "performance": false, + "constant_time": false, + "release": true + }, + "config": ["targets"], + "external_paths": ["scripts/ci/native-platform.sh", "src/**/*aarch64*"] + }, + { + "id": "aarch64-unknown-linux-gnu", + "dimensions": { + "name": "Linux AArch64 runtime", + "target": "aarch64-unknown-linux-gnu", + "group": "linux", + "operation": "native", + "platform": "linux-arm64", + "runner_type": "runson", + "runner": "linux-arm64-ci", + "contract": "development", + "components": "", + "timeout_minutes": 45, + "verify_plan": false, + "cache": true, + "compile": "native", + "runtime": "virtual-native", + "performance": true, + "constant_time": true, + "release": true + }, + "config": ["targets"], + "external_paths": [ + "scripts/ci/native-platform.sh", + "src/**/*aarch64*", + "tests/aead_kernel_equivalence.rs", + "tests/portable_fallback.rs", + "tests/vectored_dispatch.rs" + ] + }, + { + "id": "aarch64-unknown-linux-musl", + "dimensions": { + "name": "Linux AArch64 musl compile", + "target": "aarch64-unknown-linux-musl", + "group": "linux", + "operation": "cross", + "platform": "cross", + "runner_type": "runson", + "runner": "linux-x64-ci", + "contract": "development", + "components": "clippy", + "timeout_minutes": 30, + "verify_plan": true, + "cache": false, + "compile": "generic", + "runtime": "none", + "performance": false, + "constant_time": false, + "release": true + }, + "config": ["targets"], + "external_paths": ["scripts/ci/cross-targets.sh", "src/**/*aarch64*"] + }, + { + "id": "aarch64-unknown-none", + "dimensions": { + "name": "Bare-metal AArch64 compile", + "target": "aarch64-unknown-none", + "group": "no_std", + "operation": "cross", + "platform": "cross", + "runner_type": "runson", + "runner": "linux-x64-ci", + "contract": "development", + "components": "", + "timeout_minutes": 45, + "verify_plan": true, + "cache": false, + "compile": "generic", + "runtime": "none", + "performance": false, + "constant_time": false, + "release": true + }, + "config": ["targets"], + "external_paths": [ + "scripts/ci/cross-targets.sh", + "scripts/ci/nostd-wasm-suite.sh", + "scripts/lib/feature-profiles.sh", + "src/**/*aarch64*" + ] + }, + { + "id": "powerpc64le-unknown-linux-gnu", + "dimensions": { + "name": "IBM POWER10 runtime", + "target": "powerpc64le-unknown-linux-gnu", + "group": "ibm", + "operation": "native", + "platform": "ibm-power10", + "runner_type": "github", + "runner": "ubuntu-24.04-ppc64le-p10", + "contract": "nightly", + "components": "", + "timeout_minutes": 60, + "verify_plan": false, + "cache": false, + "compile": "native", + "runtime": "physical-native", + "performance": true, + "constant_time": true, + "release": true + }, + "config": ["targets"], + "external_paths": [ + "scripts/ci/native-platform.sh", + "src/**/*power*", + "tests/aead_kernel_equivalence.rs", + "tests/portable_fallback.rs", + "tests/vectored_dispatch.rs" + ] + }, + { + "id": "riscv32imac-unknown-none-elf", + "dimensions": { + "name": "Bare-metal RISC-V 32 compile", + "target": "riscv32imac-unknown-none-elf", + "group": "no_std", + "operation": "cross", + "platform": "cross", + "runner_type": "runson", + "runner": "linux-x64-ci", + "contract": "nightly", + "components": "", + "timeout_minutes": 45, + "verify_plan": true, + "cache": false, + "compile": "generic", + "runtime": "none", + "performance": false, + "constant_time": false, + "release": true + }, + "config": ["targets"], + "external_paths": [ + "scripts/ci/cross-targets.sh", + "scripts/ci/nostd-wasm-suite.sh", + "scripts/lib/feature-profiles.sh", + "src/**/*riscv*" + ] + }, + { + "id": "riscv64gc-unknown-linux-gnu", + "dimensions": { + "name": "RISE RISC-V runtime", + "target": "riscv64gc-unknown-linux-gnu", + "group": "linux", + "operation": "native", + "platform": "rise-riscv", + "runner_type": "github", + "runner": "ubuntu-24.04-riscv", + "contract": "nightly", + "components": "", + "timeout_minutes": 60, + "verify_plan": false, + "cache": false, + "compile": "native", + "runtime": "physical-native", + "performance": true, + "constant_time": true, + "release": true + }, + "config": ["targets"], + "external_paths": [ + "scripts/ci/native-platform.sh", + "src/**/*riscv*", + "tests/aead_kernel_equivalence.rs", + "tests/portable_fallback.rs", + "tests/vectored_dispatch.rs" + ] + }, + { + "id": "s390x-unknown-linux-gnu", + "dimensions": { + "name": "IBM Z runtime", + "target": "s390x-unknown-linux-gnu", + "group": "ibm", + "operation": "native", + "platform": "ibm-s390x", + "runner_type": "github", + "runner": "ubuntu-24.04-s390x", + "contract": "nightly", + "components": "", + "timeout_minutes": 60, + "verify_plan": false, + "cache": false, + "compile": "native", + "runtime": "physical-native", + "performance": true, + "constant_time": true, + "release": true + }, + "config": ["targets"], + "external_paths": [ + "scripts/ci/native-platform.sh", + "src/**/*s390*", + "tests/aead_kernel_equivalence.rs", + "tests/portable_fallback.rs", + "tests/vectored_dispatch.rs" + ] + }, + { + "id": "thumbv6m-none-eabi", + "dimensions": { + "name": "Bare-metal Armv6-M compile", + "target": "thumbv6m-none-eabi", + "group": "no_std", + "operation": "cross", + "platform": "cross", + "runner_type": "runson", + "runner": "linux-x64-ci", + "contract": "development", + "components": "", + "timeout_minutes": 45, + "verify_plan": true, + "cache": false, + "compile": "generic", + "runtime": "none", + "performance": false, + "constant_time": false, + "release": true + }, + "config": ["targets"], + "external_paths": [ + "scripts/ci/cross-targets.sh", + "scripts/ci/nostd-wasm-suite.sh", + "scripts/lib/feature-profiles.sh" + ] + }, + { + "id": "wasm32-unknown-unknown", + "dimensions": { + "name": "WebAssembly compile", + "target": "wasm32-unknown-unknown", + "group": "wasm", + "operation": "cross", + "platform": "cross", + "runner_type": "runson", + "runner": "linux-x64-ci", + "contract": "development", + "components": "", + "timeout_minutes": 45, + "verify_plan": true, + "cache": false, + "compile": "generic", + "runtime": "none", + "performance": false, + "constant_time": false, + "release": true + }, + "config": ["targets"], + "external_paths": [ + "scripts/ci/cross-targets.sh", + "scripts/ci/nostd-wasm-suite.sh", + "scripts/lib/feature-profiles.sh", + "src/**/*wasm*", + "tools/wasm-runtime-vectors/**" + ] + }, + { + "id": "wasm32-wasip1", + "dimensions": { + "name": "WASI compile and emulated runtime", + "target": "wasm32-wasip1", + "group": "wasm", + "operation": "cross", + "platform": "cross", + "runner_type": "runson", + "runner": "linux-x64-ci", + "contract": "development", + "components": "", + "timeout_minutes": 45, + "verify_plan": true, + "cache": false, + "compile": "generic", + "runtime": "emulated", + "performance": false, + "constant_time": false, + "release": true + }, + "config": ["targets"], + "external_paths": [ + "scripts/ci/cross-targets.sh", + "scripts/ci/nostd-wasm-suite.sh", + "scripts/lib/feature-profiles.sh", + "src/**/*wasm*", + "tools/wasm-runtime-vectors/**" + ] + }, + { + "id": "x86-64-amx", + "dimensions": { + "name": "Linux x86-64 AMX permission", + "target": "x86_64-unknown-linux-gnu", + "group": "linux", + "operation": "amx", + "platform": "intel-spr", + "runner_type": "runson", + "runner": "intel-spr", + "contract": "nightly", + "components": "", + "timeout_minutes": 20, + "verify_plan": true, + "cache": true, + "compile": "native", + "runtime": "physical-native", + "performance": false, + "constant_time": false, + "release": true + }, + "config": ["targets"], + "external_paths": ["scripts/ci/native-platform.sh", "src/platform/**", "tests/platform_amx_permission.rs"] + }, + { + "id": "x86-64-apple-darwin", + "dimensions": { + "name": "macOS x86-64 runtime", + "target": "x86_64-apple-darwin", + "group": "macos", + "operation": "native", + "platform": "macos-x64", + "runner_type": "github", + "runner": "macos-15-intel", + "contract": "development", + "components": "", + "timeout_minutes": 45, + "verify_plan": false, + "cache": false, + "compile": "native", + "runtime": "virtual-native", + "performance": false, + "constant_time": false, + "release": true + }, + "config": ["targets"], + "external_paths": [ + "scripts/ci/native-platform.sh", + "src/**/*x86*", + "tests/aead_kernel_equivalence.rs", + "tests/portable_fallback.rs", + "tests/vectored_dispatch.rs" + ] + }, + { + "id": "x86-64-pc-windows-msvc", + "dimensions": { + "name": "Windows x86-64 runtime", + "target": "x86_64-pc-windows-msvc", + "group": "win", + "operation": "native", + "platform": "windows-x64", + "runner_type": "github", + "runner": "windows-latest", + "contract": "development", + "components": "clippy", + "timeout_minutes": 45, + "verify_plan": false, + "cache": true, + "compile": "native", + "runtime": "virtual-native", + "performance": false, + "constant_time": false, + "release": true + }, + "config": ["targets"], + "external_paths": [ + "scripts/ci/native-platform.sh", + "src/**/*x86*", + "tests/blake3_differential.rs", + "tests/blake3_official_vectors.rs" + ] + }, + { + "id": "x86-64-unknown-linux-gnu", + "dimensions": { + "name": "Linux x86-64 runtime (core owner)", + "target": "x86_64-unknown-linux-gnu", + "group": "linux", + "operation": "core", + "platform": "linux-x64", + "runner_type": "runson", + "runner": "linux-x64-ci", + "contract": "development", + "components": "", + "timeout_minutes": 0, + "verify_plan": true, + "cache": true, + "compile": "core", + "runtime": "virtual-native", + "performance": true, + "constant_time": true, + "release": true + }, + "config": ["targets"], + "external_paths": ["src/**/*x86*"] + }, + { + "id": "x86-64-unknown-linux-musl", + "dimensions": { + "name": "Linux x86-64 musl compile", + "target": "x86_64-unknown-linux-musl", + "group": "linux", + "operation": "cross", + "platform": "cross", + "runner_type": "runson", + "runner": "linux-x64-ci", + "contract": "development", + "components": "clippy", + "timeout_minutes": 30, + "verify_plan": true, + "cache": false, + "compile": "generic", + "runtime": "none", + "performance": false, + "constant_time": false, + "release": true + }, + "config": ["targets"], + "external_paths": ["scripts/ci/cross-targets.sh", "src/**/*x86*"] + }, + { + "id": "x86-64-unknown-none", + "dimensions": { + "name": "Bare-metal x86-64 compile", + "target": "x86_64-unknown-none", + "group": "no_std", + "operation": "cross", + "platform": "cross", + "runner_type": "runson", + "runner": "linux-x64-ci", + "contract": "development", + "components": "", + "timeout_minutes": 45, + "verify_plan": true, + "cache": false, + "compile": "generic", + "runtime": "none", + "performance": false, + "constant_time": false, + "release": true + }, + "config": ["targets"], + "external_paths": [ + "scripts/ci/cross-targets.sh", + "scripts/ci/nostd-wasm-suite.sh", + "scripts/lib/feature-profiles.sh", + "src/**/*x86*" + ] + } + ] } diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index d5686711..50944d37 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -19,3 +19,13 @@ self-hosted-runner: - ubuntu-24.04-s390x - ubuntu-24.04-ppc64le - ubuntu-24.04-ppc64le-p10 + + # Donated RISE RISC-V runner + - ubuntu-24.04-riscv + +paths: + ".github/workflows/*.{yml,yaml}": + ignore: + # actionlint 1.7.12 predates GitHub's exact-commit self-repository syntax. + - 'specifying action "\$/\.github/actions/[A-Za-z0-9._/-]+" in invalid format because ref is missing' + - 'reusable workflow call "\$/\.github/workflows/[A-Za-z0-9._/-]+" at "uses" is not following the format' diff --git a/.github/actions/plan/action.yaml b/.github/actions/plan/action.yaml new file mode 100644 index 00000000..d9c1d181 --- /dev/null +++ b/.github/actions/plan/action.yaml @@ -0,0 +1,30 @@ +name: Restore Cargo Rail plan +description: Restore and verify one Linux x86-64 plan artifact before execution. + +inputs: + artifact: + description: Saved plan artifact name. + required: true + identity: + description: Expected Cargo Rail plan identity. + required: true + head-commit: + description: Expected checkout commit. + required: true + +runs: + using: composite + steps: + - name: Download saved plan + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ inputs.artifact }} + path: ${{ runner.temp }}/rscrypto-cargo-rail-plan + + - name: Activate saved plan + shell: bash + env: + HEAD_COMMIT: ${{ inputs.head-commit }} + PLAN_IDENTITY: ${{ inputs.identity }} + PLAN_ROOT: ${{ runner.temp }}/rscrypto-cargo-rail-plan + run: scripts/ci/activate-plan.sh "$PLAN_ROOT" "$PLAN_IDENTITY" "$HEAD_COMMIT" diff --git a/.github/actions/rust/action.yaml b/.github/actions/rust/action.yaml new file mode 100644 index 00000000..ca979b9c --- /dev/null +++ b/.github/actions/rust/action.yaml @@ -0,0 +1,99 @@ +name: Setup Rust +description: Install one repository-owned Rust toolchain contract. + +inputs: + contract: + description: Toolchain contract (development, msrv, or nightly). + required: false + default: development + components: + description: Comma-separated rustup components required by the job. + required: false + default: "" + cache-url: + description: Cargo Rail remote cache authority; required when cache mode is enabled. + required: false + default: "" + cache-mode: + description: Cargo Rail remote cache authority (off, read, or read-write). + required: false + default: "off" + cache-access-key-id: + description: R2 S3 access key ID. + required: false + default: "" + cache-secret-access-key: + description: R2 S3 secret access key. + required: false + default: "" + +outputs: + toolchain: + description: Exact installed toolchain. + value: ${{ steps.resolve.outputs.toolchain }} + +runs: + using: composite + steps: + - id: resolve + name: Resolve toolchain + shell: bash + env: + CONTRACT: ${{ inputs.contract }} + run: | + case "$CONTRACT" in + development) toolchain=$(scripts/lib/toolchain.sh) ;; + msrv) toolchain=$(scripts/lib/toolchain.sh --msrv) ;; + nightly) toolchain=$(scripts/lib/toolchain.sh --nightly) ;; + *) echo "unsupported Rust toolchain contract: $CONTRACT" >&2; exit 2 ;; + esac + echo "toolchain=$toolchain" >>"$GITHUB_OUTPUT" + + - name: Install toolchain + shell: bash + env: + COMPONENTS: ${{ inputs.components }} + TOOLCHAIN: ${{ steps.resolve.outputs.toolchain }} + run: scripts/ci/setup-toolchain.sh "$TOOLCHAIN" "$COMPONENTS" "$GITHUB_ENV" + + - name: Authorize compiler cache + if: inputs.cache-mode != 'off' + shell: bash + env: + ACCESS_KEY_ID: ${{ inputs.cache-access-key-id }} + CACHE_MODE: ${{ inputs.cache-mode }} + CACHE_URL: ${{ inputs.cache-url }} + SECRET_ACCESS_KEY: ${{ inputs.cache-secret-access-key }} + run: | + case "$CACHE_MODE" in read|read-write) ;; *) echo "invalid cache mode: $CACHE_MODE" >&2; exit 2 ;; esac + [[ -n "$CACHE_URL" ]] || { echo "cache URL is required" >&2; exit 2; } + [[ -n "$ACCESS_KEY_ID" && -n "$SECRET_ACCESS_KEY" ]] || { + echo "complete cache credentials are required" >&2 + exit 2 + } + [[ "$ACCESS_KEY_ID" != *$'\n'* && "$SECRET_ACCESS_KEY" != *$'\n'* ]] || { + echo "cache credentials contain an invalid newline" >&2 + exit 2 + } + echo "::add-mask::$ACCESS_KEY_ID" + echo "::add-mask::$SECRET_ACCESS_KEY" + { + printf 'AWS_ACCESS_KEY_ID=%s\n' "$ACCESS_KEY_ID" + printf 'AWS_SECRET_ACCESS_KEY=%s\n' "$SECRET_ACCESS_KEY" + printf 'AWS_SESSION_TOKEN=\n' + printf 'AWS_PROFILE=\n' + printf 'AWS_SHARED_CREDENTIALS_FILE=\n' + printf 'AWS_REGION=auto\n' + printf 'AWS_DEFAULT_REGION=auto\n' + printf 'AWS_EC2_METADATA_DISABLED=true\n' + } >>"$GITHUB_ENV" + + - name: Configure compiler cache + if: inputs.cache-mode != 'off' + uses: loadingalias/cargo-rail-action/cache@78ad385a85627484a5b634cf1bc3caa1de872c6a # v8.2.0 + with: + version: "0.25.0" + url: ${{ inputs.cache-url }} + mode: ${{ inputs.cache-mode }} + root-portability: remap + strict-probe: "false" diff --git a/.github/actions/setup-toolchain/action.yaml b/.github/actions/setup-toolchain/action.yaml deleted file mode 100644 index 67e7ae40..00000000 --- a/.github/actions/setup-toolchain/action.yaml +++ /dev/null @@ -1,45 +0,0 @@ -name: Setup Rust Toolchain -description: | - Install the stable development toolchain, MSRV, or pinned nightly contract. - -inputs: - contract: - description: Toolchain contract to install (development, msrv, or nightly) - required: false - default: "development" - components: - description: Comma-separated list of components to install - required: false - default: "clippy, rustfmt" - -outputs: - toolchain: - description: Exact installed toolchain version - value: ${{ steps.read-toolchain.outputs.toolchain }} - -runs: - using: composite - steps: - - name: Read Toolchain Version - id: read-toolchain - shell: bash - env: - TOOLCHAIN_CONTRACT: ${{ inputs.contract }} - run: | - case "$TOOLCHAIN_CONTRACT" in - development) TOOLCHAIN=$(bash scripts/lib/toolchain.sh) ;; - msrv) TOOLCHAIN=$(bash scripts/lib/toolchain.sh --msrv) ;; - nightly) TOOLCHAIN=$(bash scripts/lib/toolchain.sh --nightly) ;; - *) echo "Unsupported toolchain contract: $TOOLCHAIN_CONTRACT" >&2; exit 2 ;; - esac - echo "toolchain=$TOOLCHAIN" >> "$GITHUB_OUTPUT" - echo "Using toolchain: $TOOLCHAIN" - - - name: Install Rust Toolchain - shell: bash - env: - TOOLCHAIN: ${{ steps.read-toolchain.outputs.toolchain }} - TOOLCHAIN_COMPONENTS: ${{ inputs.components }} - run: | - scripts/ci/setup-toolchain.sh "$TOOLCHAIN" "$TOOLCHAIN_COMPONENTS" "$GITHUB_ENV" - RUSTUP_TOOLCHAIN="$TOOLCHAIN" rustc --version --verbose diff --git a/.github/actions/setup/action.yaml b/.github/actions/setup/action.yaml deleted file mode 100644 index 631a1fae..00000000 --- a/.github/actions/setup/action.yaml +++ /dev/null @@ -1,129 +0,0 @@ -name: Setup rscrypto Environment -description: | - Install the Rust toolchain, configure Cargo Rail compiler reuse, and install cargo tools. - Unified host setup for GHA + runs-on.com + IBM/RISC-V runners. - -inputs: - toolchain-contract: - description: Toolchain contract to install (development, msrv, or nightly). - required: false - default: "development" - tools-mode: - description: Tool install mode (standard, quality, release, rail, ci, supply-chain, bench, ibm, fuzz, coverage, ct-linux, minimal, none). - required: false - default: "standard" - toolchain-components: - description: Comma-separated rust toolchain components. - required: false - default: "clippy, rustfmt, rust-src" - cache-url: - description: Machine-owned Cargo Rail L2 URL. Empty leaves existing machine cache policy unchanged. - required: false - default: "" - cache-mode: - description: Cargo Rail remote authority (read or read-write). - required: false - default: "read" - cache-access-key-id: - description: Caller-selected Cloudflare R2 access key for the requested cache authority. - required: false - default: "" - cache-secret-access-key: - description: Caller-selected Cloudflare R2 secret key for the requested cache authority. - required: false - default: "" - cache-max-size: - description: Positive bound for the job-local verified compiler cache. - required: false - default: "10GiB" - cache-root-portability: - description: Qualified workspace-root policy (physical or remap). - required: false - default: "remap" - require-cargo-rail: - description: Ensure the exact Cargo Rail core is available for saved-plan verification. - required: false - default: "false" -outputs: - cache-enabled: - description: Whether authenticated Cargo Rail cache setup and its strict probe succeeded. - value: ${{ steps.cargo-rail-cache.outcome == 'success' && 'true' || 'false' }} -runs: - using: composite - steps: - - name: Setup Toolchain - uses: ./.github/actions/setup-toolchain - with: - contract: ${{ inputs.toolchain-contract }} - components: ${{ inputs.toolchain-components }} - - - name: Select Cargo Rail Cache Capability - id: cache-capability - shell: bash - env: - CACHE_URL: ${{ inputs.cache-url }} - run: | - if [[ -z "$CACHE_URL" ]]; then - echo "enabled=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - case "$(uname -m)" in - s390x | ppc64le) - echo "::notice::Cargo Rail Action v8.2.0 has no verified native cache archive for $(uname -m); continuing without compiler reuse" - echo "enabled=false" >> "$GITHUB_OUTPUT" - ;; - *) echo "enabled=true" >> "$GITHUB_OUTPUT" ;; - esac - - - name: Authenticate Cargo Rail Cache - if: steps.cache-capability.outputs.enabled == 'true' - shell: bash - env: - CACHE_ACCESS_KEY_ID: ${{ inputs.cache-access-key-id }} - CACHE_MODE: ${{ inputs.cache-mode }} - CACHE_SECRET_ACCESS_KEY: ${{ inputs.cache-secret-access-key }} - CACHE_URL: ${{ inputs.cache-url }} - run: | - [[ "$CACHE_URL" == r2://* ]] \ - || { echo "rscrypto's shared Cargo Rail cache must use its canonical Cloudflare R2 authority" >&2; exit 2; } - case "$CACHE_MODE" in - read | read-write) ;; - *) - echo "Unsupported Cargo Rail cache mode: $CACHE_MODE" >&2 - exit 2 - ;; - esac - [[ -n "$CACHE_ACCESS_KEY_ID" && -n "$CACHE_SECRET_ACCESS_KEY" ]] \ - || { echo "The caller-selected R2 access key and secret key are required when the shared cache is enabled" >&2; exit 2; } - printf '::add-mask::%s\n' "$CACHE_ACCESS_KEY_ID" "$CACHE_SECRET_ACCESS_KEY" - { - printf 'AWS_ACCESS_KEY_ID=%s\n' "$CACHE_ACCESS_KEY_ID" - printf 'AWS_SECRET_ACCESS_KEY=%s\n' "$CACHE_SECRET_ACCESS_KEY" - printf 'AWS_SESSION_TOKEN=\n' - printf 'AWS_PROFILE=\n' - printf 'AWS_SHARED_CREDENTIALS_FILE=\n' - printf 'AWS_REGION=auto\n' - printf 'AWS_DEFAULT_REGION=auto\n' - printf 'AWS_EC2_METADATA_DISABLED=true\n' - } >> "$GITHUB_ENV" - - - name: Setup Cargo Rail Cache - id: cargo-rail-cache - if: steps.cache-capability.outputs.enabled == 'true' - uses: loadingalias/cargo-rail-action/cache@78ad385a85627484a5b634cf1bc3caa1de872c6a # v8.2.0 - with: - version: "0.25.0" - url: ${{ inputs.cache-url }} - mode: ${{ inputs.cache-mode }} - max-size: ${{ inputs.cache-max-size }} - root-portability: ${{ inputs.cache-root-portability }} - strict-probe: "true" - - - name: Install Cargo Tools - if: inputs.tools-mode != 'none' || inputs.require-cargo-rail == 'true' - shell: bash - env: - RSCRYPTO_AUTHENTICATED_CARGO_RAIL: ${{ steps.cargo-rail-cache.outcome == 'success' && 'true' || 'false' }} - RSCRYPTO_REQUIRE_CARGO_RAIL: ${{ inputs.require-cargo-rail }} - TOOLS_MODE: ${{ inputs.tools-mode }} - run: scripts/ci/install-tools.sh "$TOOLS_MODE" diff --git a/.github/repository-settings/release-immutability.json b/.github/repository-settings/release-immutability.json deleted file mode 100644 index 4e609c71..00000000 --- a/.github/repository-settings/release-immutability.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "enabled": true -} diff --git a/.github/rulesets/protect-main.json b/.github/rulesets/protect-main.json deleted file mode 100644 index e85538ac..00000000 --- a/.github/rulesets/protect-main.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "protect-main", - "target": "branch", - "enforcement": "active", - "conditions": { - "ref_name": { - "exclude": [], - "include": ["~DEFAULT_BRANCH"] - } - }, - "bypass_actors": [], - "rules": [ - { - "type": "deletion" - }, - { - "type": "non_fast_forward" - }, - { - "type": "pull_request", - "parameters": { - "required_approving_review_count": 0, - "dismiss_stale_reviews_on_push": false, - "required_reviewers": [], - "require_code_owner_review": false, - "require_last_push_approval": false, - "require_extra_approval_for_unattributed_changes": true, - "required_review_thread_resolution": true, - "allowed_merge_methods": ["merge", "squash", "rebase"] - } - }, - { - "type": "required_status_checks", - "parameters": { - "strict_required_status_checks_policy": true, - "do_not_enforce_on_create": false, - "required_status_checks": [ - { - "context": "Complete", - "integration_id": 15368 - } - ] - } - } - ] -} diff --git a/.github/rulesets/protect-release-tags.json b/.github/rulesets/protect-release-tags.json deleted file mode 100644 index 1dfe0072..00000000 --- a/.github/rulesets/protect-release-tags.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "protect-release-tags", - "target": "tag", - "enforcement": "active", - "conditions": { - "ref_name": { - "exclude": [], - "include": ["refs/tags/v*"] - } - }, - "bypass_actors": [], - "rules": [ - { - "type": "deletion" - }, - { - "type": "update" - } - ] -} diff --git a/.github/runs-on.yml b/.github/runs-on.yml index 05340c37..0867c700 100644 --- a/.github/runs-on.yml +++ b/.github/runs-on.yml @@ -91,11 +91,11 @@ runners: - arch:aarch64 - uarch:neoverse-v2 - # General CI runners (backup/overflow only). + # One predictable build host replaces the old many-job CI fan-out. linux-x64-ci: - cpu: [2, 4] - ram: [4, 8] - family: ["c7i", "m7i", "c7a", "m7a", "c6i", "m6i"] + cpu: 8 + ram: 16 + family: ["c7i", "c7a"] image: ubuntu24-full-x64 spot: false volume: 100gb:gp3 @@ -105,9 +105,9 @@ runners: - arch:x86-64 linux-arm64-ci: - cpu: [2, 4] - ram: [4, 8] - family: ["c7g", "m7g", "c8g", "m8g"] + cpu: 8 + ram: 16 + family: ["c8g", "c7g"] image: ubuntu24-full-arm64 spot: false volume: 100gb:gp3 diff --git a/.github/workflows/_ci-suite.yaml b/.github/workflows/_ci-suite.yaml deleted file mode 100644 index 1a76461d..00000000 --- a/.github/workflows/_ci-suite.yaml +++ /dev/null @@ -1,81 +0,0 @@ -name: _ci-suite - -# Cargo Rail owns row selection. This workflow only maps selected, typed rows -# onto repository-owned commands and runner policy. -on: - workflow_call: - inputs: - matrix: - description: "Strict Cargo Rail ci-suite matrix object." - required: true - type: string - plan_artifact: - description: "Artifact containing the exact plan and its strict reader." - required: true - type: string - plan_identity: - description: "Root-independent identity of the exact plan." - required: true - type: string - plan_head_commit: - description: "Immutable checkout commit bound to the plan." - required: true - type: string - supply_chain_mode: - description: "'light' (advisories) or 'full' (deny all plus audit)." - required: true - type: string - test_mode: - description: "Test mode passed to rscrypto test scripts." - required: false - type: string - default: local - artifact_retention_days: - description: "Retention for evidence artifacts produced by this suite." - required: false - type: number - default: 90 - cache_mode: - description: "Provider-enforced Cargo Rail cache authority for selected work." - required: false - type: string - default: read - secrets: - cache_access_key_id: - description: "Caller-selected bucket-scoped R2 access key for the selected cache mode." - required: false - cache_secret_access_key: - description: "Caller-selected bucket-scoped R2 secret key for the selected cache mode." - required: false - -permissions: - contents: read - -jobs: - selected: - name: ${{ matrix.work.display_name }} - strategy: - fail-fast: false - matrix: ${{ fromJSON(inputs.matrix) }} - uses: ./.github/workflows/_rust-job.yaml - with: - runner: ${{ matrix.work.runner_type == 'runson' && format('runs-on={0}/runner={1}', github.run_id, matrix.work.runner) || matrix.work.runner }} - timeout_minutes: ${{ matrix.work.timeout_minutes }} - tools_mode: ${{ matrix.work.tools_mode }} - toolchain_contract: ${{ matrix.work.toolchain_contract }} - toolchain_components: ${{ matrix.work.toolchain_components }} - cache_mode: ${{ inputs.cache_mode }} - operation: ${{ matrix.work.operation }} - target: ${{ matrix.work.target || '' }} - test_mode: ${{ inputs.test_mode }} - supply_chain_mode: ${{ inputs.supply_chain_mode }} - plan_artifact: ${{ inputs.plan_artifact }} - plan_identity: ${{ inputs.plan_identity }} - plan_head_commit: ${{ inputs.plan_head_commit }} - artifact_name: ${{ matrix.work.operation == 'cargo-graph' && format('cargo-graph-{0}', inputs.test_mode) || '' }} - artifact_path: ${{ matrix.work.operation == 'cargo-graph' && 'target/cargo-rail/unify-result.json' || '' }} - artifact_always: true - artifact_retention_days: ${{ inputs.artifact_retention_days }} - secrets: - cache_access_key_id: ${{ secrets.cache_access_key_id }} - cache_secret_access_key: ${{ secrets.cache_secret_access_key }} diff --git a/.github/workflows/_rust-job.yaml b/.github/workflows/_rust-job.yaml deleted file mode 100644 index 881a883a..00000000 --- a/.github/workflows/_rust-job.yaml +++ /dev/null @@ -1,246 +0,0 @@ -name: _rust-job - -on: - workflow_call: - inputs: - operation: - required: true - type: string - runner: - required: true - type: string - checkout_ref: - required: false - type: string - default: "" - timeout_minutes: - required: false - type: number - default: 30 - tools_mode: - required: false - type: string - default: standard - toolchain_contract: - required: false - type: string - default: development - toolchain_components: - required: false - type: string - default: "clippy, rustfmt, rust-src" - cache_mode: - required: false - type: string - default: read - test_mode: - required: false - type: string - default: "" - target: - required: false - type: string - default: "" - platform: - required: false - type: string - default: "" - supply_chain_mode: - required: false - type: string - default: "" - base_sha: - required: false - type: string - default: "" - plan_artifact: - required: false - type: string - default: "" - plan_identity: - required: false - type: string - default: "" - plan_head_commit: - required: false - type: string - default: "" - miri_tree_borrows: - required: false - type: boolean - default: false - bench_targets: - required: false - type: string - default: all - bench_filter: - required: false - type: string - default: "" - bench_quick: - required: false - type: boolean - default: false - dudect_timeout: - required: false - type: number - default: 1800 - dudect_filter: - required: false - type: string - default: "" - dudect_gate: - required: false - type: string - default: required - binsec_timeout: - required: false - type: number - default: 900 - rustflags: - required: false - type: string - default: "" - upload_raw_artifacts: - required: false - type: boolean - default: false - artifact_name: - required: false - type: string - default: "" - artifact_path: - required: false - type: string - default: "" - artifact_always: - required: false - type: boolean - default: true - artifact_retention_days: - required: false - type: number - default: 90 - secrets: - cache_access_key_id: - description: "Caller-selected bucket-scoped R2 access key for this job's cache mode." - required: false - cache_secret_access_key: - description: "Caller-selected bucket-scoped R2 secret key for this job's cache mode." - required: false - -permissions: - contents: read - -jobs: - run: - runs-on: ${{ inputs.runner }} - timeout-minutes: ${{ inputs.timeout_minutes }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - ref: ${{ inputs.plan_head_commit || inputs.checkout_ref || github.sha }} - - - name: Download exact work plan - if: inputs.plan_artifact != '' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ inputs.plan_artifact }} - path: ${{ runner.temp }}/cargo-rail-plan - - - name: Setup - id: setup - uses: ./.github/actions/setup - with: - toolchain-contract: ${{ inputs.toolchain_contract }} - tools-mode: ${{ inputs.tools_mode }} - toolchain-components: ${{ inputs.toolchain_components }} - cache-url: ${{ secrets.cache_access_key_id != '' && secrets.cache_secret_access_key != '' && vars.CARGO_RAIL_CACHE_URL || '' }} - cache-mode: ${{ inputs.cache_mode }} - cache-access-key-id: ${{ secrets.cache_access_key_id }} - cache-secret-access-key: ${{ secrets.cache_secret_access_key }} - require-cargo-rail: ${{ inputs.plan_artifact != '' }} - - - name: Verify checkout against exact work plan - if: inputs.plan_artifact != '' - shell: bash - env: - EXPECTED_PLAN_IDENTITY: ${{ inputs.plan_identity }} - PLAN_DIRECTORY: ${{ runner.temp }}/cargo-rail-plan - run: | - PLAN_FILE=$PLAN_DIRECTORY/plan.json - PLAN_READER=$PLAN_DIRECTORY/read.py - PYTHON=$(scripts/lib/python.sh --print) - [[ "$("$PYTHON" "$PLAN_READER" identity "$PLAN_FILE")" == "$EXPECTED_PLAN_IDENTITY" ]] - "$PYTHON" "$PLAN_READER" verify-checkout "$PLAN_FILE" - - - name: Run - shell: bash - env: - CARGO_TARGET_S390X_UNKNOWN_LINUX_GNU_RUSTFLAGS: ${{ inputs.target == 's390x-unknown-linux-gnu' && '-C target-feature=+vector' || '' }} - RSCRYPTO_CI_RUSTFLAGS: ${{ inputs.rustflags }} - RSCRYPTO_CI_OPERATION: ${{ inputs.operation }} - RSCRYPTO_CI_RUNNER: ${{ inputs.runner }} - RSCRYPTO_CI_TEST_MODE: ${{ inputs.test_mode }} - RSCRYPTO_CI_TARGET: ${{ inputs.target }} - RSCRYPTO_CI_PLATFORM: ${{ inputs.platform }} - RSCRYPTO_CI_SUPPLY_CHAIN_MODE: ${{ inputs.supply_chain_mode }} - RSCRYPTO_CI_BASE_SHA: ${{ inputs.base_sha }} - RSCRYPTO_CI_MIRI_TREE_BORROWS: ${{ inputs.miri_tree_borrows && 'true' || 'false' }} - RSCRYPTO_CI_BENCH_TARGETS: ${{ inputs.bench_targets }} - RSCRYPTO_CI_BENCH_FILTER: ${{ inputs.bench_filter }} - RSCRYPTO_CI_BENCH_QUICK: ${{ inputs.bench_quick && 'true' || 'false' }} - RSCRYPTO_CI_DUDECT_TIMEOUT: ${{ inputs.dudect_timeout }} - RSCRYPTO_CI_DUDECT_FILTER: ${{ inputs.dudect_filter }} - RSCRYPTO_CI_DUDECT_GATE: ${{ inputs.dudect_gate }} - RSCRYPTO_CI_BINSEC_TIMEOUT: ${{ inputs.binsec_timeout }} - RSCRYPTO_CI_UPLOAD_RAW_ARTIFACTS: ${{ inputs.upload_raw_artifacts && 'true' || 'false' }} - RAIL_PLAN_FILE: ${{ inputs.plan_artifact != '' && format('{0}/cargo-rail-plan/plan.json', runner.temp) || '' }} - RAIL_PLAN_READER: ${{ inputs.plan_artifact != '' && format('{0}/cargo-rail-plan/read.py', runner.temp) || '' }} - RAIL_PLAN_CHECKOUT_VERIFIED: ${{ inputs.plan_artifact != '' && 'true' || 'false' }} - run: | - if [[ -n "$RSCRYPTO_CI_RUSTFLAGS" ]]; then - export RUSTFLAGS="$RSCRYPTO_CI_RUSTFLAGS" - fi - exec scripts/ci/run-rust-job.sh - - - name: Capture Cargo Rail Cache Status - id: cache-status - if: ${{ !cancelled() && steps.setup.outputs.cache-enabled == 'true' }} - shell: bash - env: - RSCRYPTO_CI_OPERATION: ${{ inputs.operation }} - RSCRYPTO_CI_PLATFORM: ${{ inputs.platform }} - RSCRYPTO_CI_RUNNER: ${{ inputs.runner }} - RSCRYPTO_CI_TARGET: ${{ inputs.target }} - run: scripts/ci/capture-cache-status.sh - - - name: Upload Cargo Rail Cache Status - if: ${{ !cancelled() && steps.setup.outputs.cache-enabled == 'true' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ steps.cache-status.outputs.artifact_name }} - path: target/cargo-rail/cache-status.json - retention-days: 14 - if-no-files-found: error - - - name: Upload Artifact after completion - if: ${{ !cancelled() && inputs.artifact_name != '' && inputs.artifact_path != '' && inputs.artifact_always }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ inputs.artifact_name }} - path: ${{ inputs.artifact_path }} - retention-days: ${{ inputs.artifact_retention_days }} - if-no-files-found: error - - - name: Upload Artifact (success) - if: inputs.artifact_name != '' && inputs.artifact_path != '' && !inputs.artifact_always - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ inputs.artifact_name }} - path: ${{ inputs.artifact_path }} - retention-days: ${{ inputs.artifact_retention_days }} - if-no-files-found: error diff --git a/.github/workflows/bench.yaml b/.github/workflows/bench.yaml index f1db6bdb..0bbaf853 100644 --- a/.github/workflows/bench.yaml +++ b/.github/workflows/bench.yaml @@ -1,86 +1,88 @@ -name: Bench +name: Benchmarks on: workflow_dispatch: inputs: targets: - description: "PRIMITIVES/SELECTORS (Empty or all = all public primitive benches; aead_diag = diagnostic AEAD rows)" + description: Primitive or benchmark selectors (comma separated, or all). required: false type: string - default: "all" + default: all platforms: - description: "RUNNERS (Empty = All). Options: zen4, spr, icl, zen5, g3, g4, s390x, power10." + description: Platform aliases (comma separated, or all). required: false type: string - default: "all" + default: all filter: - description: "FILTERS (Optional)" + description: Optional benchmark filter. required: false type: string quick: - description: "QUICK (Optional)" + description: Use the short measurement profile. required: false type: boolean default: true -env: - RUST_BACKTRACE: 1 - CARGO_TERM_COLOR: always - RSCRYPTO_BENCH_MODE: ci - CARGO_INCREMENTAL: 0 - permissions: contents: read +env: + CARGO_INCREMENTAL: 0 + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + jobs: plan: + name: Plan runs-on: ubuntu-latest + timeout-minutes: 5 outputs: - has_targets: ${{ steps.plan.outputs.has_targets }} - matrix: ${{ steps.plan.outputs.matrix }} + matrix: ${{ steps.matrix.outputs.matrix }} steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 1 persist-credentials: false - - - name: Build Lane Matrix - id: plan - shell: bash + - id: matrix + name: Select platforms env: - GH_RUN_ID: ${{ github.run_id }} BENCH_PLATFORMS: ${{ inputs.platforms }} + GH_RUN_ID: ${{ github.run_id }} run: scripts/ci/emit-manual-matrix.sh bench - bench: - name: Benchmark (${{ matrix.display_name }}) + benchmark: + name: ${{ matrix.display_name }} needs: plan - if: ${{ needs.plan.outputs.has_targets == 'true' }} - permissions: - contents: read - concurrency: - # Multiple bench runs can overlap across lanes; serialize per lane. - group: bench-${{ github.ref }}-${{ matrix.platform }} - cancel-in-progress: false strategy: fail-fast: false matrix: include: ${{ fromJSON(needs.plan.outputs.matrix) }} - uses: ./.github/workflows/_rust-job.yaml - with: - runner: ${{ matrix.runner }} - timeout_minutes: ${{ matrix.timeout_minutes }} - tools_mode: ${{ matrix.tools_mode }} - toolchain_contract: ${{ (matrix.platform == 'ibm-s390x' || matrix.platform == 'ibm-power10') && 'nightly' || 'development' }} - toolchain_components: ${{ matrix.toolchain_components }} - operation: benchmark - platform: ${{ matrix.platform }} - bench_targets: ${{ inputs.targets || 'all' }} - bench_filter: ${{ inputs.filter }} - bench_quick: ${{ inputs.quick }} - artifact_name: benchmark-${{ matrix.artifact_suffix }} - artifact_path: target/benchmark_results/ - secrets: - cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} + runs-on: ${{ matrix.runner }} + timeout-minutes: ${{ matrix.timeout_minutes }} + concurrency: + group: benchmark-${{ github.ref }}-${{ matrix.platform }} + cancel-in-progress: false + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: $/.github/actions/rust + with: + contract: ${{ (matrix.platform == 'ibm-s390x' || matrix.platform == 'ibm-power10' || matrix.platform == 'rise-riscv') && 'nightly' || 'development' }} + - name: Measure + env: + BENCH_FILTER: ${{ inputs.filter }} + BENCH_ONLY: ${{ inputs.targets }} + BENCH_OUTPUT_DIR: target/benchmark_results + BENCH_PLATFORM: ${{ matrix.platform }} + BENCH_QUICK: ${{ inputs.quick }} + BENCH_RESULTS_DIR: target/benchmark_results + RSCRYPTO_BENCH_MODE: ci + run: scripts/ci/run-bench.sh + - name: Upload results + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: benchmark-${{ matrix.artifact_suffix }} + path: target/benchmark_results/ + retention-days: 30 + if-no-files-found: error diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7c540bf9..5ddd01f2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,168 +8,410 @@ on: workflow_dispatch: concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + group: ci-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true -env: - RUST_BACKTRACE: 1 - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: 0 - permissions: contents: read +env: + CARGO_INCREMENTAL: 0 + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + jobs: - rail-plan: - name: Cargo Rail Plan - if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.actor != 'dependabot[bot]') }} + plan: + name: Plan and policy + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: ubuntu-latest + timeout-minutes: 10 outputs: - matrix: ${{ steps.matrix.outputs.matrix }} - has-suite: ${{ steps.matrix.outputs.has_suite }} - identity: ${{ steps.rail.outputs.plan-identity }} + artifact: ${{ steps.stage.outputs.artifact }} + core: ${{ steps.select.outputs.core }} + ct: ${{ steps.select.outputs.ct }} + features: ${{ steps.select.outputs.features }} + feature-matrix: ${{ steps.select.outputs.feature-matrix }} + fuzz: ${{ steps.select.outputs.fuzz }} + fuzz-rows: ${{ steps.select.outputs.fuzz-rows }} head-commit: ${{ steps.rail.outputs.head-commit }} - required-work: ${{ steps.rail.outputs.required-work }} + identity: ${{ steps.rail.outputs.plan-identity }} + miri: ${{ steps.select.outputs.miri }} + miri-rows: ${{ steps.select.outputs.miri-rows }} + platform-matrix: ${{ steps.select.outputs.platform-matrix }} + platforms: ${{ steps.select.outputs.platforms }} + rsa: ${{ steps.select.outputs.rsa }} + supply-chain: ${{ steps.select.outputs.supply-chain }} + tests: ${{ steps.select.outputs.tests }} steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - name: Build exact work plan - id: rail + - id: rail + name: Select affected work uses: loadingalias/cargo-rail-action@78ad385a85627484a5b634cf1bc3caa1de872c6a # v8.2.0 with: version: "0.25.0" - components: surface since: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'push' && github.event.before || '' }} all: ${{ github.event_name == 'workflow_dispatch' }} - - name: Materialize selected CI matrix - id: matrix + - id: select + name: Export executor decisions shell: bash env: - PLAN_FILE: ${{ steps.rail.outputs.plan-file }} - PLAN_READER: ${{ steps.rail.outputs.plan-reader }} - run: scripts/ci/materialize-rail-plan.sh "$PLAN_FILE" "$PLAN_READER" "$GITHUB_OUTPUT" - - - name: Check public source surface - if: contains(fromJSON(steps.rail.outputs.required-work), 'surface') - shell: bash + PLAN: ${{ steps.rail.outputs.plan-file }} + READER: ${{ steps.rail.outputs.plan-reader }} run: | - mkdir -p target/cargo-rail - cargo rail surface --check --json > target/cargo-rail/surface-result.json + set -euo pipefail + required_any() { + local decision work + for work in "$@"; do + decision=$(python3 "$READER" is-required "$PLAN" "$work") + case "$decision" in + true) echo true; return ;; + false) ;; + *) echo "invalid decision for $work: $decision" >&2; exit 2 ;; + esac + done + echo false + } + feature_matrix=$( + RAIL_PLAN_FILE="$PLAN" RAIL_PLAN_READER="$READER" \ + scripts/check/feature-contracts.sh matrix + ) + feature_count=$(jq -r '.include | length' <<<"$feature_matrix") + platform_matrix=$(scripts/ci/target-contracts.sh matrix "$PLAN" "$READER") + platform_count=$(jq -r '.include | length' <<<"$platform_matrix") + miri_rows=$(scripts/test/miri-contracts.sh rows "$PLAN" "$READER") + fuzz_rows=$(scripts/test/fuzz-contracts.sh rows "$PLAN" "$READER") + { + echo "actions=$(required_any policy.actions)" + echo "core=$(required_any cargo.fmt cargo.build cargo.clippy cargo.doc cargo.doctest cargo.test contracts.auxiliary)" + echo "ct=$(required_any assurance.ct)" + [[ "$feature_count" -gt 0 ]] && echo "features=true" || echo "features=false" + echo "feature-matrix=$feature_matrix" + [[ -n "$fuzz_rows" ]] && echo "fuzz=true" || echo "fuzz=false" + echo "fuzz-rows=$fuzz_rows" + [[ -n "$miri_rows" ]] && echo "miri=true" || echo "miri=false" + echo "miri-rows=$miri_rows" + echo "platform-matrix=$platform_matrix" + [[ "$platform_count" -gt 0 ]] && echo "platforms=true" || echo "platforms=false" + echo "rsa=$(required_any assurance.rsa)" + echo "supply-chain=$(required_any dependency-policy dependencies.auxiliary)" + echo "tests=$(required_any cargo.test)" + } >>"$GITHUB_OUTPUT" + + - name: Install actionlint + if: steps.select.outputs.actions == 'true' + run: scripts/ci/install-actionlint.sh + + - name: Install Zizmor + if: steps.select.outputs.actions == 'true' + uses: taiki-e/install-action@6a1bd70eaac3c8bdf093356838d7ee09fda951cf # v2.85.5 + with: + tool: zizmor@1.30.0 + fallback: none + + - name: Run affected repository policy + env: + RAIL_PLAN_FILE: ${{ steps.rail.outputs.plan-file }} + RAIL_PLAN_READER: ${{ steps.rail.outputs.plan-reader }} + RAIL_PLAN_IDENTITY: ${{ steps.rail.outputs.plan-identity }} + RAIL_PLAN_HEAD_COMMIT: ${{ steps.rail.outputs.head-commit }} + run: scripts/check/policy.sh - - name: Check Release Intent Coverage - if: github.event_name == 'pull_request' && !(startsWith(github.head_ref, 'rail/release-') && github.event.pull_request.head.repo.full_name == github.repository) + - name: Check release intent + if: github.event_name == 'pull_request' && github.actor != 'dependabot[bot]' && !(startsWith(github.head_ref, 'rail/release-') && github.event.pull_request.head.repo.full_name == github.repository) env: RAIL_BASE_REF: ${{ steps.rail.outputs.base }} run: cargo rail change check --since "$RAIL_BASE_REF" --required - - name: Upload exact work plan + - id: stage + name: Stage immutable plan + shell: bash + env: + PLAN: ${{ steps.rail.outputs.plan-file }} + READER: ${{ steps.rail.outputs.plan-reader }} + run: | + set -euo pipefail + root="$RUNNER_TEMP/rscrypto-cargo-rail-plan" + artifact="cargo-rail-plan-$GITHUB_RUN_ID" + mkdir -p "$root" + install -m 600 "$PLAN" "$root/plan.json" + install -m 700 "$READER" "$root/read.py" + install -m 700 "$(command -v cargo-rail)" "$root/cargo-rail" + echo "artifact=$artifact" >>"$GITHUB_OUTPUT" + + - name: Upload immutable plan uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: cargo-rail-plan-${{ github.run_id }} - path: | - ${{ steps.rail.outputs.plan-file }} - ${{ steps.rail.outputs.plan-reader }} - retention-days: 1 + name: ${{ steps.stage.outputs.artifact }} + path: ${{ runner.temp }}/rscrypto-cargo-rail-plan/ + retention-days: 7 if-no-files-found: error - - name: Upload Surface evidence - if: ${{ !cancelled() && contains(fromJSON(steps.rail.outputs.required-work), 'surface') }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + core: + name: Rust + needs: plan + if: needs.plan.outputs.core == 'true' + runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - name: cargo-rail-surface-${{ github.run_id }} - path: target/cargo-rail/surface-result.json - retention-days: 14 - if-no-files-found: error + persist-credentials: false + - uses: $/.github/actions/plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - uses: $/.github/actions/rust + with: + components: clippy,rustfmt + cache-url: ${{ vars.CARGO_RAIL_CACHE_URL }} + cache-mode: ${{ vars.CARGO_RAIL_CACHE_URL != '' && github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && 'read-write' || vars.CARGO_RAIL_CACHE_URL != '' && secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && 'read' || 'off' }} + cache-access-key-id: ${{ github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID || secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} + cache-secret-access-key: ${{ github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY || secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} + - name: Install test runner + if: needs.plan.outputs.tests == 'true' + uses: taiki-e/install-action@6a1bd70eaac3c8bdf093356838d7ee09fda951cf # v2.85.5 + with: + tool: cargo-nextest@0.9.143 + fallback: none + - name: Check, lint, and document + env: + RSCRYPTO_SKIP_CHECK_SUPPLY_CHAIN: 1 + run: scripts/check/check.sh + - name: Test + env: + RSCRYPTO_TEST_MODE: commit + run: scripts/test/test.sh - dependency-smoke: - name: Dependency Smoke - if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.draft && github.actor == 'dependabot[bot]' }} - permissions: - contents: read - uses: ./.github/workflows/_rust-job.yaml - with: - runner: ubuntu-latest - timeout_minutes: 45 - tools_mode: supply-chain - toolchain_components: "clippy, rustfmt" - operation: dependabot-smoke - base_sha: ${{ github.event.pull_request.base.sha }} - secrets: - cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - suite: - name: CI Suite - needs: rail-plan - if: ${{ !cancelled() && github.event_name != 'push' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && needs.rail-plan.result == 'success' && needs.rail-plan.outputs.has-suite == 'true' }} - permissions: - contents: read - uses: ./.github/workflows/_ci-suite.yaml - with: - matrix: ${{ needs.rail-plan.outputs.matrix }} - plan_artifact: cargo-rail-plan-${{ github.run_id }} - plan_identity: ${{ needs.rail-plan.outputs.identity }} - plan_head_commit: ${{ needs.rail-plan.outputs.head-commit }} - supply_chain_mode: light - test_mode: commit - secrets: - cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - cache-seed: - name: Affected Main Cache Seeder - needs: rail-plan - if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.rail-plan.result == 'success' && needs.rail-plan.outputs.has-suite == 'true' }} - permissions: - contents: read - uses: ./.github/workflows/_ci-suite.yaml - with: - matrix: ${{ needs.rail-plan.outputs.matrix }} - plan_artifact: cargo-rail-plan-${{ github.run_id }} - plan_identity: ${{ needs.rail-plan.outputs.identity }} - plan_head_commit: ${{ needs.rail-plan.outputs.head-commit }} - supply_chain_mode: light - test_mode: commit - cache_mode: read-write - secrets: - cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID }} - cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY }} + features: + name: Features (${{ matrix.domain }} ${{ matrix.shard }}) + needs: plan + if: needs.plan.outputs.features == 'true' + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.plan.outputs.feature-matrix) }} + runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: $/.github/actions/plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - run: scripts/ci/require-work.sh contracts.features + - uses: $/.github/actions/rust + with: + cache-url: ${{ vars.CARGO_RAIL_CACHE_URL }} + cache-mode: ${{ vars.CARGO_RAIL_CACHE_URL != '' && github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && 'read-write' || vars.CARGO_RAIL_CACHE_URL != '' && secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && 'read' || 'off' }} + cache-access-key-id: ${{ github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID || secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} + cache-secret-access-key: ${{ github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY || secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} + - env: + FEATURE_DOMAIN: ${{ matrix.domain }} + FEATURE_PROFILES: ${{ matrix.profiles }} + FEATURE_SHARD: ${{ matrix.shard }} + run: scripts/check/feature-contracts.sh selected "$FEATURE_DOMAIN" "$FEATURE_SHARD" "$FEATURE_PROFILES" + + platforms: + name: ${{ matrix.name }} + needs: plan + if: needs.plan.outputs.platforms == 'true' + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.plan.outputs.platform-matrix) }} + runs-on: ${{ matrix.runner_type == 'runson' && format('runs-on={0}/runner={1}', github.run_id, matrix.runner) || matrix.runner }} + timeout-minutes: ${{ matrix.timeout_minutes }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.plan.outputs.head-commit }} + persist-credentials: false + - uses: $/.github/actions/plan + if: matrix.verify_plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - uses: $/.github/actions/rust + with: + contract: ${{ matrix.contract }} + components: ${{ matrix.components }} + cache-url: ${{ vars.CARGO_RAIL_CACHE_URL }} + cache-mode: ${{ matrix.cache && vars.CARGO_RAIL_CACHE_URL != '' && github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && 'read-write' || matrix.cache && vars.CARGO_RAIL_CACHE_URL != '' && secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && 'read' || 'off' }} + cache-access-key-id: ${{ github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID || secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} + cache-secret-access-key: ${{ github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY || secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} + - shell: bash + env: + TARGET_ROW: ${{ matrix.id }} + run: scripts/ci/target-contracts.sh run "$TARGET_ROW" shallow + + miri: + name: Miri + needs: plan + if: needs.plan.outputs.miri == 'true' + runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci + timeout-minutes: 90 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: $/.github/actions/plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - run: scripts/ci/require-work.sh assurance.miri + - uses: $/.github/actions/rust + with: + contract: nightly + components: miri,rust-src + - env: + MIRI_ROWS: ${{ needs.plan.outputs.miri-rows }} + run: scripts/test/miri-contracts.sh selected "$MIRI_ROWS" + + fuzz: + name: Fuzz + needs: plan + if: needs.plan.outputs.fuzz == 'true' + runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci + timeout-minutes: 90 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: $/.github/actions/plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - run: scripts/ci/require-work.sh assurance.fuzz + - uses: $/.github/actions/rust + with: + contract: nightly + components: rust-src + - run: scripts/ci/install-tools.sh fuzz + - env: + FUZZ_ROWS: ${{ needs.plan.outputs.fuzz-rows }} + RSCRYPTO_FUZZ_JOBS: 1 + RSCRYPTO_FUZZ_TARGET_CONCURRENCY: 2 + run: scripts/test/fuzz-contracts.sh selected "$FUZZ_ROWS" + + supply-chain: + name: Supply chain + needs: plan + if: needs.plan.outputs.supply-chain == 'true' + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: $/.github/actions/plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - run: scripts/ci/require-work.sh --any dependency-policy dependencies.auxiliary + - uses: $/.github/actions/rust + - run: scripts/ci/install-tools.sh supply-chain + - run: cargo deny --locked check all + - run: cargo audit --ignore RUSTSEC-2023-0071 + + ct: + name: Constant-time structure + needs: plan + if: needs.plan.outputs.ct == 'true' + runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci + timeout-minutes: 60 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: $/.github/actions/plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - run: scripts/ci/require-work.sh assurance.ct + - uses: $/.github/actions/rust + with: + components: llvm-tools-preview + - run: scripts/ct/structural.sh + + rsa: + name: RSA assembly + needs: plan + if: needs.plan.outputs.rsa == 'true' + runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: $/.github/actions/plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - run: scripts/ci/require-work.sh assurance.rsa + - uses: $/.github/actions/rust + with: + cache-url: ${{ vars.CARGO_RAIL_CACHE_URL }} + cache-mode: ${{ vars.CARGO_RAIL_CACHE_URL != '' && github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && 'read-write' || vars.CARGO_RAIL_CACHE_URL != '' && secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && 'read' || 'off' }} + cache-access-key-id: ${{ github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID || secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} + cache-secret-access-key: ${{ github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY || secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} + - run: scripts/test/test-rsa-linux-asm.sh complete: name: Complete - needs: [rail-plan, suite, cache-seed, dependency-smoke] - if: ${{ !cancelled() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }} + needs: [plan, core, features, platforms, miri, fuzz, supply-chain, ct, rsa] + if: ${{ !cancelled() && needs.plan.result != 'skipped' }} runs-on: ubuntu-latest + timeout-minutes: 5 steps: - - name: Assert selected work passed + - name: Require selected jobs + shell: bash env: - ACTOR: ${{ github.actor }} - DEPENDENCY_SMOKE_RESULT: ${{ needs.dependency-smoke.result }} - EVENT_NAME: ${{ github.event_name }} - HAS_SUITE: ${{ needs.rail-plan.outputs.has-suite }} - PLAN_RESULT: ${{ needs.rail-plan.result }} - SUITE_RESULT: ${{ needs.suite.result }} - CACHE_SEED_RESULT: ${{ needs.cache-seed.result }} + CORE_REQUIRED: ${{ needs.plan.outputs.core }} + CORE_RESULT: ${{ needs.core.result }} + CT_REQUIRED: ${{ needs.plan.outputs.ct }} + CT_RESULT: ${{ needs.ct.result }} + FEATURES_REQUIRED: ${{ needs.plan.outputs.features }} + FEATURES_RESULT: ${{ needs.features.result }} + FUZZ_REQUIRED: ${{ needs.plan.outputs.fuzz }} + FUZZ_RESULT: ${{ needs.fuzz.result }} + MIRI_REQUIRED: ${{ needs.plan.outputs.miri }} + MIRI_RESULT: ${{ needs.miri.result }} + PLATFORM_REQUIRED: ${{ needs.plan.outputs.platforms }} + PLATFORM_RESULT: ${{ needs.platforms.result }} + PLAN_RESULT: ${{ needs.plan.result }} + RSA_REQUIRED: ${{ needs.plan.outputs.rsa }} + RSA_RESULT: ${{ needs.rsa.result }} + SUPPLY_REQUIRED: ${{ needs.plan.outputs.supply-chain }} + SUPPLY_RESULT: ${{ needs.supply-chain.result }} run: | - if [[ "$EVENT_NAME" == "pull_request" && "$ACTOR" == "dependabot[bot]" ]]; then - [[ "$DEPENDENCY_SMOKE_RESULT" == "success" ]] - echo "Dependency smoke: $DEPENDENCY_SMOKE_RESULT" - exit 0 - fi - - [[ "$PLAN_RESULT" == "success" ]] - if [[ "$HAS_SUITE" == "true" ]]; then - if [[ "$EVENT_NAME" == "push" ]]; then - [[ "$CACHE_SEED_RESULT" == "success" && "$SUITE_RESULT" == "skipped" ]] + [[ "$PLAN_RESULT" == success ]] + for pair in \ + "$CORE_REQUIRED:$CORE_RESULT:core" \ + "$CT_REQUIRED:$CT_RESULT:ct" \ + "$FEATURES_REQUIRED:$FEATURES_RESULT:features" \ + "$FUZZ_REQUIRED:$FUZZ_RESULT:fuzz" \ + "$MIRI_REQUIRED:$MIRI_RESULT:miri" \ + "$PLATFORM_REQUIRED:$PLATFORM_RESULT:platforms" \ + "$RSA_REQUIRED:$RSA_RESULT:rsa" \ + "$SUPPLY_REQUIRED:$SUPPLY_RESULT:supply-chain" + do + IFS=: read -r required result name <<<"$pair" + if [[ "$required" == true ]]; then + [[ "$result" == success ]] || { echo "$name: $result" >&2; exit 1; } else - [[ "$SUITE_RESULT" == "success" && "$CACHE_SEED_RESULT" == "skipped" ]] + [[ "$result" == skipped ]] || { echo "$name ran unexpectedly: $result" >&2; exit 1; } fi - else - [[ "$SUITE_RESULT" == "skipped" && "$CACHE_SEED_RESULT" == "skipped" ]] - fi - echo "Cargo Rail plan: $PLAN_RESULT; selected suite: $SUITE_RESULT; main seeder: $CACHE_SEED_RESULT" + done diff --git a/.github/workflows/ct.yaml b/.github/workflows/ct.yaml index 06866cf6..491f0b93 100644 --- a/.github/workflows/ct.yaml +++ b/.github/workflows/ct.yaml @@ -1,240 +1,173 @@ -name: Constant-Time +name: Constant-time evidence on: workflow_call: inputs: + plan_artifact: + required: true + type: string + plan_identity: + required: true + type: string + head_commit: + required: true + type: string platforms: - description: "RUNNERS (Empty/all = every physical CT lane, including RISC-V)." required: false type: string - default: "all" + default: all dudect_timeout: - description: "Timeout in seconds for each per-case dudect command." required: false type: number default: 1800 dudect_filter: - description: "Optional comma-separated DudeCT case/name/filter substrings. Empty = all cases in selected gate." required: false type: string default: "" dudect_gate: - description: "DudeCT gate to run: required, diagnostic, or all. Required is release evidence." required: false type: string - default: "required" + default: required binsec_timeout: - description: "Timeout in seconds for each BINSEC kernel batch on Linux targets." required: false type: number default: 900 - upload_raw_artifacts: - description: "Upload raw CT build/proof artifacts for release evidence or debugging. Default uploads compact reports only." + raw: required: false type: boolean default: false - artifact_retention_days: - description: "Retention for CT evidence artifacts." + retention_days: required: false type: number - default: 90 - release_tag: - description: "Existing release tag whose supported CT recovery group must be regenerated." - required: false - type: string - default: "" - secrets: - CARGO_RAIL_R2_READ_ACCESS_KEY_ID: - description: "Bucket-scoped R2 access key forwarded to CT compiler jobs." - required: false - CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY: - description: "Bucket-scoped R2 secret key forwarded to CT compiler jobs." - required: false + default: 30 workflow_dispatch: inputs: platforms: - description: "RUNNERS (Empty/all = every physical CT lane, including RISC-V)." + description: Platform aliases (comma separated, or all). required: false type: string - default: "all" + default: all dudect_timeout: - description: "Timeout in seconds for each per-case dudect command." required: false type: number default: 1800 dudect_filter: - description: "Optional comma-separated DudeCT case/name/filter substrings. Empty = all cases in selected gate." required: false type: string default: "" dudect_gate: - description: "DudeCT gate to run: required, diagnostic, or all. Required is release evidence." required: false type: choice - options: - - required - - diagnostic - - all - default: "required" + options: [required, diagnostic, all] + default: required binsec_timeout: - description: "Timeout in seconds for each BINSEC kernel batch on Linux targets." required: false type: number default: 900 - upload_raw_artifacts: - description: "Upload raw CT build/proof artifacts for release evidence or debugging. Default uploads compact reports only." + raw: + description: Retain raw release evidence. required: false type: boolean default: false - artifact_retention_days: - description: "Retention for CT evidence artifacts." + retention_days: required: false type: number - default: 90 - release_tag: - description: "Existing release tag whose supported CT recovery group must be regenerated." - required: false - type: string - default: "" + default: 30 + +permissions: + contents: read env: - RUST_BACKTRACE: 1 - CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 + CARGO_TERM_COLOR: always RSCRYPTO_CT_MODE: ci - -permissions: - contents: read + RUST_BACKTRACE: 1 jobs: plan: - name: Resolve CT Matrix + name: Plan runs-on: ubuntu-latest + timeout-minutes: 10 outputs: - checkout_ref: ${{ steps.source.outputs.checkout_ref }} - recovery_rustflags: ${{ steps.source.outputs.recovery_rustflags }} - has_targets: ${{ steps.plan.outputs.has_targets }} - matrix: ${{ steps.plan.outputs.matrix }} + head-commit: ${{ steps.commit.outputs.value }} + matrix: ${{ steps.matrix.outputs.matrix }} steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 0 + ref: ${{ inputs.head_commit || github.sha }} persist-credentials: false - - - name: Resolve CT source - id: source - shell: bash - env: - ARTIFACT_RETENTION_DAYS: ${{ inputs.artifact_retention_days }} - BINSEC_TIMEOUT: ${{ inputs.binsec_timeout }} - DUDECT_FILTER: ${{ inputs.dudect_filter }} - DUDECT_GATE: ${{ inputs.dudect_gate }} - DUDECT_TIMEOUT: ${{ inputs.dudect_timeout }} - EVENT_NAME: ${{ github.event_name }} - PLATFORMS: ${{ inputs.platforms }} - RELEASE_TAG: ${{ inputs.release_tag }} - UPLOAD_RAW_ARTIFACTS: ${{ inputs.upload_raw_artifacts }} - run: | - set -euo pipefail - if [[ -z "$RELEASE_TAG" ]]; then - echo "checkout_ref=" >> "$GITHUB_OUTPUT" - echo "recovery_rustflags=" >> "$GITHUB_OUTPUT" - exit 0 - fi - if [[ "$EVENT_NAME" != "workflow_dispatch" || "$GITHUB_REF" != "refs/heads/main" ]]; then - echo "release CT recovery must run from protected main" >&2 - exit 1 - fi - if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$ ]]; then - echo "release CT recovery tag is invalid: $RELEASE_TAG" >&2 - exit 1 - fi - git rev-parse -q --verify "${RELEASE_TAG}^{tag}" >/dev/null \ - || { echo "release CT recovery requires an annotated tag" >&2; exit 1; } - case "$PLATFORMS" in - ibm-s390x) - recovery_rustflags="" - ;; - amd-zen4,intel-spr,intel-icl,amd-zen5) - recovery_rustflags="" - ;; - *) - echo "release CT recovery must run the complete raw ibm-s390x or x86_64 platform group" >&2 - exit 1 - ;; - esac - if [[ "$DUDECT_GATE" != "required" \ - || -n "$DUDECT_FILTER" || "$UPLOAD_RAW_ARTIFACTS" != "true" \ - || "$ARTIFACT_RETENTION_DAYS" != "90" || "$DUDECT_TIMEOUT" != "1800" \ - || "$BINSEC_TIMEOUT" != "900" ]]; then - echo "release CT recovery must use the complete required gate, release timeouts, raw artifacts, and 90-day retention" >&2 - exit 1 - fi - echo "checkout_ref=$RELEASE_TAG" >> "$GITHUB_OUTPUT" - echo "recovery_rustflags=$recovery_rustflags" >> "$GITHUB_OUTPUT" - - - name: Build CT Matrix - id: plan - shell: bash + - uses: $/.github/actions/plan + if: inputs.plan_artifact != '' + with: + artifact: ${{ inputs.plan_artifact }} + identity: ${{ inputs.plan_identity }} + head-commit: ${{ inputs.head_commit }} + - if: inputs.plan_artifact != '' + run: scripts/ci/require-work.sh assurance.ct + - id: commit + run: echo "value=$(git rev-parse HEAD)" >>"$GITHUB_OUTPUT" + - id: matrix + name: Select platforms env: + CT_PLATFORMS: ${{ inputs.platforms }} GH_RUN_ID: ${{ github.run_id }} - CT_PLATFORMS: ${{ inputs.platforms || 'all' }} run: scripts/ci/emit-manual-matrix.sh ct - ct: - name: CT Full (${{ matrix.display_name }}) + evidence: + name: ${{ matrix.display_name }} needs: plan - if: ${{ needs.plan.outputs.has_targets == 'true' }} - permissions: - contents: read strategy: fail-fast: false matrix: include: ${{ fromJSON(needs.plan.outputs.matrix) }} - uses: ./.github/workflows/_rust-job.yaml - with: - checkout_ref: ${{ needs.plan.outputs.checkout_ref }} - runner: ${{ matrix.runner }} - timeout_minutes: ${{ matrix.timeout_minutes }} - tools_mode: ${{ matrix.tools_mode }} - toolchain_contract: ${{ (matrix.platform == 'ibm-s390x' || matrix.platform == 'ibm-power10' || matrix.platform == 'rise-riscv') && 'nightly' || 'development' }} - toolchain_components: ${{ matrix.toolchain_components }} - operation: constant-time - platform: ${{ matrix.platform }} - target: ${{ matrix.target }} - dudect_timeout: ${{ fromJSON(format('{0}', inputs.dudect_timeout)) }} - dudect_filter: ${{ inputs.dudect_filter }} - dudect_gate: ${{ inputs.dudect_gate }} - binsec_timeout: ${{ fromJSON(format('{0}', inputs.binsec_timeout)) }} - rustflags: ${{ needs.plan.outputs.recovery_rustflags }} - upload_raw_artifacts: ${{ inputs.upload_raw_artifacts }} - artifact_name: ct-${{ inputs.upload_raw_artifacts && 'raw-' || '' }}${{ matrix.artifact_suffix }} - artifact_path: target/ct-evidence-package/ - artifact_always: true - artifact_retention_days: ${{ fromJSON(format('{0}', inputs.artifact_retention_days)) }} - secrets: - cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} + runs-on: ${{ matrix.runner }} + timeout-minutes: ${{ matrix.timeout_minutes }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.plan.outputs.head-commit }} + persist-credentials: false + - uses: $/.github/actions/rust + with: + contract: ${{ (matrix.platform == 'ibm-s390x' || matrix.platform == 'ibm-power10' || matrix.platform == 'rise-riscv') && 'nightly' || 'development' }} + components: ${{ matrix.toolchain_components }} + - name: Install formal-analysis tools + if: matrix.formal + run: scripts/ci/install-tools.sh ct-linux + - name: Build and analyze + env: + BINSEC_TIMEOUT: ${{ inputs.binsec_timeout }} + DUDECT_FILTER: ${{ inputs.dudect_filter }} + DUDECT_GATE: ${{ inputs.dudect_gate }} + DUDECT_TIMEOUT: ${{ inputs.dudect_timeout }} + RAW: ${{ inputs.raw }} + run: | + args=( + --platform "${{ matrix.platform }}" + --target "${{ matrix.target }}" + --dudect-timeout "$DUDECT_TIMEOUT" + --binsec-timeout "$BINSEC_TIMEOUT" + --dudect-filter "$DUDECT_FILTER" + --dudect-gate "$DUDECT_GATE" + ) + [[ "$RAW" == true ]] && args+=(--raw) + scripts/ct/ci.sh "${args[@]}" + - name: Upload evidence + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ct-${{ inputs.raw && 'raw-' || '' }}${{ matrix.artifact_suffix }} + path: target/ct-evidence-package/ + retention-days: ${{ inputs.retention_days }} + if-no-files-found: error complete: - name: Complete (CT) - needs: [plan, ct] + name: Complete + needs: [plan, evidence] if: ${{ !cancelled() }} runs-on: ubuntu-latest + timeout-minutes: 5 steps: - - name: Assert CT lanes passed - shell: bash - run: | - set -euo pipefail - plan="${{ needs.plan.result }}" - ct="${{ needs.ct.result }}" - if [[ "$plan" != "success" ]]; then - echo "CT matrix plan failed: $plan" - exit 1 - fi - if [[ "$ct" != "success" ]]; then - echo "CT full result: $ct" - exit 1 - fi - echo "CT full: $ct" + - run: | + [[ "${{ needs.plan.result }}" == success ]] + [[ "${{ needs.evidence.result }}" == success ]] diff --git a/.github/workflows/qualification.yaml b/.github/workflows/qualification.yaml new file mode 100644 index 00000000..5d8c4bda --- /dev/null +++ b/.github/workflows/qualification.yaml @@ -0,0 +1,480 @@ +name: Qualification + +on: + schedule: + - cron: "0 3 * * 0" + workflow_call: + inputs: + head_commit: + description: Exact commit to qualify. + required: true + type: string + mode: + description: Qualification evidence mode. + required: true + type: string + secrets: + CARGO_RAIL_R2_READ_ACCESS_KEY_ID: + required: false + CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY: + required: false + CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID: + required: false + CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY: + required: false + workflow_dispatch: + inputs: + mode: + description: Release mode retains exact-commit publication evidence. + required: true + type: choice + options: [assurance, release] + default: assurance + +concurrency: + group: qualification-${{ inputs.head_commit || github.sha }}-${{ inputs.mode || 'assurance' }} + cancel-in-progress: true + +permissions: + contents: read + +env: + CARGO_INCREMENTAL: 0 + CARGO_TERM_COLOR: always + RSCRYPTO_TEST_MODE: weekly + RUST_BACKTRACE: 1 + +jobs: + plan: + name: Plan + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + artifact: ${{ steps.stage.outputs.artifact }} + feature-matrix: ${{ steps.features.outputs.matrix }} + head-commit: ${{ steps.rail.outputs.head-commit }} + identity: ${{ steps.rail.outputs.plan-identity }} + mode: ${{ steps.mode.outputs.value }} + platform-matrix: ${{ steps.platforms.outputs.matrix }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ inputs.head_commit || github.sha }} + - id: mode + env: + REQUESTED: ${{ inputs.mode }} + run: | + mode="${REQUESTED:-assurance}" + [[ "$mode" == assurance || "$mode" == release ]] || { + echo "unknown qualification mode: $mode" >&2 + exit 2 + } + echo "value=$mode" >> "$GITHUB_OUTPUT" + - id: rail + name: Capture all-work plan + uses: loadingalias/cargo-rail-action@78ad385a85627484a5b634cf1bc3caa1de872c6a # v8.2.0 + with: + version: "0.25.0" + all: true + + - id: features + env: + PLAN: ${{ steps.rail.outputs.plan-file }} + READER: ${{ steps.rail.outputs.plan-reader }} + run: | + matrix=$( + RAIL_PLAN_FILE="$PLAN" RAIL_PLAN_READER="$READER" \ + scripts/check/feature-contracts.sh matrix + ) + echo "matrix=$matrix" >>"$GITHUB_OUTPUT" + + - id: platforms + env: + PLAN: ${{ steps.rail.outputs.plan-file }} + READER: ${{ steps.rail.outputs.plan-reader }} + run: | + matrix=$(scripts/ci/target-contracts.sh matrix "$PLAN" "$READER") + echo "matrix=$matrix" >>"$GITHUB_OUTPUT" + + - name: Run repository policy + env: + RAIL_PLAN_FILE: ${{ steps.rail.outputs.plan-file }} + RAIL_PLAN_READER: ${{ steps.rail.outputs.plan-reader }} + RAIL_PLAN_IDENTITY: ${{ steps.rail.outputs.plan-identity }} + RAIL_PLAN_HEAD_COMMIT: ${{ steps.rail.outputs.head-commit }} + run: scripts/check/policy.sh + + - id: stage + name: Stage immutable plan + shell: bash + env: + PLAN: ${{ steps.rail.outputs.plan-file }} + READER: ${{ steps.rail.outputs.plan-reader }} + run: | + set -euo pipefail + root="$RUNNER_TEMP/rscrypto-cargo-rail-plan" + artifact="qualification-plan-$GITHUB_RUN_ID" + mkdir -p "$root" + install -m 600 "$PLAN" "$root/plan.json" + install -m 700 "$READER" "$root/read.py" + install -m 700 "$(command -v cargo-rail)" "$root/cargo-rail" + echo "artifact=$artifact" >>"$GITHUB_OUTPUT" + + - name: Preserve exact plan + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ steps.stage.outputs.artifact }} + path: ${{ runner.temp }}/rscrypto-cargo-rail-plan/ + retention-days: ${{ steps.mode.outputs.value == 'release' && 90 || 14 }} + if-no-files-found: error + + core: + name: Rust + needs: plan + runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: $/.github/actions/plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - uses: $/.github/actions/rust + with: + components: clippy,rustfmt + cache-url: ${{ vars.CARGO_RAIL_CACHE_URL }} + cache-mode: ${{ vars.CARGO_RAIL_CACHE_URL != '' && github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && 'read-write' || vars.CARGO_RAIL_CACHE_URL != '' && secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && 'read' || 'off' }} + cache-access-key-id: ${{ github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID || secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} + cache-secret-access-key: ${{ github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY || secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} + - name: Install test runner + uses: taiki-e/install-action@6a1bd70eaac3c8bdf093356838d7ee09fda951cf # v2.85.5 + with: + tool: cargo-nextest@0.9.143 + fallback: none + - name: Check, lint, and document + env: + RSCRYPTO_SKIP_CHECK_SUPPLY_CHAIN: 1 + run: scripts/check/check.sh + - name: Test + run: scripts/test/test.sh + + features: + name: Features (${{ matrix.domain }} ${{ matrix.shard }}) + needs: plan + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.plan.outputs.feature-matrix) }} + runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.plan.outputs.head-commit }} + persist-credentials: false + - uses: $/.github/actions/plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - run: scripts/ci/require-work.sh contracts.features + - uses: $/.github/actions/rust + with: + cache-url: ${{ vars.CARGO_RAIL_CACHE_URL }} + cache-mode: ${{ vars.CARGO_RAIL_CACHE_URL != '' && github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && 'read-write' || vars.CARGO_RAIL_CACHE_URL != '' && secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && 'read' || 'off' }} + cache-access-key-id: ${{ github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID || secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} + cache-secret-access-key: ${{ github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY || secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} + - env: + FEATURE_DOMAIN: ${{ matrix.domain }} + FEATURE_PROFILES: ${{ matrix.profiles }} + FEATURE_SHARD: ${{ matrix.shard }} + run: scripts/check/feature-contracts.sh selected "$FEATURE_DOMAIN" "$FEATURE_SHARD" "$FEATURE_PROFILES" + + platforms: + name: ${{ matrix.name }} + needs: plan + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.plan.outputs.platform-matrix) }} + runs-on: ${{ matrix.runner_type == 'runson' && format('runs-on={0}/runner={1}', github.run_id, matrix.runner) || matrix.runner }} + timeout-minutes: ${{ matrix.timeout_minutes }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.plan.outputs.head-commit }} + persist-credentials: false + - uses: $/.github/actions/plan + if: matrix.verify_plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - uses: $/.github/actions/rust + with: + contract: ${{ matrix.contract }} + components: ${{ matrix.components }} + cache-url: ${{ vars.CARGO_RAIL_CACHE_URL }} + cache-mode: ${{ matrix.cache && vars.CARGO_RAIL_CACHE_URL != '' && github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && 'read-write' || matrix.cache && vars.CARGO_RAIL_CACHE_URL != '' && secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && 'read' || 'off' }} + cache-access-key-id: ${{ github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID || secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} + cache-secret-access-key: ${{ github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY || secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} + - shell: bash + env: + TARGET_ROW: ${{ matrix.id }} + run: scripts/ci/target-contracts.sh run "$TARGET_ROW" deep + + miri: + name: Miri + needs: plan + runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci + timeout-minutes: 90 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: $/.github/actions/plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - run: scripts/ci/require-work.sh assurance.miri + - uses: $/.github/actions/rust + with: + contract: nightly + components: miri,rust-src + - run: scripts/test/miri-contracts.sh run portable + - run: scripts/test/miri-contracts.sh run rsa + - name: Tree Borrows + env: + MIRIFLAGS: -Zmiri-tree-borrows + run: scripts/test/miri-contracts.sh run portable + + fuzz: + name: Fuzz and ASan + needs: plan + runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci + timeout-minutes: 180 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: $/.github/actions/plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - run: scripts/ci/require-work.sh assurance.fuzz + - uses: $/.github/actions/rust + with: + contract: nightly + components: rust-src + - run: scripts/ci/install-tools.sh fuzz + - name: Fuzz + env: + RSCRYPTO_FUZZ_DURATION_SECS: 60 + RSCRYPTO_FUZZ_JOBS: 1 + RSCRYPTO_FUZZ_TARGET_CONCURRENCY: 2 + run: scripts/test/test-fuzz.sh --all + - name: Replay corpora under ASan + run: scripts/test/test-fuzz-asan.sh --all + + mlkem: + name: ML-KEM (${{ matrix.platform }}) + needs: plan + strategy: + fail-fast: false + matrix: + platform: [graviton3, graviton4] + runs-on: runs-on=${{ github.run_id }}/runner=${{ matrix.platform }} + timeout-minutes: 120 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.plan.outputs.head-commit }} + persist-credentials: false + - uses: $/.github/actions/rust + with: + components: clippy,rustfmt,rust-src + - name: Run native gate + env: + MLKEM_AARCH64_GATE_PLATFORM: ${{ matrix.platform }} + run: scripts/ci/mlkem-aarch64-gate.sh + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ !cancelled() }} + with: + name: mlkem-${{ matrix.platform }} + path: mlkem-aarch64-gate/ + retention-days: ${{ needs.plan.outputs.mode == 'release' && 90 || 14 }} + if-no-files-found: error + + rsa-leakage: + name: RSA leakage (${{ matrix.arch }}) + needs: plan + strategy: + fail-fast: false + matrix: + include: + - {arch: x64, runner: "runs-on=${{ github.run_id }}/runner=linux-x64-ci"} + - {arch: arm64, runner: "runs-on=${{ github.run_id }}/runner=linux-arm64-ci"} + runs-on: ${{ matrix.runner }} + timeout-minutes: 120 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.plan.outputs.head-commit }} + persist-credentials: false + - uses: $/.github/actions/plan + if: matrix.arch == 'x64' + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - if: matrix.arch == 'x64' + run: scripts/ci/require-work.sh assurance.rsa + - uses: $/.github/actions/rust + - name: Measure + run: | + mkdir -p ci-evidence + scripts/test/test-rsa-leakage.sh 2>&1 | tee "ci-evidence/rsa-leakage-${{ matrix.arch }}.log" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ !cancelled() }} + with: + name: rsa-leakage-${{ matrix.arch }} + path: ci-evidence/ + retention-days: ${{ needs.plan.outputs.mode == 'release' && 90 || 14 }} + if-no-files-found: error + + rsa-asm: + name: RSA assembly + needs: plan + runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: $/.github/actions/plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - run: scripts/ci/require-work.sh assurance.rsa + - uses: $/.github/actions/rust + - run: scripts/test/test-rsa-linux-asm.sh + + supply-chain: + name: Supply chain + needs: plan + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: $/.github/actions/plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - run: scripts/ci/require-work.sh --any dependency-policy dependencies.auxiliary + - uses: $/.github/actions/rust + - run: scripts/ci/install-tools.sh supply-chain + - run: cargo deny --locked check all + - run: cargo audit --ignore RUSTSEC-2023-0071 + + coverage: + name: Coverage + needs: plan + runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci + timeout-minutes: 180 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: $/.github/actions/plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - run: scripts/ci/require-work.sh cargo.test + - run: scripts/ci/require-work.sh assurance.fuzz + - uses: $/.github/actions/rust + with: + components: llvm-tools-preview + - name: Install coverage tools + uses: taiki-e/install-action@6a1bd70eaac3c8bdf093356838d7ee09fda951cf # v2.85.5 + with: + tool: cargo-llvm-cov@0.9.0,cargo-nextest@0.9.143 + fallback: none + - run: scripts/test/test-coverage.sh + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: coverage-reports + path: | + coverage/total.lcov + coverage/SUMMARY.md + retention-days: ${{ needs.plan.outputs.mode == 'release' && 90 || 14 }} + if-no-files-found: error + + zeroization: + name: Optimized zeroization + needs: plan + runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: $/.github/actions/plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - run: scripts/ci/require-work.sh assurance.ct + - uses: $/.github/actions/rust + - run: scripts/check/zeroize-evidence.sh + + ct: + name: Constant time + needs: plan + uses: $/.github/workflows/ct.yaml + with: + head_commit: ${{ needs.plan.outputs.head-commit }} + plan_artifact: ${{ needs.plan.outputs.artifact }} + plan_identity: ${{ needs.plan.outputs.identity }} + platforms: all + raw: ${{ needs.plan.outputs.mode == 'release' }} + retention_days: ${{ needs.plan.outputs.mode == 'release' && 90 || 14 }} + + complete: + name: Complete (${{ needs.plan.outputs.mode }}) + needs: [plan, core, features, platforms, miri, fuzz, mlkem, rsa-leakage, rsa-asm, supply-chain, coverage, zeroization, ct] + if: ${{ !cancelled() }} + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Require every assurance domain + shell: bash + run: | + failed=0 + for lane in \ + "plan=${{ needs.plan.result }}" \ + "core=${{ needs.core.result }}" \ + "features=${{ needs.features.result }}" \ + "platforms=${{ needs.platforms.result }}" \ + "miri=${{ needs.miri.result }}" \ + "fuzz=${{ needs.fuzz.result }}" \ + "mlkem=${{ needs.mlkem.result }}" \ + "rsa-leakage=${{ needs.rsa-leakage.result }}" \ + "rsa-asm=${{ needs.rsa-asm.result }}" \ + "supply-chain=${{ needs.supply-chain.result }}" \ + "coverage=${{ needs.coverage.result }}" \ + "zeroization=${{ needs.zeroization.result }}" \ + "ct=${{ needs.ct.result }}" + do + [[ "${lane#*=}" == success ]] || { echo "$lane" >&2; failed=1; } + done + exit "$failed" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c6e7e164..e38dca8e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,259 +4,116 @@ on: push: tags: - "v*" - workflow_dispatch: - inputs: - tag: - description: Existing signed release tag to recover - required: true - type: string - s390x_ct_run: - description: Optional reviewed exact-tag s390x CT recovery run ID - required: false - type: string - x86_64_ct_run: - description: Optional reviewed exact-tag x86_64 CT recovery run ID - required: false - type: string - -env: - RUST_BACKTRACE: 1 - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: 0 permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }} + group: release-${{ github.ref }} cancel-in-progress: false jobs: - preflight: - name: Preflight - runs-on: ubuntu-latest - timeout-minutes: 90 + qualification: + name: Qualification permissions: - actions: read contents: read + uses: $/.github/workflows/qualification.yaml + with: + head_commit: ${{ github.sha }} + mode: release + secrets: + CARGO_RAIL_R2_READ_ACCESS_KEY_ID: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} + CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} + + package: + name: Package + runs-on: ubuntu-latest + timeout-minutes: 90 outputs: - release_commit: ${{ steps.identity.outputs.release_commit }} - release_tag: ${{ steps.identity.outputs.release_tag }} crate_name: ${{ steps.preflight.outputs.crate_name }} crate_sha256: ${{ steps.preflight.outputs.crate_sha256 }} crate_version: ${{ steps.preflight.outputs.crate_version }} source_name: ${{ steps.preflight.outputs.source_name }} source_sha256: ${{ steps.preflight.outputs.source_sha256 }} - qualification_run_id: ${{ steps.evidence.outputs.qualification_run_id }} - qualification_run_url: ${{ steps.evidence.outputs.qualification_run_url }} - qualification_commit: ${{ steps.evidence.outputs.qualification_commit }} - qualification_version: ${{ steps.evidence.outputs.qualification_version }} - qualification_evidence_mode: ${{ steps.evidence.outputs.qualification_evidence_mode }} - s390x_ct_run_id: ${{ steps.s390x_ct.outputs.recovery_run_id }} - s390x_ct_run_url: ${{ steps.s390x_ct.outputs.recovery_run_url }} - x86_64_ct_run_id: ${{ steps.x86_64_ct.outputs.recovery_run_id }} - x86_64_ct_run_url: ${{ steps.x86_64_ct.outputs.recovery_run_url }} steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }} - - - name: Resolve release identity - id: identity - env: - EVENT_NAME: ${{ github.event_name }} - REQUESTED_TAG: ${{ inputs.tag }} - WORKFLOW_COMMIT: ${{ github.sha }} - run: | - set -euo pipefail - if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then - if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then - echo "release recovery must run from main, not $GITHUB_REF" >&2 - exit 1 - fi - release_tag="$REQUESTED_TAG" - else - release_tag="$GITHUB_REF_NAME" - fi - if [[ ! "$release_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$ ]]; then - echo "release tag must be valid semver prefixed with v: $release_tag" >&2 - exit 1 - fi - if ! git rev-parse -q --verify "${release_tag}^{tag}" >/dev/null; then - echo "release ref must be an annotated tag: $release_tag" >&2 - exit 1 - fi - release_commit="$(git rev-parse "${release_tag}^{commit}")" - checkout_commit="$(git rev-parse HEAD)" - if [[ "$checkout_commit" != "$release_commit" ]]; then - echo "checked-out commit $checkout_commit does not match $release_tag commit $release_commit" >&2 - exit 1 - fi - if [[ "$EVENT_NAME" == "push" && "$WORKFLOW_COMMIT" != "$release_commit" ]]; then - echo "tag push commit $WORKFLOW_COMMIT does not match $release_tag commit $release_commit" >&2 - exit 1 - fi - { - echo "release_commit=$release_commit" - echo "release_tag=$release_tag" - } >> "$GITHUB_OUTPUT" - - - name: Verify CT and RSA release evidence - id: evidence - env: - GH_TOKEN: ${{ github.token }} - RELEASE_COMMIT: ${{ steps.identity.outputs.release_commit }} - run: scripts/ci/release-evidence-check.sh --commit "$RELEASE_COMMIT" - - - name: Checkout reviewed recovery tooling - if: github.event_name == 'workflow_dispatch' - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 1 - path: target/release-automation - persist-credentials: false - ref: ${{ github.sha }} - - - name: Verify s390x CT recovery evidence - if: github.event_name == 'workflow_dispatch' && inputs.s390x_ct_run != '' - id: s390x_ct - env: - GH_TOKEN: ${{ github.token }} - S390X_CT_RUN: ${{ inputs.s390x_ct_run }} - WORKFLOW_COMMIT: ${{ github.sha }} - run: | - target/release-automation/scripts/ci/release-ct-recovery-check.sh \ - --run-id "$S390X_CT_RUN" \ - --platform-group s390x \ - --workflow-commit "$WORKFLOW_COMMIT" - - - name: Verify x86_64 CT recovery evidence - if: github.event_name == 'workflow_dispatch' && inputs.x86_64_ct_run != '' - id: x86_64_ct - env: - GH_TOKEN: ${{ github.token }} - WORKFLOW_COMMIT: ${{ github.sha }} - X86_64_CT_RUN: ${{ inputs.x86_64_ct_run }} - run: | - target/release-automation/scripts/ci/release-ct-recovery-check.sh \ - --run-id "$X86_64_CT_RUN" \ - --platform-group x86_64 \ - --workflow-commit "$WORKFLOW_COMMIT" - - - name: Setup - uses: ./.github/actions/setup + ref: ${{ github.ref }} + - uses: $/.github/actions/rust with: - tools-mode: rail - toolchain-components: "" - cache-url: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && vars.CARGO_RAIL_CACHE_URL || '' }} - cache-mode: read + cache-url: ${{ vars.CARGO_RAIL_CACHE_URL }} + cache-mode: ${{ vars.CARGO_RAIL_CACHE_URL != '' && secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && 'read' || 'off' }} cache-access-key-id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} cache-secret-access-key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - - name: Release preflight - id: preflight + - id: preflight + name: Validate release package env: - EVENT_NAME: ${{ github.event_name }} - RELEASE_TAG: ${{ steps.identity.outputs.release_tag }} - run: | - set -euo pipefail - preflight=scripts/ci/release-preflight.sh - if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then - preflight=target/release-automation/scripts/ci/release-preflight.sh - fi - "$preflight" --crate rscrypto --tag "$RELEASE_TAG" - - - name: Remove reviewed recovery tooling - if: github.event_name == 'workflow_dispatch' - run: | - set -euo pipefail - rm -rf target/release-automation - - - name: Preserve validated release inputs + RELEASE_TAG: ${{ github.ref_name }} + run: scripts/ci/release-preflight.sh --crate rscrypto --tag "$RELEASE_TAG" + - name: Preserve validated package uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: release-inputs-${{ steps.identity.outputs.release_commit }} + name: release-inputs-${{ github.sha }} path: | ${{ steps.preflight.outputs.crate_path }} ${{ steps.preflight.outputs.source_path }} if-no-files-found: error - retention-days: 1 + retention-days: 7 compression-level: 0 publish: name: Publish - needs: preflight + needs: [qualification, package] runs-on: ubuntu-latest timeout-minutes: 90 environment: crates-io permissions: actions: read - contents: write - id-token: write attestations: write artifact-metadata: write + contents: write + id-token: write steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - ref: ${{ needs.preflight.outputs.release_tag }} - - - name: Checkout reviewed recovery tooling - if: github.event_name == 'workflow_dispatch' - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 1 - path: target/release-automation - persist-credentials: false - ref: ${{ github.sha }} - - - name: Setup - uses: ./.github/actions/setup + ref: ${{ github.ref }} + - uses: $/.github/actions/rust with: - tools-mode: none - toolchain-components: "" - cache-url: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && vars.CARGO_RAIL_CACHE_URL || '' }} - cache-mode: read + cache-url: ${{ vars.CARGO_RAIL_CACHE_URL }} + cache-mode: ${{ vars.CARGO_RAIL_CACHE_URL != '' && secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && 'read' || 'off' }} cache-access-key-id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} cache-secret-access-key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - - name: Restore validated release inputs + - name: Restore validated package uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: release-inputs-${{ needs.preflight.outputs.release_commit }} + name: release-inputs-${{ github.sha }} path: target/package - - - name: Verify validated release inputs - id: package + - id: package + name: Verify validated package env: - CRATE_NAME: ${{ needs.preflight.outputs.crate_name }} - CRATE_SHA256: ${{ needs.preflight.outputs.crate_sha256 }} - CRATE_VERSION: ${{ needs.preflight.outputs.crate_version }} - SOURCE_NAME: ${{ needs.preflight.outputs.source_name }} - SOURCE_SHA256: ${{ needs.preflight.outputs.source_sha256 }} + CRATE_NAME: ${{ needs.package.outputs.crate_name }} + CRATE_SHA256: ${{ needs.package.outputs.crate_sha256 }} + CRATE_VERSION: ${{ needs.package.outputs.crate_version }} + SOURCE_NAME: ${{ needs.package.outputs.source_name }} + SOURCE_SHA256: ${{ needs.package.outputs.source_sha256 }} run: | set -euo pipefail crate_path="target/package/$CRATE_NAME" source_path="target/package/$SOURCE_NAME" actual_crate_sha256="$(sha256sum "$crate_path" | awk '{print $1}')" actual_source_sha256="$(sha256sum "$source_path" | awk '{print $1}')" - if [[ "$actual_crate_sha256" != "$CRATE_SHA256" ]]; then - echo "validated crate sha256 changed during artifact transfer" >&2 - echo "expected: $CRATE_SHA256" >&2 - echo "actual: $actual_crate_sha256" >&2 + [[ "$actual_crate_sha256" == "$CRATE_SHA256" ]] || { + echo "validated crate changed during artifact transfer" >&2 exit 1 - fi - if [[ "$actual_source_sha256" != "$SOURCE_SHA256" ]]; then - echo "validated source archive sha256 changed during artifact transfer" >&2 - echo "expected: $SOURCE_SHA256" >&2 - echo "actual: $actual_source_sha256" >&2 + } + [[ "$actual_source_sha256" == "$SOURCE_SHA256" ]] || { + echo "validated source archive changed during artifact transfer" >&2 exit 1 - fi + } { echo "crate_path=$crate_path" echo "crate_name=$CRATE_NAME" @@ -266,133 +123,31 @@ jobs: echo "source_name=$SOURCE_NAME" echo "source_sha256=$SOURCE_SHA256" } >> "$GITHUB_OUTPUT" - + - name: Download constant-time evidence + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: ct-raw-* + path: ct-release-artifacts + - id: ct_evidence + name: Package constant-time evidence + env: + CRATE_VERSION: ${{ steps.package.outputs.crate_version }} + RELEASE_COMMIT: ${{ github.sha }} + run: scripts/ci/package-release-ct-evidence.sh --version "$CRATE_VERSION" --commit "$RELEASE_COMMIT" - name: Attest crate provenance uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-path: ${{ steps.package.outputs.crate_path }} - - name: Attest source archive provenance uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-path: ${{ steps.package.outputs.source_path }} - - - name: Capture repository controls - id: repository_controls - env: - CRATE_VERSION: ${{ steps.package.outputs.crate_version }} - EVENT_NAME: ${{ github.event_name }} - GH_TOKEN: ${{ github.token }} - RELEASE_COMMIT: ${{ needs.preflight.outputs.release_commit }} - run: | - set -euo pipefail - controls=scripts/ci/repository-controls-evidence.sh - policy_args=() - if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then - controls=target/release-automation/scripts/ci/repository-controls-evidence.sh - policy_args=(--policy-root target/release-automation) - fi - "$controls" \ - --commit "$RELEASE_COMMIT" \ - --output "target/repository-controls/rscrypto-${CRATE_VERSION}-repository-controls.json" \ - --allow-redacted-bypass \ - "${policy_args[@]}" - - - name: Attest repository controls - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 - with: - subject-path: ${{ steps.repository_controls.outputs.evidence_path }} - - - name: Download qualification CT evidence - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: ct-raw-* - path: ct-release-artifacts - github-token: ${{ github.token }} - run-id: ${{ needs.preflight.outputs.qualification_run_id }} - - - name: Remove replaced x86_64 CT evidence - if: needs.preflight.outputs.x86_64_ct_run_id != '' - run: | - rm -rf \ - ct-release-artifacts/ct-raw-amd-zen4 \ - ct-release-artifacts/ct-raw-amd-zen5 \ - ct-release-artifacts/ct-raw-intel-icl \ - ct-release-artifacts/ct-raw-intel-spr - - - name: Download recovered x86_64 CT evidence - if: needs.preflight.outputs.x86_64_ct_run_id != '' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: ct-raw-* - path: ct-release-artifacts - github-token: ${{ github.token }} - run-id: ${{ needs.preflight.outputs.x86_64_ct_run_id }} - - - name: Remove replaced s390x CT evidence - if: needs.preflight.outputs.s390x_ct_run_id != '' - run: rm -rf ct-release-artifacts/ct-raw-ibm-s390x - - - name: Download recovered s390x CT evidence - if: needs.preflight.outputs.s390x_ct_run_id != '' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ct-raw-ibm-s390x - path: ct-release-artifacts/ct-raw-ibm-s390x - github-token: ${{ github.token }} - run-id: ${{ needs.preflight.outputs.s390x_ct_run_id }} - - - name: Package CT release evidence - id: ct_evidence - env: - CRATE_VERSION: ${{ steps.package.outputs.crate_version }} - EVIDENCE_COMMIT: ${{ needs.preflight.outputs.qualification_commit }} - EVIDENCE_VERSION: ${{ needs.preflight.outputs.qualification_version }} - RELEASE_COMMIT: ${{ needs.preflight.outputs.release_commit }} - run: | - scripts/ci/package-release-ct-evidence.sh \ - --version "$CRATE_VERSION" \ - --commit "$RELEASE_COMMIT" \ - --evidence-version "$EVIDENCE_VERSION" \ - --evidence-commit "$EVIDENCE_COMMIT" - - - name: Attest CT evidence provenance + - name: Attest constant-time evidence provenance uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-path: ${{ steps.ct_evidence.outputs.bundle_path }} - - - name: Write release identity manifest - id: release_manifest - env: - CRATE_VERSION: ${{ steps.package.outputs.crate_version }} - CRATE_PATH: ${{ steps.package.outputs.crate_path }} - SOURCE_PATH: ${{ steps.package.outputs.source_path }} - CT_EVIDENCE_PATH: ${{ steps.ct_evidence.outputs.bundle_path }} - REPOSITORY_CONTROLS_PATH: ${{ steps.repository_controls.outputs.evidence_path }} - EVIDENCE_COMMIT: ${{ needs.preflight.outputs.qualification_commit }} - EVIDENCE_MODE: ${{ needs.preflight.outputs.qualification_evidence_mode }} - RELEASE_COMMIT: ${{ needs.preflight.outputs.release_commit }} - RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }} - run: | - scripts/ci/write-release-manifest.sh \ - --version "$CRATE_VERSION" \ - --tag "$RELEASE_TAG" \ - --commit "$RELEASE_COMMIT" \ - --source "$SOURCE_PATH" \ - --crate "$CRATE_PATH" \ - --ct-evidence "$CT_EVIDENCE_PATH" \ - --repository-controls "$REPOSITORY_CONTROLS_PATH" \ - --evidence-commit "$EVIDENCE_COMMIT" \ - --evidence-mode "$EVIDENCE_MODE" \ - --output "release-artifacts/rscrypto-${CRATE_VERSION}-release-manifest.json" - - - name: Attest release identity manifest - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 - with: - subject-path: ${{ steps.release_manifest.outputs.manifest_path }} - - - name: Check existing crates.io package - id: existing + - id: existing + name: Check crates.io env: CRATE_SHA256: ${{ steps.package.outputs.crate_sha256 }} CRATE_VERSION: ${{ steps.package.outputs.crate_version }} @@ -401,17 +156,53 @@ jobs: url="https://crates.io/api/v1/crates/rscrypto/${CRATE_VERSION}/download" if curl -fsSL -A "rscrypto-release/${CRATE_VERSION} (https://github.com/loadingalias/rscrypto)" "$url" -o published.crate; then published_sha="$(sha256sum published.crate | awk '{print $1}')" - if [[ "$published_sha" != "$CRATE_SHA256" ]]; then - echo "crates.io already has rscrypto ${CRATE_VERSION}, but sha256 differs" >&2 - echo "expected: $CRATE_SHA256" >&2 - echo "actual: $published_sha" >&2 + [[ "$published_sha" == "$CRATE_SHA256" ]] || { + echo "crates.io already has rscrypto ${CRATE_VERSION} with different bytes" >&2 exit 1 - fi + } echo "published=true" >> "$GITHUB_OUTPUT" else echo "published=false" >> "$GITHUB_OUTPUT" fi - + - name: Authenticate with crates.io + if: steps.existing.outputs.published != 'true' + id: auth + uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5 + - name: Publish to crates.io + if: steps.existing.outputs.published != 'true' + env: + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} + CRATE_PATH: ${{ steps.package.outputs.crate_path }} + CRATE_SHA256: ${{ steps.package.outputs.crate_sha256 }} + run: | + set -euo pipefail + cargo publish -p rscrypto --locked + published_sha="$(sha256sum "$CRATE_PATH" | awk '{print $1}')" + [[ "$published_sha" == "$CRATE_SHA256" ]] || { + echo "cargo publish produced different package bytes" >&2 + exit 1 + } + - name: Verify crates.io package + env: + CRATE_SHA256: ${{ steps.package.outputs.crate_sha256 }} + CRATE_VERSION: ${{ steps.package.outputs.crate_version }} + run: | + set -euo pipefail + url="https://crates.io/api/v1/crates/rscrypto/${CRATE_VERSION}/download" + for attempt in {1..18}; do + if curl -fsSL -A "rscrypto-release/${CRATE_VERSION} (https://github.com/loadingalias/rscrypto)" "$url" -o published.crate; then + published_sha="$(sha256sum published.crate | awk '{print $1}')" + [[ "$published_sha" == "$CRATE_SHA256" ]] || { + echo "crates.io returned different package bytes" >&2 + exit 1 + } + exit 0 + fi + echo "crates.io package not available yet; retry $attempt/18" + sleep 10 + done + echo "timed out waiting for rscrypto ${CRATE_VERSION} on crates.io" >&2 + exit 1 - name: Write release checksums env: CRATE_NAME: ${{ steps.package.outputs.crate_name }} @@ -420,25 +211,17 @@ jobs: SOURCE_SHA256: ${{ steps.package.outputs.source_sha256 }} CT_BUNDLE_NAME: ${{ steps.ct_evidence.outputs.bundle_name }} CT_BUNDLE_SHA256: ${{ steps.ct_evidence.outputs.bundle_sha256 }} - REPOSITORY_CONTROLS_NAME: ${{ steps.repository_controls.outputs.evidence_name }} - REPOSITORY_CONTROLS_SHA256: ${{ steps.repository_controls.outputs.evidence_sha256 }} - RELEASE_MANIFEST_NAME: ${{ steps.release_manifest.outputs.manifest_name }} - RELEASE_MANIFEST_SHA256: ${{ steps.release_manifest.outputs.manifest_sha256 }} run: | set -euo pipefail { printf '%s %s\n' "$CRATE_SHA256" "$CRATE_NAME" printf '%s %s\n' "$SOURCE_SHA256" "$SOURCE_NAME" printf '%s %s\n' "$CT_BUNDLE_SHA256" "$CT_BUNDLE_NAME" - printf '%s %s\n' "$REPOSITORY_CONTROLS_SHA256" "$REPOSITORY_CONTROLS_NAME" - printf '%s %s\n' "$RELEASE_MANIFEST_SHA256" "$RELEASE_MANIFEST_NAME" } > SHA256SUMS - - name: Attest release checksums uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-path: SHA256SUMS - - name: Create GitHub release env: GH_TOKEN: ${{ github.token }} @@ -451,35 +234,15 @@ jobs: CT_BUNDLE_NAME: ${{ steps.ct_evidence.outputs.bundle_name }} CT_BUNDLE_PATH: ${{ steps.ct_evidence.outputs.bundle_path }} CT_BUNDLE_SHA256: ${{ steps.ct_evidence.outputs.bundle_sha256 }} - CT_EVIDENCE_COMMIT: ${{ needs.preflight.outputs.qualification_commit }} - CT_EVIDENCE_MODE: ${{ needs.preflight.outputs.qualification_evidence_mode }} - CT_EVIDENCE_QUALIFICATION_RUN: ${{ needs.preflight.outputs.qualification_run_url }} - CT_EVIDENCE_S390X_RUN: ${{ needs.preflight.outputs.s390x_ct_run_url }} - CT_EVIDENCE_X86_64_RUN: ${{ needs.preflight.outputs.x86_64_ct_run_url }} - REPOSITORY_CONTROLS_NAME: ${{ steps.repository_controls.outputs.evidence_name }} - REPOSITORY_CONTROLS_PATH: ${{ steps.repository_controls.outputs.evidence_path }} - REPOSITORY_CONTROLS_SHA256: ${{ steps.repository_controls.outputs.evidence_sha256 }} - RELEASE_MANIFEST_NAME: ${{ steps.release_manifest.outputs.manifest_name }} - RELEASE_MANIFEST_PATH: ${{ steps.release_manifest.outputs.manifest_path }} - RELEASE_MANIFEST_SHA256: ${{ steps.release_manifest.outputs.manifest_sha256 }} - RELEASE_COMMIT: ${{ needs.preflight.outputs.release_commit }} - RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }} - WORKFLOW_COMMIT: ${{ github.sha }} + RELEASE_COMMIT: ${{ github.sha }} + RELEASE_TAG: ${{ github.ref_name }} + RELEASE_RUN: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | set -euo pipefail notes_file="$(mktemp)" - s390x_recovery_note="" - x86_64_recovery_note="" - if [[ -n "$CT_EVIDENCE_S390X_RUN" ]]; then - s390x_recovery_note="s390x recovery evidence run: $CT_EVIDENCE_S390X_RUN" - fi - if [[ -n "$CT_EVIDENCE_X86_64_RUN" ]]; then - x86_64_recovery_note="x86_64 recovery evidence run: $CT_EVIDENCE_X86_64_RUN" - fi cat > "$notes_file" <&2 - echo "before: $before_sha" >&2 - echo "after: $after_sha" >&2 - echo "expected: $CRATE_SHA256" >&2 - exit 1 - fi - - - name: Verify crates.io package - env: - CRATE_SHA256: ${{ steps.package.outputs.crate_sha256 }} - CRATE_VERSION: ${{ steps.package.outputs.crate_version }} - run: | - set -euo pipefail - url="https://crates.io/api/v1/crates/rscrypto/${CRATE_VERSION}/download" - for attempt in {1..18}; do - if curl -fsSL -A "rscrypto-release/${CRATE_VERSION} (https://github.com/loadingalias/rscrypto)" "$url" -o published.crate; then - published_sha="$(sha256sum published.crate | awk '{print $1}')" - if [[ "$published_sha" == "$CRATE_SHA256" ]]; then - exit 0 - fi - echo "crates.io sha256 mismatch for rscrypto ${CRATE_VERSION}" >&2 - echo "expected: $CRATE_SHA256" >&2 - echo "actual: $published_sha" >&2 - exit 1 - fi - echo "crates.io package not available yet; retry $attempt/18" - sleep 10 - done - echo "timed out waiting for rscrypto ${CRATE_VERSION} on crates.io" >&2 - exit 1 + --asset SHA256SUMS diff --git a/.github/workflows/riscv.yaml b/.github/workflows/riscv.yaml deleted file mode 100644 index f9a570f9..00000000 --- a/.github/workflows/riscv.yaml +++ /dev/null @@ -1,147 +0,0 @@ -name: RISC-V - -on: - workflow_dispatch: - inputs: - mode: - description: "LANES (Evidence = native + constant-time)" - required: true - type: choice - options: - - evidence - - native - - constant-time - - benchmark - - all - default: evidence - targets: - description: "BENCH PRIMITIVES/SELECTORS (Empty or all = all public primitive benches)" - required: false - type: string - default: "all" - filter: - description: "BENCH FILTERS (Optional)" - required: false - type: string - quick: - description: "QUICK BENCHMARK (Optional)" - required: false - type: boolean - default: true - -env: - RUST_BACKTRACE: 1 - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: 0 - -permissions: - contents: read - -jobs: - native: - if: ${{ inputs.mode == 'evidence' || inputs.mode == 'native' || inputs.mode == 'all' }} - name: Native CI - permissions: - contents: read - concurrency: - group: riscv-native-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && 'manual' || 'assurance' }} - cancel-in-progress: true - uses: ./.github/workflows/_rust-job.yaml - with: - runner: ubuntu-24.04-riscv - timeout_minutes: 240 - tools_mode: none - toolchain_contract: nightly - toolchain_components: "clippy, rustfmt" - operation: native-riscv - secrets: - cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - ct: - if: ${{ inputs.mode == 'evidence' || inputs.mode == 'constant-time' || inputs.mode == 'all' }} - name: Constant-Time Evidence (RISC-V) - permissions: - contents: read - concurrency: - group: riscv-ct-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && 'manual' || 'assurance' }} - cancel-in-progress: true - uses: ./.github/workflows/ct.yaml - with: - platforms: rise-riscv - dudect_timeout: 1800 - binsec_timeout: 900 - upload_raw_artifacts: true - artifact_retention_days: 90 - secrets: - CARGO_RAIL_R2_READ_ACCESS_KEY_ID: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - benchmark: - if: ${{ inputs.mode == 'benchmark' || inputs.mode == 'all' }} - name: Benchmark - permissions: - contents: read - concurrency: - group: riscv-benchmark-${{ github.ref }} - cancel-in-progress: false - uses: ./.github/workflows/_rust-job.yaml - with: - runner: ubuntu-24.04-riscv - timeout_minutes: 240 - tools_mode: ibm - toolchain_contract: nightly - toolchain_components: "clippy, rustfmt" - operation: benchmark - platform: rise-riscv - bench_targets: ${{ inputs.targets || 'all' }} - bench_filter: ${{ inputs.filter }} - bench_quick: ${{ inputs.quick }} - artifact_name: benchmark-rise-riscv - artifact_path: target/benchmark_results/ - secrets: - cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - complete: - name: Complete (RISC-V) - needs: [native, ct, benchmark] - if: ${{ !cancelled() }} - runs-on: ubuntu-latest - steps: - - name: Assert selected lanes passed - shell: bash - env: - MODE: ${{ inputs.mode }} - NATIVE_RESULT: ${{ needs.native.result }} - CT_RESULT: ${{ needs.ct.result }} - BENCHMARK_RESULT: ${{ needs.benchmark.result }} - run: | - set -euo pipefail - case "$MODE" in - evidence) required=(native ct) ;; - native) required=(native) ;; - constant-time) required=(ct) ;; - benchmark) required=(benchmark) ;; - all) required=(native ct benchmark) ;; - *) - echo "Unknown RISC-V mode: $MODE" >&2 - exit 2 - ;; - esac - - failed=0 - for lane in "${required[@]}"; do - case "$lane" in - native) result="$NATIVE_RESULT" ;; - ct) result="$CT_RESULT" ;; - benchmark) result="$BENCHMARK_RESULT" ;; - esac - if [[ "$result" == "success" ]]; then - echo " OK: $lane" - else - echo "FAIL: $lane ($result)" - failed=1 - fi - done - exit "$failed" diff --git a/.github/workflows/rsa.yaml b/.github/workflows/rsa.yaml deleted file mode 100644 index bcf64b87..00000000 --- a/.github/workflows/rsa.yaml +++ /dev/null @@ -1,131 +0,0 @@ -name: RSA - -on: - workflow_call: - inputs: - artifact_retention_days: - description: "Retention for RSA evidence artifacts." - required: false - type: number - default: 90 - secrets: - CARGO_RAIL_R2_READ_ACCESS_KEY_ID: - description: "Bucket-scoped R2 access key forwarded to RSA compiler jobs." - required: false - CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY: - description: "Bucket-scoped R2 secret key forwarded to RSA compiler jobs." - required: false - workflow_dispatch: - inputs: - artifact_retention_days: - description: "Retention for RSA evidence artifacts." - required: false - type: number - default: 90 - -concurrency: - group: rsa-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -env: - RUST_BACKTRACE: 1 - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: 0 - RSCRYPTO_TEST_MODE: rsa - -permissions: - contents: read - -jobs: - rsa-miri: - name: RSA Miri (Linux x64) - permissions: - contents: read - uses: ./.github/workflows/_rust-job.yaml - with: - runner: runs-on=${{ github.run_id }}/runner=linux-x64-ci - timeout_minutes: 60 - tools_mode: minimal - toolchain_contract: nightly - toolchain_components: "miri, rust-src" - operation: rsa-miri - artifact_name: rsa-miri-linux-x64 - artifact_path: ci-evidence/rsa-miri-linux-x64.log - artifact_always: true - artifact_retention_days: ${{ inputs.artifact_retention_days }} - secrets: - cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - rsa-leakage: - name: RSA Leakage (${{ matrix.target.name }}) - permissions: - contents: read - strategy: - fail-fast: false - matrix: - target: - - name: linux-x64 - runner: runs-on=${{ github.run_id }}/runner=linux-x64-ci - - name: linux-arm64 - runner: runs-on=${{ github.run_id }}/runner=linux-arm64-ci - uses: ./.github/workflows/_rust-job.yaml - with: - runner: ${{ matrix.target.runner }} - timeout_minutes: 180 - tools_mode: minimal - operation: rsa-leakage - target: ${{ matrix.target.name }} - artifact_name: rsa-leakage-${{ matrix.target.name }} - artifact_path: ci-evidence/rsa-leakage-${{ matrix.target.name }}.log - artifact_always: true - artifact_retention_days: ${{ inputs.artifact_retention_days }} - secrets: - cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - rsa-linux-x86_64-asm: - name: RSA Assembly (Linux x86-64) - permissions: - contents: read - uses: ./.github/workflows/_rust-job.yaml - with: - runner: runs-on=${{ github.run_id }}/runner=linux-x64-ci - timeout_minutes: 45 - tools_mode: none - toolchain_components: "" - operation: rsa-linux-x64-asm - artifact_name: rsa-linux-x86_64-asm - artifact_path: ci-evidence/rsa-linux-x86_64-asm.log - artifact_always: true - artifact_retention_days: ${{ inputs.artifact_retention_days }} - secrets: - cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - complete: - name: Complete (RSA) - needs: [rsa-miri, rsa-leakage, rsa-linux-x86_64-asm] - if: ${{ !cancelled() }} - runs-on: ubuntu-latest - steps: - - name: Assert RSA lanes passed - run: | - failed=0 - for lane in \ - "rsa-miri=${{ needs.rsa-miri.result }}" \ - "rsa-leakage=${{ needs.rsa-leakage.result }}" \ - "rsa-linux-x86_64-asm=${{ needs.rsa-linux-x86_64-asm.result }}" - do - name="${lane%%=*}" - result="${lane#*=}" - if [ "$result" != "success" ]; then - echo "FAIL: $name ($result)" - failed=1 - else - echo " OK: $name" - fi - done - if [ "$failed" -eq 1 ]; then - exit 1 - fi diff --git a/.github/workflows/scorecard.yaml b/.github/workflows/scorecard.yaml index d6242379..d538ea21 100644 --- a/.github/workflows/scorecard.yaml +++ b/.github/workflows/scorecard.yaml @@ -17,6 +17,7 @@ jobs: scorecard: name: Scorecard runs-on: ubuntu-latest + timeout-minutes: 30 permissions: contents: read id-token: write diff --git a/.github/workflows/weekly.yaml b/.github/workflows/weekly.yaml deleted file mode 100644 index bc3ffac5..00000000 --- a/.github/workflows/weekly.yaml +++ /dev/null @@ -1,474 +0,0 @@ -name: Qualification - -on: - schedule: - - cron: "0 3 * * 0" - workflow_dispatch: - inputs: - mode: - description: "Assurance is routine coverage; release adds release-grade evidence." - required: true - type: choice - options: - - assurance - - release - default: assurance - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && inputs.mode == 'release' && 'release' || 'assurance' }} - cancel-in-progress: true - -env: - RUST_BACKTRACE: 1 - CARGO_TERM_COLOR: always - RSCRYPTO_TEST_MODE: weekly - CARGO_INCREMENTAL: 0 - -permissions: - contents: read - -jobs: - mode: - name: Resolve Qualification Mode - runs-on: ubuntu-latest - outputs: - mode: ${{ steps.mode.outputs.mode }} - steps: - - name: Resolve mode - id: mode - shell: bash - env: - EVENT_NAME: ${{ github.event_name }} - REQUESTED_MODE: ${{ inputs.mode }} - run: | - set -euo pipefail - case "$EVENT_NAME" in - schedule) - mode=assurance - ;; - workflow_dispatch) - case "$REQUESTED_MODE" in - assurance | release) mode="$REQUESTED_MODE" ;; - *) echo "Unknown Qualification mode: $REQUESTED_MODE" >&2; exit 2 ;; - esac - ;; - *) - echo "Unsupported Qualification event: $EVENT_NAME" >&2 - exit 2 - ;; - esac - echo "mode=$mode" >> "$GITHUB_OUTPUT" - echo "Qualification mode: $mode" - - rail-plan: - name: Cargo Rail Full Plan - needs: mode - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.matrix.outputs.matrix }} - identity: ${{ steps.rail.outputs.plan-identity }} - head-commit: ${{ steps.rail.outputs.head-commit }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Build full exact work plan - id: rail - uses: loadingalias/cargo-rail-action@78ad385a85627484a5b634cf1bc3caa1de872c6a # v8.2.0 - with: - version: "0.25.0" - components: surface - all: true - - - name: Materialize full CI matrix - id: matrix - shell: bash - env: - PLAN_FILE: ${{ steps.rail.outputs.plan-file }} - PLAN_READER: ${{ steps.rail.outputs.plan-reader }} - run: scripts/ci/materialize-rail-plan.sh "$PLAN_FILE" "$PLAN_READER" "$GITHUB_OUTPUT" - - - name: Check public source surface - if: contains(fromJSON(steps.rail.outputs.required-work), 'surface') - shell: bash - run: | - mkdir -p target/cargo-rail - cargo rail surface --check --json > target/cargo-rail/surface-result.json - - - name: Upload exact work plan - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: cargo-rail-plan-${{ github.run_id }} - path: | - ${{ steps.rail.outputs.plan-file }} - ${{ steps.rail.outputs.plan-reader }} - retention-days: 1 - if-no-files-found: error - - - name: Upload Surface evidence - if: ${{ !cancelled() && contains(fromJSON(steps.rail.outputs.required-work), 'surface') }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: cargo-rail-surface-${{ github.run_id }} - path: target/cargo-rail/surface-result.json - retention-days: ${{ needs.mode.outputs.mode == 'release' && 90 || 14 }} - if-no-files-found: error - - # Both modes run the full planned platform, graph, and feature suite. - # Release mode adds full supply-chain policy and longer evidence retention. - suite: - name: CI Suite (${{ needs.mode.outputs.mode }}) - needs: [mode, rail-plan] - permissions: - contents: read - uses: ./.github/workflows/_ci-suite.yaml - with: - matrix: ${{ needs.rail-plan.outputs.matrix }} - plan_artifact: cargo-rail-plan-${{ github.run_id }} - plan_identity: ${{ needs.rail-plan.outputs.identity }} - plan_head_commit: ${{ needs.rail-plan.outputs.head-commit }} - supply_chain_mode: ${{ needs.mode.outputs.mode == 'release' && 'full' || 'light' }} - test_mode: weekly - artifact_retention_days: ${{ needs.mode.outputs.mode == 'release' && 90 || 14 }} - secrets: - cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - # ─── Miri memory-safety (stacked borrows). ── - miri: - name: Miri (${{ matrix.target.name }}) - permissions: - contents: read - strategy: - fail-fast: false - matrix: - target: - - name: x86_64 - runner: runs-on=${{ github.run_id }}/runner=linux-x64-ci - - name: aarch64 - runner: runs-on=${{ github.run_id }}/runner=linux-arm64-ci - uses: ./.github/workflows/_rust-job.yaml - with: - runner: ${{ matrix.target.runner }} - timeout_minutes: 60 - tools_mode: minimal - toolchain_contract: nightly - toolchain_components: "miri, rust-src" - operation: miri - secrets: - cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - # ─── Miri memory-safety (tree borrows). ── - miri-tree-borrows: - name: Miri (Tree Borrows) - permissions: - contents: read - uses: ./.github/workflows/_rust-job.yaml - with: - runner: runs-on=${{ github.run_id }}/runner=linux-x64-ci - timeout_minutes: 60 - tools_mode: minimal - toolchain_contract: nightly - toolchain_components: "miri, rust-src" - operation: miri - miri_tree_borrows: true - secrets: - cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - # ─── Fuzzing (libFuzzer full + scoped packages). ── - fuzzing: - name: Fuzzing (${{ matrix.target.name }}) - needs: mode - permissions: - contents: read - strategy: - fail-fast: false - matrix: - target: - - name: x86_64 - runner: runs-on=${{ github.run_id }}/runner=linux-x64-ci - - name: aarch64 - runner: runs-on=${{ github.run_id }}/runner=linux-arm64-ci - uses: ./.github/workflows/_rust-job.yaml - with: - runner: ${{ matrix.target.runner }} - timeout_minutes: 120 - tools_mode: fuzz - toolchain_contract: nightly - operation: fuzz - artifact_name: fuzz-output-weekly-${{ matrix.target.name }} - artifact_path: | - fuzz-output/corpus.tar.gz - fuzz/artifacts/ - fuzz-packages/*/artifacts/ - artifact_always: true - artifact_retention_days: ${{ needs.mode.outputs.mode == 'release' && 90 || 14 }} - secrets: - cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - asan-fuzzing: - name: ASan Fuzz Corpus (${{ matrix.target.name }}) - permissions: - contents: read - strategy: - fail-fast: false - matrix: - target: - - name: x86_64 - runner: runs-on=${{ github.run_id }}/runner=linux-x64-ci - - name: aarch64 - runner: runs-on=${{ github.run_id }}/runner=linux-arm64-ci - uses: ./.github/workflows/_rust-job.yaml - with: - runner: ${{ matrix.target.runner }} - timeout_minutes: 120 - tools_mode: fuzz - toolchain_contract: nightly - toolchain_components: "rust-src" - operation: fuzz-asan - secrets: - cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - # --- Native ML-KEM promotion gate on the physical AWS targets. --- - mlkem-graviton: - name: ML-KEM Graviton Gate (${{ matrix.display_name }}) - needs: mode - permissions: - contents: read - strategy: - fail-fast: false - matrix: - include: - - platform: graviton3 - display_name: AWS Graviton3 - - platform: graviton4 - display_name: AWS Graviton4 - uses: ./.github/workflows/_rust-job.yaml - with: - runner: runs-on=${{ github.run_id }}/runner=${{ matrix.platform }} - timeout_minutes: 180 - tools_mode: minimal - toolchain_components: "clippy, rustfmt, rust-src" - operation: mlkem-aarch64 - platform: ${{ matrix.platform }} - artifact_name: mlkem-aarch64-gate-${{ matrix.platform }} - artifact_path: mlkem-aarch64-gate/ - artifact_always: true - artifact_retention_days: ${{ needs.mode.outputs.mode == 'release' && 90 || 14 }} - secrets: - cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - # ─── Constant-time evidence on benchmark-published physical lanes. ── - ct: - name: Constant-Time Evidence (${{ needs.mode.outputs.mode }}) - needs: mode - permissions: - contents: read - uses: ./.github/workflows/ct.yaml - with: - platforms: all - dudect_timeout: 1800 - binsec_timeout: 900 - upload_raw_artifacts: ${{ needs.mode.outputs.mode == 'release' }} - artifact_retention_days: ${{ needs.mode.outputs.mode == 'release' && 90 || 14 }} - secrets: - CARGO_RAIL_R2_READ_ACCESS_KEY_ID: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - rsa: - name: RSA Evidence (${{ needs.mode.outputs.mode }}) - needs: mode - permissions: - contents: read - uses: ./.github/workflows/rsa.yaml - with: - artifact_retention_days: ${{ needs.mode.outputs.mode == 'release' && 90 || 14 }} - secrets: - CARGO_RAIL_R2_READ_ACCESS_KEY_ID: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - # RISC-V is a target/evidence variant in the same qualification graph, not - # a second release ritual with an independently selected source tree. - riscv-native: - name: RISC-V Native Evidence - permissions: - contents: read - uses: ./.github/workflows/_rust-job.yaml - with: - runner: ubuntu-24.04-riscv - timeout_minutes: 240 - tools_mode: none - toolchain_contract: nightly - toolchain_components: "clippy, rustfmt" - operation: native-riscv - secrets: - cache_access_key_id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - cache_secret_access_key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - riscv-ct: - name: RISC-V CT Evidence (${{ needs.mode.outputs.mode }}) - needs: mode - permissions: - contents: read - uses: ./.github/workflows/ct.yaml - with: - platforms: rise-riscv - dudect_timeout: 1800 - binsec_timeout: 900 - upload_raw_artifacts: ${{ needs.mode.outputs.mode == 'release' }} - artifact_retention_days: ${{ needs.mode.outputs.mode == 'release' && 90 || 14 }} - secrets: - CARGO_RAIL_R2_READ_ACCESS_KEY_ID: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - # ─── Coverage (nextest + fuzz corpus → codecov). ── - coverage: - name: Coverage - needs: [mode, suite, fuzzing] - permissions: - contents: read - runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci - timeout-minutes: 120 - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Setup - id: setup - uses: ./.github/actions/setup - with: - tools-mode: coverage - toolchain-components: "rust-src" - cache-url: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && vars.CARGO_RAIL_CACHE_URL || '' }} - cache-mode: read - cache-access-key-id: ${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} - cache-secret-access-key: ${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - - - name: Download fuzz corpus artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: fuzz-output-weekly-* - path: fuzz-output-artifacts - - - name: Restore fuzz corpora - shell: bash - run: | - shopt -s nullglob - archives=(fuzz-output-artifacts/*/fuzz-output/corpus.tar.gz) - if [ "${#archives[@]}" -eq 0 ]; then - echo "No fuzz corpus archives found" - exit 0 - fi - for archive in "${archives[@]}"; do - echo "Extracting $archive" - tar -xzf "$archive" - done - - - name: Total Coverage (nextest + fuzz corpus replay) - run: just test-coverage - - - name: Capture Cargo Rail Cache Status - id: cache-status - if: ${{ !cancelled() && steps.setup.outputs.cache-enabled == 'true' }} - shell: bash - env: - RSCRYPTO_CI_OPERATION: coverage - RSCRYPTO_CI_RUNNER: linux-x64-ci - run: scripts/ci/capture-cache-status.sh - - - name: Upload Cargo Rail Cache Status - if: ${{ !cancelled() && steps.setup.outputs.cache-enabled == 'true' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ steps.cache-status.outputs.artifact_name }} - path: target/cargo-rail/cache-status.json - retention-days: 14 - if-no-files-found: error - - - name: Install authenticated Codecov CLI - id: codecov - run: scripts/ci/install-codecov.sh - - - name: Upload to Codecov - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - binary: ${{ steps.codecov.outputs.binary }} - token: ${{ secrets.CODECOV_TOKEN }} - files: coverage/total.lcov - flags: total - name: total - fail_ci_if_error: true - - - name: Upload Coverage Artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: coverage-reports - path: | - coverage/total.lcov - coverage/SUMMARY.md - retention-days: ${{ needs.mode.outputs.mode == 'release' && 90 || 14 }} - if-no-files-found: warn - - # The mode-specific terminal name is part of the release qualification contract. - complete: - name: Complete (${{ needs.mode.outputs.mode }}) - needs: - [ - mode, - suite, - miri, - miri-tree-borrows, - fuzzing, - asan-fuzzing, - mlkem-graviton, - ct, - rsa, - riscv-native, - riscv-ct, - coverage, - ] - if: ${{ !cancelled() }} - runs-on: ubuntu-latest - steps: - - name: Assert all lanes passed - run: | - failed=0 - for lane in \ - "mode=${{ needs.mode.result }}" \ - "suite=${{ needs.suite.result }}" \ - "miri=${{ needs.miri.result }}" \ - "miri-tree-borrows=${{ needs.miri-tree-borrows.result }}" \ - "fuzzing=${{ needs.fuzzing.result }}" \ - "asan-fuzzing=${{ needs.asan-fuzzing.result }}" \ - "mlkem-graviton=${{ needs.mlkem-graviton.result }}" \ - "ct=${{ needs.ct.result }}" \ - "rsa=${{ needs.rsa.result }}" \ - "riscv-native=${{ needs.riscv-native.result }}" \ - "riscv-ct=${{ needs.riscv-ct.result }}" \ - "coverage=${{ needs.coverage.result }}" - do - name="${lane%%=*}" - result="${lane#*=}" - if [ "$result" != "success" ]; then - echo "FAIL: $name ($result)" - failed=1 - else - echo " OK: $name" - fi - done - if [ "$failed" -eq 1 ]; then - echo "" - echo "One or more required lanes failed." - exit 1 - fi diff --git a/docs/constant-time.md b/docs/constant-time.md index 0ccb1c41..229fda04 100644 --- a/docs/constant-time.md +++ b/docs/constant-time.md @@ -36,6 +36,13 @@ builds them, runs available timing checks, and emits reports. A release claim requires the target-specific lanes required by `ct.toml`; a local host cannot stand in for another target. +Affected pull requests run `just ct-structural` when Cargo Rail selects +`assurance.ct`. That bounded x86-64 gate builds the optimized release harness, +inspects its generated code, and validates strict manifest/artifact coverage. +It is an early compiler-regression gate, not timing or formal evidence. +Scheduled and release Qualification consume the same immutable plan and retain +the full `ct.yaml` physical DudeCT and BINSEC matrix. + ## Public decisions and exclusions Ordinary equality is permitted for public values such as nonces, encoded public diff --git a/docs/features.md b/docs/features.md index 8ff0b130..01d329a0 100644 --- a/docs/features.md +++ b/docs/features.md @@ -55,7 +55,14 @@ compile-time `target_feature` settings. See [`platforms.md`](platforms.md). ```sh cargo check --no-default-features --features sha2 -just check-feature-matrix +just plan +just check +just feature-contracts compile ``` +`just check` asks Cargo Rail for the affected feature groups and runs only the +compile profiles whose resolved Cargo feature graph includes those groups. +`just validate` also runs selected runtime capability profiles. The explicit +`just feature-contracts compile` command remains the full compile contract. + Use [docs.rs](https://docs.rs/rscrypto) to see which items each feature exposes. diff --git a/docs/platforms.md b/docs/platforms.md index 1c56e4f1..8cf0b23c 100644 --- a/docs/platforms.md +++ b/docs/platforms.md @@ -25,13 +25,48 @@ target builds; runtime behavior requires target execution. ## Supported targets -[`.config/target-matrix.json`](../.config/target-matrix.json) owns the target -groups and CI execution lanes. Targets outside that matrix may compile, but are -not part of the tested support contract. +[`.config/target-matrix.json`](../.config/target-matrix.json) is both the target +support catalog and the Cargo Rail variant catalog. Targets outside it may +compile, but are not part of the tested support contract. `ci.yaml` executes +only affected rows; `qualification.yaml` executes every row. The ordinary +Linux x86-64 row is already owned by the core Rust job and is not duplicated in +the platform matrix. -The matrix includes native Windows, macOS, Linux, IBM, bare-metal `no_std`, and -WASM targets. Only the listed GitHub Actions and native runner lanes provide -runtime evidence. +| Target | Compile proof | Runtime proof | Perf | CT | Release | +| --- | --- | --- | --- | --- | --- | +| `aarch64-apple-darwin` | Native | Virtual native | No | No | Yes | +| `aarch64-pc-windows-msvc` | Hosted | None | No | No | Yes | +| `aarch64-unknown-linux-gnu` | Native | Virtual native | Yes | Yes | Yes | +| `aarch64-unknown-linux-musl` | Generic cross | None | No | No | Yes | +| `aarch64-unknown-none` | Generic cross | None | No | No | Yes | +| `powerpc64le-unknown-linux-gnu` | Native | Physical native | Yes | Yes | Yes | +| `riscv32imac-unknown-none-elf` | Generic cross | None | No | No | Yes | +| `riscv64gc-unknown-linux-gnu` | Native | Physical native | Yes | Yes | Yes | +| `s390x-unknown-linux-gnu` | Native | Physical native | Yes | Yes | Yes | +| `thumbv6m-none-eabi` | Generic cross | None | No | No | Yes | +| `wasm32-unknown-unknown` | Generic cross | None | No | No | Yes | +| `wasm32-wasip1` | Generic cross | Wasmtime emulation | No | No | Yes | +| `x86_64-apple-darwin` | Native | Virtual native | No | No | Yes | +| `x86_64-pc-windows-msvc` | Native | Virtual native | No | No | Yes | +| `x86_64-unknown-linux-gnu` | Core job | Virtual native | Yes | Yes | Yes | +| `x86_64-unknown-linux-musl` | Generic cross | None | No | No | Yes | +| `x86_64-unknown-none` | Generic cross | None | No | No | Yes | + +Performance and CT entries refer to their separate hardware workflows; a +compile row never supplies those claims. The catalog also contains a separate +physical Intel Sapphire Rapids proof for Linux AMX process authorization. + +Native platform rows fail before Cargo work if the Rust host triple or machine +architecture does not match the catalog. Donated POWER, IBM Z, and RISC-V +machines run only native unit/backend evidence and focused portable-versus- +accelerated tests. Windows AArch64 compiles but does not claim runtime evidence; +Windows x86-64 does. Both declared Apple targets have routine native ownership. + +Local and remote machines reproduce a row with +`just target-contract ROW [shallow|deep]` or +`just ssh-just MACHINE target-contract ROW deep`. `ssh-list` remains the +authority for development-machine names; rscrypto does not duplicate that +provider catalog. Backend availability varies by primitive, target, compiler, and CPU. Use `rscrypto::platform` and the `introspect` example to inspect one build: diff --git a/docs/release.md b/docs/release.md index f77e01f9..ed67f8a0 100644 --- a/docs/release.md +++ b/docs/release.md @@ -1,61 +1,31 @@ # Release process -Releases are prepared by Cargo Rail, approved by the maintainer, and published -by GitHub Actions. Do not run `cargo publish` locally. +Cargo Rail owns release planning, mutation, exact-SHA readiness, and the signed +tag. GitHub Actions qualifies that tag and publishes it with short-lived +credentials. Do not run `cargo publish` locally. ## Release -1. Start from clean, current `main` after all intended changes have merged. - Every user-visible change needs a reviewed `.changes/*.md` entry. - - ```sh - git switch main - git pull --ff-only - git status --short - cargo rail change status - ``` - -2. Open the generated release pull request: - - ```sh - just release-prepare - ``` - - Cargo Rail builds and atomically applies the release plan, consumes the - required change files, updates the version and changelog, and opens the - release pull request. The same transaction computes and binds every - standalone Cargo lockfile declared by - `release.auxiliary_cargo_manifests`. Wait for `Complete`, review the diff, - and merge in GitHub. - -3. Record the merged candidate and dispatch exact-commit evidence before - another change reaches `main`: - - ```sh - git switch main - git pull --ff-only - candidate=$(git rev-parse HEAD) - gh workflow run weekly.yaml --ref main -f mode=release - ``` - - Confirm the Qualification run uses `$candidate`. Its single immutable Cargo - Rail plan starts the platform, graph, feature, CT, RSA, coverage, and RISC-V - evidence lanes concurrently. Scheduled or assurance-mode runs do not - satisfy the release gate. Any change to source, dependencies, features, - build inputs, or test policy creates a new candidate and requires a new - release-mode Qualification run. - -4. After Qualification passes, create the signed tag: - - ```sh - test "$(git rev-parse HEAD)" = "$candidate" - just release-tag - ``` - - The tag starts the `Release` workflow. Approve its `crates-io` environment - only after prerequisite jobs pass. CI publishes an immutable, attested - GitHub Release, then publishes the same crate through crates.io Trusted - Publishing. +After every intended change and its reviewed `.changes/*.md` entry has merged, +start from clean, current, green `main` and run: + +```sh +cargo rail release run rscrypto --wait +``` + +Cargo Rail owns the complete local transaction. It infers the bump, consumes +release intent, updates the version, changelog, root lockfile, and every +standalone lockfile declared by `release.auxiliary_cargo_manifests`, commits the +exact mutation, and pushes it. It then waits for the normal `Complete` check on +that commit—and every other exact-SHA check—to succeed before creating and +pushing the signed tag. A dirty, stale, non-default, or rejected checkout fails +without creating the tag. + +The tag starts the `Release` workflow. That run captures one all-work Cargo +Rail plan and runs every qualification domain concurrently while the release +package is validated in parallel. Publication waits for both results. Approve +the `crates-io` environment only then; CI publishes the exact validated crate +through Trusted Publishing and cuts the immutable, attested GitHub Release. ## Release intent @@ -74,8 +44,10 @@ guidance, and release notes must still be updated. - Configure crates.io Trusted Publishing for owner `loadingalias`, repository `rscrypto`, workflow `release.yaml`, and environment `crates-io`. -- Enable the committed `protect-main` and `protect-release-tags` rulesets with - no bypass actors. +- Restrict the `main` bypass to the maintainer identity used by Cargo Rail; + direct release commits require that authority. Keep force pushes, deletion, + and release-tag updates blocked. Cargo Rail does not push the tag until the + exact release commit's checks, including `Complete`, succeed. - Enable immutable GitHub Releases. - Require maintainer approval for the `crates-io` environment and disable administrator bypass. @@ -85,73 +57,27 @@ The environment name must match crates.io and `.github/workflows/release.yaml`. After the first successful trusted release, enable crates.io Trusted Publishing Only Mode. -## Recovery +## Retry -Rerun a transient failure on the same tag and commit: +Rerun a transient failure on the same tag and commit. Successful qualification +and package jobs remain authoritative; rerun only the failed jobs: ```sh gh run rerun RUN_ID --failed ``` -If the committed workflow needs repair, merge the smallest fix through -`Complete`, then dispatch recovery from protected `main`: - -```sh -gh workflow run release.yaml --ref main -f tag=vX.Y.Z -``` - -Recovery checks out and verifies the existing signed tag. It may repair a draft -release, but it cannot replace a published immutable release or publish bytes -that differ from an existing crates.io version. - -If only the s390x constant-time artifact must be regenerated, run the complete -native lane against the existing tag, then pass that run to recovery: - -```sh -gh workflow run ct.yaml --ref main \ - -f platforms=ibm-s390x \ - -f dudect_gate=required \ - -f upload_raw_artifacts=true \ - -f artifact_retention_days=90 \ - -f release_tag=vX.Y.Z - -gh workflow run release.yaml --ref main \ - -f tag=vX.Y.Z \ - -f s390x_ct_run=RUN_ID -``` - -For x86_64, the recovery group is all four physical timing lanes and runs them -in parallel: - -```sh -gh workflow run ct.yaml --ref main \ - -f platforms=amd-zen4,intel-spr,intel-icl,amd-zen5 \ - -f dudect_gate=required \ - -f upload_raw_artifacts=true \ - -f artifact_retention_days=90 \ - -f release_tag=vX.Y.Z - -gh workflow run release.yaml --ref main \ - -f tag=vX.Y.Z \ - -f x86_64_ct_run=RUN_ID -``` - -Run both dispatches from the same reviewed `main` commit. The CT recovery is -limited to a complete supported platform group and checks out the immutable -tag; the release preflight rejects any replacement run from another workflow, -branch, repository, or commit. The normal release evidence packager then -validates every replacement artifact's tag commit, crate version, cases, -hashes, and target provenance before publication. +There is no second recovery protocol. A workflow defect requires a new reviewed +candidate, a new release-mode Qualification run, and a new signed tag. Published +immutable release assets are never replaced. ## Verify a release ```sh gh release download vX.Y.Z --repo loadingalias/rscrypto \ - -p 'rscrypto-X.Y.Z.crate' \ - -p 'rscrypto-X.Y.Z-source.tar.gz' \ - -p 'rscrypto-X.Y.Z-ct-evidence.tar.gz' \ - -p 'rscrypto-X.Y.Z-release-manifest.json' \ - -p SHA256SUMS + -p 'rscrypto-X.Y.Z.crate' \ + -p 'rscrypto-X.Y.Z-source.tar.gz' \ + -p 'rscrypto-X.Y.Z-ct-evidence.tar.gz' \ + -p SHA256SUMS sha256sum --check SHA256SUMS gh release verify vX.Y.Z --repo loadingalias/rscrypto gh attestation verify rscrypto-X.Y.Z.crate --repo loadingalias/rscrypto diff --git a/docs/secret-lifecycle.md b/docs/secret-lifecycle.md index 6bc84a32..fb3018ac 100644 --- a/docs/secret-lifecycle.md +++ b/docs/secret-lifecycle.md @@ -45,6 +45,9 @@ stores and host-architecture zero-store instructions across these shapes: - AEAD authentication, header protection, and AES-SIV state. - RSA success, entropy failure, and staged private-key validation. +Scheduled and release Qualification run the same check cache-cold on Linux +x86-64. + This evidence binds the generated host binary. Each target needs its own run; source review remains the only evidence for an untested target. diff --git a/docs/test-vector-coverage.md b/docs/test-vector-coverage.md index 3e1cd459..48c1fe95 100644 --- a/docs/test-vector-coverage.md +++ b/docs/test-vector-coverage.md @@ -29,7 +29,7 @@ makes no collision-resistance or authentication claim. ```sh just test --all -just test-feature-matrix +just feature-contracts runtime just test-fuzz ``` diff --git a/justfile b/justfile index 639d6034..7d0f9021 100644 --- a/justfile +++ b/justfile @@ -50,11 +50,11 @@ ssh-collect-bench target run_id destination: ssh-list: @"{{ dev_machine }}" list rscrypto -# Install and verify the canonical remapped Cargo Rail cache policy. +# Preview, install, and verify the canonical remapped Cargo Rail cache policy. rail-cache-setup *args="": - @cargo rail cache setup --remote "$CARGO_RAIL_CACHE_REMOTE" --remote-mode "$CARGO_RAIL_CACHE_MODE" --root-portability remap {{ args }} @cargo rail cache setup --check --remote "$CARGO_RAIL_CACHE_REMOTE" --remote-mode "$CARGO_RAIL_CACHE_MODE" --root-portability remap {{ args }} - @cargo rail cache probe + @cargo rail cache setup --remote "$CARGO_RAIL_CACHE_REMOTE" --remote-mode "$CARGO_RAIL_CACHE_MODE" --root-portability remap {{ args }} + @cargo rail cache probe --json # Report the effective Cargo Rail cache policy and usage. cache-status: @@ -66,30 +66,34 @@ build *args="": cargo build --locked --workspace --all-targets --all-features {{ args }} # Checks -# Run affected local checks; pass --all for the full workspace. +# Explain the affected Cargo Rail work; accepts planner arguments. +plan *args="": + @cargo rail plan --explain {{ args }} + +# Run affected policy and Cargo checks from one plan; pass --all to widen. check *args="": - @scripts/check/check.sh {{ args }} + @scripts/check/affected.sh {{ args }} + +# Run affected policy, checks, and tests from one immutable plan. +validate *args="": + @scripts/check/affected.sh --with-tests {{ args }} # Run the broad local check set. check-all: @scripts/check/check-all.sh -# Check every supported feature profile. -check-feature-matrix: - @scripts/check/check-feature-matrix.sh +# Compile and test feature contracts; optionally select a domain and shard. +feature-contracts *args="": + @scripts/check/feature-contracts.sh {{ args }} + +# Reproduce one catalogued CI platform proof locally or through ssh-just. +target-contract row depth="deep": + @scripts/ci/target-contracts.sh run "{{ row }}" "{{ depth }}" # Rebuild and verify optimized zeroization evidence. check-zeroize-evidence: @scripts/check/zeroize-evidence.sh -# Run the CI quality policy locally. -ci-check: - @scripts/ci/ci-check.sh - -# Test every supported feature profile. -test-feature-matrix: - @scripts/test/test-feature-matrix.sh - # Tests # Test the affected scope or the full workspace with --all. test *args="": @@ -103,6 +107,10 @@ test-examples: test-miri *args="": @scripts/test/test-miri.sh {{ args }} +# Reproduce one Cargo Rail Miri proof row. +miri-contract row: + @scripts/test/miri-contracts.sh run "{{ row }}" + # Run the RSA leakage evidence harness. test-rsa-leakage: @scripts/test/test-rsa-leakage.sh @@ -111,15 +119,27 @@ test-rsa-leakage: test-rsa-macos-asm: @scripts/test/test-rsa-macos-asm.sh +# Test x86-64 RSA assembly on a physical Linux host. +test-rsa-linux-asm: + @scripts/test/test-rsa-linux-asm.sh + # Run fuzz targets or replay the full fuzz set with --all. test-fuzz *args="": @scripts/test/test-fuzz.sh {{ args }} +# Reproduce one Cargo Rail fuzz target group. +fuzz-contract row: + @scripts/test/fuzz-contracts.sh run "{{ row }}" + # Run fuzz targets with AddressSanitizer. test-fuzz-asan *args="": @scripts/test/test-fuzz-asan.sh {{ args }} # Constant-Time (CT) Validation Engine +# Build and validate the bounded x86-64 CT structure gate used by affected CI. +ct-structural: + @scripts/ct/structural.sh + # Run DudeCT Timing Checks ct-dudect *args="": @scripts/ct/dudect.sh {{ args }} @@ -173,50 +193,14 @@ perf-llvm-lines *args="": @command -v cargo-llvm-lines >/dev/null || { echo "error: cargo-llvm-lines is required" >&2; exit 1; } cargo llvm-lines --locked --release --lib --features full {{ args }} -# Maintenance - -# Build and apply one Cargo Rail release transaction, including standalone lockfiles. -release-prepare: - cargo rail release run rscrypto --bump auto --yes --pr - -# Verify exact-commit release evidence and create the signed release tag. -release-tag: - scripts/ci/repository-controls-evidence.sh \ - --commit "$(git rev-parse HEAD)" \ - --output target/repository-controls.json - scripts/ci/release-evidence-check.sh --commit "$(git rev-parse HEAD)" - cargo rail release finalize rscrypto --yes --skip-publish - # Update coordinated Cargo manifests # Update coordinated Cargo manifests, or preview with --check. update *args="": @scripts/update/update-all.sh {{ args }} -# Validate GitHub Actions, local actions, and their repository policy tests. +# Validate the CI/CD configuration and its focused adapters. check-actions: - @scripts/ci/check-action-pins.sh - @scripts/ci/check-action-pins-test.sh - @scripts/ci/tool-integrity-test.sh - @scripts/ci/remote-cache-recipes-test.sh - @scripts/ci/capture-cache-status-test.sh - @scripts/ci/dependabot-smoke-test.sh - @scripts/ci/check-ci-ownership.sh - @scripts/ci/check-ci-ownership-test.sh - @scripts/ci/run-rust-job-test.sh - @scripts/test/test-fuzz-scheduler-test.sh - @scripts/ci/emit-manual-matrix-test.sh - @scripts/ci/materialize-rail-plan-test.sh - @scripts/ci/changed-test-planning-test.sh - @scripts/ci/check-worktree-test.sh - @scripts/ci/pre-push-test.sh - @scripts/ci/release-evidence-check-test.sh - @scripts/ci/release-ct-recovery-check-test.sh - @scripts/ci/repository-controls-evidence-test.sh - @scripts/ci/release-identity-test.sh - @scripts/ci/publish-immutable-release-test.sh - @scripts/ci/release-recipes-test.sh - @actionlint - @zizmor .github/workflows .github/actions + @scripts/ci/actions-policy.sh # Run the pre-push policy and push the current branch. push: diff --git a/scripts/README.md b/scripts/README.md index 125f9712..14ea722d 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -8,67 +8,58 @@ the recipes reported by `just --list`. | Script | Callers | | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -| `check/check.sh` | `just check` | +| `check/affected.sh` | `just check`, `just validate`; creates one saved plan for the selected local composition | +| `check/policy.sh` | `check/affected.sh`, `ci.yaml`, `qualification.yaml` | +| `check/check.sh` | `check/affected.sh`, `check/check-all.sh`, `ci.yaml`, `qualification.yaml` | | `check/check-all.sh` | `just check-all` | -| `check/check-feature-matrix.sh` | `just check-feature-matrix`, `scripts/check/check.sh`, `ci/run-rust-job.sh` | -| `check/asm-ledger.sh` | `scripts/check/check.sh` | +| `check/feature-contracts.sh` | `just feature-contracts`, `scripts/check/affected.sh`, `scripts/check/check-all.sh`, `ci.yaml`, `qualification.yaml` | +| `check/asm-ledger.sh` | `scripts/check/policy.sh` | | `check/rsa-asm-provenance.sh` | `check/asm-ledger.sh`; direct `--archive PATH` reconstructs the three pinned RSA snapshots offline | | `check/signature-asm-provenance.py` | `check/asm-ledger.sh`; direct `--upstream-repo PATH [--clang PATH]` reproduces the 36 pinned ECDSA, Ed25519, and X25519 snapshots | -| `check/hash-vector-provenance.py` | `scripts/check/check.sh`; optional exact upstream checkouts reproduce hash-vector corpora | -| `check/auth-vector-provenance.py` | `scripts/check/check.sh`; optional `--upstream-root PATH` reproduces the pinned C2SP/Wycheproof corpus | -| `check/feature-boundaries.py` | `scripts/check/check.sh` | -| `check/zeroize-evidence.sh` | `just check-zeroize-evidence`, `scripts/check/check-all.sh` | -| `ci/ci-check.sh` | `just ci-check`, `ci/run-rust-job.sh` | -| `ci/check-locked-cargo.sh` | `ci/ci-check.sh` | -| `ci/check-locked-cargo-test.sh` | `ci/ci-check.sh` | -| `ci/capture-cache-status.sh` | `_rust-job.yaml` cache telemetry | -| `ci/capture-cache-status-test.sh` | `just check-actions` | -| `ci/native-check.sh` | `ci/run-rust-job.sh` | -| `test/test.sh` | `just test` (`--all` for the full workspace), `ci/run-rust-job.sh` | -| `test/test-examples.sh` | `just test-examples`; executed in CI through `ci/run-rust-job.sh` | -| `test/test-feature-matrix.sh` | `just test-feature-matrix`, `scripts/check/check.sh`, `ci/run-rust-job.sh` | -| `test/test-miri.sh` | `just test-miri`, `ci/run-rust-job.sh` | -| `test/test-fuzz.sh` | `just test-fuzz`, `ci/run-rust-job.sh` | -| `test/test-fuzz-scheduler-test.sh` | `just check-actions` | -| `test/test-fuzz-asan.sh` | `just test-fuzz-asan`, `ci/run-rust-job.sh` | -| `test/test-rsa-leakage.sh` | `just test-rsa-leakage`, `ci/run-rust-job.sh` | +| `check/hash-vector-provenance.py` | `scripts/check/policy.sh`; optional exact upstream checkouts reproduce hash-vector corpora | +| `check/auth-vector-provenance.py` | `scripts/check/policy.sh`; optional `--upstream-root PATH` reproduces the pinned C2SP/Wycheproof corpus | +| `check/feature-boundaries.py` | `scripts/check/policy.sh` | +| `check/zeroize-evidence.sh` | `just check-zeroize-evidence`, `scripts/check/check-all.sh`, `qualification.yaml` | +| `ci/check-locked-cargo.sh` | `scripts/ci/actions-policy.sh` | +| `ci/check-locked-cargo-test.sh` | `scripts/ci/actions-policy.sh` | +| `ci/target-contracts.sh` | `just target-contract`, `scripts/check/check-all.sh`, `ci.yaml`, `qualification.yaml` | +| `test/test.sh` | `just test` (`--all` for the full workspace), `ci.yaml`, `qualification.yaml` | +| `test/test-examples.sh` | `just test-examples` | +| `test/miri-contracts.sh` | `just miri-contract`, `scripts/check/affected.sh`, `ci.yaml`, `qualification.yaml` | +| `test/test-miri.sh` | `just test-miri`, `test/miri-contracts.sh` | +| `test/fuzz-contracts.sh` | `just fuzz-contract`, `scripts/check/affected.sh`, `ci.yaml` | +| `test/test-fuzz.sh` | `just test-fuzz`, `test/fuzz-contracts.sh`, `qualification.yaml` | +| `test/test-fuzz-scheduler-test.sh` | `just check-actions`; proves bounded concurrency, exact selection, corpus retention, and aggregated failure | +| `test/test-fuzz-asan.sh` | `just test-fuzz-asan`, `qualification.yaml` | +| `test/test-rsa-leakage.sh` | `just test-rsa-leakage`, `qualification.yaml` | | `test/test-rsa-macos-asm.sh` | `just test-rsa-macos-asm` on a physical local Apple Silicon Mac | -| `test/test-coverage.sh` | `just test-coverage` (`--nextest` or `--fuzz` for one source), `weekly.yaml` | +| `test/test-rsa-linux-asm.sh` | `just test-rsa-linux-asm`, `qualification.yaml` on physical Linux x86-64 | +| `test/test-coverage.sh` | `just test-coverage` (`--nextest` or `--fuzz` for one source), `qualification.yaml` | | `bench/bench.sh` | `just bench` (`--quick` for reduced measurement time) | | `bench/profile.sh` | `just profile` | -| `ci/check-action-pins.sh` | `just check-actions`, `ci/ci-check.sh`, `ci/dependabot-smoke.sh` | -| `ci/check-action-pins-test.sh` | `just check-actions`, `ci/dependabot-smoke.sh` | -| `ci/tool-integrity-test.sh` | `just check-actions` | +| `ci/check-action-pins.sh` | `just check-actions`, `ci.yaml` | +| `ci/actions-policy.sh` | `scripts/check/policy.sh` | +| `ci/check-action-pins-test.sh` | `just check-actions` | | `ci/remote-cache-recipes-test.sh` | `just check-actions` | -| `ci/dependabot-smoke-test.sh` | `just check-actions` | -| `ci/check-ci-ownership.sh` | `just check-actions`, `ci/check-ci-ownership-test.sh` | -| `ci/check-ci-ownership-test.sh` | `just check-actions` | -| `ci/run-rust-job-test.sh` | `just check-actions` | +| `ci/feature-contracts-test.sh` | `just check-actions`; proves unique compile graphs, focused runtime scopes, and disjoint deterministic shards | +| `ci/feature-planning-test.sh` | `just check-actions`; proves exact algorithm groups, full feature-policy selection, and fail-closed unattributed inputs | +| `ci/activate-plan.sh` | `.github/actions/plan/action.yaml`; validates and exports one transported plan | +| `ci/require-work.sh` | Direct CI/Qualification executors with repository-scoped work | | `ci/emit-manual-matrix-test.sh` | `just check-actions` | -| `ci/materialize-rail-plan.sh` | `ci.yaml`, `weekly.yaml`, and its regression test | -| `ci/materialize-rail-plan-test.sh` | `just check-actions` | | `ci/changed-test-planning-test.sh` | `just check-actions` | | `ci/check-worktree-test.sh` | `just check-actions` | | `ci/pre-push-test.sh` | `just check-actions` | -| `ci/release-evidence-check.sh` | `just release-tag`, `release.yaml`, `ci/release-evidence-check-test.sh` | -| `ci/release-evidence-check-test.sh` | `just check-actions` | -| `ci/release-ct-recovery-check.sh` | `release.yaml`, `ci/release-ct-recovery-check-test.sh` | -| `ci/release-ct-recovery-check-test.sh` | `just check-actions` | -| `ci/repository-controls-evidence.sh` | `just release-tag`, `release.yaml`, `ci/repository-controls-evidence-test.sh` | -| `ci/repository-controls-evidence-test.sh` | `just check-actions` | | `ci/package-release-source.sh` | `release.yaml`, `ci/release-identity-test.sh` | | `ci/package-release-ct-evidence.sh` | `release.yaml` | | `ci/release-package-guard.sh` | `ci/release-preflight.sh` | | `ci/release-preflight.sh` | `release.yaml` | -| `ci/write-release-manifest.sh` | `release.yaml`, `ci/release-identity-test.sh` | -| `ci/release-identity-test.sh` | `just check-actions` | +| `ci/release-identity-test.sh` | `just check-actions`; verifies deterministic source packaging and immutable tag identity | | `ci/publish-immutable-release.sh` | `release.yaml`, `ci/publish-immutable-release-test.sh` | | `ci/publish-immutable-release-test.sh` | `just check-actions` | -| `ci/release-recipes-test.sh` | `just check-actions` | | `ci/pre-push.sh` | `just push` | | `ct/artifacts.sh` | `just ct-artifacts`, `scripts/ct/full.py` | | `ct/dudect.sh` | `just ct-dudect`, `scripts/ct/full.py` | -| `ct/dudect_report_test.py` | `scripts/check/check.sh` | +| `ct/dudect_report_test.py` | `scripts/check/policy.sh` | | `lib/python.sh` | Resolves Python 3.11+ for Cargo Rail readers and Python-backed CT, check, benchmark, and release tooling | | `update/update-all.sh` | `just update` (`--check` for a non-mutating preview) | | `render_perf_chart.rs` | `just chart` compiles and executes this source directly | @@ -81,49 +72,45 @@ claim in [`docs/secret-lifecycle.md`](../docs/secret-lifecycle.md). | Script | Callers | | -------------------------------------- | ------------------------------------------------------------ | -| `check/check-win.sh` | `scripts/check/check-all.sh` | -| `check/check-zig.sh` | `scripts/check/check-all.sh` | | `check/lint-independent-workspaces.sh` | `scripts/check/check.sh --all` | -| `check/zig-cc.sh` | `scripts/check/check-zig.sh` | +| `check/zig-cc.sh` | `scripts/ct/artifacts.sh`, `scripts/ct/binsec.py` | ## Bench Internals | Script | Callers | | --------------------------------- | -------------------------------------------------------------------------- | -| `ci/run-bench.sh` | `scripts/bench/bench.sh`, `ci/mlkem-aarch64-gate.sh`, `ci/run-rust-job.sh` | +| `ci/run-bench.sh` | `scripts/bench/bench.sh`, `ci/mlkem-aarch64-gate.sh`, `bench.yaml` | | `bench/blake3-gap-gate.sh` | `scripts/ci/run-bench.sh` | | `bench/benchmark_catalog.py` | `ci/run-bench.sh`, `bench/profile.sh`, `benchmark_catalog_test.py` | -| `bench/benchmark_catalog_test.py` | `scripts/check/check.sh` | +| `bench/benchmark_catalog_test.py` | `scripts/check/policy.sh` | ## Constant-Time Internals | Script | Callers and validation | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -| `ct/full.py` | `just ct-full`, `ci/run-rust-job.sh`; its dispatch contract is covered by `ci/run-rust-job-test.sh` | +| `ct/full.py` | `just ct-full`, `ct/ci.sh` | | `ct/binsec.py` | `just ct-binsec`, `ct/full.py` | | `ct/validate.py` | `just ct-validate`, `ct/full.py` | | `ct/asm_heuristics.py` | `ct/artifacts.sh`; parsed hazards are covered by `ct/asm_heuristics_test.py` | -| `ct/asm_heuristics_test.py` | `scripts/check/check.sh` | +| `ct/asm_heuristics_test.py` | `scripts/check/policy.sh` | | `ct/dudect_report.py` | `ct/dudect.sh`; parsing and gate behavior are covered by `ct/dudect_report_test.py` | -| `ct/evidence_validation_test.py` | `scripts/check/check.sh`; covers symbol reconstruction, artifact packaging, heuristics, and release-evidence validation | -| `ct/package_evidence.py` | `ci/run-rust-job.sh`; compact-package behavior is covered by `ct/evidence_validation_test.py` | +| `ct/evidence_validation_test.py` | `scripts/check/policy.sh`; covers symbol reconstruction, artifact packaging, heuristics, and release-evidence validation | +| `ct/package_evidence.py` | `ct/ci.sh`; compact-package behavior is covered by `ct/evidence_validation_test.py` | | `ct/provenance.py` | `ct/artifacts.sh`; emitted provenance is consumed and checked by `ct/validate.py` and the full pipeline | | `ct/symbolize_linked_binary.py` | `ct/artifacts.sh`; parsers and reconstruction are covered by `ct/evidence_validation_test.py` | -| `ct/validate_release_evidence.py` | `ci/package-release-ct-evidence.sh`; covered by CT evidence validation and release-evidence regression tests | +| `ct/validate_release_evidence.py` | `ci/package-release-ct-evidence.sh`; covered by CT evidence validation tests | ## CI-only (not surfaced via `just`) | Script | Callers | | -------------------------- | --------------------------------------------- | -| `ci/install-tools.sh` | `.github/actions/setup/action.yaml` | -| `ci/install-codecov.sh` | `weekly.yaml` | -| `ci/setup-toolchain.sh` | `.github/actions/setup-toolchain/action.yaml` | -| `ci/run-rust-job.sh` | `.github/workflows/_rust-job.yaml` | -| `ci/dependabot-smoke.sh` | `ci/run-rust-job.sh` | +| `ci/install-tools.sh` | `qualification.yaml` supply-chain/fuzz lanes and `ct.yaml` formal-analysis lanes | +| `ci/setup-toolchain.sh` | `.github/actions/rust/action.yaml` | +| `ci/native-platform.sh` | `ci/target-contracts.sh` | | `ci/emit-manual-matrix.sh` | `bench.yaml`, `ct.yaml` | -| `ci/mlkem-aarch64-gate.sh` | `ci/run-rust-job.sh` | +| `ci/mlkem-aarch64-gate.sh` | `qualification.yaml` | | `ci/nostd-wasm-suite.sh` | `ci/cross-targets.sh` | -| `ci/cross-targets.sh` | `ci/run-rust-job.sh` | +| `ci/cross-targets.sh` | `ci/target-contracts.sh` | ## Shared Libraries (sourced, not invoked) @@ -132,11 +119,11 @@ claim in [`docs/secret-lifecycle.md`](../docs/secret-lifecycle.md). | `lib/common.sh` | Check/test entry points plus native, cross-target, benchmark-gate, and pre-push CI scripts | | `lib/rail-plan.sh` | `scripts/lib/common.sh` | | `lib/fuzz-packages.sh` | `scripts/test/test-fuzz.sh`, `scripts/test/test-coverage.sh` | -| `lib/feature-profiles.sh` | Feature-matrix scripts, `scripts/check/check-all.sh`, `scripts/ci/nostd-wasm-suite.sh` | -| `lib/targets.sh` | `scripts/check/check-all.sh`, `scripts/check/check-zig.sh`, `scripts/ci/cross-targets.sh` | -| `lib/target-matrix.sh` | `scripts/lib/targets.sh`, `scripts/ci/ci-check.sh` | +| `lib/feature-profiles.sh` | `check/feature-contracts.sh`, `scripts/ci/nostd-wasm-suite.sh` | +| `lib/targets.sh` | `scripts/ci/cross-targets.sh` | +| `lib/target-matrix.sh` | `scripts/lib/targets.sh`, `scripts/ci/target-contracts.sh` | | `lib/toolchain.sh` | Toolchain setup, Miri/fuzz helpers, and cross-target check scripts | -| `lib/ci-tool-integrity.sh` | `ci/install-codecov.sh`, `ci/nostd-wasm-suite.sh` | +| `lib/ci-tool-integrity.sh` | `ci/nostd-wasm-suite.sh`, `just check-actions` | ## Python boundary @@ -148,24 +135,6 @@ runs through the same resolver. Simple JSON selection and redaction stays in `jq`; no Python compatibility package, virtual environment, or package-manager bootstrap remains. -## Script contracts - -This table is the ownership audit for inputs, side effects, failure policy, and -tests. A script belongs to exactly one row; its concrete caller remains in the -maps above. - -| Owner | Inputs | Side effects | Failure policy and evidence | -| --- | --- | --- | --- | -| Shared libraries | Sourcing script arguments, repository paths, typed target and feature catalogs | Define functions and readonly data in the caller; no independent entry-point effects | Reject malformed catalogs, unknown selectors, and missing tools; exercised through every caller and the CI planning regression suite | -| Local checks and tests | Recipe arguments, Cargo metadata, manifests, target/feature catalogs, vectors, and explicit environment selectors | Cargo build output plus bounded logs or evidence under `target/` | Fail on the first violated contract or aggregate named failures without weakening assertions; focused Python tests, shell regression tests, and `just check` own coverage | -| CI planning and execution | GitHub event fields, immutable plan artifacts, typed matrix rows, operation selectors, and repository variables | Materialize plan-bound matrices, run repository commands, and write bounded artifacts under `target/` or `ci-evidence/` | Reject missing or mismatched plan identity, commit, operation, target, tool mode, and trust mode before execution; `just check-actions` runs every shell regression fixture plus `actionlint` and `zizmor` | -| Tool installation | Exact tool mode, toolchain contract, integrity catalog, runner OS, and architecture | Install exact tools into runner-temporary roots and emit environment paths | Reject absent checksums, version drift, unsupported hosts, mutable downloads, or unauthenticated Cargo Rail; integrity and installer fixtures run under `just check-actions` | -| Constant-time evidence | `ct.toml`, target/profile/gate selectors, exact toolchain and linker state, harness output, and release-bound artifacts | Write target-scoped assembly, disassembly, timing, formal-analysis, provenance, report, and package files under `target/` | Distinguish pass, diagnostic, unsupported, timeout, and blocking failure exactly as `ct.toml` declares; parser/unit fixtures run in `just check`, while target execution uses the focused CT recipes | -| Benchmarks and profiling | Catalog selectors, platform facts, filters, sample mode, and profiler arguments | Write Criterion, structural, profile, code-generation, or chart artifacts in their documented result roots | Reject unknown catalog rows and unsupported platform/tool combinations; catalog tests run in `just check`, while measurements remain non-correctness evidence | -| Release and update | Exact commit/tag, downloaded qualification artifacts, Cargo Rail release state, lockfiles, and `--check` preview mode | Prepare locks and release manifests, verify or publish immutable artifacts, or update coordinated manifests | Fail closed on identity, evidence, worktree, package, signature, or publication mismatch; release and update adapters are covered by `just check-actions` and the release recipes | -| Remote support | Target plus arguments passed by the `ssh-*` recipes | No repository script owns provider state; the external `dev-machine` front door owns creation, sync, bootstrap, and teardown | Provider and lease validation live in `dev-machine`; repository recipes preserve the `rscrypto` project scope and propagate failure | -| Performance chart | `benchmark_results/OVERVIEW.md` | Rewrites `assets/readme/perf.svg` through `just chart` | Rejects missing or malformed benchmark rows during compilation or execution; the generated SVG is reviewed with its source data | - ## CI tool integrity Direct executable downloads are declared in @@ -174,120 +143,109 @@ filename, HTTPS URL, and repository-owned SHA-256; the shared verifier checks the digest before extraction, installation, or execution. OCI tools use an image digest in their local action definition. -Package-manager tools install into a fresh runner-temporary root; CI never -restores Cargo binaries, Cargo install metadata, Go module state, or OPAM -switches from a cache. Cargo installs exact crates from crates.io and -authenticates crate contents against registry checksums. Go installs an exact -module through the public checksum database. Ubuntu 24.04 APT dependencies -resolve from signed repository metadata; installation pins each signed -candidate selected after the metadata refresh, verifies the installed version, -and refuses downgrades. OPAM uses exact packages from a repository pinned to a -full Git commit and verifies package source hashes from that immutable metadata. -CT formal reports bind the resulting BINSEC executable by SHA-256. Rustup -receives only the exact stable or nightly contract declared in -`rust-toolchain.toml` and `.config/toolchains.toml`; runner images must provide -rustup, which verifies component downloads against the exact distribution -manifest, because network bootstrap installers are rejected. - -## Planned CI - -Cargo Rail creates one validated named-work plan per CI or Qualification run. -`.config/ci-plan-variants.json` owns the selectable suite rows and their typed -execution dimensions; repository scripts own command implementations, while -workflows enforce trust. The planner uploads the exact plan and bundled strict -reader together. Every selected job checks out the plan-bound commit, verifies -the plan identity and complete checkout, and then executes its catalog -operation. No consumer replans or infers work from changed paths. - -The repository-scoped `ci-policy` work item widens the matrix to the complete -catalog when shared workflows or dispatch infrastructure change. This keeps -source-only changes narrow without pretending that a shared executor edit can -be validated by one arbitrarily selected row. - -Pull requests select affected Cargo work and CI rows. Manual CI and Qualification use -Cargo Rail's typed all-work override. The planner installs the authenticated -Surface component, but `.config/rail.toml` temporarily disables Surface while -Cargo Rail cannot distinguish expected `compile_fail` doctest invocations from -compiler failures. Planning and every selected CI row remain fail-closed. - -Cargo Rail Action v8.2.0 does not publish verified native cache components for -the IBM Z and POWER hosts. Those two native rows still install Cargo Rail core, -verify the saved plan, and run in full; only compiler-result reuse is skipped. - -## Compiler-result reuse - -`.github/actions/setup/action.yaml` is the only CI compiler-cache owner. It -uses the same immutable Cargo Rail action revision and authenticated Cargo Rail -version as pull-request planning, installs the cache before repository Cargo -tools, and leaves tool executables and package-manager state uncached. - -The repository variable `CARGO_RAIL_CACHE_URL` selects rscrypto's canonical -Cloudflare R2 L2 authority. The bucket-scoped -`CARGO_RAIL_R2_READ_ACCESS_KEY_ID` / `CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY` -secret pair can only read it; the corresponding `WRITE` pair can read and -write it. The URL contains no credentials and does not belong in -`.config/rail.toml`. A missing secret pair skips L2 cleanly, which keeps fork -and Dependabot jobs correct without disclosing repository credentials. - -CI applies qualified root remapping and requires Cargo Rail's authenticated -provider/protocol probe whenever the URL is configured. Configure -the `just ssh-*` machines with the same normalized URL and remap policy when -their provider identities may share compiler results. Repository code selects -trust (`read` for pull requests, `read-write` for trusted jobs); -`~/dev-machines` owns the corresponding remote machine setup and credentials. -Different URLs or physical-root mode produce isolated caches by design. - -Ordinary, pull-request, qualification, and release jobs select `read`; their -provider credential is also read-only. The affected main-branch seeder alone -selects `read-write` and receives the distinct writer credential. Setup fails -if R2 authentication or the `native-v6` protocol marker is unavailable; later -per-compilation transport failures take Cargo Rail's verified fallback path. -Representative compiler jobs preserve a redacted cache-status artifact so -local and remote origins, misses, bypasses, conflicts, failures, capacity, -mode, provider, and setup health remain visible. - -Local development uses the same machine-owned setup documented in -[`CONTRIBUTING.md`](../CONTRIBUTING.md). Miri and optimized zeroization evidence -set `CARGO_RAIL_CACHE=off` because those checks require a deliberately cold -compiler path. Cross-target and otherwise unsupported compiler operations rely -on Cargo Rail's typed bypass instead of clearing a global wrapper. - -## Runner and container audit - -The 2026-08-28 audit used CI run `33140323747` and Qualification run -`33094159266`. GitHub step timestamps produced this cold-run breakdown: - -| Representative job | Checkout | Setup | Repository operation | Artifact/upload | -| --- | ---: | ---: | ---: | ---: | -| Linux x86-64 native | 4 s | 545 s | 1,608 s | none | -| Linux AArch64 native | 4 s | 443 s | 1,444 s | none | -| Windows x86-64 native | 9 s | 13 s | 281 s | none | -| Linux feature contracts | 5 s | 104 s | 3,544 s | none | -| Linux coverage | 13 s | 565 s | 2,100 s | 7 s | - -The x86-64 native log further split setup into approximately 11 seconds for -the exact Rust toolchain, 426 seconds to compile `cargo-nextest`, and 108 -seconds to compile `just`. RunsOn reported 22.33 seconds from job creation to a -ready runner. The repository-operation column contains compilation and command -execution because each repository script deliberately remains one policy -boundary; Cargo and Nextest logs retain the finer per-command timing. - -`.github/runs-on.yml` uses the maintained Ubuntu 24 full x86-64 and AArch64 -images, provider labels for physical evidence, non-spot runners, bounded gp3 -volumes, and no RunsOn extras or MagicCache. The ordinary CI lanes allow the -documented family fallback; benchmark and evidence lanes retain explicit -families. The existing Linux setup cost is material, but this change installs -Cargo Rail before Cargo tools so the authenticated L2 can reuse those exact -compilations. Keep the maintained images until cache-status artifacts from -seeded runs show the residual setup cost. Introduce versioned custom Linux -images only if that evidence still shows repeated tool compilation; the -13-second Windows setup does not justify a custom image. - -The only repository Dockerfile is `oss-fuzz/Dockerfile`. It pins the OSS-Fuzz -Rust builder by digest, accepts an explicit source ref, crosses no provider -credential boundary, and contains no compiler cache or `cargo-chef` layer. -Cargo Rail is therefore the sole compiler-result cache, and `cargo-chef` has no -retained role. +Package-manager tools install into a fresh runner-temporary root. The installer +has only three modes: `supply-chain`, `fuzz`, and `ct-linux`. Cargo installs are +exact-versioned. Ubuntu packages come from signed metadata, and the OPAM +repository is pinned to one commit before BINSEC is built. Rustup installs only +the exact repository toolchain contracts. Actions policy does not require +`yq`; it uses the already-required Ruby and Python standard libraries. +Qualification installs its two exact coverage tools from supported prebuilt +releases with fallback disabled. + +## CI architecture + +The workflow split follows proof domain and frequency, not algorithms or CPU +architectures: + +| Workflow | Responsibility | +| --- | --- | +| `ci.yaml` | Fast affected pull-request and main-branch gate | +| `qualification.yaml` | Weekly and release-grade cross-platform assurance | +| `ct.yaml` | Reusable/manual constant-time evidence matrix | +| `bench.yaml` | Manual performance measurements on named hardware | +| `release.yaml` | Exact-commit package and publication transaction | +| `scorecard.yaml` | GitHub supply-chain scorecard | + +`release.yaml` calls `qualification.yaml`, which calls `ct.yaml`; both calls +stay on the exact release commit. IBM, RISC-V, Windows, macOS, x86-64, and +AArch64 are matrix rows. RSA and ML-KEM are assurance lanes inside +Qualification; they are not workflow boundaries. + +Cargo Rail plans exactly once in `ci.yaml`. The planner also runs selected cheap +repository policy, so a workflow-only change does not start a second runner. +Built-in Cargo work starts the single warm host-Rust job; `contracts.features` +uses `.config/feature-matrix.json` to select algorithm or capability groups, +resolves every affected compile profile from Cargo's feature graph, and packs +only those profiles into at most two compile and three runtime shards. +Manifest, catalog, shared-surface, or unattributed inputs widen to the complete +59-compile/9-runtime contract. `targets.platforms` independently materializes +only affected platform proof rows; built-in `dependency-policy` starts the +dependency audit. The core job already owns ordinary Linux x86-64 and is not +duplicated. Affected `assurance.ct` work runs one cold x86-64 structural gate: +release harness construction, generated-code inspection, and strict manifest +and artifact validation. Affected `assurance.rsa` work runs the x86-64 assembly +differential and symbol contract, with eligible compiler work using the same R2 +policy as the other trusted native lanes. Neither decision starts physical +timing, formal analysis, or a cross-platform assurance sweep in pull requests. + +Affected `assurance.miri` work selects only the portable unsafe-boundary row, +the focused RSA row, or both. Affected `assurance.fuzz` work selects algorithm- +sized rows and unions their exact target names into one executor, so tool setup +and compatible builds are shared without converting each target into a job. +Both lanes are deliberately cache-cold. Manifest, catalog, shared harness, and +unattributed inputs widen fail-closed; the root lockfile alone selects neither +nightly lane. Qualification still runs both Miri rows, the portable row again +under Tree Borrows, every fuzz target, and every committed corpus under ASan. +Miri remains on x86-64 because it forces portable execution; deep Linux and +macOS AArch64 rows own native runtime and backend-differential proof instead of +duplicating interpreter and fuzz hosts. + +The selected Actions policy lane downloads the exact checksum-verified +actionlint release and the exact prebuilt Zizmor release, then runs the same +`scripts/ci/actions-policy.sh` entry point as `just check-actions`. No CI linter +is compiled from source. + +Qualification captures one `--all` plan, restores the complete feature +contract, and materializes every platform catalog row as an independent retry +unit. Its reusable CT workflow verifies that same plan once, checks out the +planned commit on every evidence host, and retains the complete physical/formal +matrix. Core test jobs and coverage install the exact prebuilt Nextest release +with source fallback disabled. Coverage runs deterministic nextest and +committed-corpus replay in its own lane. Optimized zeroization runs cold and +retains no compiler cache. RSA +leakage and cross-architecture evidence likewise remain qualification work. +Generic cross compilation stays on Linux x86-64; +hosted and donated machines run only irreducibly native evidence. Linux x86-64 +rows restore and verify the plan. Other rows check out the exact planned commit; +the local Cargo Rail issue records the missing lean verifier path for donated +architectures and macOS x86-64. + +The Cargo Rail compiler cache is acceleration, never selection or correctness +authority. `just rail-cache-setup` previews, installs, and probes the same +remapped policy used by development machines. CI reads R2 for trusted PRs and +writes only from protected `main`; missing fork secrets disable the cache. +Qualification enables reuse for host, feature-contract, and supported native +platform rows; release preflight is read-only. Cross targets, Clippy, rustdoc, +doctests, Miri, fuzzing, CT, benchmarks, macOS x86-64, and donated hosts stay +cold because the released cache deliberately bypasses or cannot install on +those classes. + +Local and remote development use the same affected commands: `just plan`, +`just check`, `just test`, and `just validate`. `just check` runs selected +compile feature contracts; `just validate` adds selected runtime contracts and +shares one saved plan across policy, checks, feature contracts, and tests. +`just feature-contracts [compile|runtime] [N/M]` reproduces any CI shard. +`just target-contract ROW [shallow|deep]` reproduces any independently +executable platform row, locally or through `ssh-just`. +`just miri-contract ROW` and `just fuzz-contract ROW` reproduce the same +algorithm-sized proof rows used by affected CI. `just validate` consumes the +saved local plan and adds only its selected Miri and fuzz rows after ordinary +tests. +`just ct-structural` reproduces the affected CI constant-time structure gate; +`just ct-full` and `just test-rsa-leakage` remain deliberate assurance commands. +`ssh-just TARGET validate` creates the plan after the development machine's +exact repository sync; provider lifecycle and short-lived R2 credentials remain +outside this repository. ## Results layout diff --git a/scripts/check/affected.sh b/scripts/check/affected.sh new file mode 100755 index 00000000..a7ea48a2 --- /dev/null +++ b/scripts/check/affected.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Create one immutable plan and share it across the normal local proof set. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +force_all=false +with_tests=false +for arg in "$@"; do + case "$arg" in + --all) force_all=true ;; + --with-tests) with_tests=true ;; + *) echo "Usage: $0 [--all] [--with-tests]" >&2; exit 2 ;; + esac +done + +plan_file=$(mktemp "${TMPDIR:-/tmp}/rscrypto-plan-v8.XXXXXX") +trap 'rm -f "$plan_file"' EXIT + +plan_args=(rail plan --quiet --json) +[[ "$force_all" == true ]] && plan_args+=(--all) +cargo "${plan_args[@]}" >"$plan_file" +cargo rail plan --verify "$plan_file" +printf 'Cargo Rail plan: %s\n' "$(jq -r '.identity' "$plan_file")" +printf 'Required work: %s\n' "$(jq -r '.required | if length == 0 then "none" else join(", ") end' "$plan_file")" + +export RAIL_PLAN_FILE="$plan_file" +export RAIL_PLAN_LOCAL=true + +"$SCRIPT_DIR/policy.sh" +"$SCRIPT_DIR/check.sh" + +feature_matrix=$("$SCRIPT_DIR/feature-contracts.sh" matrix) +feature_count=$(jq -r '.include | length' <<<"$feature_matrix") +if [[ "$feature_count" -eq 0 ]]; then + echo "Feature contracts: not required by Cargo Rail" +else + while IFS=$'\t' read -r domain shard profiles; do + if [[ "$domain" == runtime && "$with_tests" == false ]]; then + continue + fi + "$SCRIPT_DIR/feature-contracts.sh" selected "$domain" "$shard" "$profiles" + done < <(jq -r '.include[] | [.domain, .shard, .profiles] | @tsv' <<<"$feature_matrix") +fi + +if [[ "$with_tests" == true ]]; then + "$SCRIPT_DIR/../test/test.sh" + + miri_rows=$("$SCRIPT_DIR/../test/miri-contracts.sh" rows) + if [[ -n "$miri_rows" ]]; then + "$SCRIPT_DIR/../test/miri-contracts.sh" selected "$miri_rows" + else + echo "Miri contracts: not required by Cargo Rail" + fi + + fuzz_rows=$("$SCRIPT_DIR/../test/fuzz-contracts.sh" rows) + if [[ -n "$fuzz_rows" ]]; then + "$SCRIPT_DIR/../test/fuzz-contracts.sh" selected "$fuzz_rows" + else + echo "Fuzz contracts: not required by Cargo Rail" + fi +fi diff --git a/scripts/check/check-all.sh b/scripts/check/check-all.sh index 9c58ebce..5f40e2f5 100755 --- a/scripts/check/check-all.sh +++ b/scripts/check/check-all.sh @@ -1,181 +1,58 @@ #!/usr/bin/env bash -# Complete host and cross-target validation for rscrypto. +# Complete host, feature, and generic cross-target validation for rscrypto. set -euo pipefail [[ $# -eq 0 ]] || { echo "Usage: $0" >&2; exit 2; } SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" # shellcheck source=../lib/common.sh source "$SCRIPT_DIR/../lib/common.sh" -# shellcheck source=../lib/targets.sh -source "$SCRIPT_DIR/../lib/targets.sh" -# shellcheck source=../lib/feature-profiles.sh -source "$SCRIPT_DIR/../lib/feature-profiles.sh" - -NIGHTLY_TOOLCHAIN=$("$SCRIPT_DIR/../lib/toolchain.sh" --nightly) - -cargo_for_target() { - local target=$1 - shift - if [[ "$target" == riscv32* ]]; then - RUSTUP_TOOLCHAIN="$NIGHTLY_TOOLCHAIN" cargo "$@" - else - cargo "$@" - fi -} - -run_constrained_check() { - local target=$1 - local target_dir=$2 - local log_file=$3 - local feature_set=${4:-} - local args=( - check - -p rscrypto - --no-default-features - --target "$target" - --lib - --locked - ) - - if [[ -n "$feature_set" ]]; then - args+=(--features "$feature_set") - fi - - CARGO_TARGET_DIR="$target_dir" cargo_for_target "$target" "${args[@]}" \ - >>"$log_file" 2>&1 -} - -run_constrained_target() { - local target=$1 - local log_dir=$2 - local target_dir="target/cross-check/$target" - local log_file="$log_dir/$target.log" - - if [[ "$target" == riscv32* ]]; then - ensure_target "$target" "$NIGHTLY_TOOLCHAIN" - else - ensure_target "$target" - fi - mkdir -p "$target_dir" - : >"$log_file" - - step "$target check (no features)" - if run_constrained_check "$target" "$target_dir" "$log_file"; then - ok - else - fail - show_error "$log_file" - return 1 - fi - - step "$target check (feature contract)" - for feature_set in alloc "${CONSTRAINED_FEATURE_SETS[@]}"; do - if ! run_constrained_check "$target" "$target_dir" "$log_file" "$feature_set"; then - fail - show_error "$log_file" - return 1 - fi - done - ok - - step "$target release build (no features)" - if CARGO_TARGET_DIR="$target_dir" cargo_for_target "$target" build --locked \ - -p rscrypto --no-default-features --target "$target" --lib --release \ - >>"$log_file" 2>&1; then - ok - else - fail - show_error "$log_file" - return 1 - fi - - step "$target release build (alloc)" - if CARGO_TARGET_DIR="$target_dir" cargo_for_target "$target" build --locked \ - -p rscrypto --no-default-features --features alloc --target "$target" --lib \ - --release >>"$log_file" 2>&1; then - ok - else - fail - show_error "$log_file" - return 1 - fi -} - -run_constrained_checks() { - local log_dir - log_dir=$(mktemp -d) - trap 'rm -rf "$log_dir"' EXIT - - local constrained_targets=( - "${NOSTD_TARGETS[@]:+${NOSTD_TARGETS[@]}}" - "${WASM_TARGETS[@]:+${WASM_TARGETS[@]}}" - ) - - echo "" - echo "Constrained targets ${DIM}(rscrypto, parallel)${RESET}" - if [[ ${#constrained_targets[@]} -eq 0 ]]; then - skip "no constrained targets configured" ".config/target-matrix.json" - return 0 - fi - - local pids=() - local target - for i in "${!constrained_targets[@]}"; do - target=${constrained_targets[$i]} - (run_constrained_target "$target" "$log_dir") & - pids[i]=$! - done - - local failures=0 - for i in "${!constrained_targets[@]}"; do - target=${constrained_targets[$i]} - step "$target group" - if wait "${pids[$i]}"; then - ok - else - fail - failures=1 - fi - done - - [[ "$failures" -eq 0 ]] || return 1 - echo "${GREEN}✓${RESET} Constrained targets passed" -} echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Complete rscrypto validation" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" -"$SCRIPT_DIR/check.sh" --all --feature-matrix +"$SCRIPT_DIR/policy.sh" --all +"$SCRIPT_DIR/check.sh" --all +"$SCRIPT_DIR/feature-contracts.sh" all "$SCRIPT_DIR/zeroize-evidence.sh" +rows=() +while IFS= read -r row; do + rows+=("$row") +done < <( + jq -r '.variants[] | select(.dimensions.operation == "cross") | .id' \ + "$REPO_ROOT/.config/target-matrix.json" +) +[[ ${#rows[@]} -gt 0 ]] || { + echo "No generic cross-target proofs are configured" >&2 + exit 1 +} + echo "" echo "Cross targets ${DIM}(parallel)${RESET}" log_dir=$(mktemp -d) trap 'rm -rf "$log_dir"' EXIT -jobs=(windows linux ibm constrained) -("$SCRIPT_DIR/check-win.sh") >"$log_dir/windows.log" 2>&1 & -pids=("$!") -("$SCRIPT_DIR/check-zig.sh" linux) >"$log_dir/linux.log" 2>&1 & -pids+=("$!") -("$SCRIPT_DIR/check-zig.sh" ibm) >"$log_dir/ibm.log" 2>&1 & -pids+=("$!") -(run_constrained_checks) >"$log_dir/constrained.log" 2>&1 & -pids+=("$!") +pids=() +for i in "${!rows[@]}"; do + row=${rows[$i]} + ("$SCRIPT_DIR/../ci/target-contracts.sh" run "$row" deep) >"$log_dir/$row.log" 2>&1 & + pids[i]=$! +done failures=0 -for i in "${!jobs[@]}"; do - job=${jobs[$i]} - step "$job group" +for i in "${!rows[@]}"; do + row=${rows[$i]} + step "$row" if wait "${pids[$i]}"; then ok else fail - show_error "$log_dir/$job.log" + show_error "$log_dir/$row.log" failures=1 fi done diff --git a/scripts/check/check-feature-matrix.sh b/scripts/check/check-feature-matrix.sh deleted file mode 100755 index 83d24aac..00000000 --- a/scripts/check/check-feature-matrix.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=../lib/common.sh -source "$SCRIPT_DIR/../lib/common.sh" -# shellcheck source=../lib/feature-profiles.sh -source "$SCRIPT_DIR/../lib/feature-profiles.sh" - -LOG_DIR=$(mktemp -d) -trap 'rm -rf "$LOG_DIR"' EXIT -TOTAL=${#COMPILE_FEATURE_SETS[@]} -STARTED_AT=$SECONDS - -echo "Compilation feature matrix ($TOTAL profiles)" - -for i in "${!COMPILE_FEATURE_SETS[@]}"; do - feature_set=${COMPILE_FEATURE_SETS[$i]} - profile=$((i + 1)) - profile_started_at=$SECONDS - display=${feature_set:-no-features} - log_path="$LOG_DIR/${display//,/_}.log" - - step "[$profile/$TOTAL] cargo check --lib --tests --no-default-features --features $display" - args=(check --locked --workspace --lib --tests --no-default-features) - if [[ -n "$feature_set" ]]; then - args+=(--features "$feature_set") - fi - - if ! cargo "${args[@]}" >"$log_path" 2>&1; then - fail - show_error "$log_path" - exit 1 - fi - - ok - echo " elapsed: $((SECONDS - profile_started_at))s" -done - -echo "${GREEN}✓${RESET} Compilation feature matrix passed: $TOTAL/$TOTAL profiles in $((SECONDS - STARTED_AT))s" diff --git a/scripts/check/check-win.sh b/scripts/check/check-win.sh deleted file mode 100755 index 13369349..00000000 --- a/scripts/check/check-win.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Windows cross-compilation checks via cargo-xwin -# Usage: check-win.sh - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=../lib/common.sh -source "$SCRIPT_DIR/../lib/common.sh" -# shellcheck source=../lib/targets.sh -source "$SCRIPT_DIR/../lib/targets.sh" - -[[ $# -eq 0 ]] || { echo "Usage: $0" >&2; exit 2; } - -# Check for cargo-xwin -if ! cargo xwin --version >/dev/null 2>&1; then - echo "Windows targets ${DIM}(skipped)${RESET}" - skip "cargo-xwin not installed" "cargo install cargo-xwin && brew install llvm" - exit 0 -fi - -# Keep cargo-xwin's SDK cache inside the workspace `target/` dir so this script -# works in sandboxed environments that disallow writes to user cache locations. -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -XWIN_CACHE_DIR_DEFAULT="$REPO_ROOT/target/cross-check/xwin-cache" -mkdir -p "$XWIN_CACHE_DIR_DEFAULT" - -LOG_DIR=$(mktemp -d) -trap 'rm -rf "$LOG_DIR"' EXIT - -echo "Windows targets ${DIM}(rscrypto)${RESET}" - -if [[ ${#WIN_TARGETS[@]} -eq 0 ]]; then - skip "no Windows targets configured" ".config/target-matrix.json" - exit 0 -fi - -# Initialize xwin cache once (avoids race conditions) -step "Initializing SDK cache" -if ! XWIN_CACHE_DIR="$XWIN_CACHE_DIR_DEFAULT" \ - CARGO_TARGET_DIR="target/cross-check/xwin-init" \ - cargo xwin check --locked -p rscrypto --no-default-features --lib --target x86_64-pc-windows-msvc \ - >"$LOG_DIR/xwin-init.log" 2>&1; then - # In sandboxed/offline environments, cargo-xwin can't fetch the MSVC CRT / SDK. - # Treat that as a skip rather than a hard failure, while still surfacing - # unexpected errors. - if tail -80 "$LOG_DIR/xwin-init.log" | grep -Eq \ - 'failed to lookup address information|Could not resolve host|Name or service not known|Temporary failure in name resolution|HTTP GET request .* failed'; then - printf " %b○%b\n" "$YELLOW" "$RESET" - skip "Windows targets" "SDK cache init requires network downloads" - exit 0 - fi - - fail - show_error "$LOG_DIR/xwin-init.log" - exit 1 -fi -ok - -for target in "${WIN_TARGETS[@]}"; do - ensure_target "$target" -done - -for target in "${WIN_TARGETS[@]}"; do - mkdir -p "target/cross-check/$target" -done - -pids=() -logs=() -targets=() - -for i in "${!WIN_TARGETS[@]}"; do - target="${WIN_TARGETS[$i]}" - target_dir="target/cross-check/$target" - target_cache_dir="$XWIN_CACHE_DIR_DEFAULT/$target" - mkdir -p "$target_cache_dir" - log_file="$LOG_DIR/$target.log" - logs[i]="$log_file" - targets[i]="$target" - - ( - if ! XWIN_CACHE_DIR="$target_cache_dir" \ - CARGO_TARGET_DIR="$target_dir" \ - cargo xwin clippy -p rscrypto --lib --all-features --locked --target "$target" \ - >"$log_file" 2>&1; then - exit 1 - fi - ) & - pids[i]=$! -done - -FAILED=0 -for i in "${!targets[@]}"; do - target="${targets[$i]}" - short_name="${target%-pc-windows-msvc}" # x86_64 or aarch64 - - step "$short_name clippy" - if wait "${pids[$i]}"; then - ok - else - fail - show_error "${logs[$i]}" - FAILED=1 - fi -done - -if [ $FAILED -ne 0 ]; then - exit 1 -fi - -echo "${GREEN}✓${RESET} Windows targets passed" diff --git a/scripts/check/check-zig.sh b/scripts/check/check-zig.sh deleted file mode 100755 index 72e27bf3..00000000 --- a/scripts/check/check-zig.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash -# Cross-compile the rscrypto library with Zig for one target group. - -set -euo pipefail - -[[ $# -eq 1 ]] || { echo "Usage: $0 " >&2; exit 2; } - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=../lib/common.sh -source "$SCRIPT_DIR/../lib/common.sh" -# shellcheck source=../lib/targets.sh -source "$SCRIPT_DIR/../lib/targets.sh" - -group=$1 -case "$group" in - linux) - label=Linux - targets=("${LINUX_TARGETS[@]:+${LINUX_TARGETS[@]}}") - ;; - ibm) - label=IBM - targets=("${IBM_TARGETS[@]:+${IBM_TARGETS[@]}}") - ;; - *) - echo "Usage: $0 " >&2 - exit 2 - ;; -esac - -if ! command -v zig >/dev/null 2>&1; then - echo "$label targets ${DIM}(skipped)${RESET}" - skip "zig not installed" "brew install zig" - exit 0 -fi - -if [[ ${#targets[@]} -eq 0 ]]; then - skip "no $label targets configured" ".config/target-matrix.json" - exit 0 -fi - -export ZIG_CC="$SCRIPT_DIR/zig-cc.sh" -NIGHTLY_TOOLCHAIN=$("$SCRIPT_DIR/../lib/toolchain.sh" --nightly) - -uses_nightly() { - [[ "$group" == ibm || "$1" == riscv64* ]] -} - -for target in "${targets[@]}"; do - if uses_nightly "$target"; then - ensure_target "$target" "$NIGHTLY_TOOLCHAIN" - else - ensure_target "$target" - fi - mkdir -p "target/cross-check/$target" -done - -LOG_DIR=$(mktemp -d) -trap 'rm -rf "$LOG_DIR"' EXIT -pids=() -logs=() - -echo "$label targets ${DIM}(rscrypto)${RESET}" -for i in "${!targets[@]}"; do - target=${targets[$i]} - log_file="$LOG_DIR/$target.log" - logs[i]=$log_file - - ( - toolchain_env=(env) - if uses_nightly "$target"; then - toolchain_env+=("RUSTUP_TOOLCHAIN=$NIGHTLY_TOOLCHAIN") - fi - if ! CC="$ZIG_CC" CARGO_TARGET_DIR="target/cross-check/$target" \ - "${toolchain_env[@]}" cargo clippy -p rscrypto --lib --all-features --locked \ - --target "$target" >"$log_file" 2>&1; then - exit 1 - fi - ) & - pids[i]=$! -done - -failed=0 -for i in "${!targets[@]}"; do - target=${targets[$i]} - step "${target/unknown-linux-/} clippy" - if wait "${pids[$i]}"; then - ok - else - fail - show_error "${logs[$i]}" - failed=1 - fi -done - -[[ "$failed" -eq 0 ]] || exit 1 -echo "${GREEN}✓${RESET} $label targets passed" diff --git a/scripts/check/check.sh b/scripts/check/check.sh index 1a955e4b..f219493d 100755 --- a/scripts/check/check.sh +++ b/scripts/check/check.sh @@ -1,168 +1,86 @@ #!/usr/bin/env bash set -euo pipefail -# Host-only checks: fmt, check, opt-in feature matrices, clippy, optional -# deny/audit, and docs. -# Usage: check.sh [--all] [--feature-matrix] +# Host-only Cargo checks: fmt, check, clippy, optional deny/audit, and docs. +# Repository policy and feature contracts have separate executors. +# Usage: check.sh [--all] SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=../lib/common.sh source "$SCRIPT_DIR/../lib/common.sh" -RUN_FEATURE_MATRIX=false FORCE_ALL=false -for arg in "$@"; do - case "$arg" in - --feature-matrix) - RUN_FEATURE_MATRIX=true - ;; - --all) - FORCE_ALL=true - ;; - *) - echo "Usage: $0 [--all] [--feature-matrix]" >&2 - exit 2 - ;; - esac -done - -SCOPE_STATUS=0 -select_cargo_scope cargo.build "$FORCE_ALL" || SCOPE_STATUS=$? -if [[ "$SCOPE_STATUS" -gt 1 ]]; then - exit "$SCOPE_STATUS" -fi -CARGO_SELECTED=true -[[ "$SCOPE_STATUS" -eq 0 ]] || CARGO_SELECTED=false - -# Determine if full workspace (for audit/deny) -FULL_WORKSPACE=false -if [[ "$CARGO_SELECTED" == true && "$CARGO_SCOPE_KIND" == workspace ]]; then - FULL_WORKSPACE=true -fi - -CHECK_RSCRYPTO_FEATURE_MATRIX=false -if [[ "$CARGO_SELECTED" == true && "$RUN_FEATURE_MATRIX" == true ]]; then - CHECK_RSCRYPTO_FEATURE_MATRIX=true -fi +case "$#" in + 0) ;; + 1) + [[ "$1" == --all ]] || { echo "Usage: $0 [--all]" >&2; exit 2; } + FORCE_ALL=true + ;; + *) echo "Usage: $0 [--all]" >&2; exit 2 ;; +esac + +if [[ "$FORCE_ALL" == false ]]; then + rail_prime_plan +fi + +work_required() { + local work_id=$1 + if [[ "$FORCE_ALL" == true ]]; then + return 0 + fi + local status=0 + rail_work_required "$work_id" || status=$? + [[ "$status" -le 1 ]] || exit "$status" + return "$status" +} LOG_DIR=$(mktemp -d) trap 'rm -rf "$LOG_DIR"' EXIT -PYTHON="$("$SCRIPT_DIR/../lib/python.sh" --print)" -echo "Host checks ${DIM}($SCOPE_DESC)${RESET}" +echo "Host checks" # Format -step "Formatting" -if ! cargo fmt --all -- --check >"$LOG_DIR/fmt.log" 2>&1; then - fail - show_error "$LOG_DIR/fmt.log" - exit 1 -fi -ok - -step "Checking assembly ledger" -if ! "$SCRIPT_DIR/asm-ledger.sh" >"$LOG_DIR/asm-ledger.log" 2>&1; then - fail - show_error "$LOG_DIR/asm-ledger.log" - exit 1 -fi -ok - -step "Checking hash vector provenance" -if ! "$PYTHON" "$SCRIPT_DIR/hash-vector-provenance.py" >"$LOG_DIR/hash-vectors.log" 2>&1; then - fail - show_error "$LOG_DIR/hash-vectors.log" - exit 1 -fi -ok - -step "Checking authentication vector provenance" -if ! "$PYTHON" "$SCRIPT_DIR/auth-vector-provenance.py" >"$LOG_DIR/auth-vectors.log" 2>&1; then - fail - show_error "$LOG_DIR/auth-vectors.log" - exit 1 -fi -ok - -step "Checking feature boundaries" -if ! "$PYTHON" "$SCRIPT_DIR/feature-boundaries.py" >"$LOG_DIR/feature-boundaries.log" 2>&1; then - fail - show_error "$LOG_DIR/feature-boundaries.log" - exit 1 -fi -ok - -step "Checking benchmark catalog" -if ! "$PYTHON" "$SCRIPT_DIR/../bench/benchmark_catalog_test.py" >"$LOG_DIR/benchmark-catalog.log" 2>&1; then - fail - show_error "$LOG_DIR/benchmark-catalog.log" - exit 1 -fi -ok - -step "Checking CT assembly scanner" -if ! "$PYTHON" "$SCRIPT_DIR/../ct/asm_heuristics_test.py" >"$LOG_DIR/ct-asm-scanner.log" 2>&1; then - fail - show_error "$LOG_DIR/ct-asm-scanner.log" - exit 1 -fi -ok - -step "Checking DudeCT evidence parsing" -if ! "$PYTHON" "$SCRIPT_DIR/../ct/dudect_report_test.py" >"$LOG_DIR/ct-dudect-report.log" 2>&1; then - fail - show_error "$LOG_DIR/ct-dudect-report.log" - exit 1 -fi -ok - -step "Checking CT evidence validation" -if ! "$PYTHON" "$SCRIPT_DIR/../ct/evidence_validation_test.py" >"$LOG_DIR/ct-evidence-validation.log" 2>&1; then - fail - show_error "$LOG_DIR/ct-evidence-validation.log" - exit 1 -fi -ok - -# Check -if [[ "$CARGO_SELECTED" == false ]]; then - skip "Checking" "no affected targets" -else - step "Checking" - if ! cargo check "${CARGO_ARGS[@]}" --all-targets --all-features --locked >"$LOG_DIR/check.log" 2>&1; then +if work_required cargo.fmt; then + step "Formatting" + if ! cargo fmt --all -- --check >"$LOG_DIR/fmt.log" 2>&1; then fail - show_error "$LOG_DIR/check.log" + show_error "$LOG_DIR/fmt.log" exit 1 fi ok +else + skip "Formatting" "not required by Cargo Rail" fi -if [[ "$CHECK_RSCRYPTO_FEATURE_MATRIX" == true ]]; then - step "Checking rscrypto no_std matrix" - if ! "$SCRIPT_DIR/check-feature-matrix.sh" >>"$LOG_DIR/check.log" 2>&1; then - fail - show_error "$LOG_DIR/check.log" - exit 1 - fi - ok - - step "Testing rscrypto feature matrix" - if ! "$SCRIPT_DIR/../test/test-feature-matrix.sh" >>"$LOG_DIR/check.log" 2>&1; then +# Check +SCOPE_STATUS=0 +select_cargo_scope cargo.build "$FORCE_ALL" || SCOPE_STATUS=$? +if [[ "$SCOPE_STATUS" -gt 1 ]]; then + exit "$SCOPE_STATUS" +fi +if [[ "$SCOPE_STATUS" -ne 0 ]]; then + skip "Checking" "no affected targets" +else + step "Checking" + if ! cargo check "${CARGO_ARGS[@]:+${CARGO_ARGS[@]}}" --all-targets --all-features --locked >"$LOG_DIR/check.log" 2>&1; then fail show_error "$LOG_DIR/check.log" exit 1 fi ok -elif [[ "$CARGO_SELECTED" == true ]]; then - skip "rscrypto feature matrix" "disabled for this check profile" fi # Clippy -if [[ "$CARGO_SELECTED" == false ]]; then +SCOPE_STATUS=0 +select_cargo_scope cargo.clippy "$FORCE_ALL" || SCOPE_STATUS=$? +if [[ "$SCOPE_STATUS" -gt 1 ]]; then + exit "$SCOPE_STATUS" +fi +if [[ "$SCOPE_STATUS" -ne 0 ]]; then skip "Linting" "no affected targets" else step "Linting" - if ! cargo clippy "${CARGO_ARGS[@]}" --all-targets --all-features --locked >"$LOG_DIR/clippy.log" 2>&1; then + if ! cargo clippy "${CARGO_ARGS[@]:+${CARGO_ARGS[@]}}" --all-targets --all-features --locked >"$LOG_DIR/clippy.log" 2>&1; then fail show_error "$LOG_DIR/clippy.log" exit 1 @@ -170,7 +88,7 @@ else ok fi -if [[ "$FULL_WORKSPACE" == true ]]; then +if work_required contracts.auxiliary; then step "Linting independent workspaces" if ! "$SCRIPT_DIR/lint-independent-workspaces.sh" >"$LOG_DIR/independent-lints.log" 2>&1; then fail @@ -180,8 +98,9 @@ if [[ "$FULL_WORKSPACE" == true ]]; then ok fi -# Audit/Deny (workspace only). CI owns this in the dedicated supply-chain lane. -if [[ "$FULL_WORKSPACE" == true && "${RSCRYPTO_SKIP_CHECK_SUPPLY_CHAIN:-}" != "1" ]]; then +# CI owns affected dependency checks in its dedicated supply-chain lane. +if [[ "${RSCRYPTO_SKIP_CHECK_SUPPLY_CHAIN:-}" != "1" ]] \ + && { work_required dependency-policy || work_required dependencies.auxiliary; }; then step "Auditing deps" if ! cargo deny --locked check all >"$LOG_DIR/deny.log" 2>&1; then fail @@ -200,11 +119,16 @@ if [[ "$FULL_WORKSPACE" == true && "${RSCRYPTO_SKIP_CHECK_SUPPLY_CHAIN:-}" != "1 fi # Documentation -if [[ "$CARGO_SELECTED" == false ]]; then +SCOPE_STATUS=0 +select_cargo_scope cargo.doc "$FORCE_ALL" || SCOPE_STATUS=$? +if [[ "$SCOPE_STATUS" -gt 1 ]]; then + exit "$SCOPE_STATUS" +fi +if [[ "$SCOPE_STATUS" -ne 0 ]]; then skip "Building docs" "no affected targets" else step "Building docs" - if ! cargo doc "${CARGO_ARGS[@]}" --no-deps --all-features --locked >"$LOG_DIR/doc.log" 2>&1; then + if ! cargo doc "${CARGO_ARGS[@]:+${CARGO_ARGS[@]}}" --no-deps --all-features --locked >"$LOG_DIR/doc.log" 2>&1; then fail show_error "$LOG_DIR/doc.log" exit 1 diff --git a/scripts/check/feature-contracts.sh b/scripts/check/feature-contracts.sh new file mode 100755 index 00000000..560ec8e5 --- /dev/null +++ b/scripts/check/feature-contracts.sh @@ -0,0 +1,614 @@ +#!/usr/bin/env bash +# Execute the repository-owned feature contracts without embedding product +# feature knowledge in Just or GitHub Actions. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../lib/common.sh +source "$SCRIPT_DIR/../lib/common.sh" +# shellcheck source=../lib/feature-profiles.sh +source "$SCRIPT_DIR/../lib/feature-profiles.sh" +FEATURE_CATALOG="$SCRIPT_DIR/../../.config/feature-matrix.json" + +usage() { + cat >&2 <<'EOF' +usage: feature-contracts.sh [all | compile [N/M] | runtime [N/M] | selected DOMAIN N/M PROFILES | list | matrix] + +N/M is a one-based deterministic shard. Compile and runtime shard counts are +independent; omitting a shard runs the complete selected domain. N/M and +PROFILES in selected mode are emitted together by matrix mode. +EOF +} + +compile_profile_id() { + local feature_set=$1 + if [[ -z "$feature_set" ]]; then + printf 'compile.none\n' + else + printf 'compile.%s\n' "${feature_set//,/.}" + fi +} + +profile_is_known() { + local needle=$1 + local item + for item in "${COMPILE_PROFILE_IDS[@]}" "${RUNTIME_PROFILE_IDS_CANONICAL[@]}"; do + [[ "$needle" == "$item" ]] && return 0 + done + return 1 +} + +append_unique() { + local needle=$1 + local array_name=$2 + local -a current=() + local item + eval "current=(\"\${${array_name}[@]:+\${${array_name}[@]}}\")" + for item in "${current[@]:+${current[@]}}"; do + [[ "$item" == "$needle" ]] && return 0 + done + eval "$array_name+=(\"\$needle\")" +} + +validate_profiles() { + local expected=${#RUNTIME_PROFILE_IDS[@]} + [[ "$expected" -eq "${#RUNTIME_FEATURE_SETS[@]}" ]] || { + echo "feature runtime catalog columns have different lengths" >&2 + return 2 + } + + local i j + for i in "${!COMPILE_FEATURE_SETS[@]}"; do + for ((j = i + 1; j < ${#COMPILE_FEATURE_SETS[@]}; j++)); do + [[ "${COMPILE_FEATURE_SETS[$i]}" != "${COMPILE_FEATURE_SETS[$j]}" ]] || { + echo "duplicate compile feature root: ${COMPILE_FEATURE_SETS[$i]:-}" >&2 + return 2 + } + done + done + for i in "${!RUNTIME_PROFILE_IDS[@]}"; do + [[ -n "${RUNTIME_PROFILE_IDS[$i]}" \ + && -n "${RUNTIME_FEATURE_SETS[$i]}" ]] || { + echo "runtime profile $i has an empty field" >&2 + return 2 + } + for ((j = i + 1; j < ${#RUNTIME_PROFILE_IDS[@]}; j++)); do + [[ "${RUNTIME_PROFILE_IDS[$i]}" != "${RUNTIME_PROFILE_IDS[$j]}" ]] || { + echo "duplicate runtime profile ID: ${RUNTIME_PROFILE_IDS[$i]}" >&2 + return 2 + } + [[ "${RUNTIME_FEATURE_SETS[$i]}" != "${RUNTIME_FEATURE_SETS[$j]}" ]] || { + echo "duplicate runtime feature root: ${RUNTIME_FEATURE_SETS[$i]}" >&2 + return 2 + } + done + done + + local case_entry case_profile case_target case_filter known count profile_id + for i in "${!RUNTIME_PROFILE_IDS[@]}"; do + count=0 + for case_entry in "${RUNTIME_TEST_CASES[@]}"; do + IFS='|' read -r case_profile case_target case_filter <<<"$case_entry" + [[ "$case_profile" == "${RUNTIME_PROFILE_IDS[$i]}" ]] && count=$((count + 1)) + done + ((count > 0)) || { + echo "runtime profile ${RUNTIME_PROFILE_IDS[$i]} has no test cases" >&2 + return 2 + } + done + for i in "${!RUNTIME_TEST_CASES[@]}"; do + case_entry=${RUNTIME_TEST_CASES[$i]} + IFS='|' read -r case_profile case_target case_filter <<<"$case_entry" + [[ -n "$case_profile" && -n "$case_target" ]] || { + echo "runtime test case $i has an empty profile or target" >&2 + return 2 + } + known=false + for profile_id in "${RUNTIME_PROFILE_IDS[@]}"; do + [[ "$case_profile" == "$profile_id" ]] && known=true + done + [[ "$known" == true ]] || { + echo "runtime test case $i names unknown profile $case_profile" >&2 + return 2 + } + for ((j = i + 1; j < ${#RUNTIME_TEST_CASES[@]}; j++)); do + [[ "$case_entry" != "${RUNTIME_TEST_CASES[$j]}" ]] || { + echo "duplicate runtime test case: $case_entry" >&2 + return 2 + } + done + done + + COMPILE_PROFILE_IDS=() + for feature_set in "${COMPILE_FEATURE_SETS[@]}"; do + COMPILE_PROFILE_IDS+=("$(compile_profile_id "$feature_set")") + done + RUNTIME_PROFILE_IDS_CANONICAL=() + for profile_id in "${RUNTIME_PROFILE_IDS[@]}"; do + RUNTIME_PROFILE_IDS_CANONICAL+=("runtime.$profile_id") + done +} + +validate_variant_catalog() { + FEATURE_GRAPH=$(cargo metadata --locked --format-version 1 --no-deps \ + | jq -ce '[.packages[] | select(.name == "rscrypto") | .features] | if length == 1 then .[0] else error("rscrypto feature graph is ambiguous") end') || { + echo "cannot resolve the Cargo feature graph" >&2 + return 2 + } + + jq -e ' + .variant_catalog_version == 2 + and .work == "contracts.features" + and (.variants | type == "array" and length > 0) + and ([.variants[].id] | length == (unique | length)) + and any(.variants[]; .dimensions.full == true) + and all(.variants[]; + (.id | test("^[a-z][a-z0-9.-]*$")) + and (.dimensions | keys | sort) == ["feature_roots", "full", "group", "runtime_profiles"] + and (.dimensions.group | type == "string" and length > 0) + and (.dimensions.feature_roots | type == "string") + and (.dimensions.runtime_profiles | type == "string") + and (.dimensions.full | type == "boolean") + and (if .dimensions.full then + .dimensions.feature_roots == "" and .dimensions.runtime_profiles == "" + else + (.dimensions.feature_roots | length > 0) or (.dimensions.runtime_profiles | length > 0) + end) + and (.external_paths | type == "array" and length > 0 and length == (unique | length)) + and all(.external_paths[]; type == "string" and length > 0) + ) + ' "$FEATURE_CATALOG" >/dev/null || { + echo "feature variant catalog is malformed" >&2 + return 2 + } + + local catalog_id + while IFS= read -r catalog_id; do + [[ -n "$catalog_id" ]] || continue + [[ "$catalog_id" == runtime.* ]] || { + echo "feature variant catalog carries malformed runtime profile $catalog_id" >&2 + return 2 + } + profile_is_known "$catalog_id" || { + echo "feature variant catalog names unknown runtime profile $catalog_id" >&2 + return 2 + } + done < <(jq -r ' + .variants[].dimensions + | .runtime_profiles + | select(length > 0) + | split(",")[] + ' "$FEATURE_CATALOG") + + local feature_root + while IFS= read -r feature_root; do + [[ -n "$feature_root" ]] || continue + jq -e --arg feature "$feature_root" 'has($feature)' <<<"$FEATURE_GRAPH" >/dev/null || { + echo "feature variant catalog names unknown Cargo feature $feature_root" >&2 + return 2 + } + done < <(jq -r ' + .variants[].dimensions.feature_roots + | select(length > 0) + | split(",")[] + ' "$FEATURE_CATALOG") +} + +parse_shard() { + local value=${1:-} + SHARD_NUMBER=1 + SHARD_COUNT=1 + [[ -n "$value" ]] || return 0 + [[ "$value" =~ ^([1-9][0-9]*)/([1-9][0-9]*)$ ]] || { + echo "invalid shard '$value'; expected one-based N/M" >&2 + return 2 + } + SHARD_NUMBER=${BASH_REMATCH[1]} + SHARD_COUNT=${BASH_REMATCH[2]} + ((SHARD_NUMBER <= SHARD_COUNT)) || { + echo "invalid shard '$value'; N must not exceed M" >&2 + return 2 + } +} + +selected_by_shard() { + local index=$1 + ((index % SHARD_COUNT == SHARD_NUMBER - 1)) +} + +selected_profile() { + local domain=$1 + local index=$2 + local id item + if [[ "$USE_PROFILE_FILTER" == false ]]; then + selected_by_shard "$index" + return + fi + if [[ "$domain" == compile ]]; then + id=${COMPILE_PROFILE_IDS[$index]} + else + id=${RUNTIME_PROFILE_IDS_CANONICAL[$index]} + fi + for item in "${PROFILE_FILTER_IDS[@]}"; do + [[ "$item" == "$id" ]] && return 0 + done + return 1 +} + +parse_profile_filter() { + local domain=$1 + local value=$2 + local -a requested=() + local id + [[ "$domain" == compile || "$domain" == runtime ]] || usage + [[ -n "$value" ]] || { + echo "selected feature profile list must not be empty" >&2 + return 2 + } + IFS=',' read -r -a requested <<<"$value" + PROFILE_FILTER_IDS=() + for id in "${requested[@]}"; do + profile_is_known "$id" || { + echo "unknown selected feature profile: $id" >&2 + return 2 + } + [[ "$id" == "$domain."* ]] || { + echo "selected $domain execution cannot consume $id" >&2 + return 2 + } + local before=${#PROFILE_FILTER_IDS[@]} + append_unique "$id" PROFILE_FILTER_IDS + [[ "${#PROFILE_FILTER_IDS[@]}" -gt "$before" ]] || { + echo "duplicate selected feature profile: $id" >&2 + return 2 + } + done + USE_PROFILE_FILTER=true +} + +quoted_command() { + local arg rendered="" + for arg in "$@"; do + printf -v rendered '%s%q ' "$rendered" "$arg" + done + printf '%s' "${rendered% }" +} + +run_logged() { + local label=$1 + local log_path=$2 + shift 2 + local reproduction + reproduction=$(quoted_command "$@") + + step "$label" + if ! "$@" >"$log_path" 2>&1; then + fail + show_error "$log_path" + echo " Reproduce: $reproduction" >&2 + return 1 + fi + ok +} + +resolved_graph() { + local feature_set=$1 + cargo metadata --locked --format-version 1 --no-default-features \ + --features "$feature_set" \ + | jq -S -c '[.resolve.nodes[] | {id, features}]' +} + +verify_compile_aliases() { + local canonical=$1 + local entry alias alias_graph canonical_graph + + for entry in "${COMPILE_FEATURE_ALIASES[@]}"; do + [[ "${entry%%|*}" == "$canonical" ]] || continue + alias=${entry#*|} + canonical_graph=$(resolved_graph "$canonical") || return 1 + alias_graph=$(resolved_graph "$alias") || return 1 + [[ "$canonical_graph" == "$alias_graph" ]] || { + echo "compile contracts '$canonical' and '$alias' no longer resolve identically" >&2 + echo "promote '$alias' back to COMPILE_FEATURE_SETS" >&2 + return 1 + } + echo " alias: $alias resolves identically" + done +} + +compile_alias_count() { + local canonical=$1 + local entry count=0 + for entry in "${COMPILE_FEATURE_ALIASES[@]}"; do + [[ "${entry%%|*}" == "$canonical" ]] && count=$((count + 1)) + done + printf '%s\n' "$count" +} + +run_compile_contracts() { + local total_unique=${#COMPILE_FEATURE_SETS[@]} + local total_named=$((total_unique + ${#COMPILE_FEATURE_ALIASES[@]})) + local selected=0 started_at=$SECONDS + local i feature_set display log_path profile_started aliases + + echo "Compile feature contracts ($total_named named, $total_unique unique; shard $SHARD_NUMBER/$SHARD_COUNT)" + for i in "${!COMPILE_FEATURE_SETS[@]}"; do + selected_profile compile "$i" || continue + selected=$((selected + 1)) + feature_set=${COMPILE_FEATURE_SETS[$i]} + display=${feature_set:-no-features} + log_path="$LOG_DIR/compile-${display//,/_}.log" + profile_started=$SECONDS + aliases=$(compile_alias_count "$feature_set") + + if ((aliases > 0)); then + step "[$((i + 1))/$total_unique] verify aliases for $display" + if ! verify_compile_aliases "$feature_set" >"$log_path" 2>&1; then + fail + show_error "$log_path" + return 1 + fi + ok + fi + + local args=(cargo check --locked --workspace --lib --tests --no-default-features) + [[ -n "$feature_set" ]] && args+=(--features "$feature_set") + run_logged "[$((i + 1))/$total_unique] compile $display" "$log_path" "${args[@]}" || return 1 + echo " elapsed: $((SECONDS - profile_started))s" + done + + ((selected > 0)) || { + echo "compile shard $SHARD_NUMBER/$SHARD_COUNT selects no profiles" >&2 + return 2 + } + echo "${GREEN}✓${RESET} Compile feature contracts passed: $selected unique graphs in $((SECONDS - started_at))s" +} + +runtime_args() { + local feature_set=$1 + local target=$2 + local filter=$3 + + RUNTIME_ARGS=(cargo test --locked --workspace --no-default-features --features "$feature_set") + case "$target" in + all) RUNTIME_ARGS+=(--lib --tests) ;; + lib) RUNTIME_ARGS+=(--lib) ;; + *) RUNTIME_ARGS+=(--test "$target") ;; + esac + [[ -n "$filter" ]] && RUNTIME_ARGS+=(-- "$filter") + return 0 +} + +run_runtime_contracts() { + local total=${#RUNTIME_PROFILE_IDS[@]} + local selected=0 started_at=$SECONDS + local i profile_id feature_set log_path profile_started + local case_entry case_profile case_target case_filter case_number cases_run + + echo "Runtime feature contracts ($total profiles; shard $SHARD_NUMBER/$SHARD_COUNT)" + for i in "${!RUNTIME_PROFILE_IDS[@]}"; do + selected_profile runtime "$i" || continue + selected=$((selected + 1)) + profile_id=${RUNTIME_PROFILE_IDS[$i]} + feature_set=${RUNTIME_FEATURE_SETS[$i]} + profile_started=$SECONDS + case_number=0 + cases_run=0 + echo " profile [$((i + 1))/$total] $profile_id ($feature_set)" + for case_entry in "${RUNTIME_TEST_CASES[@]}"; do + IFS='|' read -r case_profile case_target case_filter <<<"$case_entry" + [[ "$case_profile" == "$profile_id" ]] || continue + case_number=$((case_number + 1)) + cases_run=$((cases_run + 1)) + log_path="$LOG_DIR/runtime-$profile_id-$case_number.log" + runtime_args "$feature_set" "$case_target" "$case_filter" + run_logged "case $case_number: $case_target${case_filter:+ ($case_filter)}" \ + "$log_path" "${RUNTIME_ARGS[@]}" || return 1 + done + ((cases_run > 0)) || return 2 + echo " elapsed: $((SECONDS - profile_started))s" + done + + ((selected > 0)) || { + echo "runtime shard $SHARD_NUMBER/$SHARD_COUNT selects no profiles" >&2 + return 2 + } + echo "${GREEN}✓${RESET} Runtime feature contracts passed: $selected profiles in $((SECONDS - started_at))s" +} + +list_contracts() { + local feature_set entry + echo "compile (${#COMPILE_FEATURE_SETS[@]} unique graphs, $(( ${#COMPILE_FEATURE_SETS[@]} + ${#COMPILE_FEATURE_ALIASES[@]} )) named contracts)" + for feature_set in "${COMPILE_FEATURE_SETS[@]}"; do + printf ' %s\n' "${feature_set:-no-features}" + for entry in "${COMPILE_FEATURE_ALIASES[@]}"; do + [[ "${entry%%|*}" == "$feature_set" ]] && printf ' alias: %s\n' "${entry#*|}" + done + done + echo "runtime (${#RUNTIME_PROFILE_IDS[@]} profiles)" + local i + for i in "${!RUNTIME_PROFILE_IDS[@]}"; do + printf ' %s: %s\n' "${RUNTIME_PROFILE_IDS[$i]}" "${RUNTIME_FEATURE_SETS[$i]}" + local case_entry case_profile case_target case_filter + for case_entry in "${RUNTIME_TEST_CASES[@]}"; do + IFS='|' read -r case_profile case_target case_filter <<<"$case_entry" + [[ "$case_profile" == "${RUNTIME_PROFILE_IDS[$i]}" ]] || continue + printf ' %s%s\n' "$case_target" "${case_filter:+: $case_filter}" + done + done +} + +print_matrix() { + local selected_rows + if [[ -n "${RAIL_PLAN_FILE:-}" || -n "${RAIL_PLAN_READER:-}" ]]; then + selected_rows=$(rail_variant_matrix contracts.features) + else + selected_rows=all + fi + if [[ "$selected_rows" == all ]]; then + selected_rows=$(jq -c '{include: [.variants[] | {id: .id} + .dimensions]}' "$FEATURE_CATALOG") + fi + jq -e ' + (.include | type == "array") + and all(.include[]; + (.id | type == "string") + and (.feature_roots | type == "string") + and (.runtime_profiles | type == "string") + and (.full | type == "boolean") + ) + ' <<<"$selected_rows" >/dev/null || { + echo "Cargo Rail emitted an invalid feature variant matrix" >&2 + return 2 + } + + local id domain count shard index planned_id profiles separator="" + local -a planned=() + if jq -e 'any(.include[]; .full == true)' <<<"$selected_rows" >/dev/null; then + for id in "${COMPILE_PROFILE_IDS[@]}" "${RUNTIME_PROFILE_IDS_CANONICAL[@]}"; do + append_unique "$id" planned + done + else + local roots profile_rows="[" row_separator="" + roots=$(jq -r '[.include[].feature_roots | select(length > 0) | split(",")[]] | unique | join(",")' \ + <<<"$selected_rows") + for index in "${!COMPILE_PROFILE_IDS[@]}"; do + profile_rows+="${row_separator}{\"id\":\"${COMPILE_PROFILE_IDS[$index]}\",\"features\":\"${COMPILE_FEATURE_SETS[$index]}\"}" + row_separator=, + done + profile_rows+="]" + while IFS= read -r id; do + [[ -n "$id" ]] && append_unique "$id" planned + done < <(jq -nr \ + --argjson graph "$FEATURE_GRAPH" \ + --arg roots "$roots" \ + --argjson profiles "$profile_rows" ' + def local_edge($graph): + select(startswith("dep:") | not) + | split("/")[0] + | sub("\\?$"; "") + | select($graph[.] != null); + def closure($graph; $pending; $seen): + if ($pending | length) == 0 then $seen + else $pending[0] as $next + | if ($seen | index($next)) != null then + closure($graph; $pending[1:]; $seen) + else + [$graph[$next][]? | local_edge($graph)] as $edges + | closure($graph; $pending[1:] + $edges; $seen + [$next]) + end + end; + ($roots | split(",") | map(select(length > 0))) as $selected + | $profiles[] + | . as $profile + | closure($graph; ($profile.features | split(",") | map(select(length > 0))); []) as $resolved + | select(any($selected[]; . as $feature | ($resolved | index($feature)) != null)) + | $profile.id + ') + while IFS= read -r id; do + [[ -n "$id" ]] || continue + profile_is_known "$id" || { + echo "Cargo Rail selected unknown runtime feature profile $id" >&2 + return 2 + } + append_unique "$id" planned + done < <(jq -r ' + .include[].runtime_profiles + | select(length > 0) + | split(",")[] + ' <<<"$selected_rows") + fi + + printf '{"include":[' + for domain in compile runtime; do + if [[ "$domain" == compile ]]; then + count=$FEATURE_COMPILE_SHARDS + else + count=$FEATURE_RUNTIME_SHARDS + fi + for ((shard = 1; shard <= count; shard++)); do + profiles="" + if [[ "$domain" == compile ]]; then + for index in "${!COMPILE_PROFILE_IDS[@]}"; do + ((index % count == shard - 1)) || continue + id=${COMPILE_PROFILE_IDS[$index]} + for planned_id in "${planned[@]:+${planned[@]}}"; do + if [[ "$planned_id" == "$id" ]]; then + profiles="${profiles:+$profiles,}$id" + break + fi + done + done + else + for index in "${!RUNTIME_PROFILE_IDS_CANONICAL[@]}"; do + ((index % count == shard - 1)) || continue + id=${RUNTIME_PROFILE_IDS_CANONICAL[$index]} + for planned_id in "${planned[@]:+${planned[@]}}"; do + if [[ "$planned_id" == "$id" ]]; then + profiles="${profiles:+$profiles,}$id" + break + fi + done + done + fi + [[ -n "$profiles" ]] || continue + printf '%s{"domain":"%s","shard":"%s/%s","profiles":"%s"}' \ + "$separator" "$domain" "$shard" "$count" "$profiles" + separator=, + done + done + printf ']}\n' +} + +domain=${1:-all} +shard=${2:-} +profile_filter=${3:-} +USE_PROFILE_FILTER=false +PROFILE_FILTER_IDS=() +case "$domain" in + all) + [[ $# -le 1 ]] || { usage; exit 2; } + ;; + compile | runtime) + [[ $# -le 2 ]] || { usage; exit 2; } + ;; + selected) + [[ $# -eq 4 ]] || { usage; exit 2; } + domain=$shard + shard=$profile_filter + profile_filter=$4 + ;; + list | matrix) + [[ $# -eq 1 ]] || { usage; exit 2; } + ;; + *) usage; exit 2 ;; +esac + +validate_profiles +if [[ "$domain" == list ]]; then + list_contracts + exit 0 +fi +if [[ "$domain" == matrix ]]; then + validate_variant_catalog + print_matrix + exit 0 +fi + +if [[ -n "$profile_filter" ]]; then + parse_profile_filter "$domain" "$profile_filter" + parse_shard "$shard" +else + parse_shard "$shard" +fi +LOG_DIR=$(mktemp -d) +trap 'rm -rf "$LOG_DIR"' EXIT + +case "$domain" in + compile) run_compile_contracts ;; + runtime) run_runtime_contracts ;; + all) + run_compile_contracts + run_runtime_contracts + ;; +esac diff --git a/scripts/check/policy.sh b/scripts/check/policy.sh new file mode 100755 index 00000000..ba4d22cd --- /dev/null +++ b/scripts/check/policy.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# Run affected repository-owned policy. Cargo work stays in check.sh/test.sh. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../lib/common.sh +source "$SCRIPT_DIR/../lib/common.sh" + +force_all=false +case "$#" in + 0) ;; + 1) + [[ "$1" == --all ]] || { echo "Usage: $0 [--all]" >&2; exit 2; } + force_all=true + ;; + *) echo "Usage: $0 [--all]" >&2; exit 2 ;; +esac + +if [[ "$force_all" == false ]]; then + rail_prime_plan +fi + +work_required() { + local status=0 + [[ "$force_all" == true ]] && return 0 + rail_work_required "$1" || status=$? + [[ "$status" -le 1 ]] || exit "$status" + return "$status" +} + +LOG_DIR=$(mktemp -d) +trap 'rm -rf "$LOG_DIR"' EXIT +PYTHON="$("$SCRIPT_DIR/../lib/python.sh" --print)" + +echo "Repository policy" + +if work_required policy.actions; then + step "Checking Actions policy" + if ! "$SCRIPT_DIR/../ci/actions-policy.sh" >"$LOG_DIR/actions.log" 2>&1; then + fail + show_error "$LOG_DIR/actions.log" + exit 1 + fi + ok +else + skip "Actions policy" "not required by Cargo Rail" +fi + +if work_required policy.repository; then + step "Checking assembly ledger" + if ! "$SCRIPT_DIR/asm-ledger.sh" >"$LOG_DIR/asm-ledger.log" 2>&1; then + fail + show_error "$LOG_DIR/asm-ledger.log" + exit 1 + fi + ok + + step "Checking hash vector provenance" + if ! "$PYTHON" "$SCRIPT_DIR/hash-vector-provenance.py" >"$LOG_DIR/hash-vectors.log" 2>&1; then + fail + show_error "$LOG_DIR/hash-vectors.log" + exit 1 + fi + ok + + step "Checking authentication vector provenance" + if ! "$PYTHON" "$SCRIPT_DIR/auth-vector-provenance.py" >"$LOG_DIR/auth-vectors.log" 2>&1; then + fail + show_error "$LOG_DIR/auth-vectors.log" + exit 1 + fi + ok + + step "Checking feature boundaries" + if ! "$PYTHON" "$SCRIPT_DIR/feature-boundaries.py" >"$LOG_DIR/feature-boundaries.log" 2>&1; then + fail + show_error "$LOG_DIR/feature-boundaries.log" + exit 1 + fi + ok + + step "Checking benchmark catalog" + if ! "$PYTHON" "$SCRIPT_DIR/../bench/benchmark_catalog_test.py" >"$LOG_DIR/benchmark-catalog.log" 2>&1; then + fail + show_error "$LOG_DIR/benchmark-catalog.log" + exit 1 + fi + ok + + step "Checking CT assembly scanner" + if ! "$PYTHON" "$SCRIPT_DIR/../ct/asm_heuristics_test.py" >"$LOG_DIR/ct-asm-scanner.log" 2>&1; then + fail + show_error "$LOG_DIR/ct-asm-scanner.log" + exit 1 + fi + ok + + step "Checking DudeCT evidence parsing" + if ! "$PYTHON" "$SCRIPT_DIR/../ct/dudect_report_test.py" >"$LOG_DIR/ct-dudect-report.log" 2>&1; then + fail + show_error "$LOG_DIR/ct-dudect-report.log" + exit 1 + fi + ok + + step "Checking CT evidence validation" + if ! "$PYTHON" "$SCRIPT_DIR/../ct/evidence_validation_test.py" >"$LOG_DIR/ct-evidence-validation.log" 2>&1; then + fail + show_error "$LOG_DIR/ct-evidence-validation.log" + exit 1 + fi + ok +else + skip "Repository policy" "not required by Cargo Rail" +fi + +echo "${GREEN}✓${RESET} Repository policy passed" diff --git a/scripts/check/zig-cc.sh b/scripts/check/zig-cc.sh index 73e606f5..2516ed95 100755 --- a/scripts/check/zig-cc.sh +++ b/scripts/check/zig-cc.sh @@ -2,7 +2,7 @@ set -euo pipefail # zig-cc.sh - Zig CC wrapper for cross-compilation -# Used by check-zig.sh for cross-target compilation checks. +# Used by constant-time artifact and BINSEC cross-linking. # Translates Rust target triples to Zig target triples. # # Targets aligned with .cargo/config.toml (Tier A/B/C). @@ -12,7 +12,7 @@ set -euo pipefail # - Zig uses different triple format (e.g., x86_64-linux-gnu vs x86_64-unknown-linux-gnu) # - This wrapper intercepts the --target flag and translates it for zig # -# Usage (automatic via the CC environment in check-zig.sh): +# Usage (automatic via the CC/linker environment in CT scripts): # CC="path/to/zig-cc.sh" cargo check --target x86_64-unknown-linux-gnu # Extract the target from --target= argument, pass everything else through. diff --git a/scripts/ci/actions-policy.sh b/scripts/ci/actions-policy.sh new file mode 100755 index 00000000..e8efd25d --- /dev/null +++ b/scripts/ci/actions-policy.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Fast, dependency-light policy for the checked-in GitHub Actions surface. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$REPO_ROOT" + +cargo rail config validate --strict +scripts/ci/check-action-pins.sh + +while IFS= read -r -d '' script; do + bash -n "$script" +done < <(find scripts -type f -name '*.sh' -print0) + +ruby -e ' + require "yaml" + Dir[".github/**/*.{yaml,yml}"].sort.each do |path| + YAML.safe_load(File.read(path), aliases: true, filename: path) + end +' + +scripts/ci/check-action-pins-test.sh +scripts/ci/check-locked-cargo.sh +scripts/ci/check-locked-cargo-test.sh +bash -c 'source scripts/lib/ci-tool-integrity.sh; ci_tool_validate_manifest' +scripts/ci/remote-cache-recipes-test.sh +scripts/ci/feature-contracts-test.sh +scripts/ci/feature-planning-test.sh +scripts/ci/assurance-planning-test.sh +scripts/test/test-fuzz-scheduler-test.sh +scripts/ci/emit-manual-matrix-test.sh +scripts/ci/changed-test-planning-test.sh +scripts/ci/check-worktree-test.sh +scripts/ci/pre-push-test.sh +scripts/ci/release-identity-test.sh +scripts/ci/publish-immutable-release-test.sh +actionlint +zizmor .github/workflows .github/actions diff --git a/scripts/ci/activate-plan.sh b/scripts/ci/activate-plan.sh new file mode 100755 index 00000000..46daec69 --- /dev/null +++ b/scripts/ci/activate-plan.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Validate one transported plan and export it for later GitHub Actions steps. + +set -euo pipefail + +if [[ $# -ne 3 ]]; then + echo "Usage: $0 PLAN_DIRECTORY PLAN_IDENTITY HEAD_COMMIT" >&2 + exit 2 +fi + +plan_dir=$(cd "$1" && pwd) +identity=$2 +head_commit=$3 +plan_file="$plan_dir/plan.json" +plan_reader="$plan_dir/read.py" +cargo_rail="$plan_dir/cargo-rail" + +[[ -f "$plan_file" && -f "$plan_reader" && -f "$cargo_rail" ]] || { + echo "Transported Cargo Rail plan artifact is incomplete" >&2 + exit 2 +} + +chmod 700 "$plan_reader" "$cargo_rail" +export PATH="$plan_dir:$PATH" +export RAIL_PLAN_FILE="$plan_file" +export RAIL_PLAN_READER="$plan_reader" +export RAIL_PLAN_IDENTITY="$identity" +export RAIL_PLAN_HEAD_COMMIT="$head_commit" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../lib/rail-plan.sh +source "$SCRIPT_DIR/../lib/rail-plan.sh" +rail_prime_plan +printf 'Cargo Rail plan: %s\n' "$identity" +printf 'Required work: %s\n' \ + "$(jq -r '.required | if length == 0 then "none" else join(", ") end' "$plan_file")" + +{ + printf 'RAIL_PLAN_FILE=%s\n' "$plan_file" + printf 'RAIL_PLAN_READER=%s\n' "$plan_reader" + printf 'RAIL_PLAN_IDENTITY=%s\n' "$identity" + printf 'RAIL_PLAN_HEAD_COMMIT=%s\n' "$head_commit" +} >>"${GITHUB_ENV:?GITHUB_ENV is required}" +printf '%s\n' "$plan_dir" >>"${GITHUB_PATH:?GITHUB_PATH is required}" diff --git a/scripts/ci/assurance-planning-test.sh b/scripts/ci/assurance-planning-test.sh new file mode 100755 index 00000000..297054aa --- /dev/null +++ b/scripts/ci/assurance-planning-test.sh @@ -0,0 +1,355 @@ +#!/usr/bin/env bash +# Prove affected assurance selection and its CI consumers without compiling product code. + +set -euo pipefail +unset BASH_ENV + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +TMP_ROOT=$(mktemp -d) +trap 'rm -rf "$TMP_ROOT"' EXIT + +fail() { + echo "assurance planning regression failure: $*" >&2 + exit 1 +} + +export GIT_INDEX_FILE="$TMP_ROOT/index" +export GIT_AUTHOR_NAME="rscrypto CI" +export GIT_AUTHOR_EMAIL="ci@rscrypto.invalid" +export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" +export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL" +export GIT_AUTHOR_DATE="2000-01-01T00:00:00Z" +export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" + +cd "$REPO_ROOT" +scripts/test/miri-contracts.sh list >/dev/null +scripts/test/fuzz-contracts.sh list >/dev/null +git read-tree HEAD +git add -A +base_tree=$(git write-tree) +base_commit=$(printf 'effective assurance planning fixture\n' | git commit-tree "$base_tree" -p HEAD) + +plan_path() { + local path=$1 + local label=$2 + local output=$3 + local mode blob tree commit + + git read-tree "$base_tree" + mode=$(git ls-tree "$base_tree" -- "$path" | awk '{ print $1 }') + [[ -n "$mode" ]] || fail "fixture path is absent: $path" + blob=$( + { + git show "$base_commit:$path" + case "$path" in + *.lock | *.toml) printf '\n# assurance planning fixture: %s\n' "$label" ;; + *) printf '\n// assurance planning fixture: %s\n' "$label" ;; + esac + } | git hash-object -w --stdin + ) + git update-index --add --cacheinfo "$mode,$blob,$path" + tree=$(git write-tree) + commit=$(printf '%s assurance planning fixture\n' "$label" | git commit-tree "$tree" -p "$base_commit") + cargo rail plan --quiet --from "$base_commit" --to "$commit" --json >"$output" +} + +assert_decisions() { + local plan=$1 + local ct_state=$2 + local rsa_state=$3 + jq -e \ + --arg ct "$ct_state" \ + --arg rsa "$rsa_state" \ + '.work["assurance.ct"].state == $ct and .work["assurance.rsa"].state == $rsa' \ + "$plan" >/dev/null || fail "unexpected CT/RSA decisions in $plan" +} + +assert_variants() { + local plan=$1 + local work=$2 + local expected=$3 + + if [[ "$expected" == skipped ]]; then + jq -e --arg work "$work" '.work[$work].state == "skipped"' "$plan" >/dev/null \ + || fail "$work should be skipped in $plan" + return + fi + if [[ "$expected" == all ]]; then + jq -e --arg work "$work" ' + .work[$work].state == "required" + and .work[$work].scope.kind == "variants" + and .work[$work].scope.selection.kind == "all" + ' "$plan" >/dev/null || fail "$work should select all variants in $plan" + return + fi + + local actual + actual=$(jq -r --arg work "$work" ' + .work[$work] + | select(.state == "required" and .scope.kind == "variants" and .scope.selection.kind == "selected") + | [.scope.selection.variants[].id] | sort | join(",") + ' "$plan") + [[ "$actual" == "$expected" ]] \ + || fail "$work selected '${actual:-}' instead of '$expected' in $plan" +} + +rsa_plan="$TMP_ROOT/rsa.json" +plan_path src/auth/rsa_x86_64_asm.rs rsa "$rsa_plan" +assert_decisions "$rsa_plan" required required +assert_variants "$rsa_plan" assurance.miri skipped +assert_variants "$rsa_plan" assurance.fuzz rsa + +rsa_core_plan="$TMP_ROOT/rsa-core.json" +plan_path src/auth/rsa.rs rsa-core "$rsa_core_plan" +assert_variants "$rsa_core_plan" assurance.miri rsa +assert_variants "$rsa_core_plan" assurance.fuzz rsa + +aead_plan="$TMP_ROOT/aead.json" +plan_path src/aead/aes128gcm.rs aead "$aead_plan" +assert_decisions "$aead_plan" required skipped +assert_variants "$aead_plan" assurance.miri skipped +assert_variants "$aead_plan" assurance.fuzz aes-gcm + +x25519_plan="$TMP_ROOT/x25519.json" +plan_path src/auth/x25519.rs x25519 "$x25519_plan" +assert_variants "$x25519_plan" assurance.miri portable +assert_variants "$x25519_plan" assurance.fuzz x25519 + +sha2_plan="$TMP_ROOT/sha2.json" +plan_path src/hashes/crypto/sha256/mod.rs sha2 "$sha2_plan" +assert_variants "$sha2_plan" assurance.miri skipped +assert_variants "$sha2_plan" assurance.fuzz sha2 + +crc32_plan="$TMP_ROOT/crc32.json" +plan_path src/checksum/crc32/mod.rs crc32 "$crc32_plan" +assert_variants "$crc32_plan" assurance.miri portable +assert_variants "$crc32_plan" assurance.fuzz crc32 + +scoped_fuzz_plan="$TMP_ROOT/scoped-fuzz.json" +plan_path fuzz-packages/auth-x25519/Cargo.toml scoped-fuzz "$scoped_fuzz_plan" +assert_variants "$scoped_fuzz_plan" assurance.miri skipped +assert_variants "$scoped_fuzz_plan" assurance.fuzz x25519 + +corpus_plan="$TMP_ROOT/corpus.json" +plan_path fuzz/corpus/auth_x25519/seed-basic corpus "$corpus_plan" +assert_variants "$corpus_plan" assurance.miri skipped +assert_variants "$corpus_plan" assurance.fuzz x25519 + +combined_checksum_plan="$TMP_ROOT/checksum-corpus.json" +plan_path fuzz/corpus/checksum_crc/seed-basic checksum-corpus "$combined_checksum_plan" +assert_variants "$combined_checksum_plan" assurance.miri skipped +assert_variants "$combined_checksum_plan" assurance.fuzz checksums-full + +unattributed_plan="$TMP_ROOT/unattributed.json" +plan_path src/secret.rs unattributed "$unattributed_plan" +assert_variants "$unattributed_plan" assurance.miri skipped +assert_variants "$unattributed_plan" assurance.fuzz all + +manifest_plan="$TMP_ROOT/manifest.json" +plan_path Cargo.toml manifest "$manifest_plan" +assert_decisions "$manifest_plan" required required +assert_variants "$manifest_plan" assurance.miri all +assert_variants "$manifest_plan" assurance.fuzz all + +all_plan="$TMP_ROOT/all.json" +cargo rail plan --quiet --from "$base_commit" --to "$base_commit" --all --json >"$all_plan" +expected_all_work=$( + printf '%s\n' \ + assurance.ct \ + assurance.fuzz \ + assurance.miri \ + assurance.rsa \ + cargo.build \ + cargo.clippy \ + cargo.doc \ + cargo.doctest \ + cargo.fmt \ + cargo.package \ + cargo.test \ + contracts.auxiliary \ + contracts.features \ + dependencies.auxiliary \ + dependency-policy \ + policy.actions \ + policy.repository \ + release.semver \ + surface \ + targets.platforms +) +actual_all_work=$(jq -r '.required[]' "$all_plan" | sort) +[[ "$actual_all_work" == "$expected_all_work" ]] \ + || fail "--all work inventory drifted" + +lock_plan="$TMP_ROOT/lock.json" +plan_path Cargo.lock lock "$lock_plan" +assert_decisions "$lock_plan" skipped skipped +assert_variants "$lock_plan" assurance.miri skipped +assert_variants "$lock_plan" assurance.fuzz skipped + +docs_plan="$TMP_ROOT/docs.json" +plan_path docs/features.md docs "$docs_plan" +assert_decisions "$docs_plan" skipped skipped + +ruby - "$REPO_ROOT" <<'RUBY' +require "json" +require "yaml" + +root = ARGV.fetch(0) +ci = YAML.safe_load(File.read(File.join(root, ".github/workflows/ci.yaml")), aliases: true) +jobs = ci.fetch("jobs") +plan_outputs = jobs.fetch("plan").fetch("outputs") +raise "missing CT plan output" unless plan_outputs.key?("ct") +raise "missing RSA plan output" unless plan_outputs.key?("rsa") +raise "missing test plan output" unless plan_outputs.key?("tests") +%w[fuzz fuzz-rows miri miri-rows].each do |output| + raise "missing #{output} plan output" unless plan_outputs.key?(output) +end + +plan_steps = jobs.fetch("plan").fetch("steps") +select_run = plan_steps.find { |step| step["id"] == "select" }.fetch("run") +raise "CI plan does not select Actions policy setup" unless select_run.include?('echo "actions=$(required_any policy.actions)"') +actionlint_install = plan_steps.find { |step| step["run"] == "scripts/ci/install-actionlint.sh" } +raise "CI does not install actionlint only for Actions policy" unless actionlint_install&.fetch("if") == "steps.select.outputs.actions == 'true'" +zizmor_install = plan_steps.find do |step| + step["uses"]&.start_with?("taiki-e/install-action@") && step.dig("with", "tool") == "zizmor@1.30.0" +end +raise "CI does not install pinned Zizmor only for Actions policy" unless zizmor_install&.fetch("if") == "steps.select.outputs.actions == 'true'" +raise "CI permits Zizmor source fallback" unless zizmor_install.dig("with", "fallback") == "none" + +actions_policy = File.read(File.join(root, "scripts/ci/actions-policy.sh")) +raise "Actions policy does not execute actionlint" unless actions_policy.match?(/^actionlint$/) +raise "Actions policy does not execute Zizmor" unless actions_policy.include?("zizmor .github/workflows .github/actions") + +ct_runs = jobs.fetch("ct").fetch("steps").map { |step| step["run"] }.compact +rsa_runs = jobs.fetch("rsa").fetch("steps").map { |step| step["run"] }.compact +raise "CT job bypasses its plan decision" unless ct_runs.include?("scripts/ci/require-work.sh assurance.ct") +raise "CT job bypasses its repository command" unless ct_runs.include?("scripts/ct/structural.sh") +raise "RSA job bypasses its plan decision" unless rsa_runs.include?("scripts/ci/require-work.sh assurance.rsa") +raise "RSA job bypasses its repository command" unless rsa_runs.include?("scripts/test/test-rsa-linux-asm.sh") + +miri_runs = jobs.fetch("miri").fetch("steps").map { |step| step["run"] }.compact +fuzz_runs = jobs.fetch("fuzz").fetch("steps").map { |step| step["run"] }.compact +raise "Miri job bypasses its plan decision" unless miri_runs.include?("scripts/ci/require-work.sh assurance.miri") +raise "Miri job bypasses selected rows" unless miri_runs.include?('scripts/test/miri-contracts.sh selected "$MIRI_ROWS"') +raise "Fuzz job bypasses its plan decision" unless fuzz_runs.include?("scripts/ci/require-work.sh assurance.fuzz") +raise "Fuzz job bypasses selected rows" unless fuzz_runs.include?('scripts/test/fuzz-contracts.sh selected "$FUZZ_ROWS"') + +needs = jobs.fetch("complete").fetch("needs") +raise "Complete omits CT" unless needs.include?("ct") +raise "Complete omits RSA" unless needs.include?("rsa") +raise "Complete omits Miri" unless needs.include?("miri") +raise "Complete omits fuzz" unless needs.include?("fuzz") + +qualification = YAML.safe_load(File.read(File.join(root, ".github/workflows/qualification.yaml")), aliases: true) +ct_call = qualification.fetch("jobs").fetch("ct").fetch("with") +%w[head_commit plan_artifact plan_identity].each do |input| + raise "Qualification CT omits #{input}" unless ct_call.key?(input) +end + +release = YAML.safe_load(File.read(File.join(root, ".github/workflows/release.yaml")), aliases: true) +release_jobs = release.fetch("jobs") +expected_release_jobs = %w[qualification package publish] +unless release_jobs.keys == expected_release_jobs + raise "Release DAG is not the minimal qualification/package join: #{release_jobs.keys.join(', ')}" +end +release_qualification = release_jobs.fetch("qualification") +unless release_qualification.fetch("uses") == "$/.github/workflows/qualification.yaml" + raise "Release does not call exact-commit Qualification" +end +unless release_qualification.fetch("with") == { + "head_commit" => "${{ github.sha }}", + "mode" => "release", +} + raise "Release Qualification inputs are not bound to the tag commit" +end +unless release_qualification.fetch("secrets").keys.sort == %w[ + CARGO_RAIL_R2_READ_ACCESS_KEY_ID + CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY +] + raise "Release Qualification receives more than read-only cache credentials" +end +release_publish = release_jobs.fetch("publish") +unless release_publish.fetch("needs") == %w[qualification package] + raise "Release publication does not join qualification and package results" +end +unless release_publish.fetch("environment") == "crates-io" + raise "Release publication bypasses the crates-io environment" +end + +qualification_miri = qualification.fetch("jobs").fetch("miri").fetch("steps").map { |step| step["run"] }.compact +raise "Qualification bypasses portable Miri row" unless qualification_miri.include?("scripts/test/miri-contracts.sh run portable") +raise "Qualification bypasses RSA Miri row" unless qualification_miri.include?("scripts/test/miri-contracts.sh run rsa") +qualification_fuzz = qualification.fetch("jobs").fetch("fuzz").fetch("steps").map { |step| step["run"] }.compact +raise "Qualification lost exhaustive fuzzing" unless qualification_fuzz.include?("scripts/test/test-fuzz.sh --all") +raise "Qualification lost exhaustive ASan replay" unless qualification_fuzz.include?("scripts/test/test-fuzz-asan.sh --all") + +qualification_platforms = qualification.fetch("jobs").fetch("platforms").fetch("steps").map { |step| step["run"] }.compact +raise "Qualification lost deep native platform proof" unless qualification_platforms.include?('scripts/ci/target-contracts.sh run "$TARGET_ROW" deep') + +target_catalog = JSON.parse(File.read(File.join(root, ".config/target-matrix.json"))) +%w[aarch64-unknown-linux-gnu aarch64-apple-darwin].each do |id| + row = target_catalog.fetch("variants").find { |candidate| candidate.fetch("id") == id } + raise "missing native AArch64 proof row #{id}" unless row + dimensions = row.fetch("dimensions") + raise "#{id} is not native runtime proof" unless dimensions.fetch("operation") == "native" + expected_paths = %w[tests/aead_kernel_equivalence.rs tests/portable_fallback.rs tests/vectored_dispatch.rs] + missing_paths = expected_paths - row.fetch("external_paths") + raise "#{id} lost differential proof paths: #{missing_paths.join(', ')}" unless missing_paths.empty? +end + +{"CI" => ci, "Qualification" => qualification}.each do |name, workflow| + installer = workflow.fetch("jobs").fetch("core").fetch("steps").find do |step| + step["uses"]&.start_with?("taiki-e/install-action@") + end + raise "#{name} core does not install pinned Nextest" unless installer + raise "#{name} core installs the wrong Nextest" unless installer.dig("with", "tool") == "cargo-nextest@0.9.143" + raise "#{name} core permits source fallback" unless installer.dig("with", "fallback") == "none" + if name == "CI" + raise "CI installs Nextest for non-test plans" unless installer.fetch("if") == "needs.plan.outputs.tests == 'true'" + end +end + +coverage = qualification.fetch("jobs").fetch("coverage") +coverage_runs = coverage.fetch("steps").map { |step| step["run"] }.compact +raise "Coverage is not bound to cargo.test" unless coverage_runs.include?("scripts/ci/require-work.sh cargo.test") +raise "Coverage is not bound to assurance.fuzz" unless coverage_runs.include?("scripts/ci/require-work.sh assurance.fuzz") +raise "Qualification lost total coverage" unless coverage_runs.include?("scripts/test/test-coverage.sh") +coverage_installer = coverage.fetch("steps").find { |step| step["uses"]&.start_with?("taiki-e/install-action@") } +raise "Coverage tools are not installed from a pinned action" unless coverage_installer +raise "Coverage tool fallback must remain disabled" unless coverage_installer.dig("with", "fallback") == "none" + +zeroization = qualification.fetch("jobs").fetch("zeroization") +zeroization_runs = zeroization.fetch("steps").map { |step| step["run"] }.compact +raise "Zeroization is not bound to assurance.ct" unless zeroization_runs.include?("scripts/ci/require-work.sh assurance.ct") +raise "Qualification lost optimized zeroization" unless zeroization_runs.include?("scripts/check/zeroize-evidence.sh") +zeroization_rust = zeroization.fetch("steps").find { |step| step["uses"] == "$/.github/actions/rust" } +raise "Zeroization must not enable compiler reuse" unless zeroization_rust && !zeroization_rust.key?("with") + +qualification_needs = qualification.fetch("jobs").fetch("complete").fetch("needs") +%w[coverage zeroization].each do |job| + raise "Qualification Complete omits #{job}" unless qualification_needs.include?(job) +end + +rail_config = File.read(File.join(root, ".config/rail.toml")) +raise "Cargo Rail surface check must remain disabled" unless rail_config.match?(/\[surface\].*?enabled = false/m) +raise "pre-1.0 SemVer check must remain disabled" unless rail_config.match?(/\[release\].*?semver_check = "off"/m) +package_guard = File.read(File.join(root, "scripts/ci/release-package-guard.sh")) +raise "cargo.package lost its release-only executor" unless package_guard.include?("cargo package --locked") + +mlkem_checkout = qualification.fetch("jobs").fetch("mlkem").fetch("steps").find do |step| + step["uses"]&.start_with?("actions/checkout@") +end +expected_head = "${{ needs.plan.outputs.head-commit }}" +raise "Qualification ML-KEM is not pinned to the planned commit" unless mlkem_checkout&.dig("with", "ref") == expected_head + +Dir[File.join(root, ".github/workflows/*.{yaml,yml}")].sort.each do |workflow_path| + workflow = YAML.safe_load(File.read(workflow_path), aliases: true) + workflow.fetch("jobs", {}).each do |job_name, job| + next if job.key?("uses") + raise "#{File.basename(workflow_path)} #{job_name} has no timeout" unless job.key?("timeout-minutes") + end +end +RUBY + +echo "Assurance planning regression tests passed" diff --git a/scripts/ci/capture-cache-status-test.sh b/scripts/ci/capture-cache-status-test.sh deleted file mode 100755 index a5876ecd..00000000 --- a/scripts/ci/capture-cache-status-test.sh +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -unset BASH_ENV - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CAPTURE="$SCRIPT_DIR/capture-cache-status.sh" -TMP_ROOT=$(mktemp -d) -trap 'rm -rf "$TMP_ROOT"' EXIT - -fail() { - echo "cache status regression failure: $*" >&2 - exit 1 -} - -BIN="$TMP_ROOT/bin" -mkdir -p "$BIN" -cat >"$BIN/cargo" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail -[[ "$*" == "rail cache status --scope local --format json" ]] -cat "$RSCRYPTO_MOCK_CACHE_STATUS" -EOF -chmod +x "$BIN/cargo" - -cat >"$TMP_ROOT/healthy.json" <<'EOF' -{ - "command": "cache", - "exit_code": 0, - "mode": "status", - "result": "success", - "schema_version": 1, - "scope": "local", - "status": { - "installation": { - "cache_base": "/secret/cache-base", - "cargo_home": "/secret/cargo-home", - "config_path": "/secret/config.toml", - "healthy": true, - "issues": [], - "max_bytes": 10737418240, - "root_portability": "remap", - "state": "installed", - "usage": { - "bypasses": 3, - "failures": 0, - "hits": 5, - "misses": 7 - }, - "wrapper_path": "/secret/cargo-rail-native-rustc-wrapper" - }, - "local": { - "cache": { - "native_conflicted": 0, - "native_local_origins": 11, - "native_remote_origins": 13, - "root": "/secret/local-cas-v2" - }, - "cross_workspace": true, - "present": true - }, - "remote": { - "activation": "direct_transport_selected", - "authority": "remote-authority-v1-sha256-test", - "mode": "read", - "protocol": "native-v6", - "provider": "cloudflare-r2", - "shared_environment_names": 3 - }, - "schema_version": 14 - } -} -EOF - -run_capture() { - local case_root=$1 - local status=$2 - mkdir -p "$case_root" - ( - cd "$case_root" - PATH="$BIN:$PATH" \ - GITHUB_OUTPUT="$case_root/github-output" \ - RSCRYPTO_CI_OPERATION=native \ - RSCRYPTO_CI_PLATFORM=linux \ - RSCRYPTO_CI_RUNNER=ubuntu-latest \ - RSCRYPTO_CI_TARGET=x86_64-unknown-linux-gnu \ - RSCRYPTO_MOCK_CACHE_STATUS="$status" \ - bash "$CAPTURE" - ) -} - -healthy_case="$TMP_ROOT/healthy-case" -run_capture "$healthy_case" "$TMP_ROOT/healthy.json" -output="$healthy_case/target/cargo-rail/cache-status.json" -[[ -f "$output" ]] || fail "healthy status did not produce telemetry" -[[ ! -e "$output.raw" ]] || fail "healthy status retained its raw input" -[[ $(<"$healthy_case/github-output") == \ - "artifact_name=cargo-rail-cache-native-x86_64-unknown-linux-gnu" ]] \ - || fail "cache telemetry artifact identity is not deterministic" - -jq -e ' - .status.installation.healthy == true and - .status.installation.root_portability == "remap" and - .status.installation.usage.misses == 7 and - .status.installation.usage.bypasses == 3 and - .status.installation.usage.failures == 0 and - .status.local.cache.native_remote_origins == 13 and - .status.local.cache.native_conflicted == 0 and - .status.remote.provider == "cloudflare-r2" and - .status.remote.mode == "read" and - .status.remote.protocol == "native-v6" -' "$output" >/dev/null || fail "required cache telemetry was not preserved" - -if rg -n '/secret/' "$output" >/dev/null; then - fail "cache telemetry disclosed a machine-local path" -fi -for field in cache_base cargo_home config_path wrapper_path; do - jq -e --arg field "$field" '.status.installation | has($field) | not' "$output" >/dev/null \ - || fail "cache telemetry retained installation.$field" -done -jq -e '.status.local.cache | has("root") | not' "$output" >/dev/null \ - || fail "cache telemetry retained the local CAS root" - -runner_case="$TMP_ROOT/runner-case" -mkdir -p "$runner_case" -( - cd "$runner_case" - PATH="$BIN:$PATH" \ - GITHUB_OUTPUT="$runner_case/github-output" \ - RSCRYPTO_CI_OPERATION=msrv \ - RSCRYPTO_CI_RUNNER='runs-on=123/runner=linux x64 ci' \ - RSCRYPTO_MOCK_CACHE_STATUS="$TMP_ROOT/healthy.json" \ - bash "$CAPTURE" -) -[[ $(<"$runner_case/github-output") == \ - "artifact_name=cargo-rail-cache-msrv-runs-on-123-runner-linux-x64-ci" ]] \ - || fail "cache telemetry did not normalize a runner label portably" - -expect_status_failure() { - local name=$1 - local filter=$2 - local status="$TMP_ROOT/$name.json" - local case_root="$TMP_ROOT/$name-case" - jq "$filter" "$TMP_ROOT/healthy.json" >"$status" - if run_capture "$case_root" "$status" >/dev/null 2>&1; then - fail "$name status was accepted" - fi - [[ ! -e "$case_root/target/cargo-rail/cache-status.json" ]] \ - || fail "$name status retained a publishable artifact" - [[ ! -e "$case_root/target/cargo-rail/cache-status.json.raw" ]] \ - || fail "$name status retained its raw input" - [[ ! -e "$case_root/target/cargo-rail/cache-status.json.projected" ]] \ - || fail "$name status retained its projected input" -} - -expect_status_failure unhealthy '.status.installation.healthy = false' -expect_status_failure compiler-failure '.status.installation.usage.failures = 1' -expect_status_failure conflict '.status.local.cache.native_conflicted = 1' - -echo "Cache status regression tests passed" diff --git a/scripts/ci/capture-cache-status.sh b/scripts/ci/capture-cache-status.sh deleted file mode 100755 index 4041c9af..00000000 --- a/scripts/ci/capture-cache-status.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -operation=${RSCRYPTO_CI_OPERATION:?RSCRYPTO_CI_OPERATION is required} -runner=${RSCRYPTO_CI_RUNNER:?RSCRYPTO_CI_RUNNER is required} -platform=${RSCRYPTO_CI_PLATFORM:-} -target=${RSCRYPTO_CI_TARGET:-} -output=target/cargo-rail/cache-status.json -raw=$output.raw -projected=$output.projected - -mkdir -p "$(dirname "$output")" -rm -f "$output" "$raw" "$projected" -trap 'rm -f "$raw" "$projected"' EXIT -cargo rail cache status --scope local --format json >"$raw" - -jq -S ' - if .status.installation.healthy != true then - error("Cargo Rail cache setup is unhealthy") - elif (.status.installation.usage.failures // 0) != 0 then - error("Cargo Rail cache recorded compiler-cache failures") - elif (.status.local.cache.native_conflicted // 0) != 0 then - error("Cargo Rail cache recorded conflicting results") - else - del( - .status.installation.cache_base, - .status.installation.cargo_home, - .status.installation.config_path, - .status.installation.wrapper_path, - .status.local.cache.root - ) - end -' "$raw" >"$projected" -mv "$projected" "$output" -rm -f "$raw" -trap - EXIT - -identity=$operation-${target:-${platform:-$runner}} -artifact_name=$(printf '%s' "$identity" | tr -cs '[:alnum:]_.-' '-' | sed 's/^-*//; s/-*$//') -[[ -n "$artifact_name" ]] || { echo "empty cache telemetry artifact name" >&2; exit 2; } -printf 'artifact_name=cargo-rail-cache-%s\n' "$artifact_name" >>"${GITHUB_OUTPUT:-/dev/null}" diff --git a/scripts/ci/changed-test-planning-test.sh b/scripts/ci/changed-test-planning-test.sh index 1035c254..d3e5f23e 100755 --- a/scripts/ci/changed-test-planning-test.sh +++ b/scripts/ci/changed-test-planning-test.sh @@ -1,11 +1,16 @@ #!/usr/bin/env bash set -euo pipefail unset BASH_ENV +unset RAIL_ALL RAIL_PLAN_FILE RAIL_PLAN_HEAD_COMMIT RAIL_PLAN_IDENTITY RAIL_PLAN_JSON_CACHE \ + RAIL_PLAN_JSON_CACHE_VALIDATED RAIL_PLAN_LOAD_ATTEMPTED RAIL_PLAN_LOADED RAIL_PLAN_LOCAL \ + RAIL_PLAN_READER RAIL_PLAN_USE_READER RAIL_SINCE SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" TMP_ROOT="$(mktemp -d)" trap 'rm -rf "$TMP_ROOT"' EXIT +PYTHON="$("$REPO_ROOT/scripts/lib/python.sh" --print)" +export PYTHON fail() { echo "changed-test planning regression failure: $*" >&2 @@ -43,11 +48,20 @@ make_plan() { end; { plan_contract_version: 8, + identity: "plan-v8:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + inputs: {head_commit: "0000000000000000000000000000000000000000"}, work: { "cargo.build": decision("cargo.build"), "cargo.doctest": decision("cargo.doctest"), "cargo.test": decision("cargo.test") - } + }, + required: ( + if $state == "required" then + ["cargo.build", "cargo.doctest", "cargo.test"] + else + [] + end + ) } ' } @@ -89,11 +103,18 @@ scope_mode() ( [[ $(scope_mode "$WORKSPACE_PLAN") == workspace ]] || fail "workspace selection was not preserved" [[ $(scope_mode "$PACKAGE_PLAN") == packages ]] || fail "package selection was not preserved" [[ $(scope_mode "$EMPTY_PLAN") == empty ]] || fail "skipped work was not preserved" -[[ $(scope_mode '' 9) == workspace ]] || fail "planner failure did not fail closed" -[[ $(scope_mode "$PACKAGE_PLAN" 0 9) == workspace ]] || fail "saved-plan verification failure did not fail closed" -[[ $(scope_mode '{"plan_contract_version":7}' 0 0) == workspace ]] || fail "v7 plan was accepted" +if scope_mode '' 9 >/dev/null 2>&1; then + fail "planner failure did not fail closed" +fi +if scope_mode "$PACKAGE_PLAN" 0 9 >/dev/null 2>&1; then + fail "saved-plan verification failure did not fail closed" +fi +if scope_mode '{"plan_contract_version":7}' 0 0 >/dev/null 2>&1; then + fail "v7 plan was accepted" +fi -package_args=$( +package_args_file="$TMP_ROOT/package-args" +( export PATH="$BIN:$PATH" export MOCK_LOG="$LOG" export MOCK_PLAN_OUTPUT="$PACKAGE_PLAN" @@ -101,8 +122,9 @@ package_args=$( # shellcheck source=../lib/rail-plan.sh source "$REPO_ROOT/scripts/lib/rail-plan.sh" rail_scope_cargo_args cargo.test -) -[[ "$package_args" == $'-p\nrscrypto' ]] || fail "exact typed Cargo arguments were not exposed" +) >"$package_args_file" +[[ $(od -An -tx1 -v "$package_args_file" | tr -d ' \n') == 2d7000727363727970746f00 ]] \ + || fail "exact NUL-delimited Cargo arguments were not exposed" run_test_consumer() { local plan=$1 @@ -122,7 +144,7 @@ run_test_consumer() { } run_test_consumer "$PACKAGE_PLAN" 'cargo test --locked -p rscrypto --all-features --lib --tests' -run_test_consumer "$WORKSPACE_PLAN" 'cargo test --locked --workspace --all-features --lib --tests' +run_test_consumer "$WORKSPACE_PLAN" 'cargo test --locked --all-features --lib --tests' : >"$LOG" env \ diff --git a/scripts/ci/check-action-pins-test.sh b/scripts/ci/check-action-pins-test.sh index 6518b288..475fcd85 100755 --- a/scripts/ci/check-action-pins-test.sh +++ b/scripts/ci/check-action-pins-test.sh @@ -48,11 +48,17 @@ chmod +x "$fake_bin/git" "$fake_bin/curl" make_fixture() { local fixture=$1 - mkdir -p "$fixture/.github/workflows" "$fixture/.github/actions" + mkdir -p "$fixture/.github/workflows" "$fixture/.github/actions/local" + cat >"$fixture/.github/actions/local/action.yaml" <<'YAML' +runs: + using: composite + steps: [] +YAML cat >"$fixture/.github/workflows/scorecard.yaml" </dev/null +missing_self_reference="$TMP_ROOT/missing-self-reference" +make_fixture "$missing_self_reference" +sed -i.bak 's#\$/.github/actions/local#\$/.github/actions/missing#' \ + "$missing_self_reference/.github/workflows/scorecard.yaml" +rm -f "$missing_self_reference/.github/workflows/scorecard.yaml.bak" +expect_failure "$missing_self_reference" "self-repository action does not exist" + unpinned="$TMP_ROOT/unpinned" make_fixture "$unpinned" sed -i.bak "s/@$expected_sha/@v4/" "$unpinned/.github/workflows/scorecard.yaml" diff --git a/scripts/ci/check-action-pins.sh b/scripts/ci/check-action-pins.sh index 76308d0d..2b6d293e 100755 --- a/scripts/ci/check-action-pins.sh +++ b/scripts/ci/check-action-pins.sh @@ -22,7 +22,7 @@ if [[ -z "$root" ]]; then fi root=$(cd "$root" && pwd) -for dependency in curl git sed sort yq; do +for dependency in curl git ruby sed sort; do command -v "$dependency" >/dev/null 2>&1 || { echo "action pin error: missing dependency: $dependency" >&2 exit 1 @@ -40,6 +40,34 @@ fail() { status=1 } +check_self_reference() { + local use=$1 + local location=$2 + local relative=${use#'$/'} + + if [[ ! "$relative" =~ ^[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*$ ]]; then + fail "invalid self-repository reference '$use': $location" + return + fi + case "/$relative/" in + *"/../"* | *"/./"* | *"//"*) + fail "invalid self-repository reference '$use': $location" + return + ;; + esac + + case "$relative" in + .github/workflows/*.yml | .github/workflows/*.yaml) + [[ -f "$root/$relative" ]] || fail "self-repository workflow '$use' does not exist: $location" + ;; + *) + if [[ ! -f "$root/$relative/action.yml" && ! -f "$root/$relative/action.yaml" ]]; then + fail "self-repository action '$use' has no action definition: $location" + fi + ;; + esac +} + workflow_files() { local roots=() [[ -d "$root/.github/workflows" ]] && roots+=("$root/.github/workflows") @@ -55,15 +83,20 @@ while IFS= read -r file; do use=$(sed -nE 's/^[[:space:]-]*uses:[[:space:]]*([^[:space:]#]+).*/\1/p' <<<"$line") [[ -n "$use" ]] || continue + location="${file#"$root"/}:$line_number" case "$use" in ./* | docker://*) continue ;; + '$/'*) + check_self_reference "$use" "$location" + continue + ;; esac parsed=$(sed -nE \ 's/^[[:space:]-]*uses:[[:space:]]*([^@[:space:]#]+)@([0-9a-f]{40})[[:space:]]*#[[:space:]]*(v?[0-9]+(\.[0-9]+){0,2})[[:space:]]*$/\1\ \2\ \3/p' \ <<<"$line") if [[ -z "$parsed" ]]; then - fail "external action must use a lowercase 40-character SHA and same-line semantic ref: ${file#"$root"/}:$line_number" + fail "external action must use a lowercase 40-character SHA and same-line semantic ref: $location" continue fi @@ -72,7 +105,7 @@ while IFS= read -r file; do sha=${remainder%%$'\t'*} ref=${remainder#*$'\t'} if [[ ! "$action" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*$ ]]; then - fail "invalid external action name '$action': ${file#"$root"/}:$line_number" + fail "invalid external action name '$action': $location" continue fi @@ -164,7 +197,11 @@ while IFS=$'\t' read -r action sha ref; do runtime="" for filename in action.yml action.yaml; do if fetch_definition "$repository" "$sha" "${path_prefix}${filename}" "$tmp/definition" 2>/dev/null; then - runtime=$(yq eval -r '.runs.using // ""' "$tmp/definition" 2>/dev/null || true) + runtime=$(ruby -e ' + require "yaml" + definition = YAML.safe_load(File.read(ARGV.fetch(0)), aliases: true) + puts definition.dig("runs", "using").to_s + ' "$tmp/definition" 2>/dev/null || true) [[ -n "$runtime" ]] && break fi done diff --git a/scripts/ci/check-ci-ownership-test.sh b/scripts/ci/check-ci-ownership-test.sh deleted file mode 100755 index 4fb62745..00000000 --- a/scripts/ci/check-ci-ownership-test.sh +++ /dev/null @@ -1,556 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -CHECKER="$SCRIPT_DIR/check-ci-ownership.sh" -TMP_ROOT=$(mktemp -d) -trap 'rm -rf "$TMP_ROOT"' EXIT - -make_fixture() { - local fixture=$1 - mkdir -p "$fixture/.cargo" "$fixture/.github" "$fixture/.config" \ - "$fixture/scripts/check" "$fixture/scripts/lib" "$fixture/scripts/test" - cp "$REPO_ROOT/.cargo/config.toml" "$fixture/.cargo/config.toml" - cp -R "$REPO_ROOT/.github/workflows" "$fixture/.github/workflows" - cp -R "$REPO_ROOT/.github/actions" "$fixture/.github/actions" - cp -R "$REPO_ROOT/.github/rulesets" "$fixture/.github/rulesets" - cp -R "$REPO_ROOT/.github/repository-settings" "$fixture/.github/repository-settings" - cp "$REPO_ROOT/.github/dependabot.yaml" "$fixture/.github/dependabot.yaml" - cp "$REPO_ROOT/.github/runs-on.yml" "$fixture/.github/runs-on.yml" - cp "$REPO_ROOT/.config/target-matrix.json" "$fixture/.config/target-matrix.json" - cp "$REPO_ROOT/.config/ci-plan-variants.json" "$fixture/.config/ci-plan-variants.json" - cp "$REPO_ROOT/.config/rail.toml" "$fixture/.config/rail.toml" - cp "$REPO_ROOT/.config/ci-tool-archives.tsv" "$fixture/.config/ci-tool-archives.tsv" - cp -R "$REPO_ROOT/scripts/ci" "$fixture/scripts/ci" - cp "$REPO_ROOT/scripts/lib/ci-tool-integrity.sh" "$REPO_ROOT/scripts/lib/common.sh" \ - "$REPO_ROOT/scripts/lib/feature-profiles.sh" \ - "$fixture/scripts/lib/" - cp "$REPO_ROOT/scripts/check/check-all.sh" "$REPO_ROOT/scripts/check/check-feature-matrix.sh" \ - "$REPO_ROOT/scripts/check/check.sh" "$fixture/scripts/check/" - cp "$REPO_ROOT/scripts/test/test-feature-matrix.sh" "$fixture/scripts/test/" -} - -expect_failure() { - local fixture=$1 - local description=$2 - if "$CHECKER" --root "$fixture" >/dev/null 2>&1; then - echo "expected ownership failure: $description" >&2 - exit 1 - fi -} - -baseline="$TMP_ROOT/baseline" -make_fixture "$baseline" -"$CHECKER" --root "$baseline" >/dev/null - -missing_ci_policy="$TMP_ROOT/missing-ci-policy" -make_fixture "$missing_ci_policy" -yq -oy -p toml eval 'del(.plan.work."ci-policy")' -i \ - "$missing_ci_policy/.config/rail.toml" -expect_failure "$missing_ci_policy" "shared CI infrastructure does not widen the Cargo Rail matrix" - -zig_dependent_cross_targets="$TMP_ROOT/zig-dependent-cross-targets" -make_fixture "$zig_dependent_cross_targets" -printf '\nzig version\n' >>"$zig_dependent_cross_targets/scripts/ci/cross-targets.sh" -expect_failure "$zig_dependent_cross_targets" "cross-target CI depends on Zig" - -missing_recovery_tag="$TMP_ROOT/missing-recovery-tag" -make_fixture "$missing_recovery_tag" -yq eval 'del(.on.workflow_dispatch.inputs.tag)' -i \ - "$missing_recovery_tag/.github/workflows/release.yaml" -expect_failure "$missing_recovery_tag" "release recovery has no explicit tag identity" - -unprotected_recovery="$TMP_ROOT/unprotected-recovery" -make_fixture "$unprotected_recovery" -sed -i.bak 's#refs/heads/main#refs/heads/recovery#' \ - "$unprotected_recovery/.github/workflows/release.yaml" -rm -f "$unprotected_recovery/.github/workflows/release.yaml.bak" -expect_failure "$unprotected_recovery" "release recovery accepts unprotected workflow code" - -missing_ct_recovery_tag="$TMP_ROOT/missing-ct-recovery-tag" -make_fixture "$missing_ct_recovery_tag" -yq eval 'del(.on.workflow_dispatch.inputs.release_tag)' -i \ - "$missing_ct_recovery_tag/.github/workflows/ct.yaml" -expect_failure "$missing_ct_recovery_tag" "CT recovery has no immutable tag identity" - -unprotected_ct_recovery="$TMP_ROOT/unprotected-ct-recovery" -make_fixture "$unprotected_ct_recovery" -yq eval '(.jobs.plan.steps[] | select(.name == "Resolve CT source") | .run) |= sub("refs/heads/main"; "refs/heads/recovery")' -i \ - "$unprotected_ct_recovery/.github/workflows/ct.yaml" -expect_failure "$unprotected_ct_recovery" "CT recovery accepts unprotected workflow code" - -mutable_ct_recovery_checkout="$TMP_ROOT/mutable-ct-recovery-checkout" -make_fixture "$mutable_ct_recovery_checkout" -yq eval '(.jobs.ct.with.checkout_ref) = "${{ github.sha }}"' -i \ - "$mutable_ct_recovery_checkout/.github/workflows/ct.yaml" -expect_failure "$mutable_ct_recovery_checkout" "CT recovery ignores the immutable tag source" - -untyped_ct_dispatch_numbers="$TMP_ROOT/untyped-ct-dispatch-numbers" -make_fixture "$untyped_ct_dispatch_numbers" -yq eval '(.jobs.ct.with.dudect_timeout) = "${{ inputs.dudect_timeout }}"' -i \ - "$untyped_ct_dispatch_numbers/.github/workflows/ct.yaml" -expect_failure "$untyped_ct_dispatch_numbers" "manual CT timeout bypasses typed normalization" - -missing_s390x_vector_environment="$TMP_ROOT/missing-s390x-vector-environment" -make_fixture "$missing_s390x_vector_environment" -yq eval 'del(.jobs.run.steps[] | select(.name == "Run") | .env.CARGO_TARGET_S390X_UNKNOWN_LINUX_GNU_RUSTFLAGS)' -i \ - "$missing_s390x_vector_environment/.github/workflows/_rust-job.yaml" -expect_failure "$missing_s390x_vector_environment" "s390x sibling CT processes lose the vector target environment" - -unvalidated_s390x_recovery="$TMP_ROOT/unvalidated-s390x-recovery" -make_fixture "$unvalidated_s390x_recovery" -yq eval '(.jobs.preflight.steps[] | select(.name == "Verify s390x CT recovery evidence") | .run) = "true"' -i \ - "$unvalidated_s390x_recovery/.github/workflows/release.yaml" -expect_failure "$unvalidated_s390x_recovery" "release accepts unvalidated replacement s390x evidence" - -unvalidated_x86_64_recovery="$TMP_ROOT/unvalidated-x86_64-recovery" -make_fixture "$unvalidated_x86_64_recovery" -yq eval '(.jobs.preflight.steps[] | select(.name == "Verify x86_64 CT recovery evidence") | .run) = "true"' -i \ - "$unvalidated_x86_64_recovery/.github/workflows/release.yaml" -expect_failure "$unvalidated_x86_64_recovery" "release accepts unvalidated replacement x86_64 evidence" - -unplanned_recovery_rustflags="$TMP_ROOT/unplanned-recovery-rustflags" -make_fixture "$unplanned_recovery_rustflags" -yq eval '(.jobs.ct.with.rustflags) = "-A warnings"' -i \ - "$unplanned_recovery_rustflags/.github/workflows/ct.yaml" -expect_failure "$unplanned_recovery_rustflags" "CT recovery accepts unplanned compiler flags" - -mutable_publish_checkout="$TMP_ROOT/mutable-publish-checkout" -make_fixture "$mutable_publish_checkout" -yq eval '(.jobs.publish.steps[] | select(.name == "Checkout") | .with.ref) = "${{ github.ref }}"' -i \ - "$mutable_publish_checkout/.github/workflows/release.yaml" -expect_failure "$mutable_publish_checkout" "release publication ignores the preflight-verified tag" - -hosted_macos="$TMP_ROOT/hosted-macos" -make_fixture "$hosted_macos" -yq eval '.jobs.hosted_macos = {"runs-on": "macos-15", "steps": [{"run": "true"}]}' -i \ - "$hosted_macos/.github/workflows/rsa.yaml" -expect_failure "$hosted_macos" "macOS testing is delegated to a hosted runner" - -apple_runner_alias="$TMP_ROOT/apple-runner-alias" -make_fixture "$apple_runner_alias" -yq eval '.jobs.apple_runner = {"uses": "./.github/workflows/_rust-job.yaml", "with": {"runner": "darwin", "operation": "check"}}' -i \ - "$apple_runner_alias/.github/workflows/rsa.yaml" -expect_failure "$apple_runner_alias" "Apple testing is delegated through a custom runner label" - -apple_rustflags="$TMP_ROOT/apple-rustflags" -make_fixture "$apple_rustflags" -printf '\n[target.aarch64-apple-darwin]\nrustflags = ["-C", "target-cpu=native"]\n' \ - >>"$apple_rustflags/.cargo/config.toml" -expect_failure "$apple_rustflags" "normal Apple builds inherit host-specific rustflags" - -invalid_tool_digest="$TMP_ROOT/invalid-tool-digest" -make_fixture "$invalid_tool_digest" -sed -i.bak 's/ca1d64196d2d34771084afe76ea657d581bf628e31d993ff8e52ea09cc88a56d/not-a-digest/' \ - "$invalid_tool_digest/.config/ci-tool-archives.tsv" -rm -f "$invalid_tool_digest/.config/ci-tool-archives.tsv.bak" -expect_failure "$invalid_tool_digest" "direct tool digest is malformed" - -mutable_tool_url="$TMP_ROOT/mutable-tool-url" -make_fixture "$mutable_tool_url" -sed -i.bak 's#/download/v48\.0\.0/#/download/Latest/#' \ - "$mutable_tool_url/.config/ci-tool-archives.tsv" -rm -f "$mutable_tool_url/.config/ci-tool-archives.tsv.bak" -expect_failure "$mutable_tool_url" "direct tool URL resolves a mutable release" - -unexpected_tool_filename="$TMP_ROOT/unexpected-tool-filename" -make_fixture "$unexpected_tool_filename" -awk -F '\t' -v OFS='\t' '$1 == "codecov" { $5 = "codecov-substitute" } { print }' \ - "$unexpected_tool_filename/.config/ci-tool-archives.tsv" \ - >"$unexpected_tool_filename/.config/ci-tool-archives.tsv.tmp" -mv "$unexpected_tool_filename/.config/ci-tool-archives.tsv.tmp" \ - "$unexpected_tool_filename/.config/ci-tool-archives.tsv" -expect_failure "$unexpected_tool_filename" "direct tool URL and filename disagree" - -unauthenticated_cargo_installer="$TMP_ROOT/unauthenticated-cargo-installer" -make_fixture "$unauthenticated_cargo_installer" -printf '\ncargo binstall cargo-nextest\n' \ - >>"$unauthenticated_cargo_installer/scripts/ci/install-tools.sh" -expect_failure "$unauthenticated_cargo_installer" "Cargo-binstall bypasses package integrity" - -poisonable_tool_cache="$TMP_ROOT/poisonable-tool-cache" -make_fixture "$poisonable_tool_cache" -yq eval '.runs.steps += [{"name": "Restore poisonable tools", "uses": "actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9", "with": {"path": "~/.cargo/bin", "key": "known"}}]' -i \ - "$poisonable_tool_cache/.github/actions/setup/action.yaml" -expect_failure "$poisonable_tool_cache" "CI tool executables can be restored from a poisonable cache" - -competing_rust_cache="$TMP_ROOT/competing-rust-cache" -make_fixture "$competing_rust_cache" -yq eval '.runs.steps += [{"name": "Competing Rust cache", "uses": "Swatinem/rust-cache@0123456789012345678901234567890123456789"}]' -i \ - "$competing_rust_cache/.github/actions/setup/action.yaml" -expect_failure "$competing_rust_cache" "a competing Rust compiler cache bypasses Cargo Rail" - -magic_cache_extra="$TMP_ROOT/magic-cache-extra" -make_fixture "$magic_cache_extra" -yq eval '.runners.linux-x64-ci.extras = ["s3-cache"]' -i \ - "$magic_cache_extra/.github/runs-on.yml" -expect_failure "$magic_cache_extra" "RunsOn MagicCache intercepts Cargo Rail compiler results" - -missing_coverage_telemetry="$TMP_ROOT/missing-coverage-telemetry" -make_fixture "$missing_coverage_telemetry" -yq eval 'del(.jobs.coverage.steps[] | select(.name == "Capture Cargo Rail Cache Status"))' -i \ - "$missing_coverage_telemetry/.github/workflows/weekly.yaml" -expect_failure "$missing_coverage_telemetry" "Qualification coverage omits Cargo Rail telemetry" - -unauthenticated_rustup="$TMP_ROOT/unauthenticated-rustup" -make_fixture "$unauthenticated_rustup" -printf '\n - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9\n' \ - >>"$unauthenticated_rustup/.github/actions/setup-toolchain/action.yaml" -expect_failure "$unauthenticated_rustup" "toolchain setup can run a network bootstrap installer" - -inactive_toolchain_contract="$TMP_ROOT/inactive-toolchain-contract" -make_fixture "$inactive_toolchain_contract" -sed -i.bak 's/ "\$GITHUB_ENV"$//' \ - "$inactive_toolchain_contract/.github/actions/setup-toolchain/action.yaml" -rm -f "$inactive_toolchain_contract/.github/actions/setup-toolchain/action.yaml.bak" -expect_failure "$inactive_toolchain_contract" "toolchain contract is installed but not activated" - -floating_rail_action="$TMP_ROOT/floating-rail-action" -make_fixture "$floating_rail_action" -yq eval '(.jobs."rail-plan".steps[] | select(.id == "rail") | .uses) = "loadingalias/cargo-rail-action@v6"' -i \ - "$floating_rail_action/.github/workflows/ci.yaml" -expect_failure "$floating_rail_action" "cargo-rail-action is not commit-pinned" - -floating_rail_cache="$TMP_ROOT/floating-rail-cache" -make_fixture "$floating_rail_cache" -yq eval '(.runs.steps[] | select(.name == "Setup Cargo Rail Cache") | .uses) = "loadingalias/cargo-rail-action/cache@v7"' -i \ - "$floating_rail_cache/.github/actions/setup/action.yaml" -expect_failure "$floating_rail_cache" "the Cargo Rail cache action is not commit-pinned with the planner" - -writable_pr_cache="$TMP_ROOT/writable-pr-cache" -make_fixture "$writable_pr_cache" -yq eval '(.on.workflow_call.inputs.cache_mode.default) = "read-write"' -i \ - "$writable_pr_cache/.github/workflows/_ci-suite.yaml" -expect_failure "$writable_pr_cache" "untrusted pull requests can write shared compiler results" - -untrusted_cache_seeder="$TMP_ROOT/untrusted-cache-seeder" -make_fixture "$untrusted_cache_seeder" -yq eval '(.jobs."cache-seed".if) = "github.event_name == '\''pull_request'\''"' -i \ - "$untrusted_cache_seeder/.github/workflows/ci.yaml" -expect_failure "$untrusted_cache_seeder" "an untrusted event can assume shared-cache write authority" - -missing_cache_identity="$TMP_ROOT/missing-cache-identity" -make_fixture "$missing_cache_identity" -yq eval 'del(.runs.steps[] | select(.name == "Authenticate Cargo Rail Cache"))' -i \ - "$missing_cache_identity/.github/actions/setup/action.yaml" -expect_failure "$missing_cache_identity" "shared-cache mode is not enforced by provider identity" - -ambient_session_token="$TMP_ROOT/ambient-session-token" -make_fixture "$ambient_session_token" -sed -i.bak '/AWS_SESSION_TOKEN=/d' "$ambient_session_token/.github/actions/setup/action.yaml" -rm -f "$ambient_session_token/.github/actions/setup/action.yaml.bak" -expect_failure "$ambient_session_token" "an ambient AWS session token can corrupt the selected R2 identity" - -missing_strict_probe="$TMP_ROOT/missing-strict-probe" -make_fixture "$missing_strict_probe" -yq eval 'del(.runs.steps[] | select(.name == "Setup Cargo Rail Cache") | .with."strict-probe")' -i \ - "$missing_strict_probe/.github/actions/setup/action.yaml" -expect_failure "$missing_strict_probe" "compiler jobs can start before R2 and native-v6 are authenticated" - -physical_ci_cache="$TMP_ROOT/physical-ci-cache" -make_fixture "$physical_ci_cache" -yq eval '(.inputs."cache-root-portability".default) = "physical"' -i \ - "$physical_ci_cache/.github/actions/setup/action.yaml" -expect_failure "$physical_ci_cache" "ephemeral CI cache cannot share across checkout roots" - -untrusted_rail_reuse="$TMP_ROOT/untrusted-rail-reuse" -make_fixture "$untrusted_rail_reuse" -yq eval '(.runs.steps[] | select(.name == "Install Cargo Tools") | .env.RSCRYPTO_AUTHENTICATED_CARGO_RAIL) = "true"' -i \ - "$untrusted_rail_reuse/.github/actions/setup/action.yaml" -expect_failure "$untrusted_rail_reuse" "a runner-provided Cargo Rail binary is treated as authenticated" - -mismatched_rail_version="$TMP_ROOT/mismatched-rail-version" -make_fixture "$mismatched_rail_version" -yq eval '(.jobs."rail-plan".steps[] | select(.id == "rail") | .with.version) = "0.19.1"' -i \ - "$mismatched_rail_version/.github/workflows/ci.yaml" -expect_failure "$mismatched_rail_version" "cargo-rail-action bypasses the authenticated Cargo Rail version" - -missing_surface_component="$TMP_ROOT/missing-surface-component" -make_fixture "$missing_surface_component" -yq eval 'del(.jobs."rail-plan".steps[] | select(.id == "rail") | .with.components)' -i \ - "$missing_surface_component/.github/workflows/ci.yaml" -expect_failure "$missing_surface_component" "cargo-rail-action does not prepare Surface" - -mutable_rail_base="$TMP_ROOT/mutable-rail-base" -make_fixture "$mutable_rail_base" -yq eval '(.jobs."rail-plan".steps[] | select(.id == "rail") | .with.since) = "origin/main"' -i \ - "$mutable_rail_base/.github/workflows/ci.yaml" -expect_failure "$mutable_rail_base" "cargo-rail-action plans from a mutable base" - -indirect_scorecard="$TMP_ROOT/indirect-scorecard" -make_fixture "$indirect_scorecard" -yq eval '(.jobs.scorecard.steps[] | select(.name == "Run Scorecard") | .uses) = "./.github/actions/scorecard"' -i \ - "$indirect_scorecard/.github/workflows/scorecard.yaml" -expect_failure "$indirect_scorecard" "Scorecard publication does not call the official action directly" - -floating_codecov="$TMP_ROOT/floating-codecov" -make_fixture "$floating_codecov" -sed -i.bak '/binary:.*steps\.codecov\.outputs\.binary/d' \ - "$floating_codecov/.github/workflows/weekly.yaml" -rm -f "$floating_codecov/.github/workflows/weekly.yaml.bak" -expect_failure "$floating_codecov" "Codecov action can download its floating default CLI" - -unowned_download="$TMP_ROOT/unowned-download" -make_fixture "$unowned_download" -printf '\ncurl --output /tmp/tool https://example.invalid/tool\n' \ - >>"$unowned_download/scripts/ci/run-rust-job.sh" -expect_failure "$unowned_download" "a direct download exists outside the integrity owner" - -unowned_package_install="$TMP_ROOT/unowned-package-install" -make_fixture "$unowned_package_install" -printf '\ncargo install ripgrep\n' \ - >>"$unowned_package_install/scripts/ci/run-rust-job.sh" -expect_failure "$unowned_package_install" "a package-manager install exists outside the integrity owner" - -unowned_test_download="$TMP_ROOT/unowned-test-download" -make_fixture "$unowned_test_download" -printf '#!/usr/bin/env bash\ncurl --output /tmp/tool https://example.invalid/tool\n' \ - >"$unowned_test_download/scripts/ci/unreviewed-test.sh" -expect_failure "$unowned_test_download" "a test script bypasses downloader ownership" - -missing_action_updates="$TMP_ROOT/missing-action-updates" -make_fixture "$missing_action_updates" -yq eval 'del(.updates[] | select(."package-ecosystem" == "github-actions"))' -i \ - "$missing_action_updates/.github/dependabot.yaml" -expect_failure "$missing_action_updates" "GitHub Actions updates are disabled" - -missing_main_seed="$TMP_ROOT/missing-main-seed" -make_fixture "$missing_main_seed" -yq eval 'del(.on.push)' -i "$missing_main_seed/.github/workflows/ci.yaml" -expect_failure "$missing_main_seed" "main cannot seed affected compiler results" - -missing_ready_event="$TMP_ROOT/missing-ready-event" -make_fixture "$missing_ready_event" -yq eval 'del(.on.pull_request.types[] | select(. == "ready_for_review"))' -i \ - "$missing_ready_event/.github/workflows/ci.yaml" -expect_failure "$missing_ready_event" "draft PR cannot start CI when marked ready" - -draft_runs_suite="$TMP_ROOT/draft-runs-suite" -make_fixture "$draft_runs_suite" -yq eval '.jobs.suite."if" = "always()"' -i "$draft_runs_suite/.github/workflows/ci.yaml" -expect_failure "$draft_runs_suite" "draft PR can run the expensive suite" - -planner_failure_skips_suite="$TMP_ROOT/planner-failure-skips-suite" -make_fixture "$planner_failure_skips_suite" -yq eval '.jobs.suite."if" = "github.event_name != '\''pull_request'\'' || !github.event.pull_request.draft"' -i \ - "$planner_failure_skips_suite/.github/workflows/ci.yaml" -expect_failure "$planner_failure_skips_suite" "planner failure can skip the CI suite" - -cancelled_suite_runs="$TMP_ROOT/cancelled-suite-runs" -make_fixture "$cancelled_suite_runs" -yq eval '.jobs.suite."if" |= sub("!cancelled\\(\\)"; "always()")' -i \ - "$cancelled_suite_runs/.github/workflows/ci.yaml" -expect_failure "$cancelled_suite_runs" "workflow cancellation can start the expensive suite" - -fork_release_bypasses_intent="$TMP_ROOT/fork-release-bypasses-intent" -make_fixture "$fork_release_bypasses_intent" -yq eval '(.jobs."rail-plan".steps[] | select(.name == "Check Release Intent Coverage") | .if) |= sub(" && github.event.pull_request.head.repo.full_name == github.repository"; "")' -i \ - "$fork_release_bypasses_intent/.github/workflows/ci.yaml" -expect_failure "$fork_release_bypasses_intent" "a fork can bypass release intent with a branch name" - -shell_fragment_input="$TMP_ROOT/shell-fragment-input" -make_fixture "$shell_fragment_input" -printf '\n run_script: echo caller-controlled\n' \ - >>"$shell_fragment_input/.github/workflows/_ci-suite.yaml" -expect_failure "$shell_fragment_input" "reusable workflow accepts executable shell fragments" - -missing_typed_operation="$TMP_ROOT/missing-typed-operation" -make_fixture "$missing_typed_operation" -yq eval 'del(.jobs.selected.with.operation)' -i \ - "$missing_typed_operation/.github/workflows/_ci-suite.yaml" -expect_failure "$missing_typed_operation" "reusable Rust job caller omits its operation" - -unsupported_typed_operation="$TMP_ROOT/unsupported-typed-operation" -make_fixture "$unsupported_typed_operation" -yq eval '(.jobs.selected.with.operation) = "arbitrary-shell"' -i \ - "$unsupported_typed_operation/.github/workflows/_ci-suite.yaml" -expect_failure "$unsupported_typed_operation" "reusable Rust job caller selects an unsupported operation" - -evaluated_workflow_input="$TMP_ROOT/evaluated-workflow-input" -make_fixture "$evaluated_workflow_input" -yq eval '(.jobs.run.steps[] | select(.name == "Run") | .run) = "echo \"${{ inputs.operation }}\""' -i \ - "$evaluated_workflow_input/.github/workflows/_rust-job.yaml" -expect_failure "$evaluated_workflow_input" "workflow input is evaluated as shell code" - -duplicate_matrix="$TMP_ROOT/duplicate-matrix" -make_fixture "$duplicate_matrix" -printf '\n# duplicate owner\n run: just test-feature-matrix\n' >>"$duplicate_matrix/.github/workflows/weekly.yaml" -expect_failure "$duplicate_matrix" "duplicate feature matrix" - -native_cross_sweep="$TMP_ROOT/native-cross-sweep" -make_fixture "$native_cross_sweep" -printf '\n# forbidden native sweep\n run: just check --all\n' >>"$native_cross_sweep/.github/workflows/_ci-suite.yaml" -expect_failure "$native_cross_sweep" "comprehensive check in native workflow" - -fake_musl="$TMP_ROOT/fake-musl" -make_fixture "$fake_musl" -jq '.variants += [{"id":"native-linux-musl","dimensions":{"display_name":"fake","operation":"native","runner_type":"runson","runner":"linux-x64-ci","target":"x86_64-unknown-linux-musl","timeout_minutes":30,"tools_mode":"none","toolchain_contract":"development","toolchain_components":""},"paths":[],"config":[],"cargo":[]}]' \ - "$fake_musl/.config/ci-plan-variants.json" >"$fake_musl/.config/ci-plan-variants.json.tmp" -mv "$fake_musl/.config/ci-plan-variants.json.tmp" "$fake_musl/.config/ci-plan-variants.json" -expect_failure "$fake_musl" "MUSL label without a MUSL target invocation" - -missing_cross_owner="$TMP_ROOT/missing-cross-owner" -make_fixture "$missing_cross_owner" -jq '.variants |= map(select(.dimensions.operation != "cross-targets"))' \ - "$missing_cross_owner/.config/ci-plan-variants.json" >"$missing_cross_owner/.config/ci-plan-variants.json.tmp" -mv "$missing_cross_owner/.config/ci-plan-variants.json.tmp" "$missing_cross_owner/.config/ci-plan-variants.json" -expect_failure "$missing_cross_owner" "missing cross-target owner" - -missing_graph_owner="$TMP_ROOT/missing-graph-owner" -make_fixture "$missing_graph_owner" -jq '.variants |= map(select(.dimensions.operation != "cargo-graph"))' \ - "$missing_graph_owner/.config/ci-plan-variants.json" >"$missing_graph_owner/.config/ci-plan-variants.json.tmp" -mv "$missing_graph_owner/.config/ci-plan-variants.json.tmp" "$missing_graph_owner/.config/ci-plan-variants.json" -expect_failure "$missing_graph_owner" "missing Cargo graph assurance owner" - -duplicate_release_graph="$TMP_ROOT/duplicate-release-graph" -make_fixture "$duplicate_release_graph" -printf '\n# duplicate release owner\ncargo rail unify --check --explain\n' \ - >>"$duplicate_release_graph/scripts/ci/release-preflight.sh" -expect_failure "$duplicate_release_graph" "duplicate release Cargo graph assurance owner" - -missing_release_graph_gate="$TMP_ROOT/missing-release-graph-gate" -make_fixture "$missing_release_graph_gate" -sed -i.bak '/CI Suite (release) \/ Cargo Graph Assurance \/ run/d' \ - "$missing_release_graph_gate/scripts/ci/release-evidence-check.sh" -rm -f "$missing_release_graph_gate/scripts/ci/release-evidence-check.sh.bak" -expect_failure "$missing_release_graph_gate" "missing release Cargo graph assurance gate" - -scheduled_release_mode="$TMP_ROOT/scheduled-release-mode" -make_fixture "$scheduled_release_mode" -yq eval '(.jobs.mode.steps[] | select(.id == "mode") | .run) |= sub("mode=assurance"; "mode=release")' -i \ - "$scheduled_release_mode/.github/workflows/weekly.yaml" -expect_failure "$scheduled_release_mode" "scheduled Qualification run resolves to release mode" - -release_by_default="$TMP_ROOT/release-by-default" -make_fixture "$release_by_default" -yq eval '.on.workflow_dispatch.inputs.mode.default = "release"' -i \ - "$release_by_default/.github/workflows/weekly.yaml" -expect_failure "$release_by_default" "manual Qualification run defaults to release evidence" - -generic_weekly_gate="$TMP_ROOT/generic-weekly-gate" -make_fixture "$generic_weekly_gate" -yq eval '.jobs.complete.name = "Complete (weekly)"' -i \ - "$generic_weekly_gate/.github/workflows/weekly.yaml" -expect_failure "$generic_weekly_gate" "Qualification terminal gate omits the resolved mode" - -fixed_weekly_retention="$TMP_ROOT/fixed-weekly-retention" -make_fixture "$fixed_weekly_retention" -yq eval '.jobs.ct.with.artifact_retention_days = 90' -i \ - "$fixed_weekly_retention/.github/workflows/weekly.yaml" -expect_failure "$fixed_weekly_retention" "assurance CT artifacts retain release lifetime" - -colliding_rsa_concurrency="$TMP_ROOT/colliding-rsa-concurrency" -make_fixture "$colliding_rsa_concurrency" -sed -i.bak 's/group: rsa-/group: /' "$colliding_rsa_concurrency/.github/workflows/rsa.yaml" -rm -f "$colliding_rsa_concurrency/.github/workflows/rsa.yaml.bak" -expect_failure "$colliding_rsa_concurrency" "reusable RSA workflow concurrency collision" - -missing_release_evidence_gate="$TMP_ROOT/missing-release-evidence-gate" -make_fixture "$missing_release_evidence_gate" -sed -i.bak '/release-evidence-check\.sh/d' "$missing_release_evidence_gate/.github/workflows/release.yaml" -rm -f "$missing_release_evidence_gate/.github/workflows/release.yaml.bak" -expect_failure "$missing_release_evidence_gate" "missing exact-commit release evidence gate" - -missing_repository_controls="$TMP_ROOT/missing-repository-controls" -make_fixture "$missing_repository_controls" -sed -i.bak '/repository-controls-evidence\.sh/d' "$missing_repository_controls/.github/workflows/release.yaml" -rm -f "$missing_repository_controls/.github/workflows/release.yaml.bak" -expect_failure "$missing_repository_controls" "release without repository controls evidence" - -missing_repository_policy="$TMP_ROOT/missing-repository-policy" -make_fixture "$missing_repository_policy" -rm "$missing_repository_policy/.github/rulesets/protect-main.json" -expect_failure "$missing_repository_policy" "missing repository ruleset policy" - -missing_release_tag_policy="$TMP_ROOT/missing-release-tag-policy" -make_fixture "$missing_release_tag_policy" -rm "$missing_release_tag_policy/.github/rulesets/protect-release-tags.json" -expect_failure "$missing_release_tag_policy" "missing release-tag ruleset policy" - -missing_release_immutability_policy="$TMP_ROOT/missing-release-immutability-policy" -make_fixture "$missing_release_immutability_policy" -rm "$missing_release_immutability_policy/.github/repository-settings/release-immutability.json" -expect_failure "$missing_release_immutability_policy" "missing release immutability policy" - -missing_source_archive="$TMP_ROOT/missing-source-archive" -make_fixture "$missing_source_archive" -sed -i.bak '/package-release-source\.sh/d' "$missing_source_archive/scripts/ci/release-preflight.sh" -rm -f "$missing_source_archive/scripts/ci/release-preflight.sh.bak" -expect_failure "$missing_source_archive" "release without deterministic source archive" - -missing_release_manifest="$TMP_ROOT/missing-release-manifest" -make_fixture "$missing_release_manifest" -sed -i.bak '/write-release-manifest\.sh/d' "$missing_release_manifest/.github/workflows/release.yaml" -rm -f "$missing_release_manifest/.github/workflows/release.yaml.bak" -expect_failure "$missing_release_manifest" "release without identity manifest" - -missing_riscv_workflow="$TMP_ROOT/missing-riscv-workflow" -make_fixture "$missing_riscv_workflow" -rm "$missing_riscv_workflow/.github/workflows/riscv.yaml" -expect_failure "$missing_riscv_workflow" "missing independent RISC-V workflow" - -missing_riscv_release_artifact="$TMP_ROOT/missing-riscv-release-artifact" -make_fixture "$missing_riscv_release_artifact" -sed -i.bak '/RISC-V CT Evidence (release) \/ Complete (CT)/d' \ - "$missing_riscv_release_artifact/scripts/ci/release-evidence-check.sh" -rm -f "$missing_riscv_release_artifact/scripts/ci/release-evidence-check.sh.bak" -expect_failure "$missing_riscv_release_artifact" "release without validated RISC-V evidence" - -compact_weekly_ct="$TMP_ROOT/compact-weekly-ct" -make_fixture "$compact_weekly_ct" -yq eval '.jobs.ct.with.upload_raw_artifacts = false' -i "$compact_weekly_ct/.github/workflows/weekly.yaml" -expect_failure "$compact_weekly_ct" "Qualification without raw release CT evidence" - -compact_riscv_ct="$TMP_ROOT/compact-riscv-ct" -make_fixture "$compact_riscv_ct" -yq eval '.jobs.riscv-ct.with.upload_raw_artifacts = false' -i \ - "$compact_riscv_ct/.github/workflows/weekly.yaml" -expect_failure "$compact_riscv_ct" "Qualification without raw RISC-V release CT evidence" - -missing_qualification_riscv="$TMP_ROOT/missing-qualification-riscv" -make_fixture "$missing_qualification_riscv" -yq eval 'del(.jobs.riscv-native)' -i "$missing_qualification_riscv/.github/workflows/weekly.yaml" -expect_failure "$missing_qualification_riscv" "Qualification without RISC-V native evidence" - -broken_dependabot_grouping="$TMP_ROOT/broken-dependabot-grouping" -make_fixture "$broken_dependabot_grouping" -cat >>"$broken_dependabot_grouping/.github/dependabot.yaml" <<'EOF' - groups: - broken: - group-by: dependency-name -EOF -expect_failure "$broken_dependabot_grouping" "broken cross-directory Dependabot grouping" - -missing_fuzz_packages="$TMP_ROOT/missing-fuzz-packages" -make_fixture "$missing_fuzz_packages" -sed -i.bak '/fuzz-packages/d' "$missing_fuzz_packages/.github/dependabot.yaml" -rm -f "$missing_fuzz_packages/.github/dependabot.yaml.bak" -expect_failure "$missing_fuzz_packages" "incomplete Dependabot Cargo manifest coverage" - -missing_tools="$TMP_ROOT/missing-tools" -make_fixture "$missing_tools" -sed -i.bak '/tools\/\*/d' "$missing_tools/.github/dependabot.yaml" -rm -f "$missing_tools/.github/dependabot.yaml.bak" -expect_failure "$missing_tools" "missing standalone tool dependency coverage" - -reintroduced_semver_owner="$TMP_ROOT/reintroduced-semver-owner" -make_fixture "$reintroduced_semver_owner" -printf '\n# pre-1.0 SemVer enforcement reintroduced\n run: cargo semver-checks --package rscrypto --all-features\n' >>"$reintroduced_semver_owner/.github/workflows/weekly.yaml" -expect_failure "$reintroduced_semver_owner" "reintroduced SemVer owner" - -shrunk_matrix="$TMP_ROOT/shrunk-matrix" -make_fixture "$shrunk_matrix" -sed -i.bak '/ "crc16"/d' "$shrunk_matrix/scripts/lib/feature-profiles.sh" -rm -f "$shrunk_matrix/scripts/lib/feature-profiles.sh.bak" -expect_failure "$shrunk_matrix" "removed required compile feature profile" - -uncompiled_execution="$TMP_ROOT/uncompiled-execution" -make_fixture "$uncompiled_execution" -sed -i.bak '/EXECUTABLE_FEATURE_SETS=(/,/^)/ s/ "full"/ "std,full,uncompiled-fixture"/' \ - "$uncompiled_execution/scripts/lib/feature-profiles.sh" -rm -f "$uncompiled_execution/scripts/lib/feature-profiles.sh.bak" -expect_failure "$uncompiled_execution" "executable feature profile without compile coverage" - -echo "CI ownership regression tests passed" diff --git a/scripts/ci/check-ci-ownership.sh b/scripts/ci/check-ci-ownership.sh deleted file mode 100755 index 8f8badab..00000000 --- a/scripts/ci/check-ci-ownership.sh +++ /dev/null @@ -1,918 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="" -if [[ ${1:-} == "--root" ]]; then - ROOT=${2:?missing path after --root} - shift 2 -fi -if [[ $# -ne 0 ]]; then - echo "usage: check-ci-ownership.sh [--root PATH]" >&2 - exit 2 -fi - -if [[ -z "$ROOT" ]]; then - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -fi - -WORKFLOWS="$ROOT/.github/workflows" -ACTIONS="$ROOT/.github/actions" -CI="$WORKFLOWS/ci.yaml" -SUITE="$WORKFLOWS/_ci-suite.yaml" -RUST_JOB="$WORKFLOWS/_rust-job.yaml" -WEEKLY="$WORKFLOWS/weekly.yaml" -CT="$WORKFLOWS/ct.yaml" -SCORECARD="$WORKFLOWS/scorecard.yaml" -RISCV="$WORKFLOWS/riscv.yaml" -RELEASE="$WORKFLOWS/release.yaml" -RSA="$WORKFLOWS/rsa.yaml" -SETUP_ACTION="$ACTIONS/setup/action.yaml" -TOOLCHAIN_ACTION="$ACTIONS/setup-toolchain/action.yaml" -MANIFEST="$ROOT/.config/target-matrix.json" -RAIL_CONFIG="$ROOT/.config/rail.toml" -RAIL_VARIANTS="$ROOT/.config/ci-plan-variants.json" -TOOL_ARCHIVES="$ROOT/.config/ci-tool-archives.tsv" -CARGO_CONFIG="$ROOT/.cargo/config.toml" -CROSS_SCRIPT="$ROOT/scripts/ci/cross-targets.sh" -NOSTD_WASM="$ROOT/scripts/ci/nostd-wasm-suite.sh" -INSTALL_TOOLS="$ROOT/scripts/ci/install-tools.sh" -MATERIALIZE_RAIL_PLAN="$ROOT/scripts/ci/materialize-rail-plan.sh" -INSTALL_CODECOV="$ROOT/scripts/ci/install-codecov.sh" -SETUP_TOOLCHAIN="$ROOT/scripts/ci/setup-toolchain.sh" -TOOL_INTEGRITY="$ROOT/scripts/lib/ci-tool-integrity.sh" -FEATURE_PROFILES="$ROOT/scripts/lib/feature-profiles.sh" -COMPILE_MATRIX="$ROOT/scripts/check/check-feature-matrix.sh" -HOST_CHECK="$ROOT/scripts/check/check.sh" -EXECUTABLE_MATRIX="$ROOT/scripts/test/test-feature-matrix.sh" -CHECK_ALL="$ROOT/scripts/check/check-all.sh" -CI_CHECK="$ROOT/scripts/ci/ci-check.sh" -RUN_RUST_JOB="$ROOT/scripts/ci/run-rust-job.sh" -RELEASE_PREFLIGHT="$ROOT/scripts/ci/release-preflight.sh" -RELEASE_EVIDENCE="$ROOT/scripts/ci/release-evidence-check.sh" -RELEASE_CT_RECOVERY="$ROOT/scripts/ci/release-ct-recovery-check.sh" -RELEASE_CT_RECOVERY_TEST="$ROOT/scripts/ci/release-ct-recovery-check-test.sh" -RELEASE_SOURCE="$ROOT/scripts/ci/package-release-source.sh" -RELEASE_MANIFEST="$ROOT/scripts/ci/write-release-manifest.sh" -RELEASE_IDENTITY_TEST="$ROOT/scripts/ci/release-identity-test.sh" -PUBLISH_RELEASE="$ROOT/scripts/ci/publish-immutable-release.sh" -PUBLISH_RELEASE_TEST="$ROOT/scripts/ci/publish-immutable-release-test.sh" -REPOSITORY_CONTROLS="$ROOT/scripts/ci/repository-controls-evidence.sh" -REPOSITORY_CONTROLS_TEST="$ROOT/scripts/ci/repository-controls-evidence-test.sh" -REPOSITORY_POLICY="$ROOT/.github/rulesets/protect-main.json" -RELEASE_TAG_POLICY="$ROOT/.github/rulesets/protect-release-tags.json" -RELEASE_IMMUTABILITY_POLICY="$ROOT/.github/repository-settings/release-immutability.json" -DEPENDABOT="$ROOT/.github/dependabot.yaml" -RUNS_ON="$ROOT/.github/runs-on.yml" - -fail() { - echo "CI ownership error: $*" >&2 - exit 1 -} - -require_file() { - [[ -f "$1" ]] || fail "missing $1" -} - -count_matches() { - local pattern=$1 - shift - local count - count=$({ grep -ERho "$pattern" "$@" 2>/dev/null || true; } | wc -l | tr -d ' ') - echo "$count" -} - -feature_sets() { - local file=$1 - local array=$2 - awk -v array="$array" ' - $0 == array "=(" { in_array = 1; next } - in_array && /^\)$/ { exit } - in_array && /^[[:space:]]+"/ { - value = $0 - sub(/^[[:space:]]+"/, "", value) - sub(/"$/, "", value) - print value - } - ' "$file" -} - -count_feature_sets() { - feature_sets "$1" "$2" | awk 'END { print NR + 0 }' -} - -require_unique_feature_sets() { - local file=$1 - local array=$2 - local duplicate - duplicate=$(feature_sets "$file" "$array" | sort | uniq -d | head -1) - [[ -z "$duplicate" ]] || fail "duplicate feature profile in $array: $duplicate" -} - -require_feature_subset() { - local child_file=$1 - local child_array=$2 - local parent_file=$3 - local parent_array=$4 - local missing - missing=$(comm -23 \ - <(feature_sets "$child_file" "$child_array" | sort) \ - <(feature_sets "$parent_file" "$parent_array" | sort) | head -1) - [[ -z "$missing" ]] || fail "executable feature profile lacks compile coverage: $missing" -} - -require_file "$CI" -require_file "$SUITE" -require_file "$RUST_JOB" -require_file "$WEEKLY" -require_file "$CT" -require_file "$SCORECARD" -require_file "$RISCV" -require_file "$RELEASE" -require_file "$RSA" -require_file "$SETUP_ACTION" -require_file "$TOOLCHAIN_ACTION" -require_file "$MANIFEST" -require_file "$RAIL_CONFIG" -require_file "$RAIL_VARIANTS" -require_file "$TOOL_ARCHIVES" -require_file "$CARGO_CONFIG" -require_file "$CROSS_SCRIPT" -require_file "$NOSTD_WASM" -require_file "$INSTALL_TOOLS" -require_file "$MATERIALIZE_RAIL_PLAN" -require_file "$INSTALL_CODECOV" -require_file "$SETUP_TOOLCHAIN" -require_file "$TOOL_INTEGRITY" -require_file "$FEATURE_PROFILES" -require_file "$COMPILE_MATRIX" -require_file "$HOST_CHECK" -require_file "$EXECUTABLE_MATRIX" -require_file "$CHECK_ALL" -require_file "$CI_CHECK" -require_file "$RUN_RUST_JOB" -require_file "$RELEASE_PREFLIGHT" -require_file "$RELEASE_EVIDENCE" -require_file "$RELEASE_CT_RECOVERY" -require_file "$RELEASE_CT_RECOVERY_TEST" -require_file "$RELEASE_SOURCE" -require_file "$RELEASE_MANIFEST" -require_file "$RELEASE_IDENTITY_TEST" -require_file "$PUBLISH_RELEASE" -require_file "$PUBLISH_RELEASE_TEST" -require_file "$REPOSITORY_CONTROLS" -require_file "$REPOSITORY_CONTROLS_TEST" -require_file "$REPOSITORY_POLICY" -require_file "$RELEASE_TAG_POLICY" -require_file "$RELEASE_IMMUTABILITY_POLICY" -require_file "$DEPENDABOT" -require_file "$RUNS_ON" - -[[ $(yq eval '.version' "$DEPENDABOT") == "2" ]] || fail "Dependabot config must use version 2" -[[ $(yq eval '[.updates[] | select(."package-ecosystem" == "cargo")] | length' "$DEPENDABOT") == "1" ]] \ - || fail "Dependabot must have exactly one non-overlapping Cargo update entry" -[[ $(yq eval '.updates[] | select(."package-ecosystem" == "cargo") | .directories | sort | join(",")' "$DEPENDABOT") \ - == "/,/fuzz,/fuzz-packages/*,/tools/*" ]] \ - || fail "Dependabot Cargo coverage must include root, fuzz, scoped fuzz packages, and standalone tools" -[[ $(yq eval '.updates[] | select(."package-ecosystem" == "cargo") | ."open-pull-requests-limit"' "$DEPENDABOT") == "0" ]] \ - || fail "routine Cargo version PRs must stay disabled in favor of coordinated updates" -if grep -En 'group-by:[[:space:]]*dependency-name' "$DEPENDABOT" >/dev/null; then - fail "Dependabot must not use the upstream-broken cross-directory dependency-name grouping" -fi -[[ $(yq eval '[.updates[] | select(."package-ecosystem" == "github-actions")] | length' "$DEPENDABOT") == "1" ]] \ - || fail "Dependabot must have exactly one GitHub Actions update entry" -[[ $(yq eval '.updates[] | select(."package-ecosystem" == "github-actions") | .directory' "$DEPENDABOT") == "/" ]] \ - || fail "Dependabot must scan all repository workflows" -[[ $(yq eval '.updates[] | select(."package-ecosystem" == "github-actions") | ."open-pull-requests-limit"' "$DEPENDABOT") == "1" ]] \ - || fail "Dependabot must limit GitHub Actions updates to one open pull request" - -[[ $(yq eval '.on.push.branches | join(",")' "$CI") == "main" ]] \ - || fail "CI must seed affected compiler results only after updates reach main" -[[ $(yq eval '[.on.pull_request.types[]] | sort | join(",")' "$CI") \ - == "opened,ready_for_review,reopened,synchronize" ]] \ - || fail "CI must run when a ready pull request is opened, updated, reopened, or leaves draft" -for job in rail-plan suite complete; do - [[ $(yq eval ".jobs.\"$job\".if" "$CI") == *"pull_request.draft"* ]] \ - || fail "$job must defer draft pull requests" -done - -suite_condition=$(yq eval '.jobs.suite."if"' "$CI") -[[ "$suite_condition" == *"!cancelled()"* && "$suite_condition" == *"needs.rail-plan.result == 'success'"* \ - && "$suite_condition" == *"needs.rail-plan.outputs.has-suite == 'true'"* \ - && "$suite_condition" == *"github.event_name != 'push'"* ]] \ - || fail "the CI suite must consume only a successful, non-empty Cargo Rail plan" -cache_seed_condition=$(yq eval '.jobs."cache-seed"."if"' "$CI") -[[ "$cache_seed_condition" == *"github.event_name == 'push'"* \ - && "$cache_seed_condition" == *"github.ref == 'refs/heads/main'"* \ - && "$cache_seed_condition" == *"needs.rail-plan.result == 'success'"* \ - && "$cache_seed_condition" == *"needs.rail-plan.outputs.has-suite == 'true'"* \ - && $(yq eval '.jobs."cache-seed".with.cache_mode' "$CI") == "read-write" ]] \ - || fail "only the affected main-branch seeder may request cache write authority" -# shellcheck disable=SC2016 # GitHub expressions are intentional literal workflow contracts. -grep -Fq 'matrix: ${{ steps.matrix.outputs.matrix }}' "$CI" \ - || fail "CI must export the exact Cargo Rail variant matrix" -# shellcheck disable=SC2016 # GitHub expressions are intentional literal workflow contracts. -grep -Fq 'identity: ${{ steps.rail.outputs.plan-identity }}' "$CI" \ - || fail "CI must export the exact Cargo Rail plan identity" -# shellcheck disable=SC2016 # GitHub expression is an intentional literal workflow contract. -grep -Fq 'matrix: ${{ needs.rail-plan.outputs.matrix }}' "$CI" \ - || fail "the reusable suite must consume the Cargo Rail matrix without replanning" -[[ $(yq -oy -p toml eval '.plan.work."ci-policy".scope' "$RAIL_CONFIG") == "repository" ]] \ - || fail "shared CI infrastructure must have one repository-scoped Cargo Rail policy decision" -grep -Fq 'is-required "$PLAN_FILE" ci-policy' "$MATERIALIZE_RAIL_PLAN" \ - || fail "CI matrix lowering must widen when shared Cargo Rail policy work is required" -if grep -ERn 'always\(\)' "$WORKFLOWS" >/dev/null; then - fail "workflow cancellation must not start or prolong cleanup and aggregate jobs" -fi - -if grep -ERn '^[[:space:]]+(pre_script|run_script):' "$WORKFLOWS" >/dev/null; then - fail "reusable workflows must not accept executable shell fragments" -fi -if grep -ERin '(^|[^[:alnum:]_])(macos|darwin|apple)([^[:alnum:]_]|$)' "$WORKFLOWS" >/dev/null; then - fail "Apple platform testing must remain local and must not appear in CI workflows" -fi -if awk ' - /^\[target\./ { - apple_target = tolower($0) ~ /(apple-darwin|target_os[[:space:]]*=[[:space:]]*"macos")/ - next - } - /^\[/ { apple_target = 0 } - apple_target && /^[[:space:]]*rustflags[[:space:]]*=/ { found = 1 } - END { exit !found } -' "$CARGO_CONFIG"; then - fail "Apple targets must not receive implicit rustflags from .cargo/config.toml" -fi -while IFS= read -r operation; do - [[ -n "$operation" ]] || continue - grep -Eq "^[[:space:]]+$operation\\)" "$RUN_RUST_JOB" \ - || fail "reusable Rust job caller selects unsupported operation: $operation" -done < <( - awk '/^[[:space:]]+operation:[[:space:]]+[-[:alnum:]]+[[:space:]]*$/ { print $2 }' \ - "$WORKFLOWS"/*.yaml | sort -u -) -# shellcheck disable=SC2016 # GitHub expression is an intentional literal workflow contract. -[[ $(yq eval '.jobs.selected.with.operation' "$SUITE") == '${{ matrix.work.operation }}' ]] \ - || fail "the reusable suite must pass only Cargo Rail catalog operations" -while IFS= read -r operation; do - [[ -n "$operation" ]] || continue - grep -Eq "^[[:space:]]+$operation\\)" "$RUN_RUST_JOB" \ - || fail "Cargo Rail variant catalog selects unsupported operation: $operation" -done < <(jq -r '.variants[].dimensions.operation' "$RAIL_VARIANTS" | sort -u) -[[ $(yq eval '.on.workflow_call.inputs.operation.required' "$RUST_JOB") == "true" ]] \ - || fail "the reusable Rust job operation must be required" -[[ $(yq eval '.on.workflow_call.inputs.operation.type' "$RUST_JOB") == "string" ]] \ - || fail "the reusable Rust job operation must be typed as a string" -[[ $(yq eval '.on.workflow_call.inputs.checkout_ref.type' "$RUST_JOB") == "string" ]] \ - || fail "the reusable Rust job checkout ref must be typed as a string" -[[ $(yq eval '.on.workflow_call.inputs.rustflags.type' "$RUST_JOB") == "string" ]] \ - || fail "the reusable Rust job rustflags input must be typed as a string" -# shellcheck disable=SC2016 # GitHub expression is an intentional literal workflow contract. -[[ $(yq eval '.jobs.run.steps[] | select(.name == "Checkout") | .with.ref' "$RUST_JOB") \ - == '${{ inputs.plan_head_commit || inputs.checkout_ref || github.sha }}' ]] \ - || fail "the reusable Rust job must prefer the plan-bound source ref before execution" -# shellcheck disable=SC2016 # GitHub expression is an intentional literal workflow contract. -[[ $(yq eval '.jobs.run.steps[] | select(.name == "Download exact work plan") | .with.path' "$RUST_JOB") \ - == '${{ runner.temp }}/cargo-rail-plan' ]] \ - || fail "saved plan artifacts must remain outside the captured checkout" -# shellcheck disable=SC2016 # GitHub expressions are intentional literal workflow contracts. -rail_plan_file=$(yq eval '.jobs.run.steps[] | select(.name == "Run") | .env.RAIL_PLAN_FILE' "$RUST_JOB") -rail_plan_reader=$(yq eval '.jobs.run.steps[] | select(.name == "Run") | .env.RAIL_PLAN_READER' "$RUST_JOB") -[[ "$rail_plan_file" == *"inputs.plan_artifact"* && "$rail_plan_file" == *"runner.temp"* \ - && "$rail_plan_file" == *"/cargo-rail-plan/plan.json"* \ - && "$rail_plan_reader" == *"inputs.plan_artifact"* && "$rail_plan_reader" == *"runner.temp"* \ - && "$rail_plan_reader" == *"/cargo-rail-plan/read.py"* ]] \ - || fail "saved plan execution must consume the out-of-worktree artifact" -# shellcheck disable=SC2016 # GitHub expression is an intentional literal workflow contract. -[[ $(yq eval '.jobs.run.steps[] | select(.name == "Run") | .env.CARGO_TARGET_S390X_UNKNOWN_LINUX_GNU_RUSTFLAGS' "$RUST_JOB") \ - == '${{ inputs.target == '\''s390x-unknown-linux-gnu'\'' && '\''-C target-feature=+vector'\'' || '\'''\'' }}' ]] \ - || fail "s390x CT jobs must share one explicit vector target environment" -# shellcheck disable=SC2016 # GitHub expression is an intentional literal workflow contract. -[[ $(yq eval '.jobs.run.steps[] | select(.name == "Run") | .env.RSCRYPTO_CI_RUSTFLAGS' "$RUST_JOB") \ - == '${{ inputs.rustflags }}' ]] \ - || fail "the reusable Rust job must pass reviewed rustflags as inert environment data" -rust_job_run=$(yq eval '.jobs.run.steps[] | select(.name == "Run") | .run' "$RUST_JOB") -[[ -n "$rust_job_run" && "$rust_job_run" != "null" ]] \ - || fail "the reusable Rust job must expose one fixed command step" -grep -Fq 'if [[ -n "$RSCRYPTO_CI_RUSTFLAGS" ]]' <<<"$rust_job_run" \ - || fail "the reusable Rust job must leave RUSTFLAGS unset without a reviewed override" -grep -Fq 'export RUSTFLAGS="$RSCRYPTO_CI_RUSTFLAGS"' <<<"$rust_job_run" \ - || fail "the reusable Rust job must export only the reviewed rustflags value" -grep -Fq 'exec scripts/ci/run-rust-job.sh' <<<"$rust_job_run" \ - || fail "the reusable Rust job must invoke the repository-owned dispatcher" -# shellcheck disable=SC2016 # GitHub expression is an intentional literal workflow contract. -grep -Fq 'RSCRYPTO_CI_OPERATION: ${{ inputs.operation }}' "$RUST_JOB" \ - || fail "the reusable Rust job must pass its operation as environment data" -# shellcheck disable=SC2016 # GitHub expression is an intentional literal workflow contract. -[[ $(yq eval '.jobs.run.steps[] | select(.name == "Run") | .env.RAIL_PLAN_CHECKOUT_VERIFIED' "$RUST_JOB") \ - == '${{ inputs.plan_artifact != '\'''\'' && '\''true'\'' || '\''false'\'' }}' ]] \ - || fail "only jobs with a separately verified saved plan may skip duplicate checkout verification" -if yq eval '.. | select(tag == "!!map" and has("run") and (.run | tag == "!!str")) | .run' \ - "$WORKFLOWS"/*.yaml | grep -Eq '\$\{\{[[:space:]]*inputs\.'; then - fail "workflow inputs must not be interpolated into shell programs" -fi -if grep -En '(^|[[:space:]])eval[[:space:]]|(^|[[:space:]])(bash|sh)[[:space:]]+-c|<<<' \ - "$RUN_RUST_JOB" >/dev/null; then - fail "the Rust job dispatcher must not invoke a dynamic shell interpreter" -fi - -bash -eu -o pipefail -c 'source "$1"; ci_tool_validate_manifest' _ "$TOOL_INTEGRITY" \ - || fail "direct CI tool archive manifest is invalid" - -if grep -ERn 'uses:[[:space:]]+dtolnay/rust-toolchain@' \ - "$WORKFLOWS" "$ACTIONS" >/dev/null; then - fail "CI must not delegate installation to an action with an unauthenticated executable fallback" -fi -if grep -En 'cargo[[:space:]]+binstall|cargo-binstall|releases/latest|/latest/|curl[^|]*\|[[:space:]]*(bash|sh)' \ - "$INSTALL_TOOLS" "$SETUP_TOOLCHAIN" "$NOSTD_WASM" "$CROSS_SCRIPT" "$INSTALL_CODECOV" >/dev/null; then - fail "CI tool installers must reject Cargo-binstall, mutable URLs, and piped network installers" -fi - -download_files=$( - { - grep -ERl --include='*.sh' --include='*.yaml' --include='*.yml' \ - '(^|[[:space:]])(curl|wget|aria2c)([[:space:]]|$)|Invoke-(WebRequest|RestMethod)|Start-BitsTransfer|gh[[:space:]]+release[[:space:]]+download' \ - "$ROOT/scripts" "$WORKFLOWS" "$ACTIONS" 2>/dev/null || true - } | while IFS= read -r file; do - case "$file" in - "$ROOT/scripts/ci/check-ci-ownership-test.sh" \ - | "$ROOT/scripts/ci/tool-integrity-test.sh" \ - | "$ROOT/scripts/ci/check-ci-ownership.sh") continue ;; - esac - printf '%s\n' "${file#"$ROOT"/}" - done | sort -) -expected_download_files=$(printf '%s\n' \ - '.github/workflows/release.yaml' \ - 'scripts/ci/check-action-pins.sh' \ - 'scripts/lib/ci-tool-integrity.sh') -[[ "$download_files" == "$expected_download_files" ]] \ - || fail "network downloads exist outside the tool verifier or reviewed non-tool paths" - -installer_files=$( - { - grep -ERl --include='*.sh' --include='*.yaml' --include='*.yml' \ - 'cargo[[:space:]]+(binstall|install)|go[[:space:]]+install|apt(-get)?[[:space:]]+install|opam[[:space:]]+(init|install|reinstall|switch[[:space:]]+create)|rustup[[:space:]]+(toolchain[[:space:]]+install|component[[:space:]]+add|target[[:space:]]+add)|install_args=\(toolchain[[:space:]]+install|pipx?[[:space:]]+install|uv[[:space:]]+tool[[:space:]]+install|npm[[:space:]]+(install|ci)|pnpm[[:space:]]+install|yarn[[:space:]]+install|brew[[:space:]]+install' \ - "$ROOT/scripts/ci" "$ROOT/scripts/lib" "$WORKFLOWS" "$ACTIONS" 2>/dev/null || true - } | while IFS= read -r file; do - case "$file" in - "$ROOT/scripts/ci/check-ci-ownership-test.sh" \ - | "$ROOT/scripts/ci/tool-integrity-test.sh" \ - | "$ROOT/scripts/ci/check-ci-ownership.sh") continue ;; - esac - printf '%s\n' "${file#"$ROOT"/}" - done | sort -) -expected_installer_files=$(printf '%s\n' \ - 'scripts/ci/install-tools.sh' \ - 'scripts/ci/nostd-wasm-suite.sh' \ - 'scripts/ci/setup-toolchain.sh' \ - 'scripts/lib/common.sh') -[[ "$installer_files" == "$expected_installer_files" ]] \ - || fail "package-manager installs exist outside the reviewed integrity boundaries" - -rail_action=$(yq eval '.jobs."rail-plan".steps[] | select(.id == "rail") | .uses' "$CI") -[[ "$rail_action" =~ ^loadingalias/cargo-rail-action@[0-9a-f]{40}$ ]] \ - || fail "the PR planner must use commit-pinned cargo-rail-action" -rail_version=$(sed -n 's/^CARGO_RAIL_VERSION=//p' "$INSTALL_TOOLS") -[[ "$rail_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \ - || fail "the Cargo Rail installer version must be exact" -[[ $(yq eval '.jobs."rail-plan".steps[] | select(.id == "rail") | .with.version' "$CI") \ - == "$rail_version" ]] \ - || fail "cargo-rail-action must use the authenticated Cargo Rail version" -[[ $(yq eval '.jobs."rail-plan".steps[] | select(.id == "rail") | .with.components' "$CI") \ - == "surface" ]] \ - || fail "cargo-rail-action must install and prepare the authenticated Surface component" -# shellcheck disable=SC2016 # GitHub expressions are intentional literal workflow contracts. -rail_since=$(yq eval '.jobs."rail-plan".steps[] | select(.id == "rail") | .with.since' "$CI") -[[ "$rail_since" == *'github.event.pull_request.base.sha'* \ - && "$rail_since" == *'github.event.before'* && "$rail_since" == *'||'* ]] \ - || fail "cargo-rail-action must plan pull requests and main pushes from immutable comparison commits" -# shellcheck disable=SC2016 # GitHub expression is an intentional literal workflow contract. -rail_all=$(yq eval '.jobs."rail-plan".steps[] | select(.id == "rail") | .with.all' "$CI") -[[ "$rail_all" == *"github.event_name == 'workflow_dispatch'"* ]] \ - || fail "manual CI must request Cargo Rail's typed all-work override" - -cache_action=$(yq eval '.runs.steps[] | select(.name == "Setup Cargo Rail Cache") | .uses' "$SETUP_ACTION") -expected_cache_action="${rail_action%@*}/cache@${rail_action#*@}" -[[ "$cache_action" == "$expected_cache_action" ]] \ - || fail "compiler reuse must use the planner action's immutable v8 cache implementation" -cache_auth_run=$(yq eval '.runs.steps[] | select(.name == "Authenticate Cargo Rail Cache") | .run' "$SETUP_ACTION") -grep -Fq '[[ "$CACHE_URL" == r2://* ]]' <<<"$cache_auth_run" \ - || fail "the shared cache must reject non-canonical providers before authentication" -grep -Fq 'AWS_ACCESS_KEY_ID=%s' <<<"$cache_auth_run" \ - || fail "the shared cache must export the caller-selected R2 access key for Cargo Rail" -grep -Fq 'AWS_SECRET_ACCESS_KEY=%s' <<<"$cache_auth_run" \ - || fail "the shared cache must export the caller-selected R2 secret key for Cargo Rail" -grep -Fq 'AWS_SESSION_TOKEN=' <<<"$cache_auth_run" \ - || fail "the shared cache must clear an ambient AWS session token before installing a long-lived R2 key" -grep -Fq 'AWS_PROFILE=' <<<"$cache_auth_run" \ - || fail "the shared cache must clear an ambient AWS profile before installing its selected R2 identity" -grep -Fq 'AWS_SHARED_CREDENTIALS_FILE=' <<<"$cache_auth_run" \ - || fail "the shared cache must clear an ambient AWS credentials file before installing its selected R2 identity" -grep -Fq '::add-mask::%s' <<<"$cache_auth_run" \ - || fail "the shared cache must mask both caller-selected R2 credential fields" -if grep -ERn 'aws-actions/configure-aws-credentials|CARGO_RAIL_CACHE_(READ_ROLE_ARN|WRITE_ROLE_ARN|REGION)|cache-(read-role-arn|write-role-arn|region)' \ - "$WORKFLOWS" "$ACTIONS" >/dev/null; then - fail "the canonical R2 integration must not retain AWS role or region compatibility plumbing" -fi -[[ $(yq eval '.runs.steps[] | select(.name == "Setup Cargo Rail Cache") | .with.version' "$SETUP_ACTION") \ - == "$rail_version" ]] \ - || fail "the Cargo Rail cache action must use the authenticated Cargo Rail version" -[[ $(yq eval '.runs.steps[] | select(.name == "Setup Cargo Rail Cache") | .with.url' "$SETUP_ACTION") \ - == '${{ inputs.cache-url }}' ]] \ - || fail "the Cargo Rail cache action must consume only machine-owned URL input" -[[ $(yq eval '.runs.steps[] | select(.name == "Setup Cargo Rail Cache") | .with.mode' "$SETUP_ACTION") \ - == '${{ inputs.cache-mode }}' ]] \ - || fail "the Cargo Rail cache action must consume the trust-selected remote mode" -[[ $(yq eval '.runs.steps[] | select(.name == "Setup Cargo Rail Cache") | .with."max-size"' "$SETUP_ACTION") \ - == '${{ inputs.cache-max-size }}' ]] \ - || fail "the Cargo Rail cache action must retain an explicit local size bound" -[[ $(yq eval '.runs.steps[] | select(.name == "Setup Cargo Rail Cache") | .with."root-portability"' "$SETUP_ACTION") \ - == '${{ inputs.cache-root-portability }}' ]] \ - || fail "the Cargo Rail cache action must own the selected root-portability transaction" -[[ $(yq eval '.runs.steps[] | select(.name == "Setup Cargo Rail Cache") | .with."strict-probe"' "$SETUP_ACTION") \ - == "true" ]] \ - || fail "CI cache setup must authenticate the provider and native-v6 protocol marker before compilation" -[[ $(yq eval '.runs.steps[] | select(.name == "Setup Cargo Rail Cache") | .if' "$SETUP_ACTION") \ - == "steps.cache-capability.outputs.enabled == 'true'" \ - && $(yq eval '.runs.steps[] | select(.name == "Authenticate Cargo Rail Cache") | .if' "$SETUP_ACTION") \ - == "steps.cache-capability.outputs.enabled == 'true'" ]] \ - || fail "Cargo Rail cache authentication and setup must consume one platform capability decision" -cache_capability_run=$(yq eval '.runs.steps[] | select(.name == "Select Cargo Rail Cache Capability") | .run' "$SETUP_ACTION") -grep -Fq '[[ -z "$CACHE_URL" ]]' <<<"$cache_capability_run" \ - && grep -Fq 's390x | ppc64le)' <<<"$cache_capability_run" \ - || fail "cache setup must skip absent configuration and hosts without verified native cache archives" -auth_step_index=$(yq eval '.runs.steps | to_entries | .[] | select(.value.name == "Authenticate Cargo Rail Cache") | .key' "$SETUP_ACTION") -cache_step_index=$(yq eval '.runs.steps | to_entries | .[] | select(.value.name == "Setup Cargo Rail Cache") | .key' "$SETUP_ACTION") -tools_step_index=$(yq eval '.runs.steps | to_entries | .[] | select(.value.name == "Install Cargo Tools") | .key' "$SETUP_ACTION") -[[ "$auth_step_index" =~ ^[0-9]+$ && "$cache_step_index" =~ ^[0-9]+$ \ - && "$tools_step_index" =~ ^[0-9]+$ && "$auth_step_index" -lt "$cache_step_index" \ - && "$cache_step_index" -lt "$tools_step_index" ]] \ - || fail "Cargo Rail cache setup must precede repository command execution" -[[ $(yq eval '.inputs."cache-root-portability".default' "$SETUP_ACTION") == "remap" ]] \ - || fail "ephemeral CI checkouts must select qualified cross-root Cargo Rail reuse" -# shellcheck disable=SC2016 # GitHub expression is an intentional literal workflow contract. -[[ $(yq eval '.runs.steps[] | select(.name == "Install Cargo Tools") | .env.RSCRYPTO_AUTHENTICATED_CARGO_RAIL' "$SETUP_ACTION") \ - == '${{ steps.cargo-rail-cache.outcome == '\''success'\'' && '\''true'\'' || '\''false'\'' }}' ]] \ - || fail "Cargo Rail reuse must be authorized only by the exact cache-action install output" -grep -Fq 'RSCRYPTO_AUTHENTICATED_CARGO_RAIL' "$INSTALL_TOOLS" \ - || fail "the tool installer must reuse authenticated Cargo Rail instead of reinstalling it" - -# shellcheck disable=SC2016 # GitHub expressions are intentional literal workflow contracts. -expected_cache_url="\${{ secrets.cache_access_key_id != '' && secrets.cache_secret_access_key != '' && vars.CARGO_RAIL_CACHE_URL || '' }}" -[[ $(yq eval '.jobs.run.steps[] | select(.name == "Setup") | .with."cache-url"' "$RUST_JOB") \ - == "$expected_cache_url" ]] \ - || fail "reusable Rust jobs must disable L2 when a caller-selected R2 credential is unavailable" -# shellcheck disable=SC2016 # GitHub expressions are intentional literal workflow contracts. -[[ $(yq eval '.jobs.run.steps[] | select(.name == "Setup") | .with."cache-mode"' "$RUST_JOB") \ - == '${{ inputs.cache_mode }}' \ - && $(yq eval '.on.workflow_call.inputs.cache_mode.default' "$RUST_JOB") == "read" \ - && $(yq eval '.on.workflow_call.inputs.cache_mode.default' "$SUITE") == "read" ]] \ - || fail "ordinary reusable compiler jobs must default to read-only cache authority" -# shellcheck disable=SC2016 # GitHub expressions are intentional literal workflow contracts. -[[ $(yq eval '.jobs.run.steps[] | select(.name == "Setup") | .with."cache-access-key-id"' "$RUST_JOB") \ - == '${{ secrets.cache_access_key_id }}' \ - && $(yq eval '.jobs.run.steps[] | select(.name == "Setup") | .with."cache-secret-access-key"' "$RUST_JOB") \ - == '${{ secrets.cache_secret_access_key }}' ]] \ - || fail "reusable compiler jobs must consume only the caller-selected provider identity" -[[ $(yq eval '.on.workflow_call.secrets.cache_access_key_id.required' "$RUST_JOB") == "false" \ - && $(yq eval '.on.workflow_call.secrets.cache_secret_access_key.required' "$RUST_JOB") == "false" ]] \ - || fail "fork and Dependabot jobs must be able to execute without repository R2 secrets" -[[ $(yq eval '.jobs.selected.secrets.cache_access_key_id' "$SUITE") == '${{ secrets.cache_access_key_id }}' \ - && $(yq eval '.jobs.selected.secrets.cache_secret_access_key' "$SUITE") == '${{ secrets.cache_secret_access_key }}' ]] \ - || fail "the selected CI suite must forward only its caller-selected cache identity" -[[ $(yq eval '.jobs.suite.secrets.cache_access_key_id' "$CI") == '${{ secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }}' \ - && $(yq eval '.jobs.suite.secrets.cache_secret_access_key' "$CI") == '${{ secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }}' ]] \ - || fail "ordinary pull-request CI must receive only the bucket-scoped R2 reader" -[[ $(yq eval '.jobs."cache-seed".secrets.cache_access_key_id' "$CI") == '${{ secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID }}' \ - && $(yq eval '.jobs."cache-seed".secrets.cache_secret_access_key' "$CI") == '${{ secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY }}' ]] \ - || fail "the trusted main seeder must receive the distinct bucket-scoped R2 writer" -[[ $(count_matches 'CARGO_RAIL_R2_WRITE_(ACCESS_KEY_ID|SECRET_ACCESS_KEY)' "$WORKFLOWS") -eq 2 ]] \ - || fail "R2 writer credentials must exist only at the trusted main seeder boundary" -# shellcheck disable=SC2016 # GitHub expressions are intentional literal workflow contracts. -[[ $(yq eval '.jobs.coverage.steps[] | select(.name == "Setup") | .with."cache-url"' "$WEEKLY") \ - == *'vars.CARGO_RAIL_CACHE_URL'* \ - && $(yq eval '.jobs.coverage.steps[] | select(.name == "Setup") | .with."cache-mode"' "$WEEKLY") \ - == "read" ]] \ - || fail "Qualification coverage must consume the machine-owned Cargo Rail cache read-only" -# shellcheck disable=SC2016 # GitHub expressions are intentional literal workflow contracts. -[[ $(yq eval '.jobs.preflight.steps[] | select(.name == "Setup") | .with."cache-url"' "$RELEASE") \ - == *'vars.CARGO_RAIL_CACHE_URL'* \ - && $(yq eval '.jobs.preflight.steps[] | select(.name == "Setup") | .with."cache-mode"' "$RELEASE") \ - == "read" ]] \ - || fail "release preflight must consume the machine-owned Cargo Rail cache read-only" -# shellcheck disable=SC2016 # GitHub expressions are intentional literal workflow contracts. -[[ $(yq eval '.jobs.publish.steps[] | select(.name == "Setup") | .with."cache-url"' "$RELEASE") \ - == *'vars.CARGO_RAIL_CACHE_URL'* \ - && $(yq eval '.jobs.publish.steps[] | select(.name == "Setup") | .with."cache-mode"' "$RELEASE") \ - == "read" ]] \ - || fail "release publication must consume the machine-owned Cargo Rail cache read-only" -[[ $(yq eval '[.jobs.run.steps[] | select(.name == "Capture Cargo Rail Cache Status")] | length' "$RUST_JOB") == "1" \ - && $(yq eval '[.jobs.run.steps[] | select(.name == "Upload Cargo Rail Cache Status")] | length' "$RUST_JOB") == "1" ]] \ - || fail "representative compiler jobs must preserve one fail-closed Cargo Rail telemetry artifact" -[[ $(yq eval '[.jobs.coverage.steps[] | select(.name == "Capture Cargo Rail Cache Status")] | length' "$WEEKLY") == "1" \ - && $(yq eval '[.jobs.coverage.steps[] | select(.name == "Upload Cargo Rail Cache Status")] | length' "$WEEKLY") == "1" ]] \ - || fail "Qualification coverage must preserve one fail-closed Cargo Rail telemetry artifact" -if grep -ERn 'uses:[[:space:]]+(Swatinem/rust-cache|runs-on/action|actions/cache)@' \ - "$WORKFLOWS" "$ACTIONS" >/dev/null; then - fail "Cargo Rail must be the only Rust compiler cache owner" -fi -[[ $(yq eval '[.runners[].extras[]? | select(. == "s3-cache")] | length' "$RUNS_ON") == "0" ]] \ - || fail "RunsOn MagicCache must not intercept Cargo Rail compiler results" -# shellcheck disable=SC2016 # GitHub expressions are intentional literal workflow contracts. -[[ $(yq eval '.jobs."rail-plan".steps[] | select(.name == "Check Release Intent Coverage") | .env.RAIL_BASE_REF' "$CI") \ - == '${{ steps.rail.outputs.base }}' ]] \ - || fail "release intent coverage must use cargo-rail-action's resolved base" -release_intent_condition=$(yq eval '.jobs."rail-plan".steps[] | select(.name == "Check Release Intent Coverage") | .if' "$CI") -[[ "$release_intent_condition" == *"startsWith(github.head_ref, 'rail/release-')"* \ - && "$release_intent_condition" == *"github.event.pull_request.head.repo.full_name == github.repository"* ]] \ - || fail "only repository-owned Cargo Rail release PRs may consume change intent" -grep -Fq 'scripts/ci/setup-toolchain.sh "$TOOLCHAIN" "$TOOLCHAIN_COMPONENTS" "$GITHUB_ENV"' "$TOOLCHAIN_ACTION" \ - || fail "toolchain setup must use the repository-owned rustup policy" -grep -Fq "printf 'RUSTUP_TOOLCHAIN=%s\\n' \"\$toolchain\" >>\"\$github_env\"" "$SETUP_TOOLCHAIN" \ - || fail "toolchain setup must activate the resolved contract for later steps" -grep -Fq 'RUSTUP_TOOLCHAIN="$TOOLCHAIN" rustc --version --verbose' "$TOOLCHAIN_ACTION" \ - || fail "toolchain setup must verify the activated contract without a rust-toolchain override" -if grep -Fq 'rustup default ' "$SETUP_TOOLCHAIN"; then - fail "toolchain setup must not mutate a runner-global default" -fi -if grep -Eq '[.]cargo/(bin|[.]crates)|[.]opam' "$SETUP_ACTION"; then - fail "CI tool executables and OPAM switches must not be restored from caches" -fi -if grep -Fq 'export PATH="$HOME/.cargo/bin:$PATH"' "$CI_CHECK" "$HOST_CHECK"; then - fail "CI checks must not place unverified runner tools ahead of the authenticated tool root" -fi -grep -Fq 'RSCRYPTO_TOOL_ROOT=$(mktemp -d ' "$INSTALL_TOOLS" \ - || fail "package-manager tools must install into a fresh per-run root" -grep -Fq 'export CARGO_HOME="$RSCRYPTO_CARGO_HOME"' "$INSTALL_TOOLS" \ - || fail "Cargo tool sources and executables must use the fresh per-run root" -grep -Fq 'GOMODCACHE="$RSCRYPTO_TOOL_ROOT/go/pkg/mod"' "$INSTALL_TOOLS" \ - || fail "Go tool sources must use the fresh per-run root" -grep -Fq 'export OPAMROOT="$RSCRYPTO_TOOL_ROOT/opam"' "$INSTALL_TOOLS" \ - || fail "OPAM tool sources and executables must use the fresh per-run root" -grep -Fq 'cargo install --registry crates-io "$package" --locked --version "=$version" --force' "$INSTALL_TOOLS" \ - || fail "Cargo tools must use exact authenticated crates.io installs" -grep -Fq 'go install "github.com/rhysd/actionlint/cmd/actionlint@v$ACTIONLINT_VERSION"' "$INSTALL_TOOLS" \ - || fail "Go tools must use exact checksum-database-backed module versions" -opam_commit=$(sed -n 's/^OPAM_REPOSITORY_COMMIT=//p' "$INSTALL_TOOLS") -[[ "$opam_commit" =~ ^[0-9a-f]{40}$ ]] \ - || fail "OPAM repository must use a full Git commit" -[[ $(sed -n 's/^OPAM_REPOSITORY_REMOTE=//p' "$INSTALL_TOOLS") \ - == "https://github.com/ocaml/opam-repository.git" ]] \ - || fail "OPAM repository must use the reviewed HTTPS remote" -grep -Fq 'git -C "$repository" fetch --depth=1 --no-tags' "$INSTALL_TOOLS" \ - || fail "OPAM repository must fetch only the pinned commit" -grep -Fq 'actual=$(git -C "$repository" rev-parse HEAD)' "$INSTALL_TOOLS" \ - || fail "OPAM metadata must be checked against its pinned commit" -grep -Fq 'status=$(git -C "$repository" status --short --untracked-files=all)' "$INSTALL_TOOLS" \ - || fail "OPAM metadata must match the pinned commit exactly" -grep -Fq 'actual=$(dpkg-query -W -f=' "$INSTALL_TOOLS" \ - || fail "APT packages must be validated against exact versions" -grep -Fq 'ci_tool_download wasmtime' "$NOSTD_WASM" \ - || fail "Wasmtime must use the direct archive integrity contract" -grep -Fq 'ci_tool_download wasm-tools' "$NOSTD_WASM" \ - || fail "wasm-tools must use the direct archive integrity contract" -if grep -Eiq '(^|[^[:alnum:]_])zig([^[:alnum:]_]|$)' "$CROSS_SCRIPT"; then - fail "cross-target CI must not depend on Zig" -fi -grep -Fq 'ci_tool_download codecov' "$INSTALL_CODECOV" \ - || fail "Codecov must use the direct executable integrity contract" -[[ $(yq eval '.jobs.coverage.steps[] | select(.id == "codecov") | .run' "$WEEKLY") \ - == "scripts/ci/install-codecov.sh" ]] \ - || fail "Qualification coverage must install the authenticated Codecov CLI" -# shellcheck disable=SC2016 # GitHub expression is an intentional literal contract. -grep -Fq 'binary: ${{ steps.codecov.outputs.binary }}' "$WEEKLY" \ - || fail "Codecov action must use the repository-verified CLI" -scorecard_action=$(yq eval '.jobs.scorecard.steps[] | select(.name == "Run Scorecard") | .uses' "$SCORECARD") -[[ "$scorecard_action" =~ ^ossf/scorecard-action@[0-9a-f]{40}$ ]] \ - || fail "Scorecard publication must call the official action at an immutable commit" -[[ $(yq eval '.jobs.scorecard.steps[] | select(.name == "Run Scorecard") | .with.publish_results' "$SCORECARD") \ - == "true" ]] \ - || fail "Scorecard must publish results" - -[[ $(count_feature_sets "$FEATURE_PROFILES" COMPILE_FEATURE_SETS) -eq 60 ]] \ - || fail "compile feature matrix must retain all 60 profiles" -[[ $(count_feature_sets "$FEATURE_PROFILES" EXECUTABLE_FEATURE_SETS) -eq 9 ]] \ - || fail "executable feature matrix must contain the nine behavior transitions" -[[ $(count_feature_sets "$FEATURE_PROFILES" CONSTRAINED_FEATURE_SETS) -eq 32 ]] \ - || fail "constrained feature matrix must contain all 32 portable profiles" -require_unique_feature_sets "$FEATURE_PROFILES" COMPILE_FEATURE_SETS -require_unique_feature_sets "$FEATURE_PROFILES" EXECUTABLE_FEATURE_SETS -require_unique_feature_sets "$FEATURE_PROFILES" CONSTRAINED_FEATURE_SETS -require_feature_subset "$FEATURE_PROFILES" EXECUTABLE_FEATURE_SETS "$FEATURE_PROFILES" COMPILE_FEATURE_SETS -grep -Fq 'COMPILE_FEATURE_SETS' "$COMPILE_MATRIX" \ - || fail "compile feature matrix must consume the shared profile authority" -grep -Fq 'EXECUTABLE_FEATURE_SETS' "$EXECUTABLE_MATRIX" \ - || fail "executable feature matrix must consume the shared profile authority" -grep -Fq '"$SCRIPT_DIR/check.sh" --all --feature-matrix' "$CHECK_ALL" \ - || fail "local check-all must retain one explicit feature-matrix execution" - -[[ $(count_matches 'just test-feature-matrix' "$WORKFLOWS" "$RUN_RUST_JOB") -eq 1 ]] \ - || fail "ordinary workflows must have exactly one executable feature-matrix owner" -[[ $(count_matches 'just check-feature-matrix' "$WORKFLOWS" "$RUN_RUST_JOB") -eq 1 ]] \ - || fail "ordinary workflows must have exactly one compile feature-matrix owner" -[[ $(yq eval '.on.workflow_dispatch.inputs.tag.required' "$RELEASE") == "true" ]] \ - || fail "release recovery must require an explicit existing tag" -[[ $(yq eval '.on.workflow_dispatch.inputs.tag.type' "$RELEASE") == "string" ]] \ - || fail "release recovery tag input must be a string" -[[ $(yq eval '.on.workflow_dispatch.inputs.s390x_ct_run.type' "$RELEASE") == "string" ]] \ - || fail "release recovery s390x CT run input must be a string" -[[ $(yq eval '.on.workflow_dispatch.inputs.x86_64_ct_run.type' "$RELEASE") == "string" ]] \ - || fail "release recovery x86_64 CT run input must be a string" -# shellcheck disable=SC2016 # GitHub expressions are intentional literal workflow contracts. -[[ $(yq eval '.jobs.preflight.steps[] | select(.name == "Checkout") | .with.ref' "$RELEASE") \ - == '${{ github.event_name == '\''workflow_dispatch'\'' && inputs.tag || github.ref }}' ]] \ - || fail "release preflight must check out the requested recovery tag" -# shellcheck disable=SC2016 # GitHub expressions are intentional literal workflow contracts. -[[ $(yq eval '.jobs.publish.steps[] | select(.name == "Checkout") | .with.ref' "$RELEASE") \ - == '${{ needs.preflight.outputs.release_tag }}' ]] \ - || fail "release publication must check out the preflight-verified tag" -identity_step=$(yq eval '.jobs.preflight.steps[] | select(.id == "identity") | .run' "$RELEASE") -grep -Fq 'refs/heads/main' <<<"$identity_step" \ - || fail "release recovery must reject workflow code outside protected main" -recovery_cleanup_step=$(yq eval '.jobs.preflight.steps[] | select(.name == "Remove reviewed recovery tooling") | .run' "$RELEASE") -grep -Fq 'rm -rf target/release-automation' <<<"$recovery_cleanup_step" \ - || fail "release recovery must remove its reviewed tooling checkout after preflight" -recovery_preflight_step=$(yq eval '.jobs.preflight.steps[] | select(.name == "Release preflight") | .run' "$RELEASE") -grep -Fq 'target/release-automation/scripts/ci/release-preflight.sh' <<<"$recovery_preflight_step" \ - || fail "release recovery must run reviewed preflight tooling from protected main" -if grep -Fq -- '--dependency-policy-root' <<<"$recovery_preflight_step"; then - fail "tag recovery must not revive a release-specific dependency compatibility path" -fi -recovery_controls_step=$(yq eval '.jobs.publish.steps[] | select(.name == "Capture repository controls") | .run' "$RELEASE") -grep -Fq 'target/release-automation/scripts/ci/repository-controls-evidence.sh' <<<"$recovery_controls_step" \ - || fail "release recovery must capture controls with reviewed tooling from protected main" -grep -Fq -- '--policy-root target/release-automation' <<<"$recovery_controls_step" \ - || fail "release recovery must use the reviewed repository policy from protected main" -ct_recovery_step=$(yq eval '.jobs.preflight.steps[] | select(.name == "Verify s390x CT recovery evidence") | .run' "$RELEASE") -grep -Fq 'release-ct-recovery-check.sh' <<<"$ct_recovery_step" \ - || fail "release recovery must validate replacement s390x CT evidence" -grep -Fq -- '--workflow-commit "$WORKFLOW_COMMIT"' <<<"$ct_recovery_step" \ - || fail "replacement s390x CT evidence must come from the reviewed workflow commit" -grep -Fq -- '--platform-group s390x' <<<"$ct_recovery_step" \ - || fail "replacement s390x CT evidence must validate the s390x platform group" -x86_ct_recovery_step=$(yq eval '.jobs.preflight.steps[] | select(.name == "Verify x86_64 CT recovery evidence") | .run' "$RELEASE") -grep -Fq 'release-ct-recovery-check.sh' <<<"$x86_ct_recovery_step" \ - || fail "release recovery must validate replacement x86_64 CT evidence" -grep -Fq -- '--workflow-commit "$WORKFLOW_COMMIT"' <<<"$x86_ct_recovery_step" \ - || fail "replacement x86_64 CT evidence must come from the reviewed workflow commit" -grep -Fq -- '--platform-group x86_64' <<<"$x86_ct_recovery_step" \ - || fail "replacement x86_64 CT evidence must validate the complete x86_64 platform group" -[[ $(yq -oy -p toml eval '.release.semver_check' "$RAIL_CONFIG") == "off" ]] \ - || fail "pre-1.0 Cargo Rail SemVer enforcement must remain explicitly disabled" -if grep -ERn 'cargo[ -]semver-checks' "$WORKFLOWS" "$RELEASE_PREFLIGHT" "$INSTALL_TOOLS" >/dev/null; then - fail "pre-1.0 workflows and installers must not reintroduce SemVer enforcement" -fi - -if grep -ERn 'just check --all|check-all\.sh' "$WORKFLOWS" "$RUN_RUST_JOB" >/dev/null; then - fail "native workflows must not invoke comprehensive cross-target checks" -fi - -if grep -En 'test-feature-matrix|check-feature-matrix' "$WEEKLY" >/dev/null; then - fail "weekly must inherit feature contracts from the reusable suite" -fi - -[[ $(jq '[.variants[] | select(.dimensions.operation == "cross-targets")] | length' "$RAIL_VARIANTS") -eq 1 ]] \ - || fail "the Cargo Rail catalog must have exactly one cross-target owner" -[[ $(count_matches 'scripts/ci/cross-targets\.sh' "$RUN_RUST_JOB") -eq 1 ]] \ - || fail "the Rust job dispatcher must define exactly one cross-target operation" -[[ $(jq '[.variants[] | select(.dimensions.operation == "native")] | length' "$RAIL_VARIANTS") -eq 4 ]] \ - || fail "the Cargo Rail catalog must own the four native Linux and Windows rows" -[[ $(jq '[.variants[] | select(.dimensions.operation == "native-ibm")] | length' "$RAIL_VARIANTS") -eq 2 ]] \ - || fail "the Cargo Rail catalog must own the IBM Z and POWER10 native rows" -[[ $(count_matches 'operation:[[:space:]]+native-riscv' "$RISCV") -eq 1 ]] \ - || fail "the manual RISC-V workflow must own exactly one native diagnostic lane" -[[ $(count_matches 'operation:[[:space:]]+native-riscv' "$WEEKLY") -eq 1 ]] \ - || fail "Qualification must own exactly one RISC-V native evidence lane" -[[ $(count_matches 'scripts/ci/native-check\.sh' "$RUN_RUST_JOB") -eq 3 ]] \ - || fail "the Rust job dispatcher must retain Linux, IBM, and RISC-V native operations" -if grep -Ein 'riscv' "$RAIL_VARIANTS" >/dev/null; then - fail "the Cargo Rail CI catalog must not own physical RISC-V work" -fi -if grep -Ein 'riscv' "$WORKFLOWS/bench.yaml" >/dev/null; then - fail "the generic benchmark workflow must not expose RISC-V" -fi -[[ $(yq eval '.jobs.native.with.runner' "$RISCV") == "ubuntu-24.04-riscv" ]] \ - || fail "the RISC-V workflow must own the RISE native runner" -[[ $(yq eval '.jobs.ct.with.platforms' "$RISCV") == "rise-riscv" ]] \ - || fail "the RISC-V workflow must select only the RISE CT lane" -[[ $(yq eval '.on.schedule' "$RISCV") == "null" ]] \ - || fail "standalone RISC-V diagnostics must not duplicate scheduled Qualification evidence" -[[ $(yq eval '.jobs.ct.with.upload_raw_artifacts' "$RISCV") == "true" ]] \ - || fail "manual RISC-V evidence must retain raw CT artifacts" -[[ $(yq eval '.jobs.ct.with.artifact_retention_days' "$RISCV") == "90" ]] \ - || fail "manual RISC-V evidence must retain release-grade artifacts" -[[ $(jq '[.variants[] | select(.dimensions.operation == "cargo-graph")] | length' "$RAIL_VARIANTS") -eq 1 ]] \ - || fail "the Cargo Rail catalog must have exactly one Cargo graph assurance owner" -[[ $(count_matches 'cargo rail unify --check' "$RUN_RUST_JOB") -eq 1 ]] \ - || fail "the Rust job dispatcher must define exactly one Cargo graph assurance operation" -if grep -En 'cargo rail unify --check' "$RELEASE_PREFLIGHT" >/dev/null; then - fail "tag preflight must consume exact-commit Qualification graph assurance instead of repeating it" -fi -if grep -En 'cargo (deny|audit)' "$RELEASE_PREFLIGHT" >/dev/null; then - fail "tag preflight must consume exact-commit Qualification dependency evidence instead of repeating it" -fi -[[ $(yq -o=json -I=0 '.on.workflow_dispatch.inputs.mode.options' "$WEEKLY") == '["assurance","release"]' ]] \ - || fail "Qualification must expose only assurance and release modes" -[[ $(yq eval '.on.workflow_dispatch.inputs.mode.default' "$WEEKLY") == "assurance" ]] \ - || fail "manually dispatched Qualification runs must default to assurance" -[[ $(yq eval '.concurrency.group' "$WEEKLY") == \ - "\${{ github.workflow }}-\${{ github.ref }}-\${{ github.event_name == 'workflow_dispatch' && inputs.mode == 'release' && 'release' || 'assurance' }}" ]] \ - || fail "scheduled assurance must not cancel release qualification" -weekly_mode_script=$(yq eval '.jobs.mode.steps[] | select(.id == "mode") | .run' "$WEEKLY") -[[ "$weekly_mode_script" == *$'schedule)\n mode=assurance'* ]] \ - || fail "scheduled Qualification runs must resolve to assurance" -[[ $(yq eval '.jobs.suite.with.supply_chain_mode' "$WEEKLY") == \ - "\${{ needs.mode.outputs.mode == 'release' && 'full' || 'light' }}" ]] \ - || fail "Qualification supply-chain depth must derive from the resolved mode" -[[ $(yq eval '.jobs."rail-plan".steps[] | select(.id == "rail") | .with.all' "$WEEKLY") == "true" ]] \ - || fail "Qualification must execute one typed all-work Cargo Rail plan" -[[ $(yq eval '.jobs.ct.with.upload_raw_artifacts' "$WEEKLY") == \ - "\${{ needs.mode.outputs.mode == 'release' }}" ]] \ - || fail "only release-mode Qualification may upload raw CT artifacts" -retention_expression="\${{ needs.mode.outputs.mode == 'release' && 90 || 14 }}" -[[ $(yq eval '.jobs.suite.with.artifact_retention_days' "$WEEKLY") == "$retention_expression" ]] \ - || fail "Qualification Cargo graph retention must derive from the resolved mode" -[[ $(yq eval '.jobs.fuzzing.with.artifact_retention_days' "$WEEKLY") == "$retention_expression" ]] \ - || fail "Qualification fuzz retention must derive from the resolved mode" -[[ $(yq eval '.jobs.mlkem-graviton.with.artifact_retention_days' "$WEEKLY") == "$retention_expression" ]] \ - || fail "Qualification ML-KEM retention must derive from the resolved mode" -[[ $(yq eval '.jobs.ct.with.artifact_retention_days' "$WEEKLY") == "$retention_expression" ]] \ - || fail "Qualification CT retention must derive from the resolved mode" -[[ $(yq eval '.jobs.rsa.with.artifact_retention_days' "$WEEKLY") == "$retention_expression" ]] \ - || fail "Qualification RSA retention must derive from the resolved mode" -[[ $(yq eval '.jobs.riscv-ct.with.artifact_retention_days' "$WEEKLY") == "$retention_expression" ]] \ - || fail "Qualification RISC-V CT retention must derive from the resolved mode" -[[ $(yq eval '.jobs.riscv-ct.with.upload_raw_artifacts' "$WEEKLY") == \ - "\${{ needs.mode.outputs.mode == 'release' }}" ]] \ - || fail "only release-mode Qualification may upload raw RISC-V CT artifacts" -[[ $(yq eval '.jobs.riscv-ct.with.platforms' "$WEEKLY") == "rise-riscv" ]] \ - || fail "Qualification must select the RISE CT evidence variant" -[[ $(yq eval '.jobs.riscv-native.with.runner' "$WEEKLY") == "ubuntu-24.04-riscv" ]] \ - || fail "Qualification must select the RISE native evidence runner" -[[ $(yq eval '.jobs.coverage.steps[] | select(.name == "Upload Coverage Artifacts") | .with."retention-days"' "$WEEKLY") == "$retention_expression" ]] \ - || fail "Qualification coverage retention must derive from the resolved mode" -[[ $(yq eval '.jobs.complete.name' "$WEEKLY") == 'Complete (${{ needs.mode.outputs.mode }})' ]] \ - || fail "Qualification must expose a mode-specific terminal gate" -ct_artifact_name="ct-\${{ inputs.upload_raw_artifacts && 'raw-' || '' }}\${{ matrix.artifact_suffix }}" -[[ $(yq eval '.jobs.ct.with.artifact_name' "$CT") == "$ct_artifact_name" ]] \ - || fail "raw CT artifact names must be distinguishable before release" -[[ $(yq eval '.on.workflow_call.inputs.artifact_retention_days.default' "$RUST_JOB") == "90" ]] \ - || fail "reusable Rust artifacts must preserve long retention by default" -artifact_steps='[.jobs.run.steps[] | select(.name == "Upload Artifact after completion" or .name == "Upload Artifact (success)")]' -[[ $(yq eval "$artifact_steps | length" "$RUST_JOB") -eq 2 ]] \ - || fail "reusable Rust job must retain both artifact upload paths" -[[ $(yq eval "$artifact_steps | map(.with.\"retention-days\" == \"\${{ inputs.artifact_retention_days }}\") | all" "$RUST_JOB") == "true" ]] \ - || fail "reusable Rust artifact retention must be caller-controlled" -[[ $(yq eval "$artifact_steps | map(.with.\"if-no-files-found\" == \"error\") | all" "$RUST_JOB") == "true" ]] \ - || fail "declared Rust evidence artifacts must fail closed when absent" -grep -Fq 'CI Suite (release) / Cargo Graph Assurance / run' "$RELEASE_EVIDENCE" \ - || fail "release evidence must require release-mode Cargo Graph Assurance" -grep -Fq 'Constant-Time Evidence (release) / Complete (CT)' "$RELEASE_EVIDENCE" \ - || fail "release evidence must require release-mode CT completion" -grep -Fq 'RSA Evidence (release) / Complete (RSA)' "$RELEASE_EVIDENCE" \ - || fail "release evidence must require release-mode RSA completion" -grep -Fq 'RISC-V Native Evidence / run' "$RELEASE_EVIDENCE" \ - || fail "release evidence must require RISC-V native qualification" -grep -Fq 'RISC-V CT Evidence (release) / Complete (CT)' "$RELEASE_EVIDENCE" \ - || fail "release evidence must require RISC-V CT qualification" -grep -Fq 'Complete (release)' "$RELEASE_EVIDENCE" \ - || fail "release evidence must require the release-mode terminal gate" -grep -Fq '.event == "workflow_dispatch"' "$RELEASE_EVIDENCE" \ - || fail "scheduled runs must be ineligible for release evidence" -grep -Fq 'ct-raw-' "$RELEASE_EVIDENCE" \ - || fail "release evidence must require live raw CT artifacts" -[[ $(yq eval '.concurrency.group' "$RSA") == 'rsa-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}' ]] \ - || fail "reusable RSA workflow concurrency must not collide with its caller" -grep -Fq 'pattern: ct-raw-*' "$RELEASE" \ - || fail "release must download the complete raw qualification evidence set" -grep -Fq 'scripts/ci/release-evidence-check.sh --commit "$RELEASE_COMMIT"' "$RELEASE" \ - || fail "release must require one exact-commit Qualification run" -grep -Fq 'scripts/ci/repository-controls-evidence.sh' "$RELEASE" \ - || fail "release must capture the live repository controls" -grep -Fq 'scripts/ci/package-release-source.sh' "$RELEASE_PREFLIGHT" \ - || fail "release preflight must build the exact-commit source archive" -grep -Fq 'scripts/ci/write-release-manifest.sh' "$RELEASE" \ - || fail "release must bind artifacts and toolchain metadata in one identity manifest" -grep -Fq -- '--allow-redacted-bypass' "$RELEASE" \ - || fail "release must explicitly acknowledge GitHub's workflow-token bypass redaction" -# shellcheck disable=SC2016 # GitHub expression is an intentional literal workflow contract. -grep -Fq 'subject-path: ${{ steps.repository_controls.outputs.evidence_path }}' "$RELEASE" \ - || fail "release must attest the repository controls evidence" -grep -Fq 'REPOSITORY_CONTROLS_SHA256' "$RELEASE" \ - || fail "release must checksum the repository controls evidence" -# shellcheck disable=SC2016 # Workflow shell variable is an intentional literal contract. -grep -Fq '"$REPOSITORY_CONTROLS_PATH"' "$RELEASE" \ - || fail "release must publish the repository controls evidence" -# shellcheck disable=SC2016 # GitHub expression is an intentional literal workflow contract. -grep -Fq 'subject-path: ${{ steps.package.outputs.source_path }}' "$RELEASE" \ - || fail "release must attest the deterministic source archive" -# shellcheck disable=SC2016 # GitHub expression is an intentional literal workflow contract. -grep -Fq 'subject-path: ${{ steps.release_manifest.outputs.manifest_path }}' "$RELEASE" \ - || fail "release must attest the identity manifest" -grep -Fq 'subject-path: SHA256SUMS' "$RELEASE" \ - || fail "release must attest its checksum set" -grep -Fq 'SOURCE_SHA256' "$RELEASE" \ - || fail "release must checksum the deterministic source archive" -grep -Fq 'RELEASE_MANIFEST_SHA256' "$RELEASE" \ - || fail "release must checksum the identity manifest" -grep -Fq 'scripts/ci/publish-immutable-release.sh' "$RELEASE" \ - || fail "release workflow must use the tested immutable publication state machine" -immutable_release_line=$(grep -nF 'scripts/ci/publish-immutable-release.sh' "$RELEASE" | cut -d: -f1) -crates_publish_line=$(grep -nF 'cargo publish -p rscrypto --locked' "$RELEASE" | cut -d: -f1) -[[ "$immutable_release_line" -lt "$crates_publish_line" ]] \ - || fail "release immutability must be verified before crates.io publication" -grep -Fq 'gh release create "$tag"' "$PUBLISH_RELEASE" \ - || fail "immutable publication must create the GitHub release" -grep -Fq -- '--draft' "$PUBLISH_RELEASE" \ - || fail "release assets must be assembled in a draft before immutable publication" -grep -Fq 'gh release verify "$tag"' "$PUBLISH_RELEASE" \ - || fail "release workflow must verify GitHub's immutable release attestation" -grep -Fq 'gh release verify-asset "$tag"' "$PUBLISH_RELEASE" \ - || fail "release workflow must verify assets against the immutable release" -grep -Fq -- "--jq '.assets[].name'" "$PUBLISH_RELEASE" \ - || fail "release workflow must reject missing or unexpected release assets" -grep -Fq -- '--stable-asset "$CRATE_PATH"' "$RELEASE" \ - || fail "release reruns must verify the crates.io-bound package asset" -grep -Fq -- '--stable-asset "$SOURCE_PATH"' "$RELEASE" \ - || fail "release reruns must verify the deterministic source archive" -grep -Fq '.github/rulesets/protect-main.json' "$REPOSITORY_CONTROLS" \ - || fail "repository controls evidence must validate the checked-in policy" -grep -Fq '.github/rulesets/protect-release-tags.json' "$REPOSITORY_CONTROLS" \ - || fail "repository controls evidence must validate immutable release tags" -jq -e ' - .target == "tag" - and .enforcement == "active" - and .bypass_actors == [] - and ([.rules[].type] | sort) == ["deletion", "update"] -' "$RELEASE_TAG_POLICY" >/dev/null || fail "release tags must reject updates and deletion without bypass" -jq -e '.enabled == true and (keys == ["enabled"])' "$RELEASE_IMMUTABILITY_POLICY" >/dev/null \ - || fail "repository policy must require immutable releases" -grep -Fq 'repos/$repo/immutable-releases' "$REPOSITORY_CONTROLS" \ - || fail "repository controls evidence must validate immutable releases before tagging" -grep -Fq 'current_user_can_bypass == "never"' "$REPOSITORY_CONTROLS" \ - || fail "repository controls evidence must reject bypass access" -[[ $(yq eval '.jobs | length' "$RELEASE") -eq 2 ]] \ - || fail "release must use one preflight and one publish job" -[[ $(yq eval '.concurrency.cancel-in-progress' "$RELEASE") == "false" ]] \ - || fail "release publication must not be canceled after side effects begin" -evidence_step=$(yq eval '.jobs.preflight.steps | to_entries | .[] | select(.value.id == "evidence") | .key' "$RELEASE") -setup_step=$(yq eval '.jobs.preflight.steps | to_entries | .[] | select(.value.name == "Setup") | .key' "$RELEASE") -[[ "$evidence_step" =~ ^[0-9]+$ && "$setup_step" =~ ^[0-9]+$ && "$evidence_step" -lt "$setup_step" ]] \ - || fail "release evidence must fail before expensive preflight setup" -grep -Fq 'run-id: ${{ needs.preflight.outputs.qualification_run_id }}' "$RELEASE" \ - || fail "release must consume all CT artifacts from the validated Qualification run" -grep -Fq 'run-id: ${{ needs.preflight.outputs.s390x_ct_run_id }}' "$RELEASE" \ - || fail "release must consume recovered s390x CT artifacts from the validated recovery run" -grep -Fq 'run-id: ${{ needs.preflight.outputs.x86_64_ct_run_id }}' "$RELEASE" \ - || fail "release must consume recovered x86_64 CT artifacts from the validated recovery run" -[[ $(yq eval '.on.workflow_dispatch.inputs.release_tag.type' "$CT") == "string" ]] \ - || fail "CT recovery release tag input must be a string" -ct_source_step=$(yq eval '.jobs.plan.steps[] | select(.name == "Resolve CT source") | .run' "$CT") -grep -Fq 'refs/heads/main' <<<"$ct_source_step" \ - || fail "release CT recovery must reject workflow code outside protected main" -grep -Fq 'checkout_ref=$RELEASE_TAG' <<<"$ct_source_step" \ - || fail "release CT recovery must bind execution to the immutable tag" -grep -Fq 'amd-zen4,intel-spr,intel-icl,amd-zen5)' <<<"$ct_source_step" \ - || fail "release CT recovery must require the complete x86_64 platform group" -grep -Fq 'ibm-s390x)' <<<"$ct_source_step" \ - || fail "release CT recovery must retain the complete s390x platform group" -if grep -Fq 'v0.9.0' <<<"$ct_source_step"; then - fail "completed v0.9.0 recovery compatibility must not remain in the live workflow" -fi -grep -Fq 'DUDECT_TIMEOUT" != "1800"' <<<"$ct_source_step" \ - || fail "release CT recovery must preserve the release DudeCT timeout" -grep -Fq 'BINSEC_TIMEOUT" != "900"' <<<"$ct_source_step" \ - || fail "release CT recovery must preserve the release BINSEC timeout" -grep -Fq 'UPLOAD_RAW_ARTIFACTS" != "true"' <<<"$ct_source_step" \ - || fail "release CT recovery must retain raw evidence" -grep -Fq 'ARTIFACT_RETENTION_DAYS" != "90"' <<<"$ct_source_step" \ - || fail "release CT recovery must retain evidence for the release lifetime" -# shellcheck disable=SC2016 # GitHub expression is an intentional literal workflow contract. -[[ $(yq eval '.jobs.ct.with.checkout_ref' "$CT") == '${{ needs.plan.outputs.checkout_ref }}' ]] \ - || fail "release CT recovery must execute the resolved immutable tag source" -# shellcheck disable=SC2016 # GitHub expression is an intentional literal workflow contract. -[[ $(yq eval '.jobs.ct.with.rustflags' "$CT") == '${{ needs.plan.outputs.recovery_rustflags }}' ]] \ - || fail "release CT recovery must pass only plan-resolved rustflags" -for input_name in dudect_timeout binsec_timeout artifact_retention_days; do - expected="\${{ fromJSON(format('{0}', inputs.${input_name})) }}" - [[ $(yq eval ".jobs.ct.with.${input_name}" "$CT") == "$expected" ]] \ - || fail "CT must normalize manual $input_name before the typed reusable workflow" -done -if grep -Eq 'uses: ./\.github/workflows/(ct|rsa)\.yaml' "$RELEASE"; then - fail "tag workflow must promote exact-commit evidence instead of rerunning CT or RSA" -fi -ci_musl=$(jq '[.variants[] | select(.dimensions.operation == "native" and ((.dimensions.target // "") | contains("musl")))] | length' "$RAIL_VARIANTS") -[[ "$ci_musl" -eq 0 ]] || fail "MUSL targets must not masquerade as native host jobs" - -ci_linux=$(jq '[.variants[] | select(.dimensions.operation == "native" and ((.dimensions.target // "") | endswith("unknown-linux-gnu")))] | length' "$RAIL_VARIANTS") -[[ "$ci_linux" -eq 2 ]] || fail "native CI must contain exactly x86_64 and AArch64 GNU hosts" - -group_musl=$(jq '[.groups.linux[] | select(contains("musl"))] | length' "$MANIFEST") -[[ "$group_musl" -eq 2 ]] || fail "the target manifest must retain both MUSL triples" - -# shellcheck disable=SC2016 # `$target` is an intentional literal in the workflow contract regex. -[[ $(count_matches 'cargo (check|clippy|build) --locked --target "\$target"' "$CROSS_SCRIPT") -ge 3 ]] \ - || fail "MUSL evidence must pass the target triple explicitly to Cargo" - -echo "CI ownership contract passed" diff --git a/scripts/ci/check-worktree-test.sh b/scripts/ci/check-worktree-test.sh index 8f971d8b..a41aa993 100755 --- a/scripts/ci/check-worktree-test.sh +++ b/scripts/ci/check-worktree-test.sh @@ -19,6 +19,7 @@ preflight_marker="$TMP_ROOT/locked-metadata-preflight" mkdir -p \ "$fixture/.config" \ "$fixture/scripts/check" \ + "$fixture/scripts/ci" \ "$fixture/scripts/lib" \ "$fixture/scripts/test" \ "$fixture/src" \ @@ -28,25 +29,25 @@ mkdir -p \ cp \ "$REPO_ROOT/scripts/check/asm-ledger.sh" \ "$REPO_ROOT/scripts/check/check-all.sh" \ - "$REPO_ROOT/scripts/check/check-feature-matrix.sh" \ - "$REPO_ROOT/scripts/check/check-zig.sh" \ + "$REPO_ROOT/scripts/check/feature-contracts.sh" \ "$REPO_ROOT/scripts/check/lint-independent-workspaces.sh" \ - "$REPO_ROOT/scripts/check/check-win.sh" \ "$REPO_ROOT/scripts/check/check.sh" \ - "$REPO_ROOT/scripts/check/zig-cc.sh" \ "$fixture/scripts/check/" cp "$REPO_ROOT/scripts/lib/common.sh" "$REPO_ROOT/scripts/lib/rail-plan.sh" \ "$REPO_ROOT/scripts/lib/feature-profiles.sh" "$REPO_ROOT/scripts/lib/toolchain.sh" \ "$fixture/scripts/lib/" cp "$REPO_ROOT/.config/toolchains.toml" "$fixture/.config/toolchains.toml" -cp "$REPO_ROOT/scripts/test/test-feature-matrix.sh" "$fixture/scripts/test/test-feature-matrix.sh" - -cat >"$fixture/scripts/lib/targets.sh" <<'EOF' -WIN_TARGETS=(mock-win) -LINUX_TARGETS=(mock-linux) -IBM_TARGETS=(mock-ibm) -NOSTD_TARGETS=(mock-nostd) -WASM_TARGETS=() +cp "$REPO_ROOT/.config/feature-matrix.json" "$fixture/.config/feature-matrix.json" + +cat >"$fixture/.config/target-matrix.json" <<'EOF' +{"variants":[{"id":"mock-cross","dimensions":{"operation":"cross"}}]} +EOF + +cat >"$fixture/scripts/ci/target-contracts.sh" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +[[ "$1" == run && "$2" == mock-cross && "$3" == deep ]] +cargo check --locked --lib EOF cat >"$fixture/scripts/lib/python.sh" <<'EOF' @@ -62,13 +63,27 @@ cat >"$fixture/scripts/check/zeroize-evidence.sh" <<'EOF' exit 0 EOF +cat >"$fixture/scripts/check/affected.sh" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +exec "$(dirname "$0")/check.sh" --all +EOF + +cat >"$fixture/scripts/check/policy.sh" <<'EOF' +#!/usr/bin/env bash +exit 0 +EOF + cat >"$fixture/scripts/check/rsa-asm-provenance.sh" <<'EOF' #!/usr/bin/env bash exit 0 EOF chmod +x \ + "$fixture/scripts/ci/target-contracts.sh" \ "$fixture/scripts/lib/python.sh" \ + "$fixture/scripts/check/affected.sh" \ + "$fixture/scripts/check/policy.sh" \ "$fixture/scripts/check/rsa-asm-provenance.sh" \ "$fixture/scripts/check/zeroize-evidence.sh" @@ -109,7 +124,11 @@ case "${1:-}" in metadata) require_locked "$@" : >"$MOCK_PREFLIGHT_MARKER" - printf '{"workspace_root":"%s"}\n' "$MOCK_REPO_ROOT" + if [[ " $* " == *" --no-deps "* ]]; then + printf '{"workspace_root":"%s","packages":[{"name":"rscrypto","features":{"aead":[],"aegis256":[],"aes-gcm":[],"aes-gcm-siv":[],"aes-siv":[],"argon2":[],"ascon-aead":[],"ascon-hash":[],"blake2b":[],"blake2s":[],"blake3":[],"chacha20poly1305":[],"crc16":[],"crc24":[],"crc32":[],"crc64":[],"ecdsa-p256":[],"ecdsa-p384":[],"ed25519":[],"hkdf":[],"hmac":[],"hmac-sha3":[],"kmac":[],"ml-kem":[],"parallel":[],"pbkdf2":[],"phc-strings":[],"poly1305":[],"rapidhash":[],"rsa":[],"scrypt":[],"serde":[],"serde-secrets":[],"sha2":[],"sha3":[],"websocket-sha1":[],"x25519":[],"xchacha20poly1305":[],"xxh3":[]}}]}\n' "$MOCK_REPO_ROOT" + else + printf '{"workspace_root":"%s","resolve":{"nodes":[{"id":"rscrypto","features":["alloc","auth"]}]}}\n' "$MOCK_REPO_ROOT" + fi ;; rail) if [[ "${2:-}" == "plan" ]]; then @@ -143,12 +162,7 @@ if [[ "$*" == "target list --installed" ]]; then fi EOF -cat >"$fake_bin/zig" <<'EOF' -#!/usr/bin/env bash -exit 0 -EOF - -chmod +x "$fake_bin/cargo" "$fake_bin/rustup" "$fake_bin/zig" +chmod +x "$fake_bin/cargo" "$fake_bin/rustup" ln -s /bin/bash "$fake_bin/bash" cp "$fake_bin/cargo" "$fake_home/.cargo/bin/cargo" diff --git a/scripts/ci/ci-check.sh b/scripts/ci/ci-check.sh deleted file mode 100755 index 0af74976..00000000 --- a/scripts/ci/ci-check.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Architecture-independent CI quality checks. Run once on the primary x86_64 -# CI host. Native and cross-target validation have separate owners. - -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "🔍 CI Quality Checks" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -echo "" -echo "🔗 Checking workflow ownership and action pins..." -"$SCRIPT_DIR/check-ci-ownership.sh" -"$SCRIPT_DIR/check-ci-ownership-test.sh" -"$SCRIPT_DIR/check-locked-cargo.sh" -"$SCRIPT_DIR/check-locked-cargo-test.sh" -"$SCRIPT_DIR/../lib/target-matrix.sh" --validate -"$SCRIPT_DIR/check-action-pins.sh" - -export RSCRYPTO_SKIP_CHECK_SUPPLY_CHAIN=1 -"$SCRIPT_DIR/../check/check.sh" --all - -echo "" -echo "🔨 Building all targets..." -cargo build --locked --workspace --all-targets --all-features - -echo "" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "✅ All CI checks passed!" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" diff --git a/scripts/ci/cross-targets.sh b/scripts/ci/cross-targets.sh index a40f5d1a..b3805d3a 100755 --- a/scripts/ci/cross-targets.sh +++ b/scripts/ci/cross-targets.sh @@ -1,9 +1,14 @@ #!/usr/bin/env bash set -euo pipefail -DEPTH=${1:-deep} +TARGET=${1:-} +DEPTH=${2:-deep} +if [[ -z "$TARGET" ]]; then + echo "usage: cross-targets.sh [shallow|deep]" >&2 + exit 2 +fi if [[ "$DEPTH" != "shallow" && "$DEPTH" != "deep" ]]; then - echo "usage: cross-targets.sh [shallow|deep]" >&2 + echo "usage: cross-targets.sh [shallow|deep]" >&2 exit 2 fi @@ -13,36 +18,35 @@ source "$SCRIPT_DIR/../lib/common.sh" # shellcheck source=../lib/targets.sh source "$SCRIPT_DIR/../lib/targets.sh" -MUSL_TARGETS=() -for target in "${LINUX_TARGETS[@]}"; do - if [[ "$target" == *-musl ]]; then - MUSL_TARGETS+=("$target") - fi -done - -if [[ ${#MUSL_TARGETS[@]} -ne 2 ]]; then - echo "error: expected two MUSL targets, found ${#MUSL_TARGETS[@]}" >&2 - exit 1 -fi - -for target in "${MUSL_TARGETS[@]}"; do +target_is_in() { + local candidate=$1 + shift + local item + for item in "$@"; do + [[ "$candidate" == "$item" ]] && return 0 + done + return 1 +} + +if target_is_in "$TARGET" "${LINUX_TARGETS[@]}" && [[ "$TARGET" == *-musl ]]; then echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "MUSL compile evidence: $target" + echo "MUSL compile evidence: $TARGET" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - ensure_target "$target" - target_dir="target/cross-check/$target" + ensure_target "$TARGET" + target_dir="target/cross-check/$TARGET" mkdir -p "$target_dir" CARGO_TARGET_DIR="$target_dir" \ - cargo check --locked --target "$target" --no-default-features --lib + cargo check --locked --target "$TARGET" --no-default-features --lib CARGO_TARGET_DIR="$target_dir" \ - cargo clippy --locked --target "$target" --lib --all-features + cargo clippy --locked --target "$TARGET" --lib --all-features CARGO_TARGET_DIR="$target_dir" \ - cargo build --locked --target "$target" --no-default-features --features alloc --lib --release -done - -for target in "${NOSTD_TARGETS[@]}" "${WASM_TARGETS[@]}"; do - "$SCRIPT_DIR/nostd-wasm-suite.sh" "$target" "$DEPTH" -done + cargo build --locked --target "$TARGET" --no-default-features --features alloc --lib --release +elif target_is_in "$TARGET" "${NOSTD_TARGETS[@]}" "${WASM_TARGETS[@]}"; then + "$SCRIPT_DIR/nostd-wasm-suite.sh" "$TARGET" "$DEPTH" +else + echo "target is not a generic cross-contract row: $TARGET" >&2 + exit 2 +fi -echo "Cross-target validation passed: ${#MUSL_TARGETS[@]} MUSL + ${#NOSTD_TARGETS[@]} no_std + ${#WASM_TARGETS[@]} WASM targets" +echo "Cross-target validation passed: $TARGET ($DEPTH)" diff --git a/scripts/ci/dependabot-smoke-test.sh b/scripts/ci/dependabot-smoke-test.sh deleted file mode 100755 index 5ea41b3c..00000000 --- a/scripts/ci/dependabot-smoke-test.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=scripts/ci/dependabot-smoke.sh -source "$SCRIPT_DIR/dependabot-smoke.sh" - -assert_plan() { - local expected_root=$1 - local expected_automation=$2 - local expected_manifests=$3 - shift 3 - - CHANGED=("$@") - classify_changed_files - - [[ "$RUN_ROOT" == "$expected_root" ]] - [[ "$RUN_AUTOMATION" == "$expected_automation" ]] - [[ "${#MANIFESTS[@]}" -eq "$expected_manifests" ]] - [[ "${#UNSUPPORTED[@]}" -eq 0 ]] -} - -assert_plan true false 0 Cargo.toml Cargo.lock -assert_plan false false 1 fuzz/Cargo.toml fuzz/Cargo.lock -assert_plan false false 1 fuzz-packages/fast-rapidhash/Cargo.toml fuzz-packages/fast-rapidhash/Cargo.lock -assert_plan false false 1 tools/ct-dudect/Cargo.toml tools/ct-dudect/Cargo.lock -assert_plan false true 0 .github/workflows/ci.yaml -assert_plan true true 0 Cargo.lock .github/workflows/ci.yaml .github/dependabot.yaml - -CHANGED=(src/lib.rs) -classify_changed_files -[[ "${#UNSUPPORTED[@]}" -eq 1 ]] - -echo "Dependabot smoke routing tests passed" diff --git a/scripts/ci/dependabot-smoke.sh b/scripts/ci/dependabot-smoke.sh deleted file mode 100755 index 7863c393..00000000 --- a/scripts/ci/dependabot-smoke.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Fast dependency-update gate for Dependabot PRs. This intentionally avoids the -# full platform matrix; expensive architecture evidence belongs to normal CI, -# weekly CI, and manual runs. - -BASE_SHA="${GITHUB_BASE_SHA:-${1:-}}" - -changed_files() { - if [[ -n "$BASE_SHA" ]] && git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null 2>&1; then - git diff --name-only "$BASE_SHA...HEAD" - return 0 - fi - - if [[ -n "${GITHUB_BASE_REF:-}" ]] && git rev-parse --verify "origin/${GITHUB_BASE_REF}^{commit}" >/dev/null 2>&1; then - git diff --name-only "origin/${GITHUB_BASE_REF}...HEAD" - return 0 - fi - - git diff --name-only HEAD^..HEAD -} - -add_manifest() { - local manifest=$1 - - [[ -f "$manifest" ]] || return 0 - - local existing - for existing in "${MANIFESTS[@]-}"; do - [[ "$existing" == "$manifest" ]] && return 0 - done - - MANIFESTS+=("$manifest") -} - -run_root_smoke() { - echo "Root dependency smoke" - cargo fetch --locked - cargo check --locked --workspace --all-targets --all-features - cargo test --locked --workspace --all-features --no-run - cargo deny --locked check advisories -} - -run_manifest_smoke() { - local manifest=$1 - local lockfile - lockfile="$(dirname "$manifest")/Cargo.lock" - - echo "Standalone dependency smoke: $manifest" - if [[ ! -f "$lockfile" ]]; then - cargo generate-lockfile --manifest-path "$manifest" - fi - cargo fetch --manifest-path "$manifest" --locked - cargo check --manifest-path "$manifest" --locked --all-targets --all-features - cargo test --manifest-path "$manifest" --locked --all-features --no-run -} - -run_automation_smoke() { - echo "GitHub Actions dependency smoke" - scripts/ci/check-action-pins.sh - scripts/ci/check-action-pins-test.sh - scripts/ci/check-ci-ownership.sh - scripts/ci/check-ci-ownership-test.sh - actionlint - zizmor .github/workflows .github/actions -} - -classify_changed_files() { - RUN_ROOT=false - RUN_AUTOMATION=false - MANIFESTS=() - UNSUPPORTED=() - - local path - for path in "${CHANGED[@]}"; do - case "$path" in - Cargo.toml | Cargo.lock) - RUN_ROOT=true - ;; - fuzz/Cargo.toml | fuzz/Cargo.lock | fuzz/support/Cargo.toml) - add_manifest "fuzz/Cargo.toml" - ;; - fuzz-packages/*/Cargo.toml | fuzz-packages/*/Cargo.lock) - add_manifest "${path%/*}/Cargo.toml" - ;; - tools/*/Cargo.toml | tools/*/Cargo.lock) - add_manifest "${path%/*}/Cargo.toml" - ;; - .github/dependabot.yaml | .github/workflows/* | .github/actions/*) - RUN_AUTOMATION=true - ;; - *) - UNSUPPORTED+=("$path") - ;; - esac - done -} - -main() { - mapfile -t CHANGED < <(changed_files) - - echo "Changed files:" - printf ' %s\n' "${CHANGED[@]:-}" - - classify_changed_files - - if [[ ${#UNSUPPORTED[@]} -gt 0 ]]; then - echo "Unsupported Dependabot changes:" >&2 - printf ' %s\n' "${UNSUPPORTED[@]}" >&2 - return 1 - fi - - if [[ "$RUN_ROOT" != true && "$RUN_AUTOMATION" != true && "${#MANIFESTS[@]}" -eq 0 ]]; then - echo "Dependabot change set has no recognized dependency surface" >&2 - return 1 - fi - - if [[ "$RUN_ROOT" == true ]]; then - run_root_smoke - fi - - local manifest - for manifest in "${MANIFESTS[@]}"; do - run_manifest_smoke "$manifest" - done - - if [[ "$RUN_AUTOMATION" == true ]]; then - run_automation_smoke - fi -} - -if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then - main "$@" -fi diff --git a/scripts/ci/emit-manual-matrix-test.sh b/scripts/ci/emit-manual-matrix-test.sh index a4de9931..efd2b5a6 100755 --- a/scripts/ci/emit-manual-matrix-test.sh +++ b/scripts/ci/emit-manual-matrix-test.sh @@ -13,7 +13,7 @@ jq -e ' length == 1 and .[0].platform == "rise-riscv" and .[0].target == "riscv64gc-unknown-linux-gnu" - and .[0].tools_mode == "none" + and .[0].formal == false and (.[0] | has("enable_rust_cache") | not) and (.[0] | has("enable_magic_cache") | not) ' <<<"$matrix" >/dev/null @@ -22,18 +22,13 @@ ct_default="$({ cd "$REPO_ROOT" GH_RUN_ID=1 CT_PLATFORMS=all scripts/ci/emit-manual-matrix.sh ct })" -jq -e 'length == 8 and all(.platform != "rise-riscv")' <<<"$ct_default" >/dev/null +jq -e 'length == 9 and any(.platform == "rise-riscv")' <<<"$ct_default" >/dev/null bench_default="$({ cd "$REPO_ROOT" GH_RUN_ID=1 BENCH_PLATFORMS=all scripts/ci/emit-manual-matrix.sh bench })" -jq -e 'length == 8 and all(.platform != "rise-riscv")' <<<"$bench_default" >/dev/null - -if GH_RUN_ID=1 BENCH_PLATFORMS=riscv scripts/ci/emit-manual-matrix.sh bench >/dev/null 2>&1; then - echo "generic benchmark matrix accepted the RISC-V lane" >&2 - exit 1 -fi +jq -e 'length == 9 and any(.platform == "rise-riscv")' <<<"$bench_default" >/dev/null scripts/lib/python.sh - "$REPO_ROOT/ct.toml" <<'PY' import pathlib diff --git a/scripts/ci/emit-manual-matrix.sh b/scripts/ci/emit-manual-matrix.sh index 40c0726e..7fa08947 100755 --- a/scripts/ci/emit-manual-matrix.sh +++ b/scripts/ci/emit-manual-matrix.sh @@ -54,36 +54,37 @@ normalize_platform() { esac } -# Bench matrix rows. Shape matches the inputs to _rust-job.yaml: -# runner, timeout_minutes, tools_mode, toolchain_components -# Plus bench-specific plumbing (platform, display_name, artifact_suffix). +# Manual benchmark and CT rows share runner and artifact identity fields. append_row_for_platform() { local platform="${1:-}" local runner_uarch="runs-on=${GH_RUN_ID_VAL}/runner=" case "$platform" in amd-zen4) - ROWS+=("{\"platform\":\"amd-zen4\",\"display_name\":\"AMD Zen4\",\"artifact_suffix\":\"amd-zen4\",\"timeout_minutes\":${RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}amd-zen4\",\"tools_mode\":\"minimal\",\"toolchain_components\":\"${COMPONENTS_STD}\"}") + ROWS+=("{\"platform\":\"amd-zen4\",\"display_name\":\"AMD Zen4\",\"artifact_suffix\":\"amd-zen4\",\"timeout_minutes\":${RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}amd-zen4\"}") ;; intel-spr) - ROWS+=("{\"platform\":\"intel-spr\",\"display_name\":\"Intel Sapphire Rapids\",\"artifact_suffix\":\"intel-spr\",\"timeout_minutes\":${RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}intel-spr\",\"tools_mode\":\"minimal\",\"toolchain_components\":\"${COMPONENTS_STD}\"}") + ROWS+=("{\"platform\":\"intel-spr\",\"display_name\":\"Intel Sapphire Rapids\",\"artifact_suffix\":\"intel-spr\",\"timeout_minutes\":${RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}intel-spr\"}") ;; intel-icl) - ROWS+=("{\"platform\":\"intel-icl\",\"display_name\":\"Intel Ice Lake\",\"artifact_suffix\":\"intel-icl\",\"timeout_minutes\":${RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}intel-icl\",\"tools_mode\":\"minimal\",\"toolchain_components\":\"${COMPONENTS_STD}\"}") + ROWS+=("{\"platform\":\"intel-icl\",\"display_name\":\"Intel Ice Lake\",\"artifact_suffix\":\"intel-icl\",\"timeout_minutes\":${RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}intel-icl\"}") ;; amd-zen5) - ROWS+=("{\"platform\":\"amd-zen5\",\"display_name\":\"AMD Zen5\",\"artifact_suffix\":\"amd-zen5\",\"timeout_minutes\":${RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}amd-zen5\",\"tools_mode\":\"minimal\",\"toolchain_components\":\"${COMPONENTS_STD}\"}") + ROWS+=("{\"platform\":\"amd-zen5\",\"display_name\":\"AMD Zen5\",\"artifact_suffix\":\"amd-zen5\",\"timeout_minutes\":${RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}amd-zen5\"}") ;; graviton3) - ROWS+=("{\"platform\":\"graviton3\",\"display_name\":\"AWS Graviton3\",\"artifact_suffix\":\"graviton3\",\"timeout_minutes\":${RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}graviton3\",\"tools_mode\":\"minimal\",\"toolchain_components\":\"${COMPONENTS_STD}\"}") + ROWS+=("{\"platform\":\"graviton3\",\"display_name\":\"AWS Graviton3\",\"artifact_suffix\":\"graviton3\",\"timeout_minutes\":${RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}graviton3\"}") ;; graviton4) - ROWS+=("{\"platform\":\"graviton4\",\"display_name\":\"AWS Graviton4\",\"artifact_suffix\":\"graviton4\",\"timeout_minutes\":${RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}graviton4\",\"tools_mode\":\"minimal\",\"toolchain_components\":\"${COMPONENTS_STD}\"}") + ROWS+=("{\"platform\":\"graviton4\",\"display_name\":\"AWS Graviton4\",\"artifact_suffix\":\"graviton4\",\"timeout_minutes\":${RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}graviton4\"}") ;; ibm-s390x) - ROWS+=("{\"platform\":\"ibm-s390x\",\"display_name\":\"IBM Z s390x\",\"artifact_suffix\":\"ibm-s390x\",\"timeout_minutes\":${IBM_TIMEOUT_MINUTES},\"runner\":\"ubuntu-24.04-s390x\",\"tools_mode\":\"ibm\",\"toolchain_components\":\"clippy, rustfmt\"}") + ROWS+=("{\"platform\":\"ibm-s390x\",\"display_name\":\"IBM Z s390x\",\"artifact_suffix\":\"ibm-s390x\",\"timeout_minutes\":${IBM_TIMEOUT_MINUTES},\"runner\":\"ubuntu-24.04-s390x\"}") ;; ibm-power10) - ROWS+=("{\"platform\":\"ibm-power10\",\"display_name\":\"IBM POWER10 ppc64le\",\"artifact_suffix\":\"ibm-power10\",\"timeout_minutes\":${IBM_TIMEOUT_MINUTES},\"runner\":\"ubuntu-24.04-ppc64le-p10\",\"tools_mode\":\"ibm\",\"toolchain_components\":\"clippy, rustfmt\"}") + ROWS+=("{\"platform\":\"ibm-power10\",\"display_name\":\"IBM POWER10 ppc64le\",\"artifact_suffix\":\"ibm-power10\",\"timeout_minutes\":${IBM_TIMEOUT_MINUTES},\"runner\":\"ubuntu-24.04-ppc64le-p10\"}") + ;; + rise-riscv) + ROWS+=("{\"platform\":\"rise-riscv\",\"display_name\":\"RISE RISC-V riscv64\",\"artifact_suffix\":\"rise-riscv\",\"timeout_minutes\":${IBM_TIMEOUT_MINUTES},\"runner\":\"ubuntu-24.04-riscv\"}") ;; *) echo "error: unsupported bench platform '$platform'" >&2 @@ -97,31 +98,31 @@ append_ct_row_for_platform() { local runner_uarch="runs-on=${GH_RUN_ID_VAL}/runner=" case "$platform" in amd-zen4) - ROWS+=("{\"platform\":\"amd-zen4\",\"target\":\"x86_64-unknown-linux-gnu\",\"os\":\"linux\",\"display_name\":\"AMD Zen4\",\"artifact_suffix\":\"amd-zen4\",\"timeout_minutes\":${CT_RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}amd-zen4\",\"tools_mode\":\"ct-linux\",\"toolchain_components\":\"${COMPONENTS_CT}\"}") + ROWS+=("{\"platform\":\"amd-zen4\",\"target\":\"x86_64-unknown-linux-gnu\",\"display_name\":\"AMD Zen4\",\"artifact_suffix\":\"amd-zen4\",\"timeout_minutes\":${CT_RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}amd-zen4\",\"formal\":true,\"toolchain_components\":\"${COMPONENTS_CT}\"}") ;; intel-spr) - ROWS+=("{\"platform\":\"intel-spr\",\"target\":\"x86_64-unknown-linux-gnu\",\"os\":\"linux\",\"display_name\":\"Intel Sapphire Rapids\",\"artifact_suffix\":\"intel-spr\",\"timeout_minutes\":${CT_RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}intel-spr\",\"tools_mode\":\"ct-linux\",\"toolchain_components\":\"${COMPONENTS_CT}\"}") + ROWS+=("{\"platform\":\"intel-spr\",\"target\":\"x86_64-unknown-linux-gnu\",\"display_name\":\"Intel Sapphire Rapids\",\"artifact_suffix\":\"intel-spr\",\"timeout_minutes\":${CT_RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}intel-spr\",\"formal\":true,\"toolchain_components\":\"${COMPONENTS_CT}\"}") ;; intel-icl) - ROWS+=("{\"platform\":\"intel-icl\",\"target\":\"x86_64-unknown-linux-gnu\",\"os\":\"linux\",\"display_name\":\"Intel Ice Lake\",\"artifact_suffix\":\"intel-icl\",\"timeout_minutes\":${CT_RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}intel-icl\",\"tools_mode\":\"ct-linux\",\"toolchain_components\":\"${COMPONENTS_CT}\"}") + ROWS+=("{\"platform\":\"intel-icl\",\"target\":\"x86_64-unknown-linux-gnu\",\"display_name\":\"Intel Ice Lake\",\"artifact_suffix\":\"intel-icl\",\"timeout_minutes\":${CT_RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}intel-icl\",\"formal\":true,\"toolchain_components\":\"${COMPONENTS_CT}\"}") ;; amd-zen5) - ROWS+=("{\"platform\":\"amd-zen5\",\"target\":\"x86_64-unknown-linux-gnu\",\"os\":\"linux\",\"display_name\":\"AMD Zen5\",\"artifact_suffix\":\"amd-zen5\",\"timeout_minutes\":${CT_RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}amd-zen5\",\"tools_mode\":\"ct-linux\",\"toolchain_components\":\"${COMPONENTS_CT}\"}") + ROWS+=("{\"platform\":\"amd-zen5\",\"target\":\"x86_64-unknown-linux-gnu\",\"display_name\":\"AMD Zen5\",\"artifact_suffix\":\"amd-zen5\",\"timeout_minutes\":${CT_RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}amd-zen5\",\"formal\":true,\"toolchain_components\":\"${COMPONENTS_CT}\"}") ;; graviton3) - ROWS+=("{\"platform\":\"graviton3\",\"target\":\"aarch64-unknown-linux-gnu\",\"os\":\"linux\",\"display_name\":\"AWS Graviton3\",\"artifact_suffix\":\"graviton3\",\"timeout_minutes\":${CT_RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}graviton3\",\"tools_mode\":\"ct-linux\",\"toolchain_components\":\"${COMPONENTS_CT}\"}") + ROWS+=("{\"platform\":\"graviton3\",\"target\":\"aarch64-unknown-linux-gnu\",\"display_name\":\"AWS Graviton3\",\"artifact_suffix\":\"graviton3\",\"timeout_minutes\":${CT_RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}graviton3\",\"formal\":true,\"toolchain_components\":\"${COMPONENTS_CT}\"}") ;; graviton4) - ROWS+=("{\"platform\":\"graviton4\",\"target\":\"aarch64-unknown-linux-gnu\",\"os\":\"linux\",\"display_name\":\"AWS Graviton4\",\"artifact_suffix\":\"graviton4\",\"timeout_minutes\":${CT_RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}graviton4\",\"tools_mode\":\"ct-linux\",\"toolchain_components\":\"${COMPONENTS_CT}\"}") + ROWS+=("{\"platform\":\"graviton4\",\"target\":\"aarch64-unknown-linux-gnu\",\"display_name\":\"AWS Graviton4\",\"artifact_suffix\":\"graviton4\",\"timeout_minutes\":${CT_RUNSON_TIMEOUT_MINUTES},\"runner\":\"${runner_uarch}graviton4\",\"formal\":true,\"toolchain_components\":\"${COMPONENTS_CT}\"}") ;; ibm-s390x) - ROWS+=("{\"platform\":\"ibm-s390x\",\"target\":\"s390x-unknown-linux-gnu\",\"os\":\"linux\",\"display_name\":\"IBM Z s390x\",\"artifact_suffix\":\"ibm-s390x\",\"timeout_minutes\":${CT_IBM_TIMEOUT_MINUTES},\"runner\":\"ubuntu-24.04-s390x\",\"tools_mode\":\"ibm\",\"toolchain_components\":\"${COMPONENTS_CT}\"}") + ROWS+=("{\"platform\":\"ibm-s390x\",\"target\":\"s390x-unknown-linux-gnu\",\"display_name\":\"IBM Z s390x\",\"artifact_suffix\":\"ibm-s390x\",\"timeout_minutes\":${CT_IBM_TIMEOUT_MINUTES},\"runner\":\"ubuntu-24.04-s390x\",\"formal\":false,\"toolchain_components\":\"${COMPONENTS_CT}\"}") ;; ibm-power10) - ROWS+=("{\"platform\":\"ibm-power10\",\"target\":\"powerpc64le-unknown-linux-gnu\",\"os\":\"linux\",\"display_name\":\"IBM POWER10 ppc64le\",\"artifact_suffix\":\"ibm-power10\",\"timeout_minutes\":${CT_IBM_TIMEOUT_MINUTES},\"runner\":\"ubuntu-24.04-ppc64le-p10\",\"tools_mode\":\"ibm\",\"toolchain_components\":\"${COMPONENTS_CT}\"}") + ROWS+=("{\"platform\":\"ibm-power10\",\"target\":\"powerpc64le-unknown-linux-gnu\",\"display_name\":\"IBM POWER10 ppc64le\",\"artifact_suffix\":\"ibm-power10\",\"timeout_minutes\":${CT_IBM_TIMEOUT_MINUTES},\"runner\":\"ubuntu-24.04-ppc64le-p10\",\"formal\":false,\"toolchain_components\":\"${COMPONENTS_CT}\"}") ;; rise-riscv) - ROWS+=("{\"platform\":\"rise-riscv\",\"target\":\"riscv64gc-unknown-linux-gnu\",\"os\":\"linux\",\"display_name\":\"RISE RISC-V riscv64\",\"artifact_suffix\":\"rise-riscv\",\"timeout_minutes\":${CT_RISCV_TIMEOUT_MINUTES},\"runner\":\"ubuntu-24.04-riscv\",\"tools_mode\":\"none\",\"toolchain_components\":\"${COMPONENTS_CT}\"}") + ROWS+=("{\"platform\":\"rise-riscv\",\"target\":\"riscv64gc-unknown-linux-gnu\",\"display_name\":\"RISE RISC-V riscv64\",\"artifact_suffix\":\"rise-riscv\",\"timeout_minutes\":${CT_RISCV_TIMEOUT_MINUTES},\"runner\":\"ubuntu-24.04-riscv\",\"formal\":false,\"toolchain_components\":\"${COMPONENTS_CT}\"}") ;; *) echo "error: unsupported CT platform '$platform'" >&2 @@ -137,7 +138,6 @@ if [[ -z "$GH_RUN_ID_VAL" ]]; then fi ROWS=() -COMPONENTS_STD="clippy, rustfmt, rust-src" COMPONENTS_CT="clippy, rustfmt, rust-src, llvm-tools-preview" RUNSON_TIMEOUT_MINUTES=180 IBM_TIMEOUT_MINUTES=240 @@ -150,6 +150,7 @@ BENCH_PLATFORMS_ALL=( "graviton4" "ibm-s390x" "ibm-power10" + "rise-riscv" ) CT_PLATFORMS_ALL=( "amd-zen4" @@ -162,16 +163,6 @@ CT_PLATFORMS_ALL=( "ibm-power10" "rise-riscv" ) -CT_PLATFORMS_DEFAULT=( - "amd-zen4" - "intel-spr" - "intel-icl" - "amd-zen5" - "graviton3" - "graviton4" - "ibm-s390x" - "ibm-power10" -) CT_RUNSON_TIMEOUT_MINUTES=360 CT_IBM_TIMEOUT_MINUTES=420 CT_RISCV_TIMEOUT_MINUTES=480 @@ -179,13 +170,13 @@ CT_RISCV_TIMEOUT_MINUTES=480 if [[ "$MODE" == "ct" ]]; then PLATFORMS_INPUT="${CT_PLATFORMS:-}" ALL_PLATFORMS=("${CT_PLATFORMS_ALL[@]}") - DEFAULT_PLATFORMS=("${CT_PLATFORMS_DEFAULT[@]}") + DEFAULT_PLATFORMS=("${CT_PLATFORMS_ALL[@]}") PLATFORM_ALIASES="zen4 spr icl zen5 g3 g4 s390x power10 riscv" else PLATFORMS_INPUT="${BENCH_PLATFORMS:-}" ALL_PLATFORMS=("${BENCH_PLATFORMS_ALL[@]}") DEFAULT_PLATFORMS=("${BENCH_PLATFORMS_ALL[@]}") - PLATFORM_ALIASES="zen4 spr icl zen5 g3 g4 s390x power10" + PLATFORM_ALIASES="zen4 spr icl zen5 g3 g4 s390x power10 riscv" fi PLATFORMS_INPUT="$(echo "$PLATFORMS_INPUT" | xargs)" diff --git a/scripts/ci/feature-contracts-test.sh b/scripts/ci/feature-contracts-test.sh new file mode 100755 index 00000000..9d5cae7b --- /dev/null +++ b/scripts/ci/feature-contracts-test.sh @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +# Regression coverage for feature catalog deduplication, command scopes, and +# deterministic shards. Uses a fake Cargo executable; no product code builds. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +EXECUTOR="$REPO_ROOT/scripts/check/feature-contracts.sh" +# shellcheck source=../lib/feature-profiles.sh +source "$REPO_ROOT/scripts/lib/feature-profiles.sh" +TMP_ROOT=$(mktemp -d) +trap 'rm -rf "$TMP_ROOT"' EXIT + +fail() { + echo "feature-contract executor regression failure: $*" >&2 + exit 1 +} + +fake_bin="$TMP_ROOT/bin" +command_log="$TMP_ROOT/commands.log" +real_cargo=$(command -v cargo) +mkdir -p "$fake_bin" + +for case_entry in "${RUNTIME_TEST_CASES[@]}"; do + IFS='|' read -r _ case_target case_filter <<<"$case_entry" + case "$case_target" in all | lib) continue ;; esac + if [[ "$case_target" == websocket_accept_digest ]]; then + source_path="$REPO_ROOT/tests/websocket_sha1.rs" + rg -Uq '\[\[test\]\]\nname = "websocket_accept_digest"\npath = "tests/websocket_sha1.rs"' \ + "$REPO_ROOT/Cargo.toml" || fail "WebSocket test target no longer owns its declared source" + else + source_path="$REPO_ROOT/tests/$case_target.rs" + fi + [[ -f "$source_path" ]] \ + || fail "runtime case names missing Cargo test target $case_target" + if [[ -n "$case_filter" ]] && ! grep -Fq "$case_filter" "$source_path"; then + fail "runtime filter $case_filter is absent from $case_target" + fi +done + +cat >"$fake_bin/cargo" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +if [[ "${1:-}" == metadata && " $* " == *" --no-deps "* ]]; then + exec "$REAL_CARGO" "$@" +fi + +printf 'cargo' >>"$MOCK_LOG" +printf ' %s' "$@" >>"$MOCK_LOG" +printf '\n' >>"$MOCK_LOG" + +if [[ "${1:-}" == metadata ]]; then + feature_set="" + while [[ $# -gt 0 ]]; do + if [[ "$1" == --features ]]; then + feature_set=$2 + break + fi + shift + done + if [[ "${MOCK_ALIAS_DIVERGES:-0}" == 1 && "$feature_set" == alloc,auth ]]; then + printf '%s\n' '{"resolve":{"nodes":[{"id":"rscrypto","features":["alloc","auth","different"]}]}}' + else + printf '%s\n' '{"resolve":{"nodes":[{"id":"rscrypto","features":["alloc","auth"]}]}}' + fi +fi +EOF +chmod +x "$fake_bin/cargo" + +run_executor() { + local output=$1 + shift + env -u BASH_ENV PATH="$fake_bin:$PATH" MOCK_LOG="$command_log" REAL_CARGO="$real_cargo" \ + "$EXECUTOR" "$@" >"$output" 2>&1 +} + +list_output="$TMP_ROOT/list.out" +run_executor "$list_output" list +grep -Fq 'compile (59 unique graphs, 60 named contracts)' "$list_output" \ + || fail "compile catalog counts changed" +grep -Fq 'alias: alloc,auth' "$list_output" || fail "compile alias disappeared" +grep -Fq 'runtime (9 profiles)' "$list_output" || fail "runtime catalog count changed" + +matrix_output="$TMP_ROOT/matrix.json" +run_executor "$matrix_output" matrix +jq -e ' + (.include | length) == 5 + and ([.include[].domain] | map(select(. == "compile")) | length) == 2 + and ([.include[].domain] | map(select(. == "runtime")) | length) == 3 + and all(.include[]; (.profiles | type == "string" and length > 0)) +' "$matrix_output" >/dev/null || fail "executor matrix is not the five bounded shards" +compile_matrix_count=$(jq -r ' + [.include[] | select(.domain == "compile") | .profiles | split(",")[]] + | unique | length +' "$matrix_output") +[[ "$compile_matrix_count" -eq 59 ]] || fail "full matrix omitted compile profiles" +runtime_matrix_count=$(jq -r ' + [.include[] | select(.domain == "runtime") | .profiles | split(",")[]] + | unique | length +' "$matrix_output") +[[ "$runtime_matrix_count" -eq 9 ]] || fail "full matrix omitted runtime profiles" + +fake_plan="$TMP_ROOT/plan.json" +fake_reader="$TMP_ROOT/read.py" +cat >"$fake_plan" <<'EOF' +{"plan_contract_version":8,"identity":"plan-v8:sha256:test","required":["contracts.features"],"work":{"contracts.features":{"state":"required"}}} +EOF +cat >"$fake_reader" <<'EOF' +#!/usr/bin/env python3 +import json +import sys + +command = sys.argv[1] +if command in {"validate", "verify-checkout"}: + raise SystemExit(0) +if command == "matrix": + print(json.dumps({"include": [{ + "id": "checksums", + "group": "Checksums", + "feature_roots": "crc16,crc24,crc32,crc64", + "runtime_profiles": "runtime.diagnostics", + "full": False, + }]})) + raise SystemExit(0) +raise SystemExit(2) +EOF +chmod +x "$fake_reader" +selected_matrix="$TMP_ROOT/selected-matrix.json" +env -u BASH_ENV PATH="$fake_bin:$PATH" MOCK_LOG="$command_log" REAL_CARGO="$real_cargo" \ + RAIL_PLAN_FILE="$fake_plan" RAIL_PLAN_READER="$fake_reader" \ + "$EXECUTOR" matrix >"$selected_matrix" +jq -e ' + ([.include[].profiles | split(",")[]] | index("compile.crc16")) != null + and ([.include[].profiles | split(",")[]] | index("compile.full")) != null + and ([.include[].profiles | split(",")[]] | index("runtime.diagnostics")) != null + and ([.include[].profiles | split(",")[]] | index("compile.sha2")) == null + and ([.include[].profiles | split(",")[]] | index("runtime.std-full")) == null +' "$selected_matrix" >/dev/null || fail "selected Cargo Rail groups did not narrow the feature matrix" + +: >"$command_log" +run_executor "$TMP_ROOT/selected.out" selected compile 1/2 compile.crc16,compile.crc32 +selected_compile_count=$(grep -c '^cargo check ' "$command_log") +[[ "$selected_compile_count" -eq 2 ]] || fail "selected execution did not run exactly two compile profiles" +grep -Fq 'shard 1/2' "$TMP_ROOT/selected.out" || fail "selected execution lost its planned shard identity" +grep -F -- '--features crc16' "$command_log" >/dev/null || fail "selected CRC16 profile did not run" +grep -F -- '--features crc32' "$command_log" >/dev/null || fail "selected CRC32 profile did not run" +if grep -F -- '--features sha2' "$command_log" >/dev/null; then + fail "selected execution ran an unselected profile" +fi + +: >"$command_log" +run_executor "$TMP_ROOT/compile-1.out" compile 1/2 +run_executor "$TMP_ROOT/compile-2.out" compile 2/2 +compile_count=$(grep -c '^cargo check ' "$command_log") +[[ "$compile_count" -eq 59 ]] || fail "expected 59 compile commands, found $compile_count" +compile_unique=$(grep '^cargo check ' "$command_log" | LC_ALL=C sort -u | wc -l | tr -d ' ') +[[ "$compile_unique" -eq 59 ]] || fail "compile shards overlap or omit a unique graph" +metadata_count=$(grep -c '^cargo metadata ' "$command_log") +[[ "$metadata_count" -eq 2 ]] || fail "the one compile alias was not verified exactly once" +if grep -Fq 'cargo clean' "$command_log"; then + fail "feature execution still deletes Cargo artifacts" +fi + +: >"$command_log" +run_executor "$TMP_ROOT/runtime-1.out" runtime 1/3 +run_executor "$TMP_ROOT/runtime-2.out" runtime 2/3 +run_executor "$TMP_ROOT/runtime-3.out" runtime 3/3 +runtime_count=$(grep -c '^cargo test ' "$command_log") +expected_runtime_count=${#RUNTIME_TEST_CASES[@]} +[[ "$runtime_count" -eq "$expected_runtime_count" ]] \ + || fail "expected $expected_runtime_count focused runtime commands, found $runtime_count" +runtime_unique=$(grep '^cargo test ' "$command_log" | LC_ALL=C sort -u | wc -l | tr -d ' ') +[[ "$runtime_unique" -eq "$expected_runtime_count" ]] \ + || fail "runtime shards overlap or omit a test case" +profile_count=$(cat "$TMP_ROOT"/runtime-*.out | grep -c '^ profile ') +[[ "$profile_count" -eq 9 ]] || fail "runtime shards did not execute nine profiles exactly once" + +full_count=$(grep '^cargo test ' "$command_log" | grep -c -- '--lib --tests') +[[ "$full_count" -eq 3 ]] || fail "expected three complete behavior baselines, found $full_count" +grep '^cargo test ' "$command_log" \ + | grep -F -- '--features std,full,serde --test serde_roundtrip' >/dev/null \ + || fail "public Serde delta is not focused" +grep '^cargo test ' "$command_log" \ + | grep -F -- '--features std,parallel --lib' >/dev/null \ + || fail "parallel delta lost its unit tests" +grep '^cargo test ' "$command_log" \ + | grep -F -- '--features std,parallel --test argon2_parallel' >/dev/null \ + || fail "parallel delta lost its integration tests" +grep '^cargo test ' "$command_log" \ + | grep -F -- '--features std,full,getrandom --test rsa_public_key -- private_key_outputs_verify_and_decrypt' >/dev/null \ + || fail "entropy RSA coverage is not filtered to the gated tests" +grep '^cargo test ' "$command_log" \ + | grep -F -- '--features std,full,diag --test rsa_public_key -- pss_encoded_message_oracle_failures_are_opaque' >/dev/null \ + || fail "diagnostic RSA coverage lost an exact gated test" +if grep -Fq 'CARGO_TARGET_DIR' "$command_log"; then + fail "feature execution still isolates or deletes a target tree" +fi + +if run_executor "$TMP_ROOT/invalid-shard.out" compile 0/2; then + fail "zero-based shard was accepted" +fi + +: >"$command_log" +if env -u BASH_ENV PATH="$fake_bin:$PATH" MOCK_LOG="$command_log" REAL_CARGO="$real_cargo" MOCK_ALIAS_DIVERGES=1 \ + "$EXECUTOR" compile >"$TMP_ROOT/divergent-alias.out" 2>&1; then + fail "divergent compile alias was accepted" +fi +grep -Fq "no longer resolve identically" "$TMP_ROOT/divergent-alias.out" \ + || fail "divergent alias did not explain the failure" + +echo "Feature-contract executor regression tests passed" diff --git a/scripts/ci/feature-planning-test.sh b/scripts/ci/feature-planning-test.sh new file mode 100755 index 00000000..7c3d7895 --- /dev/null +++ b/scripts/ci/feature-planning-test.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Prove exact, shared-policy, and fail-closed Cargo Rail feature selection +# without changing the repository index or compiling product code. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +TMP_ROOT=$(mktemp -d) +trap 'rm -rf "$TMP_ROOT"' EXIT + +fail() { + echo "feature planning regression failure: $*" >&2 + exit 1 +} + +export GIT_INDEX_FILE="$TMP_ROOT/index" +export GIT_AUTHOR_NAME="rscrypto CI" +export GIT_AUTHOR_EMAIL="ci@rscrypto.invalid" +export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" +export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL" +export GIT_AUTHOR_DATE="2000-01-01T00:00:00Z" +export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" + +cd "$REPO_ROOT" +git read-tree HEAD +git add -A +base_tree=$(git write-tree) +base_commit=$(printf 'effective feature planning fixture\n' | git commit-tree "$base_tree" -p HEAD) + +plan_existing_path() { + local path=$1 + local label=$2 + local output=$3 + local mode blob tree commit + git read-tree "$base_tree" + mode=$(git ls-tree "$base_tree" -- "$path" | awk '{print $1}') + [[ -n "$mode" ]] || fail "fixture path is absent: $path" + blob=$( + { + git show "$base_commit:$path" + case "$path" in + *.toml) printf '\n# feature planning fixture: %s\n' "$label" ;; + *) printf '\n// feature planning fixture: %s\n' "$label" ;; + esac + } | git hash-object -w --stdin + ) + git update-index --add --cacheinfo "$mode,$blob,$path" + tree=$(git write-tree) + commit=$(printf '%s feature planning fixture\n' "$label" | git commit-tree "$tree" -p "$base_commit") + cargo rail plan --quiet --from "$base_commit" --to "$commit" --json >"$output" +} + +plan_new_path() { + local path=$1 + local output=$2 + local blob tree commit + git read-tree "$base_tree" + blob=$(printf '// unattributed feature planning fixture\n' | git hash-object -w --stdin) + git update-index --add --cacheinfo "100644,$blob,$path" + tree=$(git write-tree) + commit=$(printf 'unattributed feature planning fixture\n' | git commit-tree "$tree" -p "$base_commit") + cargo rail plan --quiet --from "$base_commit" --to "$commit" --json >"$output" +} + +rsa_plan="$TMP_ROOT/rsa.json" +plan_existing_path src/auth/rsa.rs rsa "$rsa_plan" +jq -e ' + .work["contracts.features"].scope.selection + | .kind == "selected" + and [.variants[].id] == ["signatures"] +' "$rsa_plan" >/dev/null || fail "RSA source did not select only the signature feature group" + +policy_plan="$TMP_ROOT/policy.json" +plan_existing_path Cargo.toml policy "$policy_plan" +jq -e ' + .work["contracts.features"].scope.selection + | .kind == "selected" + and [.variants[].id] == ["feature-policy"] +' "$policy_plan" >/dev/null || fail "feature policy did not select the full-profile policy group" + +unknown_plan="$TMP_ROOT/unknown.json" +plan_new_path tests/unattributed_feature_contract.rs "$unknown_plan" +jq -e ' + .work["contracts.features"].scope.selection.kind == "all" +' "$unknown_plan" >/dev/null || fail "unattributed feature input did not widen to every group" + +echo "Feature planning regression tests passed" diff --git a/scripts/ci/install-actionlint.sh b/scripts/ci/install-actionlint.sh new file mode 100755 index 00000000..b2d95036 --- /dev/null +++ b/scripts/ci/install-actionlint.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Install the exact prebuilt actionlint release used by the Actions policy lane. + +set -euo pipefail + +version=1.7.12 +archive="actionlint_${version}_linux_amd64.tar.gz" +url="https://github.com/rhysd/actionlint/releases/download/v${version}/${archive}" +sha256=8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 +temp_root=${RUNNER_TEMP:?RUNNER_TEMP is required} +bin_dir="$temp_root/rscrypto-actionlint/bin" +download_dir=$(mktemp -d "$temp_root/rscrypto-actionlint-download.XXXXXX") +trap 'rm -rf "$download_dir"' EXIT + +[[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]] || { + echo "actionlint installer requires Linux x86-64" >&2 + exit 1 +} +mkdir -p "$bin_dir" +curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location \ + --retry 3 --retry-delay 2 --output "$download_dir/$archive" "$url" +printf '%s %s\n' "$sha256" "$download_dir/$archive" | sha256sum --check --status +tar -xzf "$download_dir/$archive" -C "$download_dir" actionlint +install -m 755 "$download_dir/actionlint" "$bin_dir/actionlint" +[[ "$("$bin_dir/actionlint" -version)" == "$version" ]] || { + echo "actionlint version check failed" >&2 + exit 1 +} +printf '%s\n' "$bin_dir" >>"${GITHUB_PATH:?GITHUB_PATH is required}" diff --git a/scripts/ci/install-codecov.sh b/scripts/ci/install-codecov.sh deleted file mode 100755 index a5e29858..00000000 --- a/scripts/ci/install-codecov.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=../lib/ci-tool-integrity.sh -source "$SCRIPT_DIR/../lib/ci-tool-integrity.sh" - -runner_temp=${RUNNER_TEMP:?RUNNER_TEMP is required} -install_dir=$(mktemp -d "$runner_temp/rscrypto-codecov.XXXXXX") -ci_tool_download codecov "$install_dir" -chmod +x "$CI_TOOL_ARCHIVE_PATH" - -version_output=$("$CI_TOOL_ARCHIVE_PATH" --version) -expected_version=${CI_TOOL_VERSION#v} -if [[ "$version_output" =~ ([0-9]+\.[0-9]+\.[0-9]+) ]]; then - installed_version=${BASH_REMATCH[1]} -else - echo "CodeCov CLI version mismatch: expected $expected_version, got $version_output" >&2 - exit 1 -fi -[[ "$installed_version" == "$expected_version" ]] || { - echo "CodeCov CLI version mismatch: expected $expected_version, got $installed_version" >&2 - exit 1 -} - -if [[ -n "${GITHUB_OUTPUT:-}" ]]; then - printf 'binary=%s\n' "$CI_TOOL_ARCHIVE_PATH" >>"$GITHUB_OUTPUT" -fi diff --git a/scripts/ci/install-tools.sh b/scripts/ci/install-tools.sh index 7f1fafa4..87ae9ed0 100755 --- a/scripts/ci/install-tools.sh +++ b/scripts/ci/install-tools.sh @@ -1,26 +1,14 @@ #!/usr/bin/env bash -# Install CI tools through authenticated package-manager boundaries. -# Usage: install-tools.sh [standard|quality|release|rail|ci|supply-chain|bench|structural-bench|profile|ibm|fuzz|coverage|ct-linux|minimal|none] +# Install the few specialized tools not provided by runner images or rustup. +# Usage: install-tools.sh [supply-chain|fuzz|ct-linux|none] set -euo pipefail -MODE=${1:-standard} +MODE=${1:-} -CARGO_NEXTEST_VERSION=0.9.143 CARGO_DENY_VERSION=0.20.2 CARGO_AUDIT_VERSION=0.22.2 -CARGO_RAIL_VERSION=0.25.0 -JUST_VERSION=1.58.0 -ZIZMOR_VERSION=1.29.0 -CARGO_CRITERION_VERSION=1.1.0 -CRITCMP_VERSION=0.1.8 -GUNGRAUN_RUNNER_VERSION=0.19.4 -CARGO_SHOW_ASM_VERSION=0.2.62 -SAMPLY_VERSION=0.13.1 -CARGO_LLVM_LINES_VERSION=0.4.48 CARGO_FUZZ_VERSION=0.13.2 -CARGO_LLVM_COV_VERSION=0.9.0 -ACTIONLINT_VERSION=1.7.12 OPAM_REPOSITORY_COMMIT=49f6d620cf20ae0168cfcbeb2c33932e06cb4b74 OPAM_REPOSITORY_REMOTE=https://github.com/ocaml/opam-repository.git @@ -83,11 +71,7 @@ cargo_tool_version() { local binary=$1 local path=$2 local output - case "$binary" in - cargo-rail) output=$("$path" rail --version 2>&1) ;; - cargo-llvm-cov) output=$("$path" llvm-cov --version 2>&1) ;; - *) output=$("$path" --version 2>&1) ;; - esac + output=$("$path" --version 2>&1) extract_version "$output" } @@ -125,47 +109,6 @@ install_cargo_tool() { verify_cargo_tool "$package" "$version" "$binary" } -ensure_cargo_rail() { - local path actual - if [[ "${RSCRYPTO_AUTHENTICATED_CARGO_RAIL:-false}" == true ]]; then - path=$(command -v cargo-rail 2>/dev/null || true) - if [[ -n "$path" ]]; then - actual=$(cargo_tool_version cargo-rail "$path" 2>/dev/null || true) - if [[ "$actual" == "$CARGO_RAIL_VERSION" ]]; then - echo " cargo-rail: reusing authenticated $actual from cargo-rail-action" - return 0 - fi - fi - fail "cargo-rail-action reported an authenticated Cargo Rail install, but the exact binary is unavailable" - fi - install_cargo_tool cargo-rail "$CARGO_RAIL_VERSION" -} - -install_actionlint() { - local binary="$RSCRYPTO_CARGO_BIN/actionlint" - [[ "$(uname -s)" == MINGW* || "$(uname -s)" == MSYS* || "$(uname -s)" == CYGWIN* ]] \ - && binary+=.exe - - echo " actionlint: installing $ACTIONLINT_VERSION through the Go checksum database" - GOBIN="$RSCRYPTO_CARGO_BIN" \ - GOPATH="$RSCRYPTO_TOOL_ROOT/go" \ - GOMODCACHE="$RSCRYPTO_TOOL_ROOT/go/pkg/mod" \ - GOCACHE="$RSCRYPTO_TOOL_ROOT/go-build" \ - GOPROXY=https://proxy.golang.org \ - GOSUMDB=sum.golang.org \ - GOPRIVATE='' \ - GONOSUMDB='' \ - GOINSECURE='' \ - go install "github.com/rhysd/actionlint/cmd/actionlint@v$ACTIONLINT_VERSION" - - [[ -x "$binary" ]] || fail "actionlint install did not produce an executable" - local actual - actual=$(extract_version "$("$binary" -version 2>&1)") \ - || fail "unable to read actionlint version" - [[ "$actual" == "$ACTIONLINT_VERSION" ]] \ - || fail "actionlint reports $actual, expected $ACTIONLINT_VERSION" -} - require_ubuntu_24_04() { [[ -f /etc/os-release ]] || fail "Ubuntu 24.04 package metadata is required" local os_id os_version @@ -278,7 +221,6 @@ install_binsec() { "$BINSEC_PACKAGE" ) opam install --switch="$OPAMSWITCH" "${required_packages[@]}" -y - opam reinstall --switch="$OPAMSWITCH" "$BINSEC_PACKAGE" -y verify_opam_repository "$repository" verify_opam_packages @@ -307,88 +249,26 @@ install_ct_linux_packages() { echo "Installing CI tools (mode: $MODE)" case "$MODE" in - standard) - install_cargo_tool cargo-nextest "$CARGO_NEXTEST_VERSION" - install_cargo_tool cargo-deny "$CARGO_DENY_VERSION" - install_cargo_tool cargo-audit "$CARGO_AUDIT_VERSION" - ensure_cargo_rail - install_cargo_tool just "$JUST_VERSION" - ;; - quality) - install_cargo_tool just "$JUST_VERSION" - install_actionlint - install_cargo_tool zizmor "$ZIZMOR_VERSION" - ;; - release) - ensure_cargo_rail - install_cargo_tool cargo-deny "$CARGO_DENY_VERSION" - install_cargo_tool cargo-audit "$CARGO_AUDIT_VERSION" - ;; - rail) - ensure_cargo_rail - ;; - ci) - install_cargo_tool cargo-nextest "$CARGO_NEXTEST_VERSION" - install_cargo_tool just "$JUST_VERSION" - ;; supply-chain) install_cargo_tool cargo-deny "$CARGO_DENY_VERSION" install_cargo_tool cargo-audit "$CARGO_AUDIT_VERSION" - install_actionlint - install_cargo_tool zizmor "$ZIZMOR_VERSION" - ;; - ibm) - install_cargo_tool just "$JUST_VERSION" - ;; - bench) - install_cargo_tool cargo-criterion "$CARGO_CRITERION_VERSION" - install_cargo_tool critcmp "$CRITCMP_VERSION" - install_cargo_tool just "$JUST_VERSION" - ;; - structural-bench) - install_cargo_tool gungraun-runner "$GUNGRAUN_RUNNER_VERSION" - install_cargo_tool just "$JUST_VERSION" - ;; - profile) - install_cargo_tool cargo-show-asm "$CARGO_SHOW_ASM_VERSION" cargo-asm - install_cargo_tool samply "$SAMPLY_VERSION" - install_cargo_tool cargo-llvm-lines "$CARGO_LLVM_LINES_VERSION" - install_cargo_tool just "$JUST_VERSION" ;; fuzz) install_cargo_tool cargo-fuzz "$CARGO_FUZZ_VERSION" - install_cargo_tool just "$JUST_VERSION" - ;; - coverage) - install_cargo_tool cargo-llvm-cov "$CARGO_LLVM_COV_VERSION" - install_cargo_tool cargo-nextest "$CARGO_NEXTEST_VERSION" - install_cargo_tool just "$JUST_VERSION" - rustup component add llvm-tools-preview ;; ct-linux) - install_cargo_tool just "$JUST_VERSION" install_ct_linux_packages install_binsec ;; - minimal) - install_cargo_tool just "$JUST_VERSION" - ;; none) ;; *) echo "Unknown mode: $MODE" >&2 - echo "Usage: install-tools.sh [standard|quality|release|rail|ci|supply-chain|bench|structural-bench|profile|ibm|fuzz|coverage|ct-linux|minimal|none]" >&2 + echo "Usage: install-tools.sh [supply-chain|fuzz|ct-linux|none]" >&2 exit 2 ;; esac -if [[ "${RSCRYPTO_REQUIRE_CARGO_RAIL:-false}" == true ]]; then - case "$MODE" in - standard | release | rail) ;; - *) ensure_cargo_rail ;; - esac -fi - if [[ -n "${GITHUB_PATH:-}" ]]; then echo "$RSCRYPTO_CARGO_BIN" >>"$GITHUB_PATH" fi diff --git a/scripts/ci/materialize-rail-plan-test.sh b/scripts/ci/materialize-rail-plan-test.sh deleted file mode 100755 index 5e3e207e..00000000 --- a/scripts/ci/materialize-rail-plan-test.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -unset BASH_ENV - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -TMP_ROOT="$(mktemp -d)" -trap 'rm -rf "$TMP_ROOT"' EXIT - -fail() { - echo "Cargo Rail matrix materialization regression failure: $*" >&2 - exit 1 -} - -READER="$TMP_ROOT/read.py" -cat >"$READER" <<'PY' -#!/usr/bin/env python3 -import os -import sys - -command = sys.argv[1] -if command == "validate": - raise SystemExit(0) -if command == "is-required" and sys.argv[3] == "ci-policy": - print(os.environ["MOCK_POLICY_REQUIRED"]) - raise SystemExit(0) -if command == "is-required" and sys.argv[3] == "ci-suite": - print(os.environ["MOCK_SUITE_REQUIRED"]) - raise SystemExit(0) -if command == "matrix" and sys.argv[3] == "ci-suite": - if os.environ["MOCK_POLICY_REQUIRED"] == "true" or os.environ["MOCK_SUITE_REQUIRED"] != "true": - raise SystemExit(3) - print(os.environ["MOCK_MATRIX"]) - raise SystemExit(0) -raise SystemExit(2) -PY -chmod +x "$READER" - -PLAN="$TMP_ROOT/plan.json" -printf '{}\n' >"$PLAN" -SELECTED=$(jq -c '{include: [.variants[] | select(.id == "quality") | ({id} + .dimensions)]}' \ - "$REPO_ROOT/.config/ci-plan-variants.json") - -materialize() { - local policy=$1 - local matrix=$2 - local suite=${3:-true} - local output="$TMP_ROOT/output" - : >"$output" - MOCK_POLICY_REQUIRED=$policy \ - MOCK_SUITE_REQUIRED=$suite \ - MOCK_MATRIX=$matrix \ - "$REPO_ROOT/scripts/ci/materialize-rail-plan.sh" \ - "$PLAN" "$READER" "$output" "$REPO_ROOT/.config/ci-plan-variants.json" - sed -n 's/^matrix=//p' "$output" -} - -selected=$(materialize false "$SELECTED") -[[ $(jq '.include | length' <<<"$selected") -eq 1 ]] \ - || fail "a source-selected matrix did not stay narrow" -[[ $(jq -r '.include[0].work.id' <<<"$selected") == quality ]] \ - || fail "the selected row identity changed during lowering" - -policy_full=$(materialize true "$SELECTED") -[[ $(jq '.include | length' <<<"$policy_full") -eq 14 ]] \ - || fail "shared CI policy did not widen to the complete catalog" - -forced_full=$(materialize false all) -[[ $(jq '.include | length' <<<"$forced_full") -eq 14 ]] \ - || fail "Cargo Rail's all selection did not materialize the complete catalog" - -empty=$(materialize false unused false) -[[ $(jq '.include | length' <<<"$empty") -eq 0 ]] \ - || fail "skipped Cargo Rail work did not produce an empty matrix" - -echo "Cargo Rail matrix materialization regression tests passed" diff --git a/scripts/ci/materialize-rail-plan.sh b/scripts/ci/materialize-rail-plan.sh deleted file mode 100755 index 19021ef5..00000000 --- a/scripts/ci/materialize-rail-plan.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PYTHON="$("$SCRIPT_DIR/../lib/python.sh" --print)" -PLAN_FILE=${1:?plan file is required} -PLAN_READER=${2:?plan reader is required} -GITHUB_OUTPUT_FILE=${3:?GitHub output file is required} -CATALOG=${4:-.config/ci-plan-variants.json} - -"$PYTHON" "$PLAN_READER" validate "$PLAN_FILE" -policy_required=$("$PYTHON" "$PLAN_READER" is-required "$PLAN_FILE" ci-policy) -if [[ "$policy_required" == true ]]; then - matrix=$(jq -c '{include: [.variants[] | {work: ({id} + .dimensions)}]}' "$CATALOG") -else - suite_required=$("$PYTHON" "$PLAN_READER" is-required "$PLAN_FILE" ci-suite) - if [[ "$suite_required" != true ]]; then - matrix='{"include":[]}' - else - matrix=$("$PYTHON" "$PLAN_READER" matrix "$PLAN_FILE" ci-suite) - if [[ "$matrix" == all ]]; then - matrix=$(jq -c '{include: [.variants[] | {work: ({id} + .dimensions)}]}' "$CATALOG") - else - matrix=$(jq -ce '{include: [.include[] | {work: .}]}' <<<"$matrix") - fi - fi -fi - -jq -e ' - .include | type == "array" and all(.[].work; - (.id | type == "string" and length > 0) - and (.display_name | type == "string" and length > 0) - and (.operation | type == "string" and length > 0) - and (.runner | type == "string" and length > 0) - and (.runner_type == "github" or .runner_type == "runson") - and (.timeout_minutes | type == "number" and floor == . and . > 0) - and (.tools_mode | type == "string") - and (.toolchain_contract | type == "string") - and (.toolchain_components | type == "string") - ) -' <<<"$matrix" >/dev/null - -has_suite=$(jq -r '.include | length > 0' <<<"$matrix") -printf 'matrix=%s\nhas_suite=%s\n' "$matrix" "$has_suite" >>"$GITHUB_OUTPUT_FILE" diff --git a/scripts/ci/native-check.sh b/scripts/ci/native-check.sh deleted file mode 100755 index 7e2d26ae..00000000 --- a/scripts/ci/native-check.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=../lib/common.sh -source "$SCRIPT_DIR/../lib/common.sh" - -ALL_TARGETS=false -for arg in "$@"; do - case "$arg" in - --all-targets) ALL_TARGETS=true ;; - *) - echo "usage: native-check.sh [--all-targets]" >&2 - exit 2 - ;; - esac -done - -apply_ci_resource_profile - -echo "Native validation: $(rustc -vV | sed -n 's/^host: //p')" - -echo "" -echo "Checking no-default-features library boundary..." -cargo check --locked --workspace --lib --no-default-features - -TARGET_ARGS=(--lib) -if [[ "$ALL_TARGETS" == true ]]; then - TARGET_ARGS=(--all-targets) -fi - -echo "" -echo "Checking all-feature native targets..." -cargo check --locked --workspace "${TARGET_ARGS[@]}" --all-features - -echo "" -echo "Linting all-feature native targets..." -cargo clippy --locked --workspace "${TARGET_ARGS[@]}" --all-features - -if [[ "$ALL_TARGETS" == true ]]; then - echo "" - echo "Building all-feature native targets..." - cargo build --locked --workspace --all-targets --all-features -fi - -echo "Native validation passed" diff --git a/scripts/ci/native-platform.sh b/scripts/ci/native-platform.sh new file mode 100755 index 00000000..f971832e --- /dev/null +++ b/scripts/ci/native-platform.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +set -euo pipefail + +[[ $# -ge 2 && $# -le 3 ]] || { + echo "usage: $0 [shallow|deep]" >&2 + exit 2 +} + +platform=$1 +target=$2 +depth=${3:-deep} +[[ "$depth" == shallow || "$depth" == deep ]] || { + echo "depth must be shallow or deep" >&2 + exit 2 +} + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../lib/common.sh +source "$SCRIPT_DIR/../lib/common.sh" + +fail() { + echo "native platform error: $*" >&2 + exit 1 +} + +assert_single_libtest() { + local test_name=$1 + shift + local listing count + listing=$("$@" --list) || fail "unable to list the test harness containing $test_name" + count=$(awk -v expected="$test_name: test" '$0 == expected { count++ } END { print count + 0 }' <<<"$listing") + [[ "$count" -eq 1 ]] || fail "expected exactly one libtest named $test_name; found $count" +} + +require_host() { + local actual + actual=$(rustc -vV | sed -n 's/^host: //p') + [[ "$actual" == "$target" ]] || fail "expected Rust host $target, found $actual" + + case "$platform" in + linux-arm64 | macos-arm64) [[ "$(uname -m)" == aarch64 || "$(uname -m)" == arm64 ]] ;; + macos-x64 | windows-x64) [[ "$(uname -m)" == x86_64 ]] ;; + windows-arm64) [[ "$(uname -m)" == aarch64 || "$(uname -m)" == arm64 ]] ;; + ibm-s390x) [[ "$(uname -m)" == s390x ]] ;; + ibm-power10) [[ "$(uname -m)" == ppc64le ]] ;; + rise-riscv) [[ "$(uname -m)" == riscv64 ]] ;; + amx) [[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]] ;; + *) fail "unsupported native platform: $platform" ;; + esac || fail "host architecture does not match $platform" +} + +run_amx() { + local flags test_name + flags=$(sed -n 's/^flags[[:space:]]*: //p' /proc/cpuinfo | head -n 1) + [[ " $flags " == *" amx_tile "* ]] || fail "intel-spr runner does not expose AMX-TILE" + export CARGO_PROFILE_TEST_DEBUG=0 + + test_name=linux_x86_64_amx_permission_and_cache_are_process_scoped + RSCRYPTO_REQUIRE_AMX=1 assert_single_libtest "$test_name" \ + cargo test --locked --test platform_amx_permission -- + RSCRYPTO_REQUIRE_AMX=1 cargo test --locked --test platform_amx_permission \ + "$test_name" -- --exact --nocapture + + test_name=platform::detect::tests::no_std_linux_x86_64_masks_compile_time_amx_without_a_permission_probe + local rustflags="-C target-feature=+amx-tile,+amx-bf16,+amx-int8" + RUSTFLAGS="$rustflags" assert_single_libtest "$test_name" \ + cargo test --locked --no-default-features --lib -- + RUSTFLAGS="$rustflags" cargo test --locked --no-default-features --lib \ + "$test_name" -- --exact --nocapture +} + +run_native_runtime() { + export RSCRYPTO_TEST_MODE=commit + if [[ "$platform" == rise-riscv ]]; then + export RSCRYPTO_CI_RESOURCE_PROFILE=constrained + apply_ci_resource_profile + elif [[ "$platform" == ibm-s390x ]]; then + export CARGO_TARGET_S390X_UNKNOWN_LINUX_GNU_RUSTFLAGS="-C target-feature=+vector" + fi + + cargo test --locked --lib --all-features + cargo run --locked --example introspect --features 'crc32,sha2,chacha20poly1305,diag' + + if [[ "$depth" == deep ]]; then + cargo test --locked --all-features \ + --test aead_kernel_equivalence \ + --test portable_fallback \ + --test vectored_dispatch + fi +} + +echo "Native platform proof: $platform ($target, $depth)" +uname -a +rustc -vV +command -v lscpu >/dev/null 2>&1 && lscpu || true +require_host + +case "$platform" in + windows-arm64) + cargo clippy --locked --workspace --lib --all-features + cargo test --locked --workspace --all-features --no-run + ;; + windows-x64) + cargo clippy --locked --workspace --lib --all-features + cargo test --locked --lib --all-features + cargo test --locked --features blake3 \ + --test blake3_official_vectors --test blake3_differential + ;; + linux-arm64 | macos-x64 | macos-arm64 | ibm-s390x | ibm-power10 | rise-riscv) + run_native_runtime + ;; + amx) run_amx ;; + *) fail "unsupported native platform: $platform" ;; +esac + +echo "Native platform proof passed: $platform ($target, $depth)" diff --git a/scripts/ci/package-release-ct-evidence.sh b/scripts/ci/package-release-ct-evidence.sh index ee4ec17b..f1a0001d 100755 --- a/scripts/ci/package-release-ct-evidence.sh +++ b/scripts/ci/package-release-ct-evidence.sh @@ -95,7 +95,33 @@ cp -R "$input_dir"/. "$staging/ct-evidence/" find CT-EVIDENCE-BUNDLE.json ct-evidence -type f -print0 | sort -z | xargs -0 sha256sum ) > "$manifest" -tar -czf "$bundle_path" -C "$staging" CT-EVIDENCE-BUNDLE.json CT-EVIDENCE-MANIFEST.txt ct-evidence +scripts/lib/python.sh - "$staging" "$bundle_path" <<'PY' +import gzip +from pathlib import Path +import sys +import tarfile + +root = Path(sys.argv[1]) +bundle = Path(sys.argv[2]) +members = [root / "CT-EVIDENCE-BUNDLE.json", root / "CT-EVIDENCE-MANIFEST.txt"] +members.extend((root / "ct-evidence").rglob("*")) +members.append(root / "ct-evidence") +members.sort(key=lambda path: path.relative_to(root).as_posix()) + +with bundle.open("wb") as raw: + with gzip.GzipFile(filename="", mode="wb", fileobj=raw, compresslevel=9, mtime=0) as compressed: + with tarfile.open(fileobj=compressed, mode="w", format=tarfile.PAX_FORMAT) as archive: + for path in members: + info = archive.gettarinfo(path, arcname=path.relative_to(root).as_posix()) + info.uid = info.gid = 0 + info.uname = info.gname = "" + info.mtime = 0 + if path.is_file(): + with path.open("rb") as source: + archive.addfile(info, source) + else: + archive.addfile(info) +PY bundle_sha256="$(sha256sum "$bundle_path" | awk '{print $1}')" if [[ -n "${GITHUB_OUTPUT:-}" ]]; then diff --git a/scripts/ci/publish-immutable-release-test.sh b/scripts/ci/publish-immutable-release-test.sh index 94d7c19d..a09a8393 100755 --- a/scripts/ci/publish-immutable-release-test.sh +++ b/scripts/ci/publish-immutable-release-test.sh @@ -82,9 +82,7 @@ publish() { --notes "$TMP_ROOT/notes.md" \ --asset "$TMP_ROOT/artifacts/rscrypto-1.2.3.crate" \ --asset "$TMP_ROOT/artifacts/rscrypto-1.2.3-source.tar.gz" \ - --asset "$TMP_ROOT/artifacts/SHA256SUMS" \ - --stable-asset "$TMP_ROOT/artifacts/rscrypto-1.2.3.crate" \ - --stable-asset "$TMP_ROOT/artifacts/rscrypto-1.2.3-source.tar.gz" + --asset "$TMP_ROOT/artifacts/SHA256SUMS" } if /bin/bash "$PUBLISHER" \ @@ -92,8 +90,7 @@ if /bin/bash "$PUBLISHER" \ --title "rscrypto v1.2.3" \ --notes "$TMP_ROOT/notes.md" \ --asset "$TMP_ROOT/artifacts/rscrypto-1.2.3.crate" \ - --asset "$TMP_ROOT/artifacts/rscrypto-1.2.3.crate" \ - --stable-asset "$TMP_ROOT/artifacts/rscrypto-1.2.3.crate" >/dev/null 2>&1; then + --asset "$TMP_ROOT/artifacts/rscrypto-1.2.3.crate" >/dev/null 2>&1; then echo "immutable release publisher accepted duplicate asset names" >&2 exit 1 fi @@ -131,7 +128,7 @@ if grep -Eq 'release (create|upload|edit)' "$FAKE_GH_LOG"; then echo "immutable release publisher modified an existing published release" >&2 exit 1 fi -[[ $(grep -c 'release verify-asset' "$FAKE_GH_LOG") -eq 2 ]] +[[ $(grep -c 'release verify-asset' "$FAKE_GH_LOG") -eq 3 ]] : > "$FAKE_GH_LOG" printf '%s\n' rscrypto-1.2.3-source.tar.gz rscrypto-1.2.3.crate > "$FAKE_GH_ASSETS" @@ -144,8 +141,8 @@ fi : > "$FAKE_GH_LOG" printf '%s\n' SHA256SUMS rscrypto-1.2.3-source.tar.gz rscrypto-1.2.3.crate > "$FAKE_GH_ASSETS" echo published > "$FAKE_GH_STATE" -if FAKE_GH_BAD_ASSET=rscrypto-1.2.3.crate publish >/dev/null 2>&1; then - echo "immutable release publisher accepted a mismatched stable asset" >&2 +if FAKE_GH_BAD_ASSET=SHA256SUMS publish >/dev/null 2>&1; then + echo "immutable release publisher accepted a mismatched asset" >&2 exit 1 fi diff --git a/scripts/ci/publish-immutable-release.sh b/scripts/ci/publish-immutable-release.sh index 1ab5bb50..b4e0c65e 100755 --- a/scripts/ci/publish-immutable-release.sh +++ b/scripts/ci/publish-immutable-release.sh @@ -10,11 +10,10 @@ Required: --title TITLE GitHub Release title --notes PATH Release notes file --asset PATH Asset to publish; repeat for every asset - --stable-asset PATH Asset that must match on a published-release rerun; repeat as needed An absent release is assembled as a draft and then published. An existing draft is repaired only when its final asset set is exact. An existing published -release is never modified and must have a valid immutable-release attestation. +release is never modified; every asset and the immutable release must verify. EOF } @@ -22,7 +21,6 @@ tag="" title="" notes="" assets=() -stable_assets=() while [[ $# -gt 0 ]]; do case "$1" in @@ -30,7 +28,6 @@ while [[ $# -gt 0 ]]; do --title) title=${2:?missing value for --title}; shift 2 ;; --notes) notes=${2:?missing value for --notes}; shift 2 ;; --asset) assets+=("${2:?missing value for --asset}"); shift 2 ;; - --stable-asset) stable_assets+=("${2:?missing value for --stable-asset}"); shift 2 ;; -h | --help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage; exit 2 ;; esac @@ -44,11 +41,6 @@ done echo "immutable release error: at least one --asset is required" >&2 exit 2 } -(( ${#stable_assets[@]} > 0 )) || { - echo "immutable release error: at least one --stable-asset is required" >&2 - exit 2 -} - asset_names=() for asset in "${assets[@]}"; do [[ -f "$asset" ]] || { @@ -63,24 +55,6 @@ duplicate_name=$(printf '%s\n' "${asset_names[@]}" | LC_ALL=C sort | uniq -d | h exit 1 } -for asset in "${stable_assets[@]}"; do - [[ -f "$asset" ]] || { - echo "immutable release error: stable asset is missing: $asset" >&2 - exit 1 - } - stable_present=false - for published_asset in "${assets[@]}"; do - if [[ "$published_asset" == "$asset" ]]; then - stable_present=true - break - fi - done - [[ "$stable_present" == true ]] || { - echo "immutable release error: stable asset must also be a published asset: $asset" >&2 - exit 1 - } -done - verify_attempts=${RSCRYPTO_RELEASE_VERIFY_ATTEMPTS:-18} verify_delay=${RSCRYPTO_RELEASE_VERIFY_DELAY:-10} [[ "$verify_attempts" =~ ^[1-9][0-9]*$ && "$verify_delay" =~ ^[0-9]+$ ]] || { @@ -123,10 +97,10 @@ fi if [[ "$release_state" == "false" ]]; then verify_immutable_release verify_asset_set - for asset in "${stable_assets[@]}"; do + for asset in "${assets[@]}"; do gh release verify-asset "$tag" "$asset" >/dev/null done - echo "Published immutable release already matches the stable assets" + echo "Published immutable release already matches every asset" exit 0 fi diff --git a/scripts/ci/release-ct-recovery-check-test.sh b/scripts/ci/release-ct-recovery-check-test.sh deleted file mode 100755 index 89eb2788..00000000 --- a/scripts/ci/release-ct-recovery-check-test.sh +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CHECKER="$SCRIPT_DIR/release-ct-recovery-check.sh" -TMP_ROOT=$(mktemp -d) -trap 'rm -rf "$TMP_ROOT"' EXIT - -mkdir -p "$TMP_ROOT/bin" -cat >"$TMP_ROOT/bin/gh" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -mode=${FAKE_GH_MODE:-success} -if [[ "$1" == "api" && "$2" == "repos/loadingalias/rscrypto/actions/runs/4242" ]]; then - branch=main - conclusion=success - event=workflow_dispatch - path=.github/workflows/ct.yaml - sha=$EXPECTED_WORKFLOW_SHA - status=completed - repo=loadingalias/rscrypto - [[ "$mode" == wrong-branch ]] && branch=recovery - [[ "$mode" == failed-run ]] && conclusion=failure - [[ "$mode" == wrong-event ]] && event=push - [[ "$mode" == incomplete-run ]] && status=in_progress - [[ "$mode" == wrong-workflow ]] && path=.github/workflows/weekly.yaml - [[ "$mode" == wrong-sha ]] && sha=0000000000000000000000000000000000000000 - [[ "$mode" == fork ]] && repo=attacker/rscrypto - cat <&2 -exit 2 -EOF -chmod +x "$TMP_ROOT/bin/gh" - -export EXPECTED_WORKFLOW_SHA=1234567890abcdef1234567890abcdef12345678 -export PATH="$TMP_ROOT/bin:$PATH" -unset BASH_ENV - -output="$TMP_ROOT/github-output" -GITHUB_OUTPUT="$output" "$CHECKER" \ - --run-id 4242 \ - --platform-group s390x \ - --workflow-commit "$EXPECTED_WORKFLOW_SHA" \ - --repo loadingalias/rscrypto >/dev/null -grep -Fxq 'recovery_run_id=4242' "$output" -grep -Fxq 'recovery_run_url=https://example.invalid/runs/4242' "$output" - -x86_output="$TMP_ROOT/github-output-x86_64" -FAKE_GH_GROUP=x86_64 GITHUB_OUTPUT="$x86_output" "$CHECKER" \ - --run-id 4242 \ - --platform-group x86_64 \ - --workflow-commit "$EXPECTED_WORKFLOW_SHA" \ - --repo loadingalias/rscrypto >/dev/null -grep -Fxq 'recovery_run_id=4242' "$x86_output" -grep -Fxq 'recovery_run_url=https://example.invalid/runs/4242' "$x86_output" - -for mode in \ - wrong-branch failed-run wrong-event incomplete-run wrong-workflow wrong-sha fork \ - failed-lane failed-complete missing-artifact expired-artifact empty-artifact \ - wrong-artifact extra-artifact; do - if FAKE_GH_MODE="$mode" "$CHECKER" \ - --run-id 4242 \ - --platform-group s390x \ - --workflow-commit "$EXPECTED_WORKFLOW_SHA" \ - --repo loadingalias/rscrypto >/dev/null 2>&1; then - echo "CT recovery check accepted $mode" >&2 - exit 1 - fi -done - -echo "Release CT recovery regression tests passed" diff --git a/scripts/ci/release-ct-recovery-check.sh b/scripts/ci/release-ct-recovery-check.sh deleted file mode 100755 index e1c0d2f3..00000000 --- a/scripts/ci/release-ct-recovery-check.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -usage() { - echo "usage: release-ct-recovery-check.sh --run-id ID --platform-group GROUP --workflow-commit SHA [--repo OWNER/REPO]" >&2 - exit 2 -} - -run_id="" -platform_group="" -workflow_commit="" -repo="${GITHUB_REPOSITORY:-loadingalias/rscrypto}" -while [[ $# -gt 0 ]]; do - case "$1" in - --run-id) run_id=${2:-}; shift 2 ;; - --platform-group) platform_group=${2:-}; shift 2 ;; - --workflow-commit) workflow_commit=${2:-}; shift 2 ;; - --repo) repo=${2:-}; shift 2 ;; - *) usage ;; - esac -done - -[[ "$run_id" =~ ^[1-9][0-9]*$ ]] || usage -[[ "$workflow_commit" =~ ^[0-9a-f]{40}$ ]] || usage -[[ "$repo" == */* ]] || usage - -required_jobs=("Resolve CT Matrix") -required_artifacts=() -case "$platform_group" in - s390x) - required_jobs+=("CT Full (IBM Z s390x) / run") - required_artifacts+=("ct-raw-ibm-s390x") - ;; - x86_64) - required_jobs+=( - "CT Full (AMD Zen4) / run" - "CT Full (AMD Zen5) / run" - "CT Full (Intel Ice Lake) / run" - "CT Full (Intel Sapphire Rapids) / run" - ) - required_artifacts+=( - "ct-raw-amd-zen4" - "ct-raw-amd-zen5" - "ct-raw-intel-icl" - "ct-raw-intel-spr" - ) - ;; - *) usage ;; -esac -required_jobs+=("Complete (CT)") - -run=$(gh api "repos/$repo/actions/runs/$run_id") -if ! jq -e \ - --arg repo "$repo" \ - --arg workflow_commit "$workflow_commit" ' - .event == "workflow_dispatch" - and .status == "completed" - and .conclusion == "success" - and .head_branch == "main" - and .head_sha == $workflow_commit - and .path == ".github/workflows/ct.yaml" - and .head_repository.full_name == $repo - ' <<<"$run" >/dev/null; then - echo "CT recovery run $run_id is not a successful protected-main ct.yaml dispatch for $workflow_commit." >&2 - exit 1 -fi - -jobs=$(gh run view "$run_id" --repo "$repo" --json jobs) -for name in "${required_jobs[@]}"; do - count=$(jq -r --arg name "$name" \ - '[.jobs[] | select(.name == $name and .conclusion == "success")] | length' <<<"$jobs") - if [[ "$count" -ne 1 ]]; then - echo "CT recovery run $run_id lacks one successful '$name' job." >&2 - exit 1 - fi -done - -artifacts=$(gh api "repos/$repo/actions/runs/$run_id/artifacts?per_page=100") -expected_artifacts=$(printf '%s\n' "${required_artifacts[@]}" | jq -Rsc 'split("\n") | map(select(length > 0))') -if ! jq -e --argjson expected "$expected_artifacts" ' - .total_count == ($expected | length) - and (.artifacts | length) == ($expected | length) - and ([.artifacts[].name] | sort) == ($expected | sort) - and all(.artifacts[]; .expired == false and .size_in_bytes > 0) -' <<<"$artifacts" >/dev/null; then - echo "CT recovery run $run_id does not contain exactly the live raw $platform_group artifact set." >&2 - exit 1 -fi - -run_url=$(jq -r '.html_url' <<<"$run") -if [[ -n ${GITHUB_OUTPUT:-} ]]; then - { - echo "recovery_run_id=$run_id" - echo "recovery_run_url=$run_url" - } >>"$GITHUB_OUTPUT" -fi - -echo "Validated $platform_group CT recovery run: $run_url" diff --git a/scripts/ci/release-evidence-check-test.sh b/scripts/ci/release-evidence-check-test.sh deleted file mode 100755 index 48e9e4d9..00000000 --- a/scripts/ci/release-evidence-check-test.sh +++ /dev/null @@ -1,320 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -unset BASH_ENV - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -CHECKER="$SCRIPT_DIR/release-evidence-check.sh" -TMP_ROOT=$(mktemp -d) -trap 'rm -rf "$TMP_ROOT"' EXIT - -mkdir -p "$TMP_ROOT/bin" -cat >"$TMP_ROOT/bin/gh" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -if [[ "$1 $2" == "run list" ]]; then - case " $* " in - *" --workflow weekly.yaml "*) - if [[ ${FAKE_GH_MODE:-success} == missing-weekly ]]; then - echo '[]' - elif [[ ${FAKE_GH_MODE:-success} == scheduled-weekly ]]; then - cat <&2 - exit 2 - ;; - esac - exit 0 -fi - -if [[ "$1 $2" == "run view" ]]; then - case "$3" in - 4242) - ct_conclusion=success - rsa_conclusion=success - graph_conclusion=success - release_conclusion=success - riscv_native_conclusion=success - riscv_ct_conclusion=success - if [[ ${FAKE_GH_MODE:-success} == failed-weekly-ct ]]; then - ct_conclusion=failure - fi - if [[ ${FAKE_GH_MODE:-success} == failed-rsa ]]; then - rsa_conclusion=failure - fi - if [[ ${FAKE_GH_MODE:-success} == failed-cargo-graph ]]; then - graph_conclusion=failure - fi - if [[ ${FAKE_GH_MODE:-success} == failed-release-gate ]]; then - release_conclusion=failure - fi - if [[ ${FAKE_GH_MODE:-success} == failed-riscv-native ]]; then - riscv_native_conclusion=failure - fi - if [[ ${FAKE_GH_MODE:-success} == failed-riscv-ct ]]; then - riscv_ct_conclusion=failure - fi - if [[ ${FAKE_GH_MODE:-success} == missing-riscv ]]; then - riscv_native_conclusion=missing - riscv_ct_conclusion=missing - fi - cat <&2 - exit 2 - ;; - esac - exit 0 -fi - -if [[ "$1" == "api" ]]; then - case " $* " in - *" repos/loadingalias/rscrypto/actions/runs/4242/artifacts?per_page=100 "*) - if [[ ${FAKE_GH_MODE:-success} == missing-weekly-raw ]]; then - cat <<'JSON' -{"total_count":2,"artifacts":[ - {"name":"ct-raw-amd-zen4","expired":false,"size_in_bytes":4096}, - {"name":"rsa-miri-linux-x64","expired":false,"size_in_bytes":512} -]} -JSON - elif [[ ${FAKE_GH_MODE:-success} == expired-weekly-raw ]]; then - cat <<'JSON' -{"total_count":4,"artifacts":[ - {"name":"ct-raw-amd-zen4","expired":false,"size_in_bytes":4096}, - {"name":"ct-raw-graviton4","expired":true,"size_in_bytes":4096}, - {"name":"ct-raw-rise-riscv","expired":false,"size_in_bytes":4096}, - {"name":"rsa-miri-linux-x64","expired":false,"size_in_bytes":512} -]} -JSON - elif [[ ${FAKE_GH_MODE:-success} == missing-riscv-raw ]]; then - cat <<'JSON' -{"total_count":3,"artifacts":[ - {"name":"ct-raw-amd-zen4","expired":false,"size_in_bytes":4096}, - {"name":"ct-raw-graviton4","expired":false,"size_in_bytes":4096}, - {"name":"rsa-miri-linux-x64","expired":false,"size_in_bytes":512} -]} -JSON - else - cat <<'JSON' -{"total_count":4,"artifacts":[ - {"name":"ct-raw-amd-zen4","expired":false,"size_in_bytes":4096}, - {"name":"ct-raw-graviton4","expired":false,"size_in_bytes":4096}, - {"name":"ct-raw-rise-riscv","expired":false,"size_in_bytes":4096}, - {"name":"rsa-miri-linux-x64","expired":false,"size_in_bytes":512} -]} -JSON - fi - ;; - *) - echo "unexpected gh api request: $*" >&2 - exit 2 - ;; - esac - exit 0 -fi - -echo "unexpected gh invocation: $*" >&2 -exit 2 -EOF -chmod +x "$TMP_ROOT/bin/gh" - -export PATH="$TMP_ROOT/bin:$PATH" - -fixture="$TMP_ROOT/repo" -mkdir -p "$fixture/src" "$fixture/scripts/ci" \ - "$fixture/tools/ct-binsec-harness" "$fixture/tools/ct-dudect" "$fixture/tools/ct-harness" -cat >"$fixture/Cargo.toml" <<'EOF' -[package] -name = "rscrypto" -version = "0.7.3" -edition = "2024" -EOF -for lock in \ - Cargo.lock \ - tools/ct-binsec-harness/Cargo.lock \ - tools/ct-dudect/Cargo.lock \ - tools/ct-harness/Cargo.lock; do - cat >"$fixture/$lock" <<'EOF' -version = 4 - -[[package]] -name = "rscrypto" -version = "0.7.3" -EOF -done -printf 'pub fn marker() {}\n' >"$fixture/src/lib.rs" -printf 'release helper v1\n' >"$fixture/scripts/ci/release-helper.sh" -printf '# Changelog\n' >"$fixture/CHANGELOG.md" - -git -C "$fixture" init -q -git -C "$fixture" config user.email test@example.invalid -git -C "$fixture" config user.name "Release Evidence Test" -git -C "$fixture" config commit.gpgsign false -git -C "$fixture" add . -git -C "$fixture" commit -qm "evidence" -evidence_sha=$(git -C "$fixture" rev-parse HEAD) - -export EXPECTED_SHA=$evidence_sha -output="$TMP_ROOT/github-output" -GITHUB_OUTPUT="$output" "$CHECKER" --root "$fixture" --commit "$evidence_sha" --repo loadingalias/rscrypto >/dev/null -grep -Fxq 'qualification_run_id=4242' "$output" -grep -Fxq 'qualification_run_url=https://example.invalid/runs/4242' "$output" -grep -Fxq "qualification_commit=$evidence_sha" "$output" -grep -Fxq 'qualification_version=0.7.3' "$output" -grep -Fxq 'qualification_evidence_mode=exact_commit' "$output" - -if FAKE_GH_MODE=missing-weekly "$CHECKER" --root "$fixture" --commit "$evidence_sha" --repo loadingalias/rscrypto >/dev/null 2>&1; then - echo "release evidence check accepted a missing exact-commit Qualification run" >&2 - exit 1 -fi - -if FAKE_GH_MODE=missing-riscv "$CHECKER" --root "$fixture" --commit "$evidence_sha" --repo loadingalias/rscrypto >/dev/null 2>&1; then - echo "release evidence check accepted missing exact-commit RISC-V evidence" >&2 - exit 1 -fi - -if FAKE_GH_MODE=scheduled-weekly "$CHECKER" --root "$fixture" --commit "$evidence_sha" --repo loadingalias/rscrypto >/dev/null 2>&1; then - echo "release evidence check promoted a scheduled Qualification assurance run" >&2 - exit 1 -fi - -if FAKE_GH_MODE=assurance-dispatch "$CHECKER" --root "$fixture" --commit "$evidence_sha" --repo loadingalias/rscrypto >/dev/null 2>&1; then - echo "release evidence check promoted a manually dispatched assurance run" >&2 - exit 1 -fi - -if FAKE_GH_MODE=failed-release-gate "$CHECKER" --root "$fixture" --commit "$evidence_sha" --repo loadingalias/rscrypto >/dev/null 2>&1; then - echo "release evidence check accepted a failed release-mode gate" >&2 - exit 1 -fi - -if FAKE_GH_MODE=failed-weekly-ct "$CHECKER" --root "$fixture" --commit "$evidence_sha" --repo loadingalias/rscrypto >/dev/null 2>&1; then - echo "release evidence check accepted failed CT evidence" >&2 - exit 1 -fi - -if FAKE_GH_MODE=failed-rsa "$CHECKER" --root "$fixture" --commit "$evidence_sha" --repo loadingalias/rscrypto >/dev/null 2>&1; then - echo "release evidence check accepted failed RSA evidence" >&2 - exit 1 -fi - -if FAKE_GH_MODE=failed-cargo-graph "$CHECKER" --root "$fixture" --commit "$evidence_sha" --repo loadingalias/rscrypto >/dev/null 2>&1; then - echo "release evidence check accepted failed exact-commit Cargo Graph Assurance" >&2 - exit 1 -fi - -if FAKE_GH_MODE=missing-weekly-raw "$CHECKER" --root "$fixture" --commit "$evidence_sha" --repo loadingalias/rscrypto >/dev/null 2>&1; then - echo "release evidence check accepted missing Qualification raw CT artifacts" >&2 - exit 1 -fi - -if FAKE_GH_MODE=expired-weekly-raw "$CHECKER" --root "$fixture" --commit "$evidence_sha" --repo loadingalias/rscrypto >/dev/null 2>&1; then - echo "release evidence check accepted expired Qualification raw CT artifacts" >&2 - exit 1 -fi - -if FAKE_GH_MODE=missing-riscv-raw "$CHECKER" --root "$fixture" --commit "$evidence_sha" --repo loadingalias/rscrypto >/dev/null 2>&1; then - echo "release evidence check accepted missing RISC-V raw CT artifacts" >&2 - exit 1 -fi - -if FAKE_GH_MODE=failed-riscv-native "$CHECKER" --root "$fixture" --commit "$evidence_sha" --repo loadingalias/rscrypto >/dev/null 2>&1; then - echo "release evidence check accepted failed RISC-V native evidence" >&2 - exit 1 -fi - -if FAKE_GH_MODE=failed-riscv-ct "$CHECKER" --root "$fixture" --commit "$evidence_sha" --repo loadingalias/rscrypto >/dev/null 2>&1; then - echo "release evidence check accepted failed RISC-V CT evidence" >&2 - exit 1 -fi - -sed -i.bak 's/0\.7\.3/0.7.4/g' "$fixture/Cargo.toml" "$fixture"/Cargo.lock "$fixture"/tools/*/Cargo.lock -rm -f "$fixture/Cargo.toml.bak" "$fixture/Cargo.lock.bak" "$fixture"/tools/*/Cargo.lock.bak -printf 'release helper v2\n' >"$fixture/scripts/ci/release-helper.sh" -printf '\n## 0.7.4\n' >>"$fixture/CHANGELOG.md" -git -C "$fixture" add . -git -C "$fixture" commit -qm "release-only delta" -release_sha=$(git -C "$fixture" rev-parse HEAD) - -: >"$output" -if GITHUB_OUTPUT="$output" "$CHECKER" --root "$fixture" --commit "$release_sha" --repo loadingalias/rscrypto >/dev/null 2>&1; then - echo "release evidence check promoted evidence across a release-only delta" >&2 - exit 1 -fi - -"$REPO_ROOT/scripts/lib/python.sh" - "$REPO_ROOT" "$fixture" "$evidence_sha" <<'PY' -import hashlib -import pathlib -import subprocess -import sys - -root = pathlib.Path(sys.argv[1]) -fixture = pathlib.Path(sys.argv[2]) -evidence_commit = sys.argv[3] -sys.path.insert(0, str(root / "scripts" / "ct")) - -from validate_release_evidence import sha256_file, sha256_git_file - -expected = hashlib.sha256(subprocess.check_output(["git", "show", f"{evidence_commit}:Cargo.lock"], cwd=fixture)).hexdigest() -assert sha256_git_file(fixture, evidence_commit, "Cargo.lock") == expected -assert sha256_git_file(fixture, evidence_commit, "Cargo.lock") != sha256_file(fixture / "Cargo.lock") -PY - -printf 'pub fn marker() { unreachable!() }\n' >"$fixture/src/lib.rs" -git -C "$fixture" add src/lib.rs -git -C "$fixture" commit -qm "runtime delta" -runtime_sha=$(git -C "$fixture" rev-parse HEAD) -if "$CHECKER" --root "$fixture" --commit "$runtime_sha" --repo loadingalias/rscrypto >/dev/null 2>&1; then - echo "release evidence check promoted evidence across a runtime source change" >&2 - exit 1 -fi - -echo "Release evidence regression tests passed" diff --git a/scripts/ci/release-evidence-check.sh b/scripts/ci/release-evidence-check.sh deleted file mode 100755 index 0b99435a..00000000 --- a/scripts/ci/release-evidence-check.sh +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -usage() { - echo "usage: release-evidence-check.sh --commit SHA [--repo OWNER/REPO] [--root PATH]" >&2 - exit 2 -} - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -commit="" -repo="${GITHUB_REPOSITORY:-loadingalias/rscrypto}" -root="$(git rev-parse --show-toplevel)" -while [[ $# -gt 0 ]]; do - case "$1" in - --commit) - commit=${2:-} - shift 2 - ;; - --repo) - repo=${2:-} - shift 2 - ;; - --root) - root=${2:-} - shift 2 - ;; - *) usage ;; - esac -done - -[[ "$commit" =~ ^[0-9a-fA-F]{40}$ ]] || usage -[[ "$repo" == */* ]] || usage - -qualification_runs=$(gh run list \ - --repo "$repo" \ - --workflow weekly.yaml \ - --status success \ - --json databaseId,headSha,status,conclusion,url,createdAt,event \ - --limit 100) - -select_qualification_run() { - local candidate=$1 - local selected - local run_id - local jobs - while IFS= read -r selected; do - run_id=$(jq -r '.databaseId' <<<"$selected") - jobs=$(gh run view "$run_id" --repo "$repo" --json jobs) - if jq -e ' - [.jobs[] | select(.name == "Complete (release)" and .conclusion == "success")] - | length == 1 - ' <<<"$jobs" >/dev/null; then - echo "$selected" - return 0 - fi - done < <(jq -c --arg commit "$candidate" ' - map(select( - .headSha == $commit - and .event == "workflow_dispatch" - and .status == "completed" - and .conclusion == "success" - )) - | sort_by(.createdAt) - | reverse - | .[] - ' <<<"$qualification_runs") - return 1 -} - -evidence_commit="$commit" -evidence_mode="exact_commit" -selected_qualification=$(select_qualification_run "$commit") || selected_qualification="" -if [[ -z "$selected_qualification" ]]; then - echo "No release-mode qualification is valid for release commit $commit." >&2 - echo "The Qualification workflow must be manually dispatched for the exact commit; scheduled or ancestor evidence cannot be promoted." >&2 - exit 1 -fi - -qualification_run_id=$(jq -r '.databaseId' <<<"$selected_qualification") -qualification_run_url=$(jq -r '.url' <<<"$selected_qualification") -evidence_version=$(git -C "$root" show "$evidence_commit:Cargo.toml" | "$SCRIPT_DIR/../lib/python.sh" -c \ - 'import sys, tomllib; print(tomllib.loads(sys.stdin.read())["package"]["version"])') -qualification_jobs=$(gh run view "$qualification_run_id" --repo "$repo" --json jobs) -qualification_artifacts=$(gh api --method GET "repos/$repo/actions/runs/$qualification_run_id/artifacts?per_page=100") - -require_job() { - local workflow=$1 - local run_id=$2 - local jobs=$3 - local name=$4 - local conclusion - conclusion=$(jq -r --arg name "$name" '[.jobs[] | select(.name == $name)] | if length == 1 then .[0].conclusion else "missing" end' <<<"$jobs") - if [[ "$conclusion" != "success" ]]; then - echo "Required $workflow job '$name' is $conclusion in run $run_id." >&2 - exit 1 - fi -} - -require_raw_ct_artifacts() { - local workflow=$1 - local run_id=$2 - local jobs=$3 - local artifacts=$4 - local expected - local returned - local total - local raw_total - local valid - local unique - expected=$(jq -r ' - [.jobs[] | select( - ( - (.name | startswith("Constant-Time Evidence (release) / CT Full (")) - or (.name | startswith("RISC-V CT Evidence (release) / CT Full (")) - ) - and .conclusion == "success" - )] - | length - ' <<<"$jobs") - returned=$(jq -r '.artifacts | length' <<<"$artifacts") - total=$(jq -r '.total_count' <<<"$artifacts") - raw_total=$(jq -r '[.artifacts[] | select(.name | startswith("ct-raw-"))] | length' <<<"$artifacts") - valid=$(jq -r '[.artifacts[] | select( - (.name | startswith("ct-raw-")) - and (.expired == false) - and (.size_in_bytes > 0) - )] | length' <<<"$artifacts") - unique=$(jq -r '[.artifacts[] | select(.name | startswith("ct-raw-")) | .name] | unique | length' <<<"$artifacts") - if [[ "$expected" -lt 1 ]]; then - echo "Required $workflow CT jobs are missing from run $run_id." >&2 - exit 1 - fi - if [[ "$returned" -ne "$total" ]]; then - echo "$workflow run $run_id returned only $returned of $total artifacts." >&2 - exit 1 - fi - if [[ "$raw_total" -ne "$expected" || "$valid" -ne "$expected" || "$unique" -ne "$expected" ]]; then - echo "$workflow run $run_id has $valid valid raw CT artifacts for $expected successful CT jobs." >&2 - exit 1 - fi -} - -require_job Qualification "$qualification_run_id" "$qualification_jobs" "Constant-Time Evidence (release) / Complete (CT)" -require_job Qualification "$qualification_run_id" "$qualification_jobs" "RSA Evidence (release) / Complete (RSA)" -require_job Qualification "$qualification_run_id" "$qualification_jobs" "RISC-V Native Evidence / run" -require_job Qualification "$qualification_run_id" "$qualification_jobs" "RISC-V CT Evidence (release) / Complete (CT)" -require_job Qualification "$qualification_run_id" "$qualification_jobs" "CI Suite (release) / Cargo Graph Assurance / run" -require_job Qualification "$qualification_run_id" "$qualification_jobs" "Complete (release)" -require_raw_ct_artifacts Qualification "$qualification_run_id" "$qualification_jobs" "$qualification_artifacts" - -if [[ -n ${GITHUB_OUTPUT:-} ]]; then - { - echo "qualification_run_id=$qualification_run_id" - echo "qualification_run_url=$qualification_run_url" - echo "qualification_commit=$evidence_commit" - echo "qualification_version=$evidence_version" - echo "qualification_evidence_mode=$evidence_mode" - } >>"$GITHUB_OUTPUT" -fi - -echo "Exact-commit release qualification passed: $qualification_run_url" diff --git a/scripts/ci/release-identity-test.sh b/scripts/ci/release-identity-test.sh index 713bde29..2cb4cb58 100755 --- a/scripts/ci/release-identity-test.sh +++ b/scripts/ci/release-identity-test.sh @@ -2,167 +2,44 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -SOURCE_PACKAGER="$SCRIPT_DIR/package-release-source.sh" -MANIFEST_WRITER="$SCRIPT_DIR/write-release-manifest.sh" +PACKAGER="$SCRIPT_DIR/package-release-source.sh" TMP_ROOT=$(mktemp -d) trap 'rm -rf "$TMP_ROOT"' EXIT fixture="$TMP_ROOT/repository" -mkdir -p "$fixture/.github/workflows" git -C "$TMP_ROOT" init -q -b main repository git -C "$fixture" config user.email test@example.com -git -C "$fixture" config user.name "Release Identity Test" +git -C "$fixture" config user.name "Release Source Test" git -C "$fixture" config commit.gpgsign false git -C "$fixture" config tag.gpgsign false - -cat > "$fixture/Cargo.toml" <<'EOF' +cat >"$fixture/Cargo.toml" <<'EOF' [package] name = "rscrypto" version = "1.2.3" edition = "2024" EOF -cat > "$fixture/Cargo.lock" <<'EOF' -# release identity fixture -version = 4 -EOF -cp "$REPO_ROOT/rust-toolchain.toml" "$fixture/rust-toolchain.toml" -cp "$REPO_ROOT/.github/workflows/release.yaml" "$fixture/.github/workflows/release.yaml" -toolchain_channel=$(sed -n 's/^channel = "\([^"]*\)"$/\1/p' "$fixture/rust-toolchain.toml") -[[ -n "$toolchain_channel" ]] || { - echo "release identity fixture could not resolve the toolchain channel" >&2 - exit 1 -} +printf 'version = 4\n' >"$fixture/Cargo.lock" +mkdir -p "$fixture/.github/workflows" +printf '[toolchain]\nchannel = "1.98.0"\n' >"$fixture/rust-toolchain.toml" +printf 'name: Release\n' >"$fixture/.github/workflows/release.yaml" git -C "$fixture" add . -git -C "$fixture" commit -qm "release fixture" +git -C "$fixture" commit -qm release commit=$(git -C "$fixture" rev-parse HEAD) git -C "$fixture" tag -a v1.2.3 -m "release v1.2.3" -artifacts="$TMP_ROOT/artifacts" -mkdir -p "$artifacts" "$TMP_ROOT/package/rscrypto-1.2.3" "$TMP_ROOT/ct" - -"$SOURCE_PACKAGER" \ - --root "$fixture" \ - --version 1.2.3 \ - --tag v1.2.3 \ - --commit "$commit" \ - --out "$artifacts" >/dev/null -"$SOURCE_PACKAGER" \ - --root "$fixture" \ - --version 1.2.3 \ - --tag v1.2.3 \ - --commit "$commit" \ - --out "$TMP_ROOT/reproduced" >/dev/null -cmp "$artifacts/rscrypto-1.2.3-source.tar.gz" "$TMP_ROOT/reproduced/rscrypto-1.2.3-source.tar.gz" - -jq -n --arg commit "$commit" \ - '{git: {sha1: $commit, dirty: false}, path_in_vcs: ""}' \ - > "$TMP_ROOT/package/rscrypto-1.2.3/.cargo_vcs_info.json" -tar -czf "$artifacts/rscrypto-1.2.3.crate" -C "$TMP_ROOT/package" rscrypto-1.2.3 - -jq -n --arg commit "$commit" '{ - schema_version: 1, - kind: "rscrypto.ct.release-evidence", - crate: "rscrypto", - crate_version: "1.2.3", - git_commit: $commit, - evidence_git_commit: $commit, - evidence_mode: "exact_commit" -}' > "$TMP_ROOT/ct/CT-EVIDENCE-BUNDLE.json" -tar -czf "$artifacts/rscrypto-1.2.3-ct-evidence.tar.gz" -C "$TMP_ROOT/ct" CT-EVIDENCE-BUNDLE.json - -jq -n --arg commit "$commit" '{ - schema_version: 3, - kind: "rscrypto.repository-controls", - release_commit: $commit -}' > "$artifacts/rscrypto-1.2.3-repository-controls.json" - -write_manifest() { - "$MANIFEST_WRITER" \ - --root "$fixture" \ - --version 1.2.3 \ - --tag v1.2.3 \ - --commit "$commit" \ - --source "${SOURCE_PATH:-$artifacts/rscrypto-1.2.3-source.tar.gz}" \ - --crate "$artifacts/rscrypto-1.2.3.crate" \ - --ct-evidence "${CT_PATH:-$artifacts/rscrypto-1.2.3-ct-evidence.tar.gz}" \ - --repository-controls "${CONTROLS_PATH:-$artifacts/rscrypto-1.2.3-repository-controls.json}" \ - --evidence-commit "${EVIDENCE_COMMIT:-$commit}" \ - --evidence-mode "${EVIDENCE_MODE:-exact_commit}" \ - --output "${MANIFEST_PATH:-$artifacts/rscrypto-1.2.3-release-manifest.json}" -} - -github_output="$TMP_ROOT/github-output" -GITHUB_OUTPUT="$github_output" write_manifest >/dev/null -manifest="$artifacts/rscrypto-1.2.3-release-manifest.json" -jq -e --arg commit "$commit" --arg toolchain_channel "$toolchain_channel" ' - .schema_version == 1 - and .kind == "rscrypto.release-manifest" - and .crate_version == "1.2.3" - and .release.tag == "v1.2.3" - and (.release.tag_object | test("^[0-9a-f]{40}$")) - and .release.git_commit == $commit - and (.release.git_tree | test("^[0-9a-f]{40}$")) - and .toolchain.channel == $toolchain_channel - and (.toolchain.manifest.sha256 | test("^[0-9a-f]{64}$")) - and .evidence.git_commit == $commit - and .evidence.mode == "exact_commit" - and .artifacts.source_archive.name == "rscrypto-1.2.3-source.tar.gz" - and .artifacts.source_archive.prefix == "rscrypto-1.2.3/" - and .artifacts.crate_package.name == "rscrypto-1.2.3.crate" -' "$manifest" >/dev/null -grep -Fxq "manifest_path=$manifest" "$github_output" -grep -Fxq "manifest_name=$(basename "$manifest")" "$github_output" -grep -Eq '^manifest_sha256=[0-9a-f]{64}$' "$github_output" - -if EVIDENCE_MODE=release_only_delta MANIFEST_PATH="$TMP_ROOT/ancestor-mode.json" write_manifest >/dev/null 2>&1; then - echo "release manifest accepted ancestor evidence mode" >&2 - exit 1 -fi -if EVIDENCE_COMMIT=0000000000000000000000000000000000000000 \ - MANIFEST_PATH="$TMP_ROOT/other-evidence-commit.json" write_manifest >/dev/null 2>&1; then - echo "release manifest accepted evidence for another commit" >&2 - exit 1 -fi - -tampered_source="$TMP_ROOT/rscrypto-1.2.3-source.tar.gz" -cp "$artifacts/rscrypto-1.2.3-source.tar.gz" "$tampered_source" -printf 'tampered' >> "$tampered_source" -if SOURCE_PATH="$tampered_source" MANIFEST_PATH="$TMP_ROOT/tampered-source.json" write_manifest >/dev/null 2>&1; then - echo "release manifest accepted a source archive not reproduced from the release commit" >&2 - exit 1 -fi - -bad_controls="$TMP_ROOT/rscrypto-1.2.3-repository-controls.json" -jq '.release_commit = "0000000000000000000000000000000000000000"' \ - "$artifacts/rscrypto-1.2.3-repository-controls.json" > "$bad_controls" -if CONTROLS_PATH="$bad_controls" MANIFEST_PATH="$TMP_ROOT/bad-controls.json" write_manifest >/dev/null 2>&1; then - echo "release manifest accepted repository controls for another commit" >&2 - exit 1 -fi - -bad_ct_dir="$TMP_ROOT/bad-ct" -mkdir -p "$bad_ct_dir" -jq '.git_commit = "0000000000000000000000000000000000000000"' \ - "$TMP_ROOT/ct/CT-EVIDENCE-BUNDLE.json" > "$bad_ct_dir/CT-EVIDENCE-BUNDLE.json" -bad_ct="$TMP_ROOT/rscrypto-1.2.3-ct-evidence.tar.gz" -tar -czf "$bad_ct" -C "$bad_ct_dir" CT-EVIDENCE-BUNDLE.json -if CT_PATH="$bad_ct" MANIFEST_PATH="$TMP_ROOT/bad-ct.json" write_manifest >/dev/null 2>&1; then - echo "release manifest accepted CT evidence for another commit" >&2 - exit 1 -fi - -git -C "$fixture" commit --allow-empty -qm "move release" -moved_commit=$(git -C "$fixture" rev-parse HEAD) -git -C "$fixture" tag -fa v1.2.3 -m "moved release" >/dev/null -if "$SOURCE_PACKAGER" \ - --root "$fixture" \ - --version 1.2.3 \ - --tag v1.2.3 \ - --commit "$commit" \ - --out "$TMP_ROOT/moved" >/dev/null 2>&1; then - echo "source packager accepted moved tag $moved_commit for release commit $commit" >&2 +"$PACKAGER" --root "$fixture" --version 1.2.3 --tag v1.2.3 \ + --commit "$commit" --out "$TMP_ROOT/first" >/dev/null +"$PACKAGER" --root "$fixture" --version 1.2.3 --tag v1.2.3 \ + --commit "$commit" --out "$TMP_ROOT/second" >/dev/null +cmp "$TMP_ROOT/first/rscrypto-1.2.3-source.tar.gz" \ + "$TMP_ROOT/second/rscrypto-1.2.3-source.tar.gz" + +git -C "$fixture" commit --allow-empty -qm moved +git -C "$fixture" tag -fa v1.2.3 -m moved >/dev/null +if "$PACKAGER" --root "$fixture" --version 1.2.3 --tag v1.2.3 \ + --commit "$commit" --out "$TMP_ROOT/moved" >/dev/null 2>&1; then + echo "source packager accepted a moved release tag" >&2 exit 1 fi -echo "Release identity regression tests passed" +echo "Release source identity regression tests passed" diff --git a/scripts/ci/release-preflight.sh b/scripts/ci/release-preflight.sh index 0e602f8f..aefc48a0 100755 --- a/scripts/ci/release-preflight.sh +++ b/scripts/ci/release-preflight.sh @@ -87,11 +87,8 @@ if ! grep -qE "^## \\[$tag_version\\]" CHANGELOG.md; then exit 1 fi -cargo rail config validate --strict -cargo rail config migrate --check -# Exact-commit Qualification release mode owns dependency and compiler-backed -# Cargo graph assurance. The release evidence gate verifies those named jobs -# before this packaging-only preflight can run. +# The release workflow runs exact-tag Qualification beside this packaging-only +# preflight and joins both results before publication. # `cargo rail release check` is a pre-tag gate. The release run consumes # `.changes` files before creating the signed tag, so tag preflight diff --git a/scripts/ci/release-recipes-test.sh b/scripts/ci/release-recipes-test.sh deleted file mode 100755 index 4dc3d1f7..00000000 --- a/scripts/ci/release-recipes-test.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" - -prepare_recipe=$(cd "$REPO_ROOT" && just --show release-prepare) -tag_recipe=$(cd "$REPO_ROOT" && just --show release-tag) -push_recipe=$(cd "$REPO_ROOT" && just --dry-run push 2>&1) - -grep -Fq "cargo rail release run rscrypto --bump auto --yes --pr" <<<"$prepare_recipe" -if grep -Fq "cargo rail release check" <<<"$prepare_recipe"; then - echo "release-prepare must not duplicate the plan built and applied by release run" >&2 - exit 1 -fi -if grep -Eq 'sync-release-locks|git (add|commit|push)' <<<"$prepare_recipe"; then - echo "release-prepare must remain one Cargo Rail transaction" >&2 - exit 1 -fi -[[ $(yq -oy -p toml eval '.release.auxiliary_cargo_manifests | length' "$REPO_ROOT/.config/rail.toml") == "39" ]] -grep -Fq "cargo rail release finalize rscrypto --yes --skip-publish" <<<"$tag_recipe" -# shellcheck disable=SC2016 # Match the literal command rendered by just. -grep -Fq 'scripts/ci/release-evidence-check.sh --commit "$(git rev-parse HEAD)"' <<<"$tag_recipe" -grep -Fq 'scripts/ci/repository-controls-evidence.sh' <<<"$tag_recipe" -if grep -Fq -- '--allow-redacted-bypass' <<<"$tag_recipe"; then - echo "the pre-tag repository controls gate must require full bypass visibility" >&2 - exit 1 -fi - -controls_line=$(grep -nF 'scripts/ci/repository-controls-evidence.sh' <<<"$tag_recipe" | cut -d: -f1) -evidence_line=$(grep -nF 'scripts/ci/release-evidence-check.sh' <<<"$tag_recipe" | cut -d: -f1) -finalize_line=$(grep -nF 'cargo rail release finalize' <<<"$tag_recipe" | cut -d: -f1) -if (( controls_line >= evidence_line || evidence_line >= finalize_line )); then - echo "release-tag must validate repository controls and exact-commit evidence before creating the tag" >&2 - exit 1 -fi - -if grep -Fq "cargo rail release check" <<<"$tag_recipe" || grep -Fq "cargo rail unify" <<<"$tag_recipe"; then - echo "release-tag must consume exact-commit evidence instead of repeating release preparation" >&2 - exit 1 -fi - -grep -Fq 'scripts/ci/pre-push.sh' <<<"$push_recipe" -grep -Fq 'git push --set-upstream origin HEAD' <<<"$push_recipe" -if (cd "$REPO_ROOT" && just --show push-full >/dev/null 2>&1); then - echo "push-full must not exist; just push is the single supported push command" >&2 - exit 1 -fi -if grep -Fq -- '--no-verify' <<<"$push_recipe"; then - echo "supported push recipes must not bypass Git hooks" >&2 - exit 1 -fi -if grep -Fq -- '--light' <<<"$push_recipe" || grep -Fq -- '--full' <<<"$push_recipe"; then - echo "the supported push recipe must not expose validation profiles" >&2 - exit 1 -fi - -echo "Release recipe regression tests passed" diff --git a/scripts/ci/remote-cache-recipes-test.sh b/scripts/ci/remote-cache-recipes-test.sh index 31777fd8..e5132858 100755 --- a/scripts/ci/remote-cache-recipes-test.sh +++ b/scripts/ci/remote-cache-recipes-test.sh @@ -17,10 +17,10 @@ cat >"$TMP_ROOT/bin/cargo" <<'SH' #!/usr/bin/env bash set -euo pipefail printf '%s\n' "$*" >>"$MOCK_CARGO_LOG" -if [[ ${MOCK_FAIL_SETUP:-0} == 1 && "$*" == "rail cache setup --remote "* ]]; then +if [[ ${MOCK_FAIL_APPLY:-0} == 1 && "$*" == "rail cache setup --remote "* ]]; then exit 40 fi -if [[ ${MOCK_FAIL_POSTCHECK:-0} == 1 && "$*" == "rail cache setup --check "* ]]; then +if [[ ${MOCK_FAIL_PREVIEW:-0} == 1 && "$*" == "rail cache setup --check "* ]]; then exit 41 fi SH @@ -33,12 +33,12 @@ export CARGO_RAIL_CACHE_MODE=read-write just --justfile "$REPO_ROOT/justfile" rail-cache-setup --max-size 10GiB cat >"$TMP_ROOT/expected-setup.log" <<'EOF' -rail cache setup --remote r2://rscrypto-cache.example/rscrypto/shared --remote-mode read-write --root-portability remap --max-size 10GiB rail cache setup --check --remote r2://rscrypto-cache.example/rscrypto/shared --remote-mode read-write --root-portability remap --max-size 10GiB -rail cache probe +rail cache setup --remote r2://rscrypto-cache.example/rscrypto/shared --remote-mode read-write --root-portability remap --max-size 10GiB +rail cache probe --json EOF cmp "$TMP_ROOT/expected-setup.log" "$MOCK_CARGO_LOG" \ - || fail "setup recipe did not preserve the canonical apply/check/probe transaction" + || fail "setup recipe did not preserve the canonical preview/apply/probe transaction" : >"$MOCK_CARGO_LOG" just --justfile "$REPO_ROOT/justfile" cache-status @@ -46,17 +46,19 @@ just --justfile "$REPO_ROOT/justfile" cache-status || fail "status recipe did not request local JSON telemetry" : >"$MOCK_CARGO_LOG" -if MOCK_FAIL_SETUP=1 just --justfile "$REPO_ROOT/justfile" rail-cache-setup --max-size 10GiB; then - fail "setup recipe continued after installation failed" +if MOCK_FAIL_PREVIEW=1 just --justfile "$REPO_ROOT/justfile" rail-cache-setup --max-size 10GiB \ + >/dev/null 2>&1; then + fail "setup recipe continued after preview failed" fi [[ $(wc -l <"$MOCK_CARGO_LOG" | tr -d ' ') == 1 ]] \ - || fail "setup recipe continued after installation failed" + || fail "setup recipe continued after preview failed" : >"$MOCK_CARGO_LOG" -if MOCK_FAIL_POSTCHECK=1 just --justfile "$REPO_ROOT/justfile" rail-cache-setup --max-size 10GiB; then - fail "setup recipe accepted a failed postcondition check" +if MOCK_FAIL_APPLY=1 just --justfile "$REPO_ROOT/justfile" rail-cache-setup --max-size 10GiB \ + >/dev/null 2>&1; then + fail "setup recipe accepted a failed installation" fi [[ $(wc -l <"$MOCK_CARGO_LOG" | tr -d ' ') == 2 ]] \ - || fail "setup recipe probed a policy that failed its postcondition check" + || fail "setup recipe probed a policy that failed installation" echo "remote cache recipe tests passed" diff --git a/scripts/ci/repository-controls-evidence-test.sh b/scripts/ci/repository-controls-evidence-test.sh deleted file mode 100755 index c6aa63f6..00000000 --- a/scripts/ci/repository-controls-evidence-test.sh +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -unset BASH_ENV - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -CHECKER="$SCRIPT_DIR/repository-controls-evidence.sh" -TMP_ROOT=$(mktemp -d) -trap 'rm -rf "$TMP_ROOT"' EXIT - -mkdir -p "$TMP_ROOT/bin" -cat >"$TMP_ROOT/bin/gh" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -endpoint=${!#} -case "$endpoint" in - repos/loadingalias/rscrypto) - echo '{"id":1115910108,"full_name":"loadingalias/rscrypto","visibility":"public","default_branch":"main"}' - ;; - repos/loadingalias/rscrypto/immutable-releases) - if [[ ${FAKE_GH_MODE:-success} == "immutability-unavailable" ]]; then - exit 1 - elif [[ ${FAKE_GH_MODE:-success} == "immutability-disabled" ]]; then - echo '{"enabled":false,"enforced_by_owner":false}' - else - echo '{"enabled":true,"enforced_by_owner":false}' - fi - ;; - 'repos/loadingalias/rscrypto/rulesets?includes_parents=true&per_page=100') - if [[ ${FAKE_GH_MODE:-success} == "missing-tag" ]]; then - echo '[{"id":19077982,"name":"protect-main","target":"branch","source_type":"Repository","source":"loadingalias/rscrypto","enforcement":"active"}]' - else - echo '[{"id":19077982,"name":"protect-main","target":"branch","source_type":"Repository","source":"loadingalias/rscrypto","enforcement":"active"},{"id":19077983,"name":"protect-release-tags","target":"tag","source_type":"Repository","source":"loadingalias/rscrypto","enforcement":"active"}]' - fi - ;; - repos/loadingalias/rscrypto/rulesets/19077982) - jq --arg mode "${FAKE_GH_MODE:-success}" ' - . + { - id: 19077982, - source_type: "Repository", - source: "loadingalias/rscrypto", - current_user_can_bypass: "never", - created_at: "2026-07-16T21:26:51-04:00", - updated_at: "2026-07-16T21:26:51-04:00" - } - | if $mode == "bypass" then - .bypass_actors = [{actor_id: 5, actor_type: "RepositoryRole", bypass_mode: "always"}] - | .current_user_can_bypass = "always" - elif $mode == "inactive" then - .enforcement = "disabled" - elif $mode == "missing-check" then - .rules |= map(select(.type != "required_status_checks")) - elif $mode == "wrong-app" then - (.rules[] | select(.type == "required_status_checks") | .parameters.required_status_checks[0].integration_id) = 42 - elif $mode == "redacted" then - del(.bypass_actors, .current_user_can_bypass) - elif $mode == "redacted-self" then - del(.bypass_actors) - else - . - end - ' "$EXPECTED_POLICY" - ;; - repos/loadingalias/rscrypto/rulesets/19077983) - jq --arg mode "${FAKE_GH_MODE:-success}" ' - . + { - id: 19077983, - source_type: "Repository", - source: "loadingalias/rscrypto", - current_user_can_bypass: "never", - created_at: "2026-07-17T12:00:00-04:00", - updated_at: "2026-07-17T12:00:00-04:00" - } - | if $mode == "tag-bypass" then - .bypass_actors = [{actor_id: 5, actor_type: "RepositoryRole", bypass_mode: "always"}] - | .current_user_can_bypass = "always" - elif $mode == "inactive-tag" then - .enforcement = "disabled" - elif $mode == "mutable-tag" then - .rules |= map(select(.type != "update")) - elif $mode == "redacted" then - del(.bypass_actors, .current_user_can_bypass) - elif $mode == "redacted-self" then - del(.bypass_actors) - else - . - end - ' "$EXPECTED_TAG_POLICY" - ;; - 'repos/loadingalias/rscrypto/rules/branches/main?per_page=100') - jq --arg mode "${FAKE_GH_MODE:-success}" ' - [.rules[] | . + { - ruleset_source_type: "Repository", - ruleset_source: "loadingalias/rscrypto", - ruleset_id: 19077982 - }] - | if $mode == "wrong-effective" then map(select(.type != "deletion")) else . end - ' "$EXPECTED_POLICY" - ;; - repos/loadingalias/rscrypto/commits/main) - jq -n --arg sha "$EXPECTED_SHA" '{sha: $sha}' - ;; - *) - echo "unexpected gh api endpoint: $endpoint" >&2 - exit 2 - ;; -esac -EOF -chmod +x "$TMP_ROOT/bin/gh" - -export PATH="$TMP_ROOT/bin:$PATH" -export EXPECTED_POLICY="$REPO_ROOT/.github/rulesets/protect-main.json" -export EXPECTED_TAG_POLICY="$REPO_ROOT/.github/rulesets/protect-release-tags.json" -export EXPECTED_SHA -EXPECTED_SHA=$(git -C "$REPO_ROOT" rev-parse HEAD) - -output="$TMP_ROOT/repository-controls.json" -github_output="$TMP_ROOT/github-output" -GITHUB_OUTPUT="$github_output" "$CHECKER" \ - --root "$REPO_ROOT" \ - --repo loadingalias/rscrypto \ - --commit "$EXPECTED_SHA" \ - --output "$output" >/dev/null - -jq -e --arg commit "$EXPECTED_SHA" ' - .schema_version == 3 - and .kind == "rscrypto.repository-controls" - and .release_commit == $commit - and .repository.full_name == "loadingalias/rscrypto" - and .repository.default_branch == "main" - and .repository.default_branch_sha == $commit - and .live.ruleset.current_user_can_bypass == "never" - and .live.release_tag_ruleset.current_user_can_bypass == "never" - and .live.release_immutability.enabled == true - and .validation.release_immutability.status == "verified_enabled" - and ([.live.release_tag_ruleset.rules[] | select(.type == "update")] | length == 1) - and ([.live.effective_rules[] | select(.type == "required_status_checks")] | length == 1) -' "$output" >/dev/null -grep -Fxq "evidence_name=$(basename "$output")" "$github_output" -grep -Fxq "evidence_path=$output" "$github_output" -grep -Eq '^evidence_sha256=[0-9a-f]{64}$' "$github_output" - -policy_root="$TMP_ROOT/policy-root" -mkdir -p "$policy_root/.github/rulesets" "$policy_root/.github/repository-settings" -cp "$REPO_ROOT/.github/rulesets/protect-main.json" "$policy_root/.github/rulesets/protect-main.json" -cp "$REPO_ROOT/.github/rulesets/protect-release-tags.json" "$policy_root/.github/rulesets/protect-release-tags.json" -cp "$REPO_ROOT/.github/repository-settings/release-immutability.json" "$policy_root/.github/repository-settings/release-immutability.json" -"$CHECKER" \ - --root "$REPO_ROOT" \ - --policy-root "$policy_root" \ - --repo loadingalias/rscrypto \ - --commit "$EXPECTED_SHA" \ - --output "$TMP_ROOT/reviewed-policy-root.json" >/dev/null - -for mode in bypass inactive missing-check wrong-app wrong-effective missing-tag tag-bypass inactive-tag mutable-tag \ - immutability-disabled immutability-unavailable redacted; do - if FAKE_GH_MODE=$mode "$CHECKER" \ - --root "$REPO_ROOT" \ - --repo loadingalias/rscrypto \ - --commit "$EXPECTED_SHA" \ - --output "$TMP_ROOT/$mode.json" >/dev/null 2>&1; then - echo "repository controls check accepted invalid mode: $mode" >&2 - exit 1 - fi -done - -redacted_output="$TMP_ROOT/redacted.json" -FAKE_GH_MODE=redacted "$CHECKER" \ - --root "$REPO_ROOT" \ - --repo loadingalias/rscrypto \ - --commit "$EXPECTED_SHA" \ - --output "$redacted_output" \ - --allow-redacted-bypass >/dev/null -jq -e ' - .validation.bypass_actors.status == "redacted_by_github_api" - and .validation.release_tag_bypass_actors.status == "redacted_by_github_api" -' "$redacted_output" >/dev/null - -FAKE_GH_MODE=redacted-self "$CHECKER" \ - --root "$REPO_ROOT" \ - --repo loadingalias/rscrypto \ - --commit "$EXPECTED_SHA" \ - --output "$TMP_ROOT/redacted-self.json" \ - --allow-redacted-bypass >/dev/null - -if FAKE_GH_MODE=bypass "$CHECKER" \ - --root "$REPO_ROOT" \ - --repo loadingalias/rscrypto \ - --commit "$EXPECTED_SHA" \ - --output "$TMP_ROOT/bypass-allowed.json" \ - --allow-redacted-bypass >/dev/null 2>&1; then - echo "repository controls check treated a visible bypass as redacted" >&2 - exit 1 -fi - -immutability_unavailable_output="$TMP_ROOT/immutability-unavailable.json" -FAKE_GH_MODE=immutability-unavailable "$CHECKER" \ - --root "$REPO_ROOT" \ - --repo loadingalias/rscrypto \ - --commit "$EXPECTED_SHA" \ - --output "$immutability_unavailable_output" \ - --allow-redacted-bypass >/dev/null -jq -e ' - .validation.release_immutability.status == "unavailable_to_workflow_token" - and .live.release_immutability == null -' "$immutability_unavailable_output" >/dev/null - -echo "Repository controls evidence regression tests passed" diff --git a/scripts/ci/repository-controls-evidence.sh b/scripts/ci/repository-controls-evidence.sh deleted file mode 100755 index 1d0ca5d1..00000000 --- a/scripts/ci/repository-controls-evidence.sh +++ /dev/null @@ -1,325 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -usage() { - echo "usage: repository-controls-evidence.sh --commit SHA --output PATH [--repo OWNER/REPO] [--root PATH] [--policy-root PATH] [--allow-redacted-bypass]" >&2 - exit 2 -} - -commit="" -output="" -repo="${GITHUB_REPOSITORY:-loadingalias/rscrypto}" -root="$(git rev-parse --show-toplevel)" -policy_root="" -allow_redacted_bypass=false - -while [[ $# -gt 0 ]]; do - case "$1" in - --commit) - commit=${2:-} - shift 2 - ;; - --output) - output=${2:-} - shift 2 - ;; - --repo) - repo=${2:-} - shift 2 - ;; - --root) - root=${2:-} - shift 2 - ;; - --policy-root) - policy_root=${2:-} - shift 2 - ;; - --allow-redacted-bypass) - allow_redacted_bypass=true - shift - ;; - *) usage ;; - esac -done - -[[ "$commit" =~ ^[0-9a-fA-F]{40}$ ]] || usage -[[ -n "$output" ]] || usage -[[ "$repo" == */* ]] || usage -git -C "$root" cat-file -e "$commit^{commit}" 2>/dev/null || { - echo "repository controls error: commit is not present in the local repository: $commit" >&2 - exit 1 -} - -if [[ -z "$policy_root" ]]; then - policy_root=$root -fi - -policy_path="$policy_root/.github/rulesets/protect-main.json" -tag_policy_path="$policy_root/.github/rulesets/protect-release-tags.json" -immutability_policy_path="$policy_root/.github/repository-settings/release-immutability.json" -[[ -f "$policy_path" ]] || { - echo "repository controls error: missing policy $policy_path" >&2 - exit 1 -} -[[ -f "$tag_policy_path" ]] || { - echo "repository controls error: missing policy $tag_policy_path" >&2 - exit 1 -} -[[ -f "$immutability_policy_path" ]] || { - echo "repository controls error: missing policy $immutability_policy_path" >&2 - exit 1 -} -jq -e 'type == "object"' "$policy_path" >/dev/null || { - echo "repository controls error: policy must be a JSON object" >&2 - exit 1 -} -jq -e 'type == "object"' "$tag_policy_path" >/dev/null || { - echo "repository controls error: policy must be a JSON object" >&2 - exit 1 -} -jq -e '.enabled == true and (keys == ["enabled"])' "$immutability_policy_path" >/dev/null || { - echo "repository controls error: release immutability policy must require enabled=true" >&2 - exit 1 -} - -api() { - gh api -H "X-GitHub-Api-Version: 2026-03-10" "$1" -} - -canonical_filter=' - def canonical: - if type == "object" then - to_entries | sort_by(.key) | map(.value |= canonical) | from_entries - elif type == "array" then - map(canonical) | sort_by(tojson) - else - . - end; - canonical -' -full_policy_filter="{name, target, enforcement, conditions, bypass_actors, rules} | $canonical_filter" -public_policy_filter="{name, target, enforcement, conditions, rules} | $canonical_filter" - -repository=$(api "repos/$repo") -immutability_status="verified_enabled" -if immutability=$(api "repos/$repo/immutable-releases" 2>/dev/null); then - jq -e '.enabled == true' <<<"$immutability" >/dev/null || { - echo "repository controls error: immutable releases are not enabled" >&2 - exit 1 - } -else - if [[ "$allow_redacted_bypass" != true ]]; then - echo "repository controls error: immutable release settings are unavailable to this token" >&2 - exit 1 - fi - immutability=null - immutability_status="unavailable_to_workflow_token" -fi -default_branch=$(jq -er '.default_branch | select(type == "string" and length > 0)' <<<"$repository") -ruleset_name=$(jq -er '.name | select(type == "string" and length > 0)' "$policy_path") -tag_ruleset_name=$(jq -er '.name | select(type == "string" and length > 0)' "$tag_policy_path") -rulesets=$(api "repos/$repo/rulesets?includes_parents=true&per_page=100") -ruleset_id=$(jq -er --arg name "$ruleset_name" --arg source "$repo" ' - [.[] | select(.name == $name and .target == "branch" and .source_type == "Repository" and .source == $source)] - | if length == 1 then .[0].id else error("expected exactly one repository branch ruleset named " + $name) end -' <<<"$rulesets") -tag_ruleset_id=$(jq -er --arg name "$tag_ruleset_name" --arg source "$repo" ' - [.[] | select(.name == $name and .target == "tag" and .source_type == "Repository" and .source == $source)] - | if length == 1 then .[0].id else error("expected exactly one repository tag ruleset named " + $name) end -' <<<"$rulesets") - -ruleset=$(api "repos/$repo/rulesets/$ruleset_id") -tag_ruleset=$(api "repos/$repo/rulesets/$tag_ruleset_id") -branch_path=$(jq -rn --arg branch "$default_branch" '$branch | @uri') -effective_rules=$(api "repos/$repo/rules/branches/$branch_path?per_page=100") -default_branch_commit=$(api "repos/$repo/commits/$branch_path") -default_branch_sha=$(jq -er '.sha | select(test("^[0-9a-fA-F]{40}$"))' <<<"$default_branch_commit") - -bypass_evidence="verified_empty" -policy_filter=$full_policy_filter -if ! jq -e 'has("bypass_actors") and (.bypass_actors | type == "array")' <<<"$ruleset" >/dev/null; then - if [[ "$allow_redacted_bypass" != true ]]; then - echo "repository controls error: GitHub redacted bypass actors; rerun with repository-rules write access" >&2 - exit 1 - fi - jq -e ' - (.bypass_actors // null) == null - and ((.current_user_can_bypass // null) == null or .current_user_can_bypass == "never") - ' <<<"$ruleset" >/dev/null || { - echo "repository controls error: partial bypass data is not acceptable" >&2 - exit 1 - } - bypass_evidence="redacted_by_github_api" - policy_filter=$public_policy_filter -fi - -if ! diff -u \ - <(jq -S "$policy_filter" "$policy_path") \ - <(jq -S "$policy_filter" <<<"$ruleset"); then - echo "repository controls error: live ruleset differs from $policy_path" >&2 - exit 1 -fi - -jq -e --arg repo "$repo" --arg bypass_evidence "$bypass_evidence" ' - .source_type == "Repository" - and .source == $repo - and ( - if $bypass_evidence == "verified_empty" then - .bypass_actors == [] and .current_user_can_bypass == "never" - else - true - end - ) -' <<<"$ruleset" >/dev/null || { - echo "repository controls error: the live ruleset is not repository-owned or permits this actor to bypass it" >&2 - exit 1 -} - -tag_bypass_evidence="verified_empty" -tag_policy_filter=$full_policy_filter -if ! jq -e 'has("bypass_actors") and (.bypass_actors | type == "array")' <<<"$tag_ruleset" >/dev/null; then - if [[ "$allow_redacted_bypass" != true ]]; then - echo "repository controls error: GitHub redacted release-tag bypass actors; rerun with repository-rules write access" >&2 - exit 1 - fi - jq -e ' - (.bypass_actors // null) == null - and ((.current_user_can_bypass // null) == null or .current_user_can_bypass == "never") - ' <<<"$tag_ruleset" >/dev/null || { - echo "repository controls error: partial release-tag bypass data is not acceptable" >&2 - exit 1 - } - tag_bypass_evidence="redacted_by_github_api" - tag_policy_filter=$public_policy_filter -fi - -if ! diff -u \ - <(jq -S "$tag_policy_filter" "$tag_policy_path") \ - <(jq -S "$tag_policy_filter" <<<"$tag_ruleset"); then - echo "repository controls error: live ruleset differs from $tag_policy_path" >&2 - exit 1 -fi - -jq -e --arg repo "$repo" --arg bypass_evidence "$tag_bypass_evidence" ' - .source_type == "Repository" - and .source == $repo - and ( - if $bypass_evidence == "verified_empty" then - .bypass_actors == [] and .current_user_can_bypass == "never" - else - true - end - ) -' <<<"$tag_ruleset" >/dev/null || { - echo "repository controls error: the live release-tag ruleset is not repository-owned or permits this actor to bypass it" >&2 - exit 1 -} - -if ! diff -u \ - <(jq -S ".rules | $canonical_filter" "$policy_path") \ - <(jq -S --argjson id "$ruleset_id" \ - "[.[] | select(.ruleset_id == \$id) | {type, parameters: (.parameters // null)} | if .parameters == null then del(.parameters) else . end] | $canonical_filter" \ - <<<"$effective_rules"); then - echo "repository controls error: effective rules on $default_branch differ from the repository ruleset" >&2 - exit 1 -fi - -captured_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ') -policy_sha256=$(sha256sum "$policy_path" | awk '{print $1}') -tag_policy_sha256=$(sha256sum "$tag_policy_path" | awk '{print $1}') -immutability_policy_sha256=$(sha256sum "$immutability_policy_path" | awk '{print $1}') -output_dir=$(dirname "$output") -mkdir -p "$output_dir" -output_tmp="${output}.tmp.$$" -trap 'rm -f "$output_tmp"' EXIT - -jq -nS \ - --arg captured_at "$captured_at" \ - --arg commit "$commit" \ - --arg default_branch "$default_branch" \ - --arg default_branch_sha "$default_branch_sha" \ - --arg bypass_evidence "$bypass_evidence" \ - --arg tag_bypass_evidence "$tag_bypass_evidence" \ - --arg immutability_status "$immutability_status" \ - --arg policy_path ".github/rulesets/protect-main.json" \ - --arg policy_sha256 "$policy_sha256" \ - --arg tag_policy_path ".github/rulesets/protect-release-tags.json" \ - --arg tag_policy_sha256 "$tag_policy_sha256" \ - --arg immutability_policy_path ".github/repository-settings/release-immutability.json" \ - --arg immutability_policy_sha256 "$immutability_policy_sha256" \ - --arg repo "$repo" \ - --argjson repository "$repository" \ - --argjson policy "$(jq -S "$canonical_filter" "$policy_path")" \ - --argjson tag_policy "$(jq -S "$canonical_filter" "$tag_policy_path")" \ - --argjson immutability_policy "$(jq -S "$canonical_filter" "$immutability_policy_path")" \ - --argjson immutability "$immutability" \ - --argjson ruleset "$ruleset" \ - --argjson tag_ruleset "$tag_ruleset" \ - --argjson effective_rules "$effective_rules" ' - { - schema_version: 3, - kind: "rscrypto.repository-controls", - captured_at: $captured_at, - release_commit: $commit, - repository: { - id: $repository.id, - full_name: $repo, - visibility: $repository.visibility, - default_branch: $default_branch, - default_branch_sha: $default_branch_sha - }, - policy: { - path: $policy_path, - sha256: $policy_sha256, - expected: $policy - }, - release_tag_policy: { - path: $tag_policy_path, - sha256: $tag_policy_sha256, - expected: $tag_policy - }, - release_immutability_policy: { - path: $immutability_policy_path, - sha256: $immutability_policy_sha256, - expected: $immutability_policy - }, - validation: { - bypass_actors: { - expected: [], - status: $bypass_evidence, - current_user_can_bypass: ($ruleset.current_user_can_bypass // null) - }, - release_tag_bypass_actors: { - expected: [], - status: $tag_bypass_evidence, - current_user_can_bypass: ($tag_ruleset.current_user_can_bypass // null) - }, - release_immutability: { - expected: {enabled: true}, - status: $immutability_status - } - }, - live: { - ruleset: $ruleset, - release_tag_ruleset: $tag_ruleset, - release_immutability: $immutability, - effective_rules: $effective_rules - } - } -' >"$output_tmp" -mv "$output_tmp" "$output" -trap - EXIT - -evidence_sha256=$(sha256sum "$output" | awk '{print $1}') -if [[ -n ${GITHUB_OUTPUT:-} ]]; then - { - echo "evidence_name=$(basename "$output")" - echo "evidence_path=$output" - echo "evidence_sha256=$evidence_sha256" - } >>"$GITHUB_OUTPUT" -fi - -echo "Repository controls match $policy_path" -echo "Evidence: $output" -echo "SHA-256: $evidence_sha256" diff --git a/scripts/ci/require-work.sh b/scripts/ci/require-work.sh new file mode 100755 index 00000000..6b569035 --- /dev/null +++ b/scripts/ci/require-work.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Assert that a direct executor is authorized by the active saved plan. + +set -euo pipefail + +mode=all +if [[ ${1:-} == --any ]]; then + mode=any + shift +fi +[[ $# -gt 0 ]] || { echo "Usage: $0 [--any] WORK_ID..." >&2; exit 2; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../lib/rail-plan.sh +source "$SCRIPT_DIR/../lib/rail-plan.sh" +rail_prime_plan + +required_count=0 +for work_id in "$@"; do + status=0 + rail_work_required "$work_id" || status=$? + case "$status" in + 0) required_count=$((required_count + 1)) ;; + 1) + if [[ "$mode" == all ]]; then + echo "Cargo Rail did not authorize $work_id" >&2 + exit 2 + fi + ;; + *) exit "$status" ;; + esac +done + +if [[ "$mode" == any && "$required_count" -eq 0 ]]; then + echo "Cargo Rail did not authorize any requested work" >&2 + exit 2 +fi diff --git a/scripts/ci/run-rust-job-test.sh b/scripts/ci/run-rust-job-test.sh deleted file mode 100755 index 22e0b043..00000000 --- a/scripts/ci/run-rust-job-test.sh +++ /dev/null @@ -1,318 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -unset BASH_ENV - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -DISPATCHER="$SCRIPT_DIR/run-rust-job.sh" -TMP_ROOT="$(mktemp -d)" -trap 'rm -rf "$TMP_ROOT"' EXIT - -fail() { - echo "rust job regression failure: $*" >&2 - exit 1 -} - -expect_failure() { - if "$@" >/dev/null 2>&1; then - fail "command unexpectedly succeeded: $*" - fi -} - -FIXTURE="$TMP_ROOT/repo" -CAPTURE="$TMP_ROOT/capture" -BIN="$TMP_ROOT/bin" -AMX_BIN="$TMP_ROOT/amx-bin" -mkdir -p "$FIXTURE/scripts/ci" "$FIXTURE/scripts/ct" "$FIXTURE/scripts/lib" \ - "$CAPTURE" "$BIN" "$AMX_BIN" -cp "$DISPATCHER" "$FIXTURE/scripts/ci/run-rust-job.sh" - -cat >"$BIN/just" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail -printf '%s\n' "$@" >"$RSCRYPTO_CI_CAPTURE_DIR/just.args" -printf 'jobs=%s concurrency=%s\n' \ - "${RSCRYPTO_FUZZ_JOBS:-}" "${RSCRYPTO_FUZZ_TARGET_CONCURRENCY:-}" \ - >"$RSCRYPTO_CI_CAPTURE_DIR/just.env" -exit "${RSCRYPTO_MOCK_JUST_STATUS:-0}" -EOF - -for command in uname lscpu sed rustc cargo; do - cat >"$BIN/$command" <<'EOF' -#!/usr/bin/env bash -exit 0 -EOF -done - -cat >"$FIXTURE/scripts/ci/run-bench.sh" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail -printf '%s' "$BENCH_ONLY" >"$RSCRYPTO_CI_CAPTURE_DIR/bench-targets" -printf '%s' "$BENCH_FILTER" >"$RSCRYPTO_CI_CAPTURE_DIR/bench-filter" -printf '%s' "$BENCH_QUICK" >"$RSCRYPTO_CI_CAPTURE_DIR/bench-quick" -printf '%s' "$BENCH_OUTPUT_DIR" >"$RSCRYPTO_CI_CAPTURE_DIR/bench-output-dir" -printf '%s' "$BENCH_RESULTS_DIR" >"$RSCRYPTO_CI_CAPTURE_DIR/bench-results-dir" -EOF - -cat >"$FIXTURE/scripts/ct/full.py" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail -printf '%s\n' "$@" >"$RSCRYPTO_CI_CAPTURE_DIR/ct.args" -EOF - -cat >"$FIXTURE/scripts/lib/python.sh" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail -if [[ "${1:-}" == --print ]]; then - printf '%s\n' "$0" - exit 0 -fi -printf '%s\n' "$@" >"$RSCRYPTO_CI_CAPTURE_DIR/ct-package.args" -EOF - -chmod +x \ - "$BIN/just" \ - "$BIN/uname" \ - "$BIN/lscpu" \ - "$BIN/sed" \ - "$BIN/rustc" \ - "$BIN/cargo" \ - "$FIXTURE/scripts/ci/run-bench.sh" \ - "$FIXTURE/scripts/ct/full.py" \ - "$FIXTURE/scripts/lib/python.sh" - -cat >"$AMX_BIN/uname" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail -case "${1:-}" in - -s) echo Linux ;; - -m) echo x86_64 ;; - *) echo "Linux AMX fixture" ;; -esac -EOF - -cat >"$AMX_BIN/lscpu" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail -echo "Architecture: x86_64" -EOF - -cat >"$AMX_BIN/rustc" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail -echo "host: x86_64-unknown-linux-gnu" -EOF - -cat >"$AMX_BIN/sed" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail -if [[ "${*: -1}" == /proc/cpuinfo ]]; then - if [[ "$*" == *'s/^flags'* ]]; then - echo "amx_tile" - else - echo "flags : amx_tile" - fi -else - /usr/bin/sed "$@" -fi -EOF - -cat >"$AMX_BIN/cargo" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail -printf 'CARGO_PROFILE_TEST_DEBUG=%s RUSTFLAGS=%s RSCRYPTO_REQUIRE_AMX=%s :: %s\n' \ - "${CARGO_PROFILE_TEST_DEBUG:-}" "${RUSTFLAGS:-}" "${RSCRYPTO_REQUIRE_AMX:-}" "$*" \ - >>"$RSCRYPTO_CI_CAPTURE_DIR/amx-cargo.args" - -if [[ " $* " != *" --list "* ]]; then - exit 0 -fi - -case "${RSCRYPTO_MOCK_AMX_LIST:-none}" in - all) - if [[ " $* " == *" --test platform_amx_permission "* ]]; then - echo "linux_x86_64_amx_permission_and_cache_are_process_scoped: test" - elif [[ " $* " == *" --lib "* ]]; then - echo "platform::detect::tests::no_std_linux_x86_64_masks_compile_time_amx_without_a_permission_probe: test" - fi - ;; - integration-only) - if [[ " $* " == *" --test platform_amx_permission "* ]]; then - echo "linux_x86_64_amx_permission_and_cache_are_process_scoped: test" - fi - ;; - none) ;; - *) exit 91 ;; -esac -EOF - -chmod +x "$AMX_BIN/uname" "$AMX_BIN/lscpu" "$AMX_BIN/rustc" "$AMX_BIN/sed" "$AMX_BIN/cargo" - -TEST_PATH="$BIN:$PATH" -RUNNER=(env PATH="$TEST_PATH" RSCRYPTO_CI_CAPTURE_DIR="$CAPTURE" bash "$FIXTURE/scripts/ci/run-rust-job.sh") - -RSCRYPTO_CI_OPERATION=quality "${RUNNER[@]}" -[[ $(<"$CAPTURE/just.args") == "ci-check" ]] || fail "quality selected the wrong command" - -RSCRYPTO_CI_OPERATION=examples "${RUNNER[@]}" -[[ $(<"$CAPTURE/just.args") == "test-examples" ]] || fail "examples selected the wrong command" - -mkdir -p \ - "$FIXTURE/fuzz/corpus/hash_cshake256" \ - "$FIXTURE/fuzz/artifacts/hash_cshake256" \ - "$FIXTURE/fuzz-packages/hash-sha3/corpus/hash_cshake256" \ - "$FIXTURE/fuzz-packages/hash-sha3/artifacts/hash_cshake256" -printf '%s' full-corpus >"$FIXTURE/fuzz/corpus/hash_cshake256/seed" -printf '%s' scoped-corpus >"$FIXTURE/fuzz-packages/hash-sha3/corpus/hash_cshake256/seed" -printf '%s' full-crash >"$FIXTURE/fuzz/artifacts/hash_cshake256/crash-fixture" -printf '%s' scoped-crash >"$FIXTURE/fuzz-packages/hash-sha3/artifacts/hash_cshake256/crash-fixture" - -fuzz_status=0 -env \ - PATH="$TEST_PATH" \ - RSCRYPTO_CI_CAPTURE_DIR="$CAPTURE" \ - RSCRYPTO_CI_OPERATION=fuzz \ - RSCRYPTO_MOCK_JUST_STATUS=23 \ - bash "$FIXTURE/scripts/ci/run-rust-job.sh" >/dev/null 2>&1 \ - || fuzz_status=$? -[[ "$fuzz_status" -eq 23 ]] || fail "fuzz operation did not preserve the fuzz command's failure status" -[[ $(<"$CAPTURE/just.env") == "jobs=1 concurrency=2" ]] \ - || fail "fuzz operation did not reserve both runner cores for independent targets" -[[ -f "$FIXTURE/fuzz-output/corpus.tar.gz" ]] || fail "fuzz failure did not produce a corpus archive" -tar -tzf "$FIXTURE/fuzz-output/corpus.tar.gz" >"$CAPTURE/fuzz-archive.entries" -grep -Fxq 'fuzz/corpus/hash_cshake256/seed' "$CAPTURE/fuzz-archive.entries" \ - || fail "fuzz failure archive omitted the full-workspace corpus" -grep -Fxq 'fuzz-packages/hash-sha3/corpus/hash_cshake256/seed' "$CAPTURE/fuzz-archive.entries" \ - || fail "fuzz failure archive omitted the scoped corpus" -[[ -f "$FIXTURE/fuzz/artifacts/hash_cshake256/crash-fixture" ]] \ - || fail "fuzz failure removed the full-workspace crash artifact" -[[ -f "$FIXTURE/fuzz-packages/hash-sha3/artifacts/hash_cshake256/crash-fixture" ]] \ - || fail "fuzz failure removed the scoped crash artifact" - -sentinel="$TMP_ROOT/injected" -# shellcheck disable=SC2016 # Command substitution is an intentional literal injection payload. -shell_payload='$(touch "'"$sentinel"'"); echo injected; #' -expect_failure env \ - PATH="$TEST_PATH" \ - RSCRYPTO_CI_CAPTURE_DIR="$CAPTURE" \ - RSCRYPTO_CI_OPERATION="quality; $shell_payload" \ - bash "$FIXTURE/scripts/ci/run-rust-job.sh" -[[ ! -e "$sentinel" ]] || fail "operation input was evaluated as shell code" - -expect_failure env \ - PATH="$TEST_PATH" \ - RSCRYPTO_CI_CAPTURE_DIR="$CAPTURE" \ - RSCRYPTO_CI_OPERATION=dependabot-smoke \ - RSCRYPTO_CI_BASE_SHA="$shell_payload" \ - bash "$FIXTURE/scripts/ci/run-rust-job.sh" -[[ ! -e "$sentinel" ]] || fail "commit input was evaluated as shell code" - -multiline_payload="$shell_payload"$'\n'"second line" -env \ - PATH="$TEST_PATH" \ - RSCRYPTO_CI_CAPTURE_DIR="$CAPTURE" \ - RSCRYPTO_CI_OPERATION=benchmark \ - RSCRYPTO_CI_PLATFORM=amd-zen4 \ - RSCRYPTO_CI_BENCH_TARGETS="$multiline_payload" \ - RSCRYPTO_CI_BENCH_FILTER="$multiline_payload" \ - RSCRYPTO_CI_BENCH_QUICK=true \ - bash "$FIXTURE/scripts/ci/run-rust-job.sh" >/dev/null -[[ $(<"$CAPTURE/bench-targets") == "$multiline_payload" ]] || fail "benchmark targets were not passed literally" -[[ $(<"$CAPTURE/bench-filter") == "$multiline_payload" ]] || fail "benchmark filter was not passed literally" -[[ $(<"$CAPTURE/bench-quick") == "true" ]] || fail "benchmark boolean was not preserved" -[[ $(<"$CAPTURE/bench-output-dir") == "target/benchmark_results" ]] \ - || fail "benchmark output used the wrong staging directory" -[[ $(<"$CAPTURE/bench-results-dir") == "target/benchmark_results" ]] \ - || fail "benchmark results used the wrong staging directory" -[[ ! -e "$sentinel" ]] || fail "benchmark input was evaluated as shell code" - -# shellcheck disable=SC2016 # Command substitution is an intentional literal injection payload. -ct_payload='$(touch${IFS}'"$sentinel"')' -env \ - PATH="$TEST_PATH" \ - RSCRYPTO_CI_CAPTURE_DIR="$CAPTURE" \ - RSCRYPTO_CI_OPERATION=constant-time \ - RSCRYPTO_CI_RUNNER=test-runner \ - RSCRYPTO_CI_PLATFORM=amd-zen4 \ - RSCRYPTO_CI_TARGET=x86_64-unknown-linux-gnu \ - RSCRYPTO_CI_DUDECT_TIMEOUT=1800 \ - RSCRYPTO_CI_DUDECT_FILTER="$ct_payload" \ - RSCRYPTO_CI_DUDECT_GATE=required \ - RSCRYPTO_CI_BINSEC_TIMEOUT=900 \ - RSCRYPTO_CI_UPLOAD_RAW_ARTIFACTS=false \ - bash "$FIXTURE/scripts/ci/run-rust-job.sh" >/dev/null -grep -Fxq -- "$ct_payload" "$CAPTURE/ct.args" || fail "DudeCT filter was not passed as one literal argument" -if grep -Fxq -- '--raw' "$CAPTURE/ct-package.args"; then - fail "compact CT packaging included raw artifacts" -fi -[[ ! -e "$sentinel" ]] || fail "DudeCT filter was evaluated as shell code" - -env \ - PATH="$TEST_PATH" \ - RSCRYPTO_CI_CAPTURE_DIR="$CAPTURE" \ - RSCRYPTO_CI_OPERATION=constant-time \ - RSCRYPTO_CI_RUNNER=test-runner \ - RSCRYPTO_CI_PLATFORM=amd-zen4 \ - RSCRYPTO_CI_TARGET=x86_64-unknown-linux-gnu \ - RSCRYPTO_CI_DUDECT_TIMEOUT=1800 \ - RSCRYPTO_CI_DUDECT_GATE=required \ - RSCRYPTO_CI_BINSEC_TIMEOUT=900 \ - RSCRYPTO_CI_UPLOAD_RAW_ARTIFACTS=true \ - bash "$FIXTURE/scripts/ci/run-rust-job.sh" >/dev/null -grep -Fxq -- '--raw' "$CAPTURE/ct-package.args" || fail "release CT packaging omitted raw artifacts" - -expect_failure env \ - PATH="$TEST_PATH" \ - RSCRYPTO_CI_CAPTURE_DIR="$CAPTURE" \ - RSCRYPTO_CI_OPERATION=constant-time \ - RSCRYPTO_CI_PLATFORM=amd-zen4 \ - RSCRYPTO_CI_TARGET=x86_64-unknown-linux-gnu \ - RSCRYPTO_CI_DUDECT_TIMEOUT="1800; $shell_payload" \ - RSCRYPTO_CI_BINSEC_TIMEOUT=900 \ - RSCRYPTO_CI_UPLOAD_RAW_ARTIFACTS=false \ - bash "$FIXTURE/scripts/ci/run-rust-job.sh" -[[ ! -e "$sentinel" ]] || fail "numeric input was evaluated as shell code" - -AMX_PATH="$AMX_BIN:$PATH" -expect_failure env \ - PATH="$AMX_PATH" \ - RSCRYPTO_CI_CAPTURE_DIR="$CAPTURE" \ - RSCRYPTO_CI_OPERATION=platform-amx \ - RSCRYPTO_CI_RUNNER=intel-spr \ - RSCRYPTO_MOCK_AMX_LIST=none \ - bash "$FIXTURE/scripts/ci/run-rust-job.sh" - -expect_failure env \ - PATH="$AMX_PATH" \ - RSCRYPTO_CI_CAPTURE_DIR="$CAPTURE" \ - RSCRYPTO_CI_OPERATION=platform-amx \ - RSCRYPTO_CI_RUNNER=intel-spr \ - RSCRYPTO_MOCK_AMX_LIST=integration-only \ - bash "$FIXTURE/scripts/ci/run-rust-job.sh" - -: >"$CAPTURE/amx-cargo.args" -env \ - PATH="$AMX_PATH" \ - RSCRYPTO_CI_CAPTURE_DIR="$CAPTURE" \ - RSCRYPTO_CI_OPERATION=platform-amx \ - RSCRYPTO_CI_RUNNER=intel-spr \ - RSCRYPTO_MOCK_AMX_LIST=all \ - bash "$FIXTURE/scripts/ci/run-rust-job.sh" >/dev/null -[[ "$(wc -l <"$CAPTURE/amx-cargo.args" | tr -d ' ')" == 4 ]] \ - || fail "AMX operation did not list and run both exact tests" -if grep -Fvq 'CARGO_PROFILE_TEST_DEBUG=0' "$CAPTURE/amx-cargo.args"; then - fail "AMX operation retained full test-profile debug artifacts" -fi -grep -Fq \ - 'RSCRYPTO_REQUIRE_AMX=1 :: test --locked --test platform_amx_permission -- --list' \ - "$CAPTURE/amx-cargo.args" \ - || fail "AMX integration test existence was not checked under the required permission contract" -grep -Fq \ - 'RUSTFLAGS=-C target-feature=+amx-tile,+amx-bf16,+amx-int8' \ - "$CAPTURE/amx-cargo.args" \ - || fail "AMX no_std test existence was not checked with forced AMX target features" - -if grep -En '(^|[[:space:]])eval[[:space:]]|(^|[[:space:]])(bash|sh)[[:space:]]+-c|<<<' "$DISPATCHER" >/dev/null; then - fail "dispatcher contains a dynamic shell interpreter" -fi - -echo "Rust job dispatcher regression tests passed" diff --git a/scripts/ci/run-rust-job.sh b/scripts/ci/run-rust-job.sh deleted file mode 100755 index 395ef706..00000000 --- a/scripts/ci/run-rust-job.sh +++ /dev/null @@ -1,496 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -cd "$REPO_ROOT" - -die() { - echo "rust job error: $*" >&2 - exit 2 -} - -require_nonempty() { - local name=$1 - local value=$2 - [[ -n "$value" ]] || die "$name is required" -} - -require_one_of() { - local name=$1 - local value=$2 - shift 2 - - local allowed - for allowed in "$@"; do - if [[ "$value" == "$allowed" ]]; then - return 0 - fi - done - - die "invalid $name: $value" -} - -require_bool() { - require_one_of "$1" "$2" true false -} - -require_positive_integer() { - local name=$1 - local value=$2 - [[ "$value" =~ ^[1-9][0-9]*$ ]] || die "$name must be a positive integer" -} - -require_commit_sha() { - local value=$1 - [[ "$value" =~ ^([[:xdigit:]]{40}|[[:xdigit:]]{64})$ ]] \ - || die "base_sha must be a full commit ID" -} - -assert_single_libtest() { - local test_name=$1 - shift - - local listing count - if ! listing=$("$@" --list); then - die "unable to list the test harness containing $test_name" - fi - count=$(printf '%s\n' "$listing" | awk -v expected="$test_name: test" '$0 == expected { count++ } END { print count + 0 }') - [[ "$count" -eq 1 ]] || die "expected exactly one libtest named $test_name; found $count" -} - -host_diagnostics() { - local cpuinfo_lines=$1 - uname -a - lscpu - sed -n "1,${cpuinfo_lines}p" /proc/cpuinfo -} - -run_quality() { - just ci-check -} - -run_examples() { - just test-examples -} - -run_msrv() { - cargo check --locked --workspace --lib --no-default-features - cargo check --locked --workspace --lib --all-features -} - -run_cargo_graph() { - mkdir -p target/cargo-rail - cargo rail config validate --strict - cargo rail config migrate --check - cargo rail unify --check --explain --format json \ - --output target/cargo-rail/unify-result.json -} - -run_feature_contracts() { - just check-feature-matrix - just test-feature-matrix -} - -run_native() { - local target=${RSCRYPTO_CI_TARGET:-} - local test_mode=${RSCRYPTO_CI_TEST_MODE:-} - require_one_of target "$target" \ - x86_64-unknown-linux-gnu \ - aarch64-unknown-linux-gnu \ - x86_64-pc-windows-msvc \ - aarch64-pc-windows-msvc - require_one_of test_mode "$test_mode" local commit weekly - export RSCRYPTO_TEST_MODE="$test_mode" - - case "$target" in - x86_64-pc-windows-msvc) - echo "Windows x86_64 lane: compile smoke plus BLAKE3 ASM runtime vectors" - cargo clippy --locked --workspace --lib --all-features - cargo test --locked --workspace --all-features --no-run - cargo test --locked --workspace --features blake3 \ - --test blake3_official_vectors \ - --test blake3_differential - ;; - aarch64-pc-windows-msvc) - echo "Windows AArch64 lane: compile-only smoke" - cargo clippy --locked --workspace --lib --all-features - cargo test --locked --workspace --all-features --no-run - ;; - x86_64-unknown-linux-gnu | aarch64-unknown-linux-gnu) - bash scripts/ci/native-check.sh --all-targets - bash scripts/test/test.sh --all - ;; - esac -} - -run_native_ibm() { - local test_mode=${RSCRYPTO_CI_TEST_MODE:-} - require_one_of test_mode "$test_mode" local commit weekly - export RSCRYPTO_TEST_MODE="$test_mode" - - host_diagnostics 50 - bash scripts/ci/native-check.sh - bash scripts/test/test.sh --all -} - -run_native_riscv() { - host_diagnostics 50 - export RSCRYPTO_TEST_MODE=weekly - export RSCRYPTO_CI_RESOURCE_PROFILE=constrained - export RSCRYPTO_SKIP_DOCTESTS=1 - bash scripts/ci/native-check.sh - bash scripts/test/test.sh --all -} - -run_platform_amx() { - local runner=${RSCRYPTO_CI_RUNNER:-} - [[ "$runner" == *"intel-spr"* ]] || die "AMX permission evidence requires the intel-spr runner" - [[ "$(uname -s)" == Linux ]] || die "AMX permission evidence requires Linux" - [[ "$(uname -m)" == x86_64 ]] || die "AMX permission evidence requires x86-64" - [[ "$(rustc -vV | sed -n 's/^host: //p')" == x86_64-unknown-linux-gnu ]] \ - || die "AMX permission evidence requires the x86_64-unknown-linux-gnu Rust host" - - host_diagnostics 80 - local flags - flags=$(sed -n 's/^flags[[:space:]]*: //p' /proc/cpuinfo | head -n 1) - [[ " $flags " == *" amx_tile "* ]] || die "intel-spr runner does not expose AMX-TILE" - - # This proof lane never consumes symbols; omitting them keeps both test - # codegen artifact sets proportional to the work. - export CARGO_PROFILE_TEST_DEBUG=0 - - RSCRYPTO_REQUIRE_AMX=1 \ - assert_single_libtest \ - linux_x86_64_amx_permission_and_cache_are_process_scoped \ - cargo test --locked --test platform_amx_permission -- - RSCRYPTO_REQUIRE_AMX=1 \ - cargo test --locked --test platform_amx_permission \ - linux_x86_64_amx_permission_and_cache_are_process_scoped -- --exact --nocapture - - # NIGHTLY: Rust target-feature names for AMX remain unstable. This lane - # deliberately forces them so the no_std permission gate is executable. - local amx_rustflags="-C target-feature=+amx-tile,+amx-bf16,+amx-int8" - RUSTFLAGS="$amx_rustflags" \ - assert_single_libtest \ - platform::detect::tests::no_std_linux_x86_64_masks_compile_time_amx_without_a_permission_probe \ - cargo test --locked --no-default-features --lib -- - RUSTFLAGS="$amx_rustflags" \ - cargo test --locked --no-default-features --lib \ - platform::detect::tests::no_std_linux_x86_64_masks_compile_time_amx_without_a_permission_probe \ - -- --exact --nocapture -} - -run_cross_targets() { - bash scripts/ci/cross-targets.sh deep -} - -run_supply_chain() { - local mode=${RSCRYPTO_CI_SUPPLY_CHAIN_MODE:-} - require_one_of supply_chain_mode "$mode" light full - - if [[ "$mode" == "full" ]]; then - cargo deny --locked check all - # RustCrypto `rsa` is used only as a dev/test/bench oracle. Production RSA - # verification is implemented in `src/auth/rsa.rs`; keep this scoped to the - # known Marvin advisory until the oracle dependency is removed or fixed. - cargo audit --ignore RUSTSEC-2023-0071 - else - cargo deny --locked check advisories - fi -} - -run_dependabot_smoke() { - local base_sha=${RSCRYPTO_CI_BASE_SHA:-} - require_commit_sha "$base_sha" - export GITHUB_BASE_SHA="$base_sha" - bash scripts/ci/dependabot-smoke.sh -} - -run_miri() { - local tree_borrows=${RSCRYPTO_CI_MIRI_TREE_BORROWS:-false} - require_bool miri_tree_borrows "$tree_borrows" - if [[ "$tree_borrows" == "true" ]]; then - export MIRIFLAGS=-Zmiri-tree-borrows - fi - just test-miri -} - -run_fuzz() { - export RSCRYPTO_FUZZ_DURATION_SECS=60 - export RSCRYPTO_FUZZ_JOBS=1 - export RSCRYPTO_FUZZ_TARGET_CONCURRENCY=2 - local fuzz_status=0 - just test-fuzz --all || fuzz_status=$? - - rm -rf -- fuzz-output - mkdir -p fuzz-output - mapfile -t corpus_dirs < <( - { - [[ -d fuzz/corpus ]] && printf '%s\n' fuzz/corpus - find fuzz-packages -mindepth 2 -maxdepth 2 -type d -name corpus - } | sort - ) - if [[ "${#corpus_dirs[@]}" -eq 0 ]]; then - tar -czf fuzz-output/corpus.tar.gz --files-from /dev/null - else - tar -czf fuzz-output/corpus.tar.gz "${corpus_dirs[@]}" - fi - - return "$fuzz_status" -} - -run_fuzz_asan() { - just test-fuzz-asan --all -} - -run_mlkem_aarch64() { - local platform=${RSCRYPTO_CI_PLATFORM:-} - local display_name - require_one_of platform "$platform" graviton3 graviton4 - case "$platform" in - graviton3) display_name="AWS Graviton3" ;; - graviton4) display_name="AWS Graviton4" ;; - esac - - echo "ML-KEM gate host: $display_name ($platform)" - host_diagnostics 80 - export MLKEM_AARCH64_GATE_PLATFORM="$platform" - bash scripts/ci/mlkem-aarch64-gate.sh -} - -run_benchmark() { - local platform=${RSCRYPTO_CI_PLATFORM:-} - local targets=${RSCRYPTO_CI_BENCH_TARGETS:-all} - local filter=${RSCRYPTO_CI_BENCH_FILTER:-} - local quick=${RSCRYPTO_CI_BENCH_QUICK:-false} - local run_date - local run_time - local run_commit - require_one_of platform "$platform" \ - amd-zen4 intel-spr intel-icl amd-zen5 \ - graviton3 graviton4 ibm-s390x ibm-power10 rise-riscv - require_bool bench_quick "$quick" - - echo "Bench host: $platform (linux)" - host_diagnostics 50 - - run_date="$(date -u +"%Y-%m-%d")" - run_time="$(date -u +"%H_%M_%S")" - run_commit="$(git rev-parse HEAD 2>/dev/null || echo unknown)" - - export BENCH_OUTPUT_DIR=target/benchmark_results - export BENCH_RESULTS_DIR=target/benchmark_results - export BENCH_RUN_DATE="$run_date" - export BENCH_RUN_TIME="$run_time" - export BENCH_RUN_COMMIT="$run_commit" - export BENCH_RUN_OS=linux - export BENCH_RUN_ARCH="$platform" - export BENCH_RUN_MODE=ci - export BENCH_ONLY="$targets" - export BENCH_FILTER="$filter" - export BENCH_QUICK="$quick" - export BENCH_ALLOW_FULL_HASHES_COMP=true - export BENCH_PLATFORM="$platform" - scripts/ci/run-bench.sh -} - -ct_target_for_platform() { - case "$1" in - amd-zen4 | intel-spr | intel-icl | amd-zen5) - echo x86_64-unknown-linux-gnu - ;; - graviton3 | graviton4) - echo aarch64-unknown-linux-gnu - ;; - ibm-s390x) - echo s390x-unknown-linux-gnu - ;; - ibm-power10) - echo powerpc64le-unknown-linux-gnu - ;; - rise-riscv) - echo riscv64gc-unknown-linux-gnu - ;; - *) - die "invalid CT platform: $1" - ;; - esac -} - -run_constant_time() { - local platform=${RSCRYPTO_CI_PLATFORM:-} - local runner=${RSCRYPTO_CI_RUNNER:-} - local dudect_timeout=${RSCRYPTO_CI_DUDECT_TIMEOUT:-1800} - local binsec_timeout=${RSCRYPTO_CI_BINSEC_TIMEOUT:-900} - local raw_dudect_filter=${RSCRYPTO_CI_DUDECT_FILTER:-} - local raw_dudect_gate=${RSCRYPTO_CI_DUDECT_GATE:-required} - local raw_artifacts=${RSCRYPTO_CI_UPLOAD_RAW_ARTIFACTS:-false} - local target=${RSCRYPTO_CI_TARGET:-} - local expected_target - expected_target="$(ct_target_for_platform "$platform")" - [[ "$target" == "$expected_target" ]] || die "CT target does not match platform" - require_positive_integer dudect_timeout "$dudect_timeout" - require_positive_integer binsec_timeout "$binsec_timeout" - require_bool upload_raw_artifacts "$raw_artifacts" - - local evidence_dir=target/ct-evidence-package - mkdir -p "$evidence_dir" - { - echo "CT platform: $platform" - echo "CT target: $target" - echo "CT runner: $runner" - uname -a || true - rustc -vV - cargo -V - lscpu || true - sed -n '1,80p' /proc/cpuinfo || true - } 2>&1 | tee "$evidence_dir/host-$platform.log" - - local dudect_filter - local dudect_gate - dudect_filter="$(printf '%s' "$raw_dudect_filter" | tr -d '[:space:]')" - dudect_gate="$(printf '%s' "$raw_dudect_gate" | tr -d '[:space:]')" - if [[ "$raw_dudect_filter" != "$dudect_filter" ]]; then - echo "Normalized whitespace in DudeCT filter input." - fi - require_one_of dudect_gate "$dudect_gate" required diagnostic all - - local -a args=( - --target "$target" - --dudect-timeout "$dudect_timeout" - --binsec-timeout "$binsec_timeout" - --dudect-gate "$dudect_gate" - ) - if [[ -n "$dudect_filter" ]]; then - args+=(--dudect-filter "$dudect_filter") - fi - - local -a package_args=( - --target "$target" - --suffix "$platform" - --out-dir "$evidence_dir" - ) - local status=0 - { - printf 'Running:' - printf ' %q' scripts/ct/full.py "${args[@]}" - printf '\n' - scripts/ct/full.py "${args[@]}" - } 2>&1 | tee "$evidence_dir/ct-full-$platform.log" || status=$? - - if [[ "$raw_artifacts" == "true" ]]; then - package_args+=(--raw) - fi - scripts/lib/python.sh scripts/ct/package_evidence.py "${package_args[@]}" - return "$status" -} - -run_rsa_miri() { - mkdir -p ci-evidence - { - uname -a - lscpu - just test-miri --rsa - } 2>&1 | tee ci-evidence/rsa-miri-linux-x64.log -} - -run_rsa_leakage() { - local target=${RSCRYPTO_CI_TARGET:-} - require_one_of target "$target" linux-x64 linux-arm64 - mkdir -p ci-evidence - { - uname -a - lscpu - RSCRYPTO_RSA_LEAKAGE_SAMPLES="${RSCRYPTO_RSA_LEAKAGE_SAMPLES:-4000}" \ - RSCRYPTO_RSA_LEAKAGE_T_THRESHOLD="${RSCRYPTO_RSA_LEAKAGE_T_THRESHOLD:-8.0}" \ - just test-rsa-leakage - } 2>&1 | tee "ci-evidence/rsa-leakage-$target.log" -} - -run_rsa_linux_x86_64_asm() { - mkdir -p ci-evidence - { - uname -a - [[ "$(uname -s)" == Linux ]] || die "RSA x86-64 assembly evidence requires Linux" - [[ "$(uname -m)" == x86_64 ]] || die "RSA x86-64 assembly evidence requires x86-64" - [[ "$(rustc -vV | sed -n 's/^host: //p')" == x86_64-unknown-linux-gnu ]] \ - || die "RSA x86-64 assembly evidence requires the x86_64-unknown-linux-gnu Rust host" - - local flags - flags=$(sed -n 's/^flags[[:space:]]*: //p' /proc/cpuinfo | head -n 1) - [[ " $flags " == *" bmi2 "* ]] || die "RSA x86-64 assembly evidence requires BMI2" - [[ " $flags " == *" adx "* ]] || die "RSA x86-64 assembly evidence requires ADX" - lscpu - - assert_single_libtest \ - auth::rsa::tests::x86_64_linux_rsa_montgomery_asm_matches_portable_across_supported_widths \ - cargo test --locked --features rsa,diag,getrandom --lib -- - cargo test --locked --features rsa,diag,getrandom --lib \ - auth::rsa::tests::x86_64_linux_rsa_montgomery_asm_matches_portable_across_supported_widths \ - -- --exact --nocapture - assert_single_libtest \ - auth::rsa::tests::x86_64_linux_rsa_montgomery_asm_matches_portable_across_supported_widths \ - cargo test --locked --release --features rsa,diag,getrandom --lib -- - cargo test --locked --release --features rsa,diag,getrandom --lib \ - auth::rsa::tests::x86_64_linux_rsa_montgomery_asm_matches_portable_across_supported_widths \ - -- --exact --nocapture - - local build_output binary - build_output=$(cargo test --locked --release --features rsa,diag \ - --test rsa_public_key --no-run --message-format=json) - binary=$(printf '%s\n' "$build_output" \ - | sed -n 's/.*"executable":"\([^"]*rsa_public_key-[^"]*\)".*/\1/p' \ - | tail -n 1) - [[ -n "$binary" && -x "$binary" ]] \ - || die "unable to resolve the optimized rsa_public_key test binary" - printf 'Optimized RSA test binary: %s\n' "$binary" - assert_single_libtest public_operation_montgomery_candidates_match_current_path "$binary" - "$binary" public_operation_montgomery_candidates_match_current_path --exact --nocapture - - local binary_description binary_symbols - binary_description=$(file "$binary") || die "unable to inspect the optimized rsa_public_key test binary" - [[ "$binary_description" == *"ELF 64-bit LSB pie executable, x86-64"* ]] \ - || die "optimized rsa_public_key test binary is not x86-64 ELF" - binary_symbols=$(nm "$binary") || die "unable to read the optimized rsa_public_key symbol table" - [[ "$binary_symbols" == *"rscrypto_rsa_bn_mulx4x_mont_x86_64_elf"* ]] \ - || die "optimized rsa_public_key test binary lacks the x86-64 Montgomery multiply" - [[ "$binary_symbols" == *"rscrypto_rsa_bn_sqr8x_mont_x86_64_elf"* ]] \ - || die "optimized rsa_public_key test binary lacks the x86-64 Montgomery square" - } 2>&1 | tee ci-evidence/rsa-linux-x86_64-asm.log -} - -main() { - if [[ $# -ne 0 ]]; then - die "usage: scripts/ci/run-rust-job.sh" - fi - - local operation=${RSCRYPTO_CI_OPERATION:-} - require_nonempty operation "$operation" - case "$operation" in - quality) run_quality ;; - examples) run_examples ;; - msrv) run_msrv ;; - cargo-graph) run_cargo_graph ;; - feature-contracts) run_feature_contracts ;; - native) run_native ;; - native-ibm) run_native_ibm ;; - native-riscv) run_native_riscv ;; - platform-amx) run_platform_amx ;; - cross-targets) run_cross_targets ;; - supply-chain) run_supply_chain ;; - dependabot-smoke) run_dependabot_smoke ;; - miri) run_miri ;; - fuzz) run_fuzz ;; - fuzz-asan) run_fuzz_asan ;; - mlkem-aarch64) run_mlkem_aarch64 ;; - benchmark) run_benchmark ;; - constant-time) run_constant_time ;; - rsa-miri) run_rsa_miri ;; - rsa-leakage) run_rsa_leakage ;; - rsa-linux-x64-asm) run_rsa_linux_x86_64_asm ;; - *) die "unsupported operation: $operation" ;; - esac -} - -main "$@" diff --git a/scripts/ci/target-contracts.sh b/scripts/ci/target-contracts.sh new file mode 100755 index 00000000..d4e0d2c3 --- /dev/null +++ b/scripts/ci/target-contracts.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Materialize and execute Cargo Rail-selected platform proof rows. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +CATALOG="$REPO_ROOT/.config/target-matrix.json" +VALIDATOR="$SCRIPT_DIR/../lib/target-matrix.sh" + +usage() { + echo "Usage: $0 matrix PLAN READER | run ROW [shallow|deep]" >&2 + exit 2 +} + +full_matrix() { + jq -c '{include: [.variants[] + | select(.dimensions.operation != "core") + | {id: .id} + .dimensions]}' "$CATALOG" +} + +materialize_matrix() { + [[ $# -eq 2 && -f "$1" && -f "$2" ]] || usage + local plan=$1 + local reader=$2 + local selected + selected=$(python3 "$reader" matrix "$plan" targets.platforms) + if [[ "$selected" == all ]]; then + full_matrix + return + fi + + jq -ce ' + .include |= map(select(.operation != "core")) + | select((.include | type) == "array") + ' <<<"$selected" +} + +run_row() { + [[ $# -ge 1 && $# -le 2 ]] || usage + local id=$1 + local depth=${2:-deep} + [[ "$id" =~ ^[a-z][a-z0-9.-]*$ ]] || usage + [[ "$depth" == shallow || "$depth" == deep ]] || usage + + local row operation target platform contract toolchain + row=$(jq -ce --arg id "$id" '.variants[] | select(.id == $id)' "$CATALOG") || { + echo "unknown platform proof row: $id" >&2 + exit 2 + } + operation=$(jq -r '.dimensions.operation' <<<"$row") + target=$(jq -r '.dimensions.target' <<<"$row") + platform=$(jq -r '.dimensions.platform' <<<"$row") + contract=$(jq -r '.dimensions.contract' <<<"$row") + case "$contract" in + development) toolchain=$("$SCRIPT_DIR/../lib/toolchain.sh") ;; + nightly) toolchain=$("$SCRIPT_DIR/../lib/toolchain.sh" --nightly) ;; + *) + echo "unsupported platform toolchain contract: $contract" >&2 + exit 2 + ;; + esac + export RUSTUP_TOOLCHAIN="$toolchain" + + case "$operation" in + cross) "$SCRIPT_DIR/cross-targets.sh" "$target" "$depth" ;; + native) "$SCRIPT_DIR/native-platform.sh" "$platform" "$target" "$depth" ;; + amx) "$SCRIPT_DIR/native-platform.sh" amx "$target" "$depth" ;; + core) + echo "$id is owned by the core Rust job and has no separate executor" >&2 + exit 2 + ;; + *) + echo "unsupported platform proof operation: $operation" >&2 + exit 2 + ;; + esac +} + +"$VALIDATOR" --validate +case "${1:-}" in + matrix) + shift + materialize_matrix "$@" + ;; + run) + shift + run_row "$@" + ;; + *) usage ;; +esac diff --git a/scripts/ci/tool-integrity-test.sh b/scripts/ci/tool-integrity-test.sh deleted file mode 100755 index b79e2aa1..00000000 --- a/scripts/ci/tool-integrity-test.sh +++ /dev/null @@ -1,693 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -unset BASH_ENV - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -TMP_ROOT=$(mktemp -d) -trap 'rm -rf "$TMP_ROOT"' EXIT - -fail() { - echo "tool integrity test failure: $*" >&2 - exit 1 -} - -sha256_file() { - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$1" | awk '{print $1}' - else - shasum -a 256 "$1" | awk '{print $1}' - fi -} - -make_direct_fixture() { - local fixture=$1 - mkdir -p "$fixture/.config" "$fixture/scripts/ci" "$fixture/scripts/lib" - cp "$REPO_ROOT/.config/ci-tool-archives.tsv" "$fixture/.config/" - cp "$REPO_ROOT/scripts/lib/ci-tool-integrity.sh" "$fixture/scripts/lib/" - cp "$REPO_ROOT/scripts/lib/feature-profiles.sh" "$fixture/scripts/lib/" - cp "$REPO_ROOT/scripts/ci/install-codecov.sh" "$REPO_ROOT/scripts/ci/nostd-wasm-suite.sh" \ - "$fixture/scripts/ci/" -} - -set_manifest_digest() { - local fixture=$1 - local tool=$2 - local os=$3 - local architecture=$4 - local digest=$5 - local manifest="$fixture/.config/ci-tool-archives.tsv" - awk -F '\t' -v OFS='\t' \ - -v tool="$tool" -v os="$os" -v architecture="$architecture" -v digest="$digest" ' - $1 == tool && $3 == os && $4 == architecture { $7 = digest } - { print } - ' "$manifest" >"$manifest.tmp" - mv "$manifest.tmp" "$manifest" -} - -direct_bin="$TMP_ROOT/direct-bin" -direct_log="$TMP_ROOT/direct.log" -mkdir -p "$direct_bin" - -cat >"$direct_bin/uname" <<'SH' -#!/usr/bin/env bash -case "${1:-}" in - -s) printf '%s\n' "${MOCK_UNAME_S:-Linux}" ;; - -m) printf '%s\n' "${MOCK_UNAME_M:-x86_64}" ;; - *) printf '%s %s\n' "${MOCK_UNAME_S:-Linux}" "${MOCK_UNAME_M:-x86_64}" ;; -esac -SH - -cat >"$direct_bin/curl" <<'SH' -#!/usr/bin/env bash -set -euo pipefail -printf 'curl %s\n' "$*" >>"$MOCK_COMMAND_LOG" -[[ "${MOCK_CURL_FAIL:-0}" != 1 ]] || exit 22 -output="" -while [[ $# -gt 0 ]]; do - case "$1" in - --output | -o) - output=$2 - shift 2 - ;; - *) shift ;; - esac -done -[[ -n "$output" ]] -cp "$MOCK_DOWNLOAD_FILE" "$output" -SH - -cat >"$direct_bin/tar" <<'SH' -#!/usr/bin/env bash -set -euo pipefail -printf 'tar %s\n' "$*" >>"$MOCK_COMMAND_LOG" -destination="" -archive="" -while [[ $# -gt 0 ]]; do - case "$1" in - -xJf | -xzf) - archive=$2 - shift 2 - ;; - -C) - destination=$2 - shift 2 - ;; - *) shift ;; - esac -done -[[ -n "$destination" && -n "$archive" ]] -case "${archive##*/}" in - wasmtime-*) - root="$destination/wasmtime-v48.0.0-x86_64-linux" - mkdir -p "$root" - cat >"$root/wasmtime" <<'EOF' -#!/usr/bin/env bash -if [[ "${1:-}" == --version ]]; then - printf 'wasmtime 48.0.0 (mock)\n' -else - printf 'wasmtime executed\n' >>"$MOCK_EXEC_LOG" -fi -EOF - chmod +x "$root/wasmtime" - ;; - wasm-tools-*) - root="$destination/wasm-tools-1.257.1-x86_64-linux" - mkdir -p "$root" - cat >"$root/wasm-tools" <<'EOF' -#!/usr/bin/env bash -case "${1:-}" in - --version) printf 'wasm-tools 1.257.1\n' ;; - validate) printf 'wasm-tools validated\n' >>"$MOCK_EXEC_LOG" ;; - print) printf '(module (func (drop (v128.const i32x4 0 0 0 0))))\n' ;; - *) exit 2 ;; -esac -EOF - chmod +x "$root/wasm-tools" - ;; - *) exit 2 ;; -esac -SH - -cat >"$direct_bin/cargo" <<'SH' -#!/usr/bin/env bash -printf 'cargo %s\n' "$*" >>"$MOCK_COMMAND_LOG" -if [[ "${1:-}" == build && -n "${CARGO_TARGET_DIR:-}" ]]; then - mkdir -p "$CARGO_TARGET_DIR/wasm32-wasip1/debug" - : >"$CARGO_TARGET_DIR/wasm32-wasip1/debug/rscrypto-wasm-runtime-vectors.wasm" -fi -SH - -cat >"$direct_bin/rustup" <<'SH' -#!/usr/bin/env bash -printf 'rustup %s\n' "$*" >>"$MOCK_COMMAND_LOG" -SH -chmod +x "$direct_bin"/* - -codecov_artifact="$TMP_ROOT/codecov" -cat >"$codecov_artifact" <<'SH' -#!/usr/bin/env bash -printf 'codecovcli version 11.3.1\n' -printf 'codecov executed\n' >>"$MOCK_EXEC_LOG" -SH -codecov_digest=$(sha256_file "$codecov_artifact") - -direct_fixture="$TMP_ROOT/direct-fixture" -make_direct_fixture "$direct_fixture" -set_manifest_digest "$direct_fixture" codecov linux x86_64 "$codecov_digest" - -codecov_temp="$TMP_ROOT/codecov-valid" -codecov_output="$TMP_ROOT/codecov.output" -codecov_exec="$TMP_ROOT/codecov.exec" -mkdir -p "$codecov_temp" -PATH="$direct_bin:$PATH" \ - RUNNER_TEMP="$codecov_temp" \ - GITHUB_OUTPUT="$codecov_output" \ - MOCK_COMMAND_LOG="$direct_log" \ - MOCK_DOWNLOAD_FILE="$codecov_artifact" \ - MOCK_EXEC_LOG="$codecov_exec" \ - "$direct_fixture/scripts/ci/install-codecov.sh" -installed_codecov=$(sed -n 's/^binary=//p' "$codecov_output") -[[ -x "$installed_codecov" ]] || fail "valid Codecov artifact was not installed" -grep -Fqx 'codecov executed' "$codecov_exec" \ - || fail "verified Codecov artifact did not execute" - -invalid_artifact="$TMP_ROOT/invalid-codecov" -cat >"$invalid_artifact" <<'SH' -#!/usr/bin/env bash -printf 'invalid artifact executed\n' >>"$MOCK_EXEC_LOG" -printf 'codecovcli version 11.3.1\n' -SH -invalid_temp="$TMP_ROOT/codecov-invalid" -invalid_exec="$TMP_ROOT/codecov-invalid.exec" -mkdir -p "$invalid_temp" -if PATH="$direct_bin:$PATH" \ - RUNNER_TEMP="$invalid_temp" \ - GITHUB_OUTPUT="$TMP_ROOT/codecov-invalid.output" \ - MOCK_COMMAND_LOG="$direct_log" \ - MOCK_DOWNLOAD_FILE="$invalid_artifact" \ - MOCK_EXEC_LOG="$invalid_exec" \ - "$direct_fixture/scripts/ci/install-codecov.sh" >/dev/null 2>&1; then - fail "checksum mismatch installed Codecov" -fi -[[ ! -e "$invalid_exec" ]] || fail "checksum mismatch executed Codecov" -[[ ! -s "$TMP_ROOT/codecov-invalid.output" ]] \ - || fail "checksum mismatch published an installed Codecov path" - -missing_temp="$TMP_ROOT/codecov-missing" -mkdir -p "$missing_temp" -if PATH="$direct_bin:$PATH" \ - RUNNER_TEMP="$missing_temp" \ - GITHUB_OUTPUT="$TMP_ROOT/codecov-missing.output" \ - MOCK_COMMAND_LOG="$direct_log" \ - MOCK_DOWNLOAD_FILE="$codecov_artifact" \ - MOCK_CURL_FAIL=1 \ - MOCK_EXEC_LOG="$TMP_ROOT/codecov-missing.exec" \ - "$direct_fixture/scripts/ci/install-codecov.sh" >/dev/null 2>&1; then - fail "missing download was accepted" -fi -[[ ! -e "$TMP_ROOT/codecov-missing.exec" ]] \ - || fail "missing download reached executable fallback" - -: >"$direct_log" -unsupported_temp="$TMP_ROOT/codecov-unsupported" -mkdir -p "$unsupported_temp" -if PATH="$direct_bin:$PATH" \ - RUNNER_TEMP="$unsupported_temp" \ - GITHUB_OUTPUT="$TMP_ROOT/codecov-unsupported.output" \ - MOCK_COMMAND_LOG="$direct_log" \ - MOCK_DOWNLOAD_FILE="$codecov_artifact" \ - MOCK_UNAME_M=riscv64 \ - MOCK_EXEC_LOG="$TMP_ROOT/codecov-unsupported.exec" \ - "$direct_fixture/scripts/ci/install-codecov.sh" >/dev/null 2>&1; then - fail "unsupported direct-tool architecture was accepted" -fi -[[ ! -s "$direct_log" ]] || fail "unsupported architecture attempted a download" - -missing_platform_fixture="$TMP_ROOT/missing-platform-fixture" -make_direct_fixture "$missing_platform_fixture" -awk -F '\t' '$1 != "codecov"' \ - "$missing_platform_fixture/.config/ci-tool-archives.tsv" \ - >"$missing_platform_fixture/.config/ci-tool-archives.tsv.tmp" -mv "$missing_platform_fixture/.config/ci-tool-archives.tsv.tmp" \ - "$missing_platform_fixture/.config/ci-tool-archives.tsv" -: >"$direct_log" -mkdir -p "$TMP_ROOT/codecov-no-platform" -if PATH="$direct_bin:$PATH" \ - RUNNER_TEMP="$TMP_ROOT/codecov-no-platform" \ - GITHUB_OUTPUT="$TMP_ROOT/codecov-no-platform.output" \ - MOCK_COMMAND_LOG="$direct_log" \ - MOCK_DOWNLOAD_FILE="$codecov_artifact" \ - MOCK_EXEC_LOG="$TMP_ROOT/codecov-no-platform.exec" \ - "$missing_platform_fixture/scripts/ci/install-codecov.sh" >/dev/null 2>&1; then - fail "missing platform contract was accepted" -fi -[[ ! -s "$direct_log" ]] || fail "missing platform contract attempted a download" - -wasmtime_artifact="$TMP_ROOT/wasmtime.tar.xz" -printf 'authenticated mock Wasmtime archive\n' >"$wasmtime_artifact" -wasmtime_digest=$(sha256_file "$wasmtime_artifact") -set_manifest_digest "$direct_fixture" wasmtime linux x86_64 "$wasmtime_digest" -set_manifest_digest "$direct_fixture" wasm-tools linux x86_64 "$wasmtime_digest" -wasmtime_home="$TMP_ROOT/wasmtime-home" -wasm_tools_home="$TMP_ROOT/wasm-tools-home" -wasmtime_exec="$TMP_ROOT/wasmtime.exec" -: >"$direct_log" -( - cd "$direct_fixture" - PATH="$direct_bin:$PATH" \ - WASMTIME_HOME="$wasmtime_home" \ - WASM_TOOLS_HOME="$wasm_tools_home" \ - MOCK_COMMAND_LOG="$direct_log" \ - MOCK_DOWNLOAD_FILE="$wasmtime_artifact" \ - MOCK_EXEC_LOG="$wasmtime_exec" \ - scripts/ci/nostd-wasm-suite.sh wasm32-wasip1 shallow -) >/dev/null -[[ -x "$wasmtime_home/bin/wasmtime" ]] || fail "verified Wasmtime was not installed" -[[ -x "$wasm_tools_home/bin/wasm-tools" ]] || fail "verified wasm-tools was not installed" -grep -Fq 'tar -xJf' "$direct_log" || fail "verified Wasmtime was not extracted" -grep -Fq 'tar -xzf' "$direct_log" || fail "verified wasm-tools was not extracted" -grep -Fqx 'wasmtime executed' "$wasmtime_exec" || fail "verified Wasmtime was not executed" -grep -Fqx 'wasm-tools validated' "$wasmtime_exec" || fail "verified wasm-tools did not validate artifacts" - -: >"$direct_log" -bad_wasmtime_exec="$TMP_ROOT/bad-wasmtime.exec" -if ( - cd "$direct_fixture" - PATH="$direct_bin:$PATH" \ - WASMTIME_HOME="$TMP_ROOT/bad-wasmtime-home" \ - WASM_TOOLS_HOME="$TMP_ROOT/bad-wasm-tools-home" \ - MOCK_COMMAND_LOG="$direct_log" \ - MOCK_DOWNLOAD_FILE="$invalid_artifact" \ - MOCK_EXEC_LOG="$bad_wasmtime_exec" \ - scripts/ci/nostd-wasm-suite.sh wasm32-wasip1 shallow -) >/dev/null 2>&1; then - fail "invalid Wasmtime archive was accepted" -fi -if grep -Fq 'tar ' "$direct_log"; then - fail "invalid Wasmtime archive reached extraction" -fi -[[ ! -e "$bad_wasmtime_exec" ]] || fail "invalid Wasmtime archive reached execution" - -package_bin="$TMP_ROOT/package-bin" -package_log="$TMP_ROOT/package.log" -package_state="$TMP_ROOT/package.state" -mkdir -p "$package_bin" -: >"$package_state" - -cat >"$package_bin/cargo" <<'SH' -#!/usr/bin/env bash -set -euo pipefail -printf 'cargo %s\n' "$*" >>"$MOCK_PACKAGE_LOG" -if [[ "${1:-}" == install && "${2:-}" == --list ]]; then - while read -r package version; do - [[ -n "$package" ]] || continue - printf '%s v%s:\n %s\n' "$package" "$version" "$package" - done <"$MOCK_CARGO_STATE" - exit 0 -fi -[[ "${1:-}" == install ]] || exit 0 -shift -package="" -required="" -while [[ $# -gt 0 ]]; do - case "$1" in - --registry) - [[ "$2" == crates-io ]] - shift 2 - ;; - --version) - required=$2 - shift 2 - ;; - --locked | --force) shift ;; - -*) exit 91 ;; - *) - [[ -z "$package" ]] || exit 92 - package=$1 - shift - ;; - esac -done -[[ -n "$package" && "$required" == =* ]] -case "$CARGO_HOME" in - "$RUNNER_TEMP"/rscrypto-ci-tools.*/cargo) ;; - *) exit 93 ;; -esac -version=${required#=} -awk -v package="$package" '$1 != package' "$MOCK_CARGO_STATE" >"$MOCK_CARGO_STATE.tmp" -printf '%s %s\n' "$package" "$version" >>"$MOCK_CARGO_STATE.tmp" -mv "$MOCK_CARGO_STATE.tmp" "$MOCK_CARGO_STATE" -case "$package" in - cargo-show-asm) binary="$CARGO_HOME/bin/cargo-asm" ;; - *) binary="$CARGO_HOME/bin/$package" ;; -esac -mkdir -p "$(dirname "$binary")" -printf '#!/usr/bin/env bash\nprintf "%%s %%s\\n" %q %q\n' "$package" "$version" >"$binary" -chmod +x "$binary" -SH - -cat >"$package_bin/go" <<'SH' -#!/usr/bin/env bash -set -euo pipefail -printf 'go %s\n' "$*" >>"$MOCK_PACKAGE_LOG" -[[ "$1" == install && "$2" == github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 ]] -case "$GOMODCACHE" in - "$RUNNER_TEMP"/rscrypto-ci-tools.*/go/pkg/mod) ;; - *) exit 93 ;; -esac -case "$GOCACHE" in - "$RUNNER_TEMP"/rscrypto-ci-tools.*/go-build) ;; - *) exit 94 ;; -esac -mkdir -p "$GOBIN" -cat >"$GOBIN/actionlint" <<'EOF' -#!/usr/bin/env bash -printf '1.7.12\n' -EOF -chmod +x "$GOBIN/actionlint" -SH - -cat >"$package_bin/rustup" <<'SH' -#!/usr/bin/env bash -printf 'rustup %s\n' "$*" >>"$MOCK_PACKAGE_LOG" -SH - -cat >"$package_bin/rustc" <<'SH' -#!/usr/bin/env bash -printf 'rustc %s\n' "$*" >>"$MOCK_PACKAGE_LOG" -printf 'rustc 1.99.0-nightly\ncommit-date: 2026-07-16\n' -SH - -cp "$direct_bin/uname" "$package_bin/uname" -chmod +x "$package_bin"/* - -package_home="$TMP_ROOT/package-home" -package_temp="$TMP_ROOT/package-temp" -mkdir -p "$package_home/.cargo/bin" -mkdir -p "$package_temp" -: >"$package_log" -for mode in standard quality release rail ci supply-chain ibm bench structural-bench profile fuzz coverage minimal none; do - HOME="$package_home" \ - RUNNER_TEMP="$package_temp" \ - PATH="$package_bin:$PATH" \ - MOCK_PACKAGE_LOG="$package_log" \ - MOCK_CARGO_STATE="$package_state" \ - "$REPO_ROOT/scripts/ci/install-tools.sh" "$mode" >/dev/null -done - -authenticated_rail_bin="$TMP_ROOT/authenticated-rail-bin" -authenticated_rail_log="$TMP_ROOT/authenticated-rail.log" -authenticated_rail_output="$TMP_ROOT/authenticated-rail.out" -mkdir -p "$authenticated_rail_bin" -: >"$authenticated_rail_log" -cat >"$authenticated_rail_bin/cargo-rail" <<'SH' -#!/usr/bin/env bash -[[ "$*" == "rail --version" ]] -printf 'cargo-rail 0.25.0\n' -SH -chmod +x "$authenticated_rail_bin/cargo-rail" -HOME="$package_home" \ - RUNNER_TEMP="$package_temp" \ - PATH="$authenticated_rail_bin:$package_bin:$PATH" \ - MOCK_PACKAGE_LOG="$authenticated_rail_log" \ - MOCK_CARGO_STATE="$package_state" \ - RSCRYPTO_AUTHENTICATED_CARGO_RAIL=true \ - RSCRYPTO_REQUIRE_CARGO_RAIL=true \ - "$REPO_ROOT/scripts/ci/install-tools.sh" none >"$authenticated_rail_output" -grep -Fq 'reusing authenticated 0.25.0 from cargo-rail-action' "$authenticated_rail_output" \ - || fail "the exact cargo-rail-action install was not reused" -if grep -Fq 'cargo install --registry crates-io cargo-rail' "$authenticated_rail_log"; then - fail "the exact cargo-rail-action install was redundantly rebuilt" -fi - -cat >"$package_bin/sudo" <<'SH' -#!/usr/bin/env bash -set -euo pipefail -"$@" -SH - -cat >"$package_bin/apt-get" <<'SH' -#!/usr/bin/env bash -set -euo pipefail -printf 'apt-get %s\n' "$*" >>"$MOCK_PACKAGE_LOG" -SH - -cat >"$package_bin/apt-cache" <<'SH' -#!/usr/bin/env bash -set -euo pipefail -[[ "$1" == policy && $# == 2 ]] -case "$2" in - build-essential) candidate=12.10ubuntu1 ;; - git) candidate=1:2.54.0-0ppa1~ubuntu24.04.1 ;; - libgmp-dev) candidate=2:6.3.0+dfsg-2ubuntu6.1 ;; - libmpfr-dev) candidate=4.2.1-1build1.1 ;; - m4) candidate=1.4.19-4build1 ;; - opam) candidate=2.1.5-1 ;; - pkg-config) candidate=1.8.1-2build1 ;; - zlib1g-dev) candidate=1:1.3.dfsg-3.1ubuntu2.1 ;; - musl-tools) candidate=1.2.4-2 ;; - *) candidate='(none)' ;; -esac -printf '%s:\n Candidate: %s\n' "$2" "$candidate" -SH - -cat >"$package_bin/dpkg-query" <<'SH' -#!/usr/bin/env bash -set -euo pipefail -case "${*: -1}" in - build-essential) printf '12.10ubuntu1' ;; - git) printf '1:2.54.0-0ppa1~ubuntu24.04.1' ;; - libgmp-dev) printf '2:6.3.0+dfsg-2ubuntu6.1' ;; - libmpfr-dev) printf '4.2.1-1build1.1' ;; - m4) printf '1.4.19-4build1' ;; - opam) printf '2.1.5-1' ;; - pkg-config) printf '1.8.1-2build1' ;; - zlib1g-dev) printf '1:1.3.dfsg-3.1ubuntu2.1' ;; - musl-tools) printf '1.2.4-2' ;; - *) exit 96 ;; -esac -SH - -cat >"$package_bin/git" <<'SH' -#!/usr/bin/env bash -set -euo pipefail -printf 'git %s\n' "$*" >>"$MOCK_PACKAGE_LOG" -case "$1" in - init) - [[ "$2" == --quiet ]] - mkdir -p "$3/.git" - ;; - -C) - repository=$2 - shift 2 - case "$1" in - fetch) - [[ "$2" == --depth=1 && "$3" == --no-tags ]] - [[ "$4" == https://github.com/ocaml/opam-repository.git ]] - [[ "$5" == 49f6d620cf20ae0168cfcbeb2c33932e06cb4b74 ]] - ;; - checkout) - [[ "$2" == --quiet && "$3" == --detach && "$4" == FETCH_HEAD ]] - ;; - rev-parse) - [[ "$2" == HEAD ]] - printf '%s\n' "${MOCK_GIT_HEAD:-49f6d620cf20ae0168cfcbeb2c33932e06cb4b74}" - ;; - status) - [[ "$2" == --short && "$3" == --untracked-files=all ]] - if [[ "${MOCK_GIT_DIRTY:-0}" == 1 ]]; then - printf ' M packages/binsec/binsec.0.11.1/opam\n' - fi - ;; - *) exit 95 ;; - esac - [[ -d "$repository/.git" ]] - ;; - *) exit 94 ;; -esac -SH - -cat >"$package_bin/opam" <<'SH' -#!/usr/bin/env bash -set -euo pipefail -printf 'opamroot=%s opam %s\n' "$OPAMROOT" "$*" >>"$MOCK_PACKAGE_LOG" -case "$1" in - init) ;; - switch) - [[ "$2" == create ]] - ;; - install) - mkdir -p "$OPAMROOT/$OPAMSWITCH/bin" - cat >"$OPAMROOT/$OPAMSWITCH/bin/binsec" <<'EOF' -#!/usr/bin/env bash -printf 'BINSEC version 0.11.1\n' -EOF - chmod +x "$OPAMROOT/$OPAMSWITCH/bin/binsec" - ;; - reinstall) ;; - list) - printf '%s\n' \ - ocaml-base-compiler.5.2.1 \ - unisim_archisec.0.0.14 \ - bitwuzla.1.0.6 \ - bitwuzla-cxx.0.9.0 \ - binsec.0.11.1 - ;; - var) - [[ "$2" == bin ]] - printf '%s\n' "$OPAMROOT/$OPAMSWITCH/bin" - ;; - *) exit 97 ;; -esac -SH -chmod +x "$package_bin/sudo" "$package_bin/apt-get" "$package_bin/apt-cache" \ - "$package_bin/dpkg-query" "$package_bin/git" "$package_bin/opam" - -ct_home="$TMP_ROOT/ct-home" -ct_temp="$TMP_ROOT/ct-temp" -ct_state="$TMP_ROOT/ct.state" -ct_log="$TMP_ROOT/ct.log" -ct_os_release="$TMP_ROOT/ct-os-release" -ct_installer="$TMP_ROOT/install-tools-ct.sh" -mkdir -p "$ct_home" "$ct_temp" -: >"$ct_state" -: >"$ct_log" -printf 'ID=ubuntu\nVERSION_ID="24.04"\n' >"$ct_os_release" -cp "$REPO_ROOT/scripts/ci/install-tools.sh" "$ct_installer" -sed -i.bak "s#/etc/os-release#$ct_os_release#g" "$ct_installer" -rm -f "$ct_installer.bak" -HOME="$ct_home" \ - RUNNER_TEMP="$ct_temp" \ - PATH="$package_bin:$PATH" \ - MOCK_PACKAGE_LOG="$ct_log" \ - MOCK_CARGO_STATE="$ct_state" \ - "$ct_installer" ct-linux >/dev/null -grep -Fq \ - 'apt-get --no-allow-insecure-repositories --error-on=any update' \ - "$ct_log" || fail "ct-linux did not require authenticated APT metadata" -grep -Fq \ - 'apt-get install -y --no-install-recommends --no-allow-unauthenticated --no-allow-downgrades --no-remove build-essential=12.10ubuntu1 git=1:2.54.0-0ppa1~ubuntu24.04.1 libgmp-dev=2:6.3.0+dfsg-2ubuntu6.1' \ - "$ct_log" || fail "ct-linux did not install signed Ubuntu package candidates exactly" -if grep -Fq -- '--allow-downgrades' "$ct_log"; then - fail "ct-linux permits APT package downgrades" -fi -grep -Eq \ - '^git -C .*/ct-temp/rscrypto-ci-tools\.[^/]+/opam-repository fetch --depth=1 --no-tags https://github.com/ocaml/opam-repository\.git 49f6d620cf20ae0168cfcbeb2c33932e06cb4b74$' \ - "$ct_log" || fail "ct-linux did not fetch the exact OPAM repository commit" -grep -Eq \ - '^opamroot=.*/ct-temp/rscrypto-ci-tools\.[^/]+/opam opam init --bare --disable-sandboxing --no-setup --no-opamrc -y default .*/ct-temp/rscrypto-ci-tools\.[^/]+/opam-repository$' \ - "$ct_log" || fail "ct-linux did not initialize OPAM from the verified checkout" -grep -Eq '^opamroot=.*/ct-temp/rscrypto-ci-tools\.[^/]+/opam opam switch create rscrypto-ct ocaml-base-compiler\.5\.2\.1 ' \ - "$ct_log" || fail "ct-linux did not use a fresh exact OPAM switch" - -bad_commit_temp="$TMP_ROOT/ct-bad-commit" -mkdir -p "$bad_commit_temp" -if HOME="$ct_home" \ - RUNNER_TEMP="$bad_commit_temp" \ - PATH="$package_bin:$PATH" \ - MOCK_PACKAGE_LOG="$TMP_ROOT/ct-bad-commit.log" \ - MOCK_CARGO_STATE="$ct_state" \ - MOCK_GIT_HEAD=0000000000000000000000000000000000000000 \ - "$ct_installer" ct-linux >/dev/null 2>&1; then - fail "ct-linux accepted the wrong OPAM repository commit" -fi - -dirty_repository_temp="$TMP_ROOT/ct-dirty-repository" -mkdir -p "$dirty_repository_temp" -if HOME="$ct_home" \ - RUNNER_TEMP="$dirty_repository_temp" \ - PATH="$package_bin:$PATH" \ - MOCK_PACKAGE_LOG="$TMP_ROOT/ct-dirty-repository.log" \ - MOCK_CARGO_STATE="$ct_state" \ - MOCK_GIT_DIRTY=1 \ - "$ct_installer" ct-linux >/dev/null 2>&1; then - fail "ct-linux accepted modified OPAM repository metadata" -fi - -for contract in \ - 'cargo-nextest =0.9.143' \ - 'cargo-deny =0.20.2' \ - 'cargo-audit =0.22.2' \ - 'cargo-rail =0.25.0' \ - 'just =1.58.0' \ - 'zizmor =1.29.0' \ - 'cargo-criterion =1.1.0' \ - 'critcmp =0.1.8' \ - 'gungraun-runner =0.19.4' \ - 'cargo-show-asm =0.2.62' \ - 'samply =0.13.1' \ - 'cargo-llvm-lines =0.4.48' \ - 'cargo-fuzz =0.13.2' \ - 'cargo-llvm-cov =0.9.0'; do - package=${contract%% *} - version=${contract#* } - grep -Fq "cargo install --registry crates-io $package --locked --version $version --force" \ - "$package_log" || fail "$package was not installed through its exact Cargo contract" -done -grep -Fq 'go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.12' "$package_log" \ - || fail "actionlint was not installed at an exact Go module version" -grep -Fq 'rustup component add llvm-tools-preview' "$package_log" \ - || fail "coverage did not use the pinned rustup toolchain boundary" -if grep -Eq 'binstall|latest' "$package_log"; then - fail "tool mode selected a mutable or Cargo-binstall path" -fi - -cached_home="$TMP_ROOT/cached-home" -cached_state="$TMP_ROOT/cached.state" -malicious_exec="$TMP_ROOT/malicious.exec" -github_path_file="$TMP_ROOT/github.path" -mkdir -p "$cached_home/.cargo/bin" -: >"$cached_state" -: >"$github_path_file" -cat >"$cached_home/.cargo/bin/just" <<'SH' -#!/usr/bin/env bash -printf 'forged exact-version cache executed\n' >>"$MALICIOUS_EXEC_LOG" -printf 'just 1.58.0\n' -SH -chmod +x "$cached_home/.cargo/bin/just" -cat >"$cached_home/.cargo/.crates.toml" <<'EOF' -[v1] -"just 1.58.0 (registry+https://github.com/rust-lang/crates.io-index)" = ["just"] -EOF -: >"$package_log" -HOME="$cached_home" \ - RUNNER_TEMP="$package_temp" \ - GITHUB_PATH="$github_path_file" \ - PATH="$package_bin:$PATH" \ - MOCK_PACKAGE_LOG="$package_log" \ - MOCK_CARGO_STATE="$cached_state" \ - MALICIOUS_EXEC_LOG="$malicious_exec" \ - "$REPO_ROOT/scripts/ci/install-tools.sh" minimal >/dev/null -[[ ! -e "$malicious_exec" ]] \ - || fail "forged exact-version cached binary executed before authenticated replacement" -grep -Fq 'cargo install --registry crates-io just --locked --version =1.58.0 --force' "$package_log" \ - || fail "forged cache did not trigger a fresh authenticated install" -trusted_bin=$(tail -n 1 "$github_path_file") -case "$trusted_bin" in - "$package_temp"/rscrypto-ci-tools.*/cargo/bin) ;; - *) fail "authenticated Cargo tool root was not exported to later CI steps" ;; -esac - -: >"$package_log" -toolchain_env="$TMP_ROOT/toolchain.env" -: >"$toolchain_env" -MOCK_PACKAGE_LOG="$package_log" PATH="$package_bin:$PATH" \ - "$REPO_ROOT/scripts/ci/setup-toolchain.sh" \ - nightly-2026-08-12 'clippy, rustfmt' "$toolchain_env" >/dev/null -grep -Fq \ - 'rustup toolchain install nightly-2026-08-12 --profile minimal --no-self-update --component clippy --component rustfmt' \ - "$package_log" || fail "rustup toolchain command was not exact" -grep -Fq 'rustc +nightly-2026-08-12 --version --verbose' "$package_log" \ - || fail "installed toolchain was not verified explicitly" -grep -Fxq 'RUSTUP_TOOLCHAIN=nightly-2026-08-12' "$toolchain_env" \ - || fail "installed toolchain was not activated for later CI steps" -if grep -Fq 'rustup default ' "$package_log"; then - fail "toolchain setup mutated the runner default" -fi -if MOCK_PACKAGE_LOG="$package_log" PATH="$package_bin:$PATH" \ - "$REPO_ROOT/scripts/ci/setup-toolchain.sh" nightly clippy >/dev/null 2>&1; then - fail "mutable rustup channel was accepted" -fi - -echo "CI tool integrity regression tests passed" diff --git a/scripts/ci/write-release-manifest.sh b/scripts/ci/write-release-manifest.sh deleted file mode 100755 index 8ed9a1a8..00000000 --- a/scripts/ci/write-release-manifest.sh +++ /dev/null @@ -1,238 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -usage() { - cat >&2 <<'EOF' -Usage: scripts/ci/write-release-manifest.sh [options] - -Required: - --version VERSION - --tag TAG - --commit SHA - --source PATH - --crate PATH - --ct-evidence PATH - --repository-controls PATH - --evidence-commit SHA - --evidence-mode exact_commit - --output PATH - -Optional: - --root PATH Repository root (default: current repository) -EOF -} - -version="" -tag="" -commit="" -source_archive="" -crate_package="" -ct_evidence="" -repository_controls="" -evidence_commit="" -evidence_mode="" -output="" -root="$(git rev-parse --show-toplevel)" - -while [[ $# -gt 0 ]]; do - case "$1" in - --version) version=${2:?}; shift 2 ;; - --tag) tag=${2:?}; shift 2 ;; - --commit) commit=${2:?}; shift 2 ;; - --source) source_archive=${2:?}; shift 2 ;; - --crate) crate_package=${2:?}; shift 2 ;; - --ct-evidence) ct_evidence=${2:?}; shift 2 ;; - --repository-controls) repository_controls=${2:?}; shift 2 ;; - --evidence-commit) evidence_commit=${2:?}; shift 2 ;; - --evidence-mode) evidence_mode=${2:?}; shift 2 ;; - --output) output=${2:?}; shift 2 ;; - --root) root=${2:?}; shift 2 ;; - -h | --help) usage; exit 0 ;; - *) echo "unknown argument: $1" >&2; usage; exit 2 ;; - esac -done - -fail() { - echo "release manifest error: $*" >&2 - exit 1 -} - -[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$ ]] || fail "invalid version: $version" -[[ "$tag" == "v$version" ]] || fail "tag $tag does not match version $version" -[[ "$commit" =~ ^[0-9a-f]{40}$ ]] || fail "release commit must be a full lowercase Git commit" -[[ "$evidence_commit" =~ ^[0-9a-f]{40}$ ]] || fail "evidence commit must be a full lowercase Git commit" -[[ "$evidence_mode" == "exact_commit" ]] || fail "invalid evidence mode: $evidence_mode" -[[ "$evidence_commit" == "$commit" ]] || fail "evidence commit must equal the release commit" -[[ -n "$output" ]] || fail "--output is required" - -for path in "$source_archive" "$crate_package" "$ct_evidence" "$repository_controls"; do - [[ -f "$path" ]] || fail "artifact is missing: $path" -done - -git -C "$root" rev-parse -q --verify "$tag^{tag}" >/dev/null || fail "release tag is not annotated: $tag" -tag_object=$(git -C "$root" rev-parse "$tag^{tag}") -tag_commit=$(git -C "$root" rev-parse "$tag^{commit}") -[[ "$tag_commit" == "$commit" ]] || fail "tag $tag resolves to $tag_commit, not $commit" -git_tree=$(git -C "$root" rev-parse "$commit^{tree}") - -expected_source_name="rscrypto-${version}-source.tar.gz" -[[ $(basename "$source_archive") == "$expected_source_name" ]] \ - || fail "unexpected source archive name: $(basename "$source_archive")" -reproduced_dir=$(mktemp -d) -committed_toolchain=$(mktemp) -committed_lock=$(mktemp) -committed_workflow=$(mktemp) -trap 'rm -rf "$reproduced_dir"; rm -f "$committed_toolchain" "$committed_lock" "$committed_workflow"' EXIT -"$(dirname "$0")/package-release-source.sh" \ - --root "$root" \ - --version "$version" \ - --tag "$tag" \ - --commit "$commit" \ - --out "$reproduced_dir" >/dev/null -cmp -s "$source_archive" "$reproduced_dir/$expected_source_name" \ - || fail "source archive is not the deterministic archive for $commit" - -expected_crate_name="rscrypto-${version}.crate" -[[ $(basename "$crate_package") == "$expected_crate_name" ]] \ - || fail "unexpected crate package name: $(basename "$crate_package")" -vcs_json=$(tar -xOf "$crate_package" "rscrypto-${version}/.cargo_vcs_info.json" 2>/dev/null) \ - || fail "crate package lacks .cargo_vcs_info.json" -jq -e --arg commit "$commit" ' - .git.sha1 == $commit - and (.git.dirty // false) == false - and ((.path_in_vcs // "") == "") -' <<< "$vcs_json" >/dev/null || fail "crate package is not bound to release commit $commit" - -expected_ct_name="rscrypto-${version}-ct-evidence.tar.gz" -[[ $(basename "$ct_evidence") == "$expected_ct_name" ]] \ - || fail "unexpected CT evidence name: $(basename "$ct_evidence")" -ct_metadata=$(tar -xOf "$ct_evidence" CT-EVIDENCE-BUNDLE.json 2>/dev/null) \ - || fail "CT evidence lacks CT-EVIDENCE-BUNDLE.json" -jq -e \ - --arg version "$version" \ - --arg commit "$commit" \ - --arg evidence_commit "$evidence_commit" \ - --arg evidence_mode "$evidence_mode" ' - .schema_version == 1 - and .kind == "rscrypto.ct.release-evidence" - and .crate == "rscrypto" - and .crate_version == $version - and .git_commit == $commit - and .evidence_git_commit == $evidence_commit - and .evidence_mode == $evidence_mode -' <<< "$ct_metadata" >/dev/null || fail "CT evidence identity does not match the release" - -expected_controls_name="rscrypto-${version}-repository-controls.json" -[[ $(basename "$repository_controls") == "$expected_controls_name" ]] \ - || fail "unexpected repository-controls name: $(basename "$repository_controls")" -jq -e --arg commit "$commit" ' - .kind == "rscrypto.repository-controls" - and .release_commit == $commit -' "$repository_controls" >/dev/null || fail "repository controls are not bound to release commit $commit" - -git -C "$root" show "$commit:rust-toolchain.toml" > "$committed_toolchain" -git -C "$root" show "$commit:Cargo.lock" > "$committed_lock" -git -C "$root" show "$commit:.github/workflows/release.yaml" > "$committed_workflow" -toolchain_channel=$(awk -F'"' '/^channel/ {print $2}' "$committed_toolchain") -[[ -n "$toolchain_channel" ]] || fail "committed rust-toolchain.toml has no channel" -active_toolchain=$(cd "$root" && rustup show active-toolchain | awk '{print $1}') -[[ "$active_toolchain" == "$toolchain_channel"* ]] \ - || fail "active toolchain $active_toolchain does not match pinned channel $toolchain_channel" -rustc_version=$(cd "$root" && rustc -Vv) -cargo_version=$(cd "$root" && cargo -V) - -sha256_file() { - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$1" | awk '{print $1}' - else - shasum -a 256 "$1" | awk '{print $1}' - fi -} - -source_sha256=$(sha256_file "$source_archive") -crate_sha256=$(sha256_file "$crate_package") -ct_sha256=$(sha256_file "$ct_evidence") -controls_sha256=$(sha256_file "$repository_controls") -toolchain_sha256=$(sha256_file "$committed_toolchain") -lock_sha256=$(sha256_file "$committed_lock") -workflow_sha256=$(sha256_file "$committed_workflow") - -mkdir -p "$(dirname "$output")" -output_tmp="${output}.tmp.$$" -trap 'rm -rf "$reproduced_dir"; rm -f "$committed_toolchain" "$committed_lock" "$committed_workflow" "$output_tmp"' EXIT -jq -nS \ - --arg version "$version" \ - --arg tag "$tag" \ - --arg tag_object "$tag_object" \ - --arg commit "$commit" \ - --arg tree "$git_tree" \ - --arg evidence_commit "$evidence_commit" \ - --arg evidence_mode "$evidence_mode" \ - --arg toolchain_channel "$toolchain_channel" \ - --arg active_toolchain "$active_toolchain" \ - --arg rustc "$rustc_version" \ - --arg cargo "$cargo_version" \ - --arg toolchain_sha256 "$toolchain_sha256" \ - --arg lock_sha256 "$lock_sha256" \ - --arg workflow_sha256 "$workflow_sha256" \ - --arg source_name "$(basename "$source_archive")" \ - --arg source_sha256 "$source_sha256" \ - --arg crate_name "$(basename "$crate_package")" \ - --arg crate_sha256 "$crate_sha256" \ - --arg ct_name "$(basename "$ct_evidence")" \ - --arg ct_sha256 "$ct_sha256" \ - --arg controls_name "$(basename "$repository_controls")" \ - --arg controls_sha256 "$controls_sha256" ' - { - schema_version: 1, - kind: "rscrypto.release-manifest", - crate: "rscrypto", - crate_version: $version, - release: { - tag: $tag, - tag_object: $tag_object, - git_commit: $commit, - git_tree: $tree - }, - toolchain: { - channel: $toolchain_channel, - active: $active_toolchain, - rustc: $rustc, - cargo: $cargo, - manifest: {path: "rust-toolchain.toml", sha256: $toolchain_sha256} - }, - inputs: { - cargo_lock: {path: "Cargo.lock", sha256: $lock_sha256}, - release_workflow: {path: ".github/workflows/release.yaml", sha256: $workflow_sha256} - }, - evidence: { - git_commit: $evidence_commit, - mode: $evidence_mode - }, - artifacts: { - source_archive: { - name: $source_name, - sha256: $source_sha256, - format: "git-archive+tar+gzip-n", - prefix: ("rscrypto-" + $version + "/") - }, - crate_package: {name: $crate_name, sha256: $crate_sha256}, - ct_evidence: {name: $ct_name, sha256: $ct_sha256}, - repository_controls: {name: $controls_name, sha256: $controls_sha256} - } - } -' > "$output_tmp" -mv "$output_tmp" "$output" -trap 'rm -rf "$reproduced_dir"; rm -f "$committed_toolchain" "$committed_lock" "$committed_workflow"' EXIT - -manifest_sha256=$(sha256_file "$output") -if [[ -n ${GITHUB_OUTPUT:-} ]]; then - { - echo "manifest_path=$output" - echo "manifest_name=$(basename "$output")" - echo "manifest_sha256=$manifest_sha256" - } >> "$GITHUB_OUTPUT" -fi - -echo "release manifest: $output" -echo "sha256: $manifest_sha256" diff --git a/scripts/ct/ci.sh b/scripts/ct/ci.sh new file mode 100755 index 00000000..3b9dabc7 --- /dev/null +++ b/scripts/ct/ci.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "usage: $0 --platform NAME --target TRIPLE [--dudect-timeout N] [--binsec-timeout N] [--dudect-filter CSV] [--dudect-gate required|diagnostic|all] [--raw]" >&2 + exit 2 +} + +platform="" +target="" +dudect_timeout=1800 +binsec_timeout=900 +dudect_filter="" +dudect_gate=required +raw=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --platform) platform=${2:-}; shift 2 ;; + --target) target=${2:-}; shift 2 ;; + --dudect-timeout) dudect_timeout=${2:-}; shift 2 ;; + --binsec-timeout) binsec_timeout=${2:-}; shift 2 ;; + --dudect-filter) dudect_filter=${2:-}; shift 2 ;; + --dudect-gate) dudect_gate=${2:-}; shift 2 ;; + --raw) raw=true; shift ;; + *) usage ;; + esac +done + +case "$platform:$target" in + amd-zen4:x86_64-unknown-linux-gnu | intel-spr:x86_64-unknown-linux-gnu | \ + intel-icl:x86_64-unknown-linux-gnu | amd-zen5:x86_64-unknown-linux-gnu | \ + graviton3:aarch64-unknown-linux-gnu | graviton4:aarch64-unknown-linux-gnu | \ + ibm-s390x:s390x-unknown-linux-gnu | \ + ibm-power10:powerpc64le-unknown-linux-gnu | \ + rise-riscv:riscv64gc-unknown-linux-gnu) ;; + *) usage ;; +esac +[[ "$dudect_timeout" =~ ^[1-9][0-9]*$ && "$binsec_timeout" =~ ^[1-9][0-9]*$ ]] || usage +case "$dudect_gate" in required | diagnostic | all) ;; *) usage ;; esac + +evidence_dir=target/ct-evidence-package +mkdir -p "$evidence_dir" +{ + echo "CT platform: $platform" + echo "CT target: $target" + uname -a + rustc -vV + cargo -V + command -v lscpu >/dev/null 2>&1 && lscpu || true +} 2>&1 | tee "$evidence_dir/host-$platform.log" + +args=( + --target "$target" + --dudect-timeout "$dudect_timeout" + --binsec-timeout "$binsec_timeout" + --dudect-gate "$dudect_gate" +) +[[ -z "$dudect_filter" ]] || args+=(--dudect-filter "$dudect_filter") + +status=0 +scripts/lib/python.sh scripts/ct/full.py "${args[@]}" \ + 2>&1 | tee "$evidence_dir/ct-full-$platform.log" || status=$? + +package_args=(--target "$target" --suffix "$platform" --out-dir "$evidence_dir") +[[ "$raw" == false ]] || package_args+=(--raw) +scripts/lib/python.sh scripts/ct/package_evidence.py "${package_args[@]}" +exit "$status" diff --git a/scripts/ct/structural.sh b/scripts/ct/structural.sh new file mode 100755 index 00000000..cc142499 --- /dev/null +++ b/scripts/ct/structural.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Build and validate the bounded constant-time structure gate used by affected CI. + +set -euo pipefail + +[[ $# -eq 0 ]] || { + echo "Usage: $0" >&2 + exit 2 +} + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$REPO_ROOT" + +target=$(rustc -vV | awk -F': ' '/^host:/ { print $2 }') +[[ "$target" == x86_64-unknown-linux-gnu ]] || { + echo "CT structural gate requires an x86_64-unknown-linux-gnu host; found $target" >&2 + exit 2 +} + +"$SCRIPT_DIR/artifacts.sh" --target "$target" --profile release +"$REPO_ROOT/scripts/lib/python.sh" "$SCRIPT_DIR/validate.py" \ + --target "$target" \ + --profile release \ + --strict-coverage diff --git a/scripts/ct/validate.py b/scripts/ct/validate.py index a21ce935..d7ff8d79 100755 --- a/scripts/ct/validate.py +++ b/scripts/ct/validate.py @@ -113,8 +113,11 @@ def sha256_file(path: Path) -> str: def matrix_targets(matrix: dict) -> set[str]: - groups = matrix.get("groups", {}) - return {target for values in groups.values() for target in values} + return { + str(row.get("dimensions", {}).get("target", "")) + for row in matrix.get("variants", []) + if row.get("dimensions", {}).get("target") + } def primitive_requires_evidence(ct: dict, primitive: dict, evidence: str) -> bool: diff --git a/scripts/ct/validate_release_evidence.py b/scripts/ct/validate_release_evidence.py index 8ba88adf..789e8554 100755 --- a/scripts/ct/validate_release_evidence.py +++ b/scripts/ct/validate_release_evidence.py @@ -65,7 +65,7 @@ def unique_file(root: Path, name: str) -> Path: def expected_lanes(root: Path, matrix_script: Path) -> dict[str, str]: env = dict(os.environ) env.pop("GITHUB_OUTPUT", None) - env.update({"GH_RUN_ID": "release-evidence-validation", "CT_PLATFORMS": "all,riscv"}) + env.update({"GH_RUN_ID": "release-evidence-validation", "CT_PLATFORMS": "all"}) try: output = subprocess.check_output([str(matrix_script), "ct"], cwd=root, env=env, text=True) matrix = json.loads(output) diff --git a/scripts/lib/ci-tool-integrity.sh b/scripts/lib/ci-tool-integrity.sh index 4a156fa4..22736e62 100644 --- a/scripts/lib/ci-tool-integrity.sh +++ b/scripts/lib/ci-tool-integrity.sh @@ -33,7 +33,7 @@ ci_tool_validate_record() { local digest=$7 case "$tool" in - wasmtime | wasm-tools | codecov) ;; + wasmtime | wasm-tools) ;; *) ci_tool_fail "unknown direct CI tool: $tool" || return ;; esac [[ "$version" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+[-+A-Za-z0-9.]*$ ]] \ @@ -103,15 +103,8 @@ ci_tool_resolve() { ci_tool_validate_manifest() { local tool - for tool in wasmtime wasm-tools codecov; do - case "$tool" in - wasmtime | wasm-tools) - local expected_platforms=(linux:x86_64 linux:aarch64 macos:x86_64 macos:aarch64) - ;; - codecov) - local expected_platforms=(linux:x86_64) - ;; - esac + for tool in wasmtime wasm-tools; do + local expected_platforms=(linux:x86_64 linux:aarch64 macos:x86_64 macos:aarch64) local platform for platform in "${expected_platforms[@]}"; do diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh index 47cc70e5..6111c282 100644 --- a/scripts/lib/common.sh +++ b/scripts/lib/common.sh @@ -63,7 +63,7 @@ show_error() { select_cargo_scope() { local work_id=$1 local force_all=${2:-false} - local arg + local arg args_file CARGO_ARGS=() CARGO_SCOPE_KIND="" @@ -76,10 +76,10 @@ select_cargo_scope() { return 0 fi - # Prime in the caller shell so subsequent command/process substitutions reuse - # the same authenticated plan instead of replanning in isolated subshells. - rail_prime_plan || true - CARGO_SCOPE_KIND="$(rail_scope_mode "$work_id")" + # Prime in the caller shell so subsequent process substitutions consume the + # same verified plan instead of replanning in isolated subshells. + rail_prime_plan || return 2 + CARGO_SCOPE_KIND="$(rail_scope_mode "$work_id")" || return 2 case "$CARGO_SCOPE_KIND" in empty) @@ -87,13 +87,18 @@ select_cargo_scope() { return 1 ;; workspace) - CARGO_ARGS=(--workspace) SCOPE_DESC="workspace (Cargo Rail)" ;; packages) - while IFS= read -r arg; do - [[ -n "$arg" ]] && CARGO_ARGS+=("$arg") - done < <(rail_scope_cargo_args "$work_id") + args_file=$(mktemp "${TMPDIR:-/tmp}/rscrypto-cargo-args.XXXXXX") + if ! rail_scope_cargo_args "$work_id" >"$args_file"; then + rm -f "$args_file" + return 2 + fi + while IFS= read -r -d '' arg; do + CARGO_ARGS+=("$arg") + done <"$args_file" + rm -f "$args_file" if [[ ${#CARGO_ARGS[@]} -eq 0 ]]; then echo "ERROR: Cargo Rail selected packages without Cargo arguments for $work_id" >&2 return 2 diff --git a/scripts/lib/feature-profiles.sh b/scripts/lib/feature-profiles.sh index d8027cb0..642c81b4 100644 --- a/scripts/lib/feature-profiles.sh +++ b/scripts/lib/feature-profiles.sh @@ -2,8 +2,12 @@ # shellcheck disable=SC2034 # Feature profile arrays are caller-visible outputs for sourced matrix scripts. -# Complete required feature profile set. Execution may be narrower, but -# compilation must not lose a declared feature contract. +FEATURE_COMPILE_SHARDS=2 +FEATURE_RUNTIME_SHARDS=3 + +# Unique resolved compile graphs. COMPILE_FEATURE_ALIASES retains named roots +# that Cargo currently resolves identically to one canonical entry. The +# executor verifies that equivalence before sharing the proof. COMPILE_FEATURE_SETS=( "" "alloc" @@ -47,7 +51,6 @@ COMPILE_FEATURE_SETS=( "signatures" "key-exchange" "auth" - "alloc,auth" "std,auth" "std,password-hashing,getrandom" "aead" @@ -67,18 +70,76 @@ COMPILE_FEATURE_SETS=( "std,full,portable-only" ) -# Execute the full primitive surface once for each behavior-changing boundary. -# Leaf and umbrella composition remains compile-checked above. -EXECUTABLE_FEATURE_SETS=( +# canonical|alias. Keep one entry per retained named contract. +COMPILE_FEATURE_ALIASES=( + "auth|alloc,auth" +) + +# Runtime contracts are ordered so modulo-three sharding places one broad +# behavior baseline in each shard. Focused scopes exercise only the capability +# delta; `all` retains the complete library and integration suite where the +# feature changes the whole execution surface. +RUNTIME_PROFILE_IDS=( + "no-std-full" + "std-full" + "portable-fallback" "websocket-sha1" + "entropy" + "diagnostics" + "parallel" + "public-serde" + "secret-serde" +) + +RUNTIME_FEATURE_SETS=( "full" "std,full" + "std,full,portable-only" + "websocket-sha1" "std,full,getrandom" "std,full,diag" + "std,parallel" "std,full,serde" "std,full,serde-secrets" - "std,parallel" - "std,full,portable-only" +) + +# profile|target|optional libtest substring. `lib` selects unit tests and `all` +# selects the complete library/integration suite. Focused cases cover every +# test gated by the named capability without rerunning unrelated vectors. +RUNTIME_TEST_CASES=( + "no-std-full|all|" + "std-full|all|" + "portable-fallback|all|" + "websocket-sha1|lib|" + "websocket-sha1|root_surface|websocket" + "websocket-sha1|websocket_accept_digest|" + "websocket-sha1|websocket_sha1_allocations|" + "entropy|lib|" + "entropy|aead_foundations|aead_seal_random" + "entropy|api_consistency|rsa_signature_signer_shape_is_profile_bound" + "entropy|api_consistency|aead_random_to_vec_seals_and_opens" + "entropy|getrandom_smoke|" + "entropy|phc_roundtrip|" + "entropy|root_surface|generated_key_end_to_end" + "entropy|rsa_allocations|reused_scratch_rsa_operations_do_not_allocate" + "entropy|rsa_nist_cavp|nist_cavp_sha2_siggen_profile_signing_matches_expected_results" + "entropy|rsa_public_key|private_key_outputs_verify_and_decrypt" + "diagnostics|lib|" + "diagnostics|aead_kernel_equivalence|" + "diagnostics|argon2_kernels|" + "diagnostics|chacha20poly1305|chacha20poly1305_diag" + "diagnostics|root_surface|root_surface_aead_exports_compile" + "diagnostics|root_surface|advanced_checksum_modules_compile" + "diagnostics|root_surface|advanced_hash_modules_compile" + "diagnostics|rsa_public_key|diagnostic_private_exponentiation_matches_independent_fixed_width_reference" + "diagnostics|rsa_public_key|public_operation_bitserial_baseline_matches_montgomery_path" + "diagnostics|rsa_public_key|public_operation_montgomery_candidates_match_current_path" + "diagnostics|rsa_public_key|pss_encoded_message_oracle_failures_are_opaque" + "diagnostics|rsa_public_key|pkcs1v15_encoded_message_oracle_failures_are_opaque" + "public-serde|serde_roundtrip|" + "secret-serde|serde_roundtrip|" + "parallel|lib|" + "parallel|argon2_parallel|" ) # Portable feature contract for targets without the standard library. diff --git a/scripts/lib/rail-plan.sh b/scripts/lib/rail-plan.sh index c067edbd..9612ba25 100644 --- a/scripts/lib/rail-plan.sh +++ b/scripts/lib/rail-plan.sh @@ -1,92 +1,227 @@ #!/usr/bin/env bash -# Strict Cargo Rail v8 plan helpers for repository scripts. +# One fail-closed Cargo Rail v8 plan consumer for repository scripts. RAIL_PLAN_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" _rail_load_plan() { - if [[ -n "${RAIL_PLAN_JSON_CACHE:-}" && "${RAIL_PLAN_JSON_CACHE_VALIDATED:-false}" == true ]]; then + if [[ "${RAIL_PLAN_LOADED:-false}" == true ]]; then return 0 fi if [[ "${RAIL_PLAN_LOAD_ATTEMPTED:-false}" == true ]]; then - return 1 + echo "Cargo Rail plan loading already failed" >&2 + return 2 fi RAIL_PLAN_LOAD_ATTEMPTED=true - local plan_output + local python + python="$("$RAIL_PLAN_LIB_DIR/python.sh" --print)" || return 2 + if [[ -n "${RAIL_PLAN_FILE:-}" || -n "${RAIL_PLAN_READER:-}" ]]; then - [[ -n "${RAIL_PLAN_FILE:-}" && -n "${RAIL_PLAN_READER:-}" ]] || return 1 - [[ -f "$RAIL_PLAN_FILE" && -f "$RAIL_PLAN_READER" ]] || return 1 - local python - python="$("$RAIL_PLAN_LIB_DIR/python.sh" --print)" || return 1 - "$python" "$RAIL_PLAN_READER" validate "$RAIL_PLAN_FILE" >/dev/null || return 1 - if [[ "${RAIL_PLAN_CHECKOUT_VERIFIED:-false}" != true ]]; then - "$python" "$RAIL_PLAN_READER" verify-checkout "$RAIL_PLAN_FILE" >/dev/null || return 1 + [[ -n "${RAIL_PLAN_FILE:-}" && -f "$RAIL_PLAN_FILE" ]] || { + echo "RAIL_PLAN_FILE must name a saved plan" >&2 + return 2 + } + if [[ -n "${RAIL_PLAN_READER:-}" ]]; then + [[ -f "$RAIL_PLAN_READER" ]] || { + echo "RAIL_PLAN_READER must name the matching strict reader" >&2 + return 2 + } + "$python" "$RAIL_PLAN_READER" validate "$RAIL_PLAN_FILE" || return 2 + "$python" "$RAIL_PLAN_READER" verify-checkout "$RAIL_PLAN_FILE" || return 2 + RAIL_PLAN_USE_READER=true + elif [[ "${RAIL_PLAN_LOCAL:-false}" == true ]]; then + cargo rail plan --verify "$RAIL_PLAN_FILE" || return 2 + RAIL_PLAN_USE_READER=false + else + echo "A transported plan requires its matching RAIL_PLAN_READER" >&2 + return 2 fi - plan_output=$(<"$RAIL_PLAN_FILE") + RAIL_PLAN_JSON_CACHE=$(<"$RAIL_PLAN_FILE") else local plan_args=(rail plan --quiet --json) if [[ -n "${RAIL_SINCE:-}" ]]; then plan_args+=(--since "$RAIL_SINCE") fi - plan_output=$(cargo "${plan_args[@]}" 2>/dev/null) || return 1 + if [[ "${RAIL_ALL:-false}" == true ]]; then + plan_args+=(--all) + fi + RAIL_PLAN_JSON_CACHE=$(cargo "${plan_args[@]}") || return 2 local plan_file - plan_file=$(mktemp) - printf '%s\n' "$plan_output" >"$plan_file" - if ! cargo rail plan --verify "$plan_file" >/dev/null 2>&1; then + plan_file=$(mktemp "${TMPDIR:-/tmp}/rscrypto-plan-v8.XXXXXX") + printf '%s\n' "$RAIL_PLAN_JSON_CACHE" >"$plan_file" + if ! cargo rail plan --verify "$plan_file"; then rm -f "$plan_file" - return 1 + return 2 fi rm -f "$plan_file" + RAIL_PLAN_USE_READER=false fi - jq -e '.plan_contract_version == 8' <<<"$plan_output" >/dev/null 2>&1 || return 1 - RAIL_PLAN_JSON_CACHE=$plan_output - RAIL_PLAN_JSON_CACHE_VALIDATED=true + jq -e ' + .plan_contract_version == 8 + and (.identity | type == "string" and startswith("plan-v8:sha256:")) + and (.required | type == "array") + and (.work | type == "object") + ' <<<"$RAIL_PLAN_JSON_CACHE" >/dev/null || { + echo "Cargo Rail emitted an incompatible plan" >&2 + return 2 + } + + if [[ -n "${RAIL_PLAN_IDENTITY:-}" ]]; then + local actual_identity + actual_identity=$(jq -r '.identity' <<<"$RAIL_PLAN_JSON_CACHE") + [[ "$actual_identity" == "$RAIL_PLAN_IDENTITY" ]] || { + echo "Cargo Rail plan identity mismatch" >&2 + return 2 + } + fi + if [[ -n "${RAIL_PLAN_HEAD_COMMIT:-}" ]]; then + local actual_head + actual_head=$(jq -r '.inputs.head_commit' <<<"$RAIL_PLAN_JSON_CACHE") + [[ "$actual_head" == "$RAIL_PLAN_HEAD_COMMIT" ]] || { + echo "Cargo Rail plan checkout mismatch" >&2 + return 2 + } + fi + + RAIL_PLAN_LOADED=true } rail_prime_plan() { _rail_load_plan } +rail_work_required() { + local work_id=$1 + _rail_load_plan || return 2 + + if [[ "$RAIL_PLAN_USE_READER" == true ]]; then + local python required status=0 + python="$("$RAIL_PLAN_LIB_DIR/python.sh" --print)" || return 2 + required=$("$python" "$RAIL_PLAN_READER" is-required "$RAIL_PLAN_FILE" "$work_id") || status=$? + [[ "$status" -eq 0 ]] || return 2 + case "$required" in + true) return 0 ;; + false) return 1 ;; + *) + echo "Cargo Rail reader emitted an invalid required-work decision" >&2 + return 2 + ;; + esac + fi + + local status=0 + jq -e --arg work_id "$work_id" ' + .work[$work_id] as $decision + | if $decision == null then error("unknown work ID") else $decision.state == "required" end + ' <<<"$RAIL_PLAN_JSON_CACHE" >/dev/null || status=$? + case "$status" in + 0) return 0 ;; + 1) return 1 ;; + *) return 2 ;; + esac +} + rail_scope_json() { - local work_id=${1:-${RAIL_WORK_ID:-cargo.build}} - _rail_load_plan || return 1 + local work_id=${1:-} + [[ -n "$work_id" ]] || { + echo "Cargo Rail work ID is required" >&2 + return 2 + } + _rail_load_plan || return 2 jq -ce --arg work_id "$work_id" ' .work[$work_id] as $decision - | if $decision.state == "skipped" then + | if $decision == null then + error("unknown work ID") + elif $decision.state == "skipped" then {mode: "empty", cargo_args: []} elif $decision.state == "required" and $decision.scope.kind == "cargo" - and $decision.scope.selection.kind == "workspace" then - {mode: "workspace", cargo_args: ["--workspace"]} - elif $decision.state == "required" - and $decision.scope.kind == "cargo" - and $decision.scope.selection.kind == "packages" then + and ($decision.scope.selection.kind == "workspace" or $decision.scope.selection.kind == "packages") + and ($decision.scope.selection.cargo_args | type == "array") then { - mode: "packages", + mode: $decision.scope.selection.kind, cargo_args: $decision.scope.selection.cargo_args } else - empty + error("work item does not carry Cargo scope") end ' <<<"$RAIL_PLAN_JSON_CACHE" } rail_scope_mode() { - local scope_output - if ! scope_output=$(rail_scope_json "${1:-}"); then - echo workspace - return 0 + _rail_load_plan || return 2 + if [[ "$RAIL_PLAN_USE_READER" == true ]]; then + local python mode status=0 + python="$("$RAIL_PLAN_LIB_DIR/python.sh" --print)" || return 2 + mode=$("$python" "$RAIL_PLAN_READER" cargo-scope "$RAIL_PLAN_FILE" "$1") || status=$? + [[ "$status" -eq 0 ]] || return 2 + case "$mode" in + skipped) printf 'empty\n' ;; + workspace | packages) printf '%s\n' "$mode" ;; + *) + echo "Cargo Rail reader emitted an invalid Cargo scope" >&2 + return 2 + ;; + esac + return fi + + local scope_output + scope_output=$(rail_scope_json "$1") || return 2 jq -r '.mode' <<<"$scope_output" } rail_scope_cargo_args() { - local scope_output - if ! scope_output=$(rail_scope_json "${1:-}"); then - return 0 + local work_id=$1 + _rail_load_plan || return 2 + + if [[ "$RAIL_PLAN_USE_READER" == true ]]; then + local python + python="$("$RAIL_PLAN_LIB_DIR/python.sh" --print)" || return 2 + "$python" "$RAIL_PLAN_READER" cargo-args "$RAIL_PLAN_FILE" "$work_id" + return fi - jq -r 'select(.mode != "empty") | .cargo_args[]' <<<"$scope_output" + + rail_scope_json "$work_id" | jq -j '.cargo_args[] | ., "\u0000"' +} + +rail_variant_matrix() { + local work_id=${1:-} + [[ -n "$work_id" ]] || { + echo "Cargo Rail work ID is required" >&2 + return 2 + } + _rail_load_plan || return 2 + + if [[ "$RAIL_PLAN_USE_READER" == true ]]; then + local python + python="$("$RAIL_PLAN_LIB_DIR/python.sh" --print)" || return 2 + "$python" "$RAIL_PLAN_READER" matrix "$RAIL_PLAN_FILE" "$work_id" + return + fi + + jq -cer --arg work_id "$work_id" ' + .work[$work_id] as $decision + | if $decision == null then + error("unknown work ID") + elif $decision.state == "skipped" then + {include: []} + elif $decision.state != "required" or $decision.scope.kind != "variants" then + error("work item does not carry variant scope") + elif $decision.scope.selection.kind == "all" then + "all" + elif $decision.scope.selection.kind == "selected" + and ($decision.scope.selection.variants | type == "array") + and ($decision.scope.selection.variants | length) > 0 then + {include: [ + $decision.scope.selection.variants[] + | {id: .id} + .dimensions + ]} + else + error("work item carries an invalid variant selection") + end + ' <<<"$RAIL_PLAN_JSON_CACHE" } diff --git a/scripts/lib/target-matrix.sh b/scripts/lib/target-matrix.sh index 2adac272..b9dc357c 100755 --- a/scripts/lib/target-matrix.sh +++ b/scripts/lib/target-matrix.sh @@ -1,9 +1,10 @@ #!/usr/bin/env bash -# Validate .config/target-matrix.json and its generated projections. +# Validate the Cargo Rail platform catalog and its target projections. # # Single source of truth: .config/target-matrix.json # -# `.config/ci-plan-variants.json` owns CI runner rows and Cargo Rail selection. +# Every row is one independently selectable proof unit. The ordinary Linux +# x86-64 row is executed by the core job; AMX is a second proof for that target. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -26,17 +27,49 @@ fi validate_manifest() { jq -e ' . as $root | - ($root.groups | keys) == ["ibm", "linux", "macos", "no_std", "wasm", "win"] and - all($root.groups[]; type == "array" and length > 0 and . == sort and all(.[]; type == "string" and length > 0)) and - ([$root.groups[][]] | length) == ([$root.groups[][]] | unique | length) and - ($root | keys) == ["groups"] + ($root | keys) == ["variant_catalog_version", "variants", "work"] and + $root.variant_catalog_version == 2 and + $root.work == "targets.platforms" and + ($root.variants | type == "array" and length == 18) and + ([$root.variants[].id] == ([$root.variants[].id] | sort | unique)) and + all($root.variants[]; + (. | keys | sort) == (["config", "dimensions", "external_paths", "id"] | sort) and + (.id | type == "string" and test("^[a-z][a-z0-9.-]*$")) and + (.external_paths | type == "array" and length > 0 and . == sort and length == (unique | length)) and + (.config == ["targets"]) and + (.dimensions | keys | sort) == ([ + "cache", "compile", "components", "constant_time", "contract", "group", + "name", "operation", "performance", "platform", "release", "runner", + "runner_type", "runtime", "target", "timeout_minutes", "verify_plan" + ] | sort) and + (.dimensions.group | IN("ibm", "linux", "macos", "no_std", "wasm", "win")) and + (.dimensions.operation | IN("amx", "core", "cross", "native")) and + (.dimensions.runner_type | IN("github", "runson")) and + (.dimensions.contract | IN("development", "nightly")) and + (.dimensions.compile | IN("core", "generic", "hosted", "native")) and + (.dimensions.runtime | IN("emulated", "none", "physical-native", "virtual-native")) and + (.dimensions.timeout_minutes | type == "number" and . >= 0) and + (.dimensions.cache | type == "boolean") and + (.dimensions.constant_time | type == "boolean") and + (.dimensions.performance | type == "boolean") and + (.dimensions.release == true) and + (.dimensions.verify_plan | type == "boolean") + ) and + ([$root.variants[] | select(.dimensions.operation != "amx") | .dimensions.target] | length) == 17 and + ([$root.variants[] | .dimensions.target] | unique | length) == 17 and + ([$root.variants[] | select(.dimensions.operation == "core")] | length) == 1 and + ([$root.variants[] | select(.dimensions.operation == "amx")] | length) == 1 and + ($root.variants[] | select(.id == "aarch64-pc-windows-msvc") | .dimensions.runtime) == "none" and + ($root.variants[] | select(.id == "x86-64-pc-windows-msvc") | .dimensions.runtime) == "virtual-native" and + ($root.variants[] | select(.id == "wasm32-wasip1") | .dimensions.runtime) == "emulated" and + all($root.variants[] | select(.dimensions.verify_plan); .dimensions.runner_type == "runson") ' "$MANIFEST" >/dev/null || { echo "ERROR: invalid target matrix schema: $MANIFEST" >&2 return 1 } local matrix_targets - matrix_targets="$(jq -r '.groups[][]' "$MANIFEST" | LC_ALL=C sort)" + matrix_targets="$(jq -r '[.variants[].dimensions.target] | unique[]' "$MANIFEST" | LC_ALL=C sort)" local projection for projection in "$REPO_ROOT/.config/rail.toml" "$REPO_ROOT/deny.toml"; do diff --git a/scripts/lib/targets.sh b/scripts/lib/targets.sh index 4c248485..fbd502d3 100644 --- a/scripts/lib/targets.sh +++ b/scripts/lib/targets.sh @@ -11,9 +11,7 @@ TARGET_MATRIX_JSON="$TARGETS_LIB_DIR/../../.config/target-matrix.json" # Always define arrays up front so callers using `set -u` never trip on # unbound vars when target matrix loading fails. -declare -a WIN_TARGETS=() declare -a LINUX_TARGETS=() -declare -a IBM_TARGETS=() declare -a NOSTD_TARGETS=() declare -a WASM_TARGETS=() @@ -29,17 +27,20 @@ load_target_group() { local target while IFS= read -r target; do case "$group" in - win) WIN_TARGETS+=("$target") ;; linux) LINUX_TARGETS+=("$target") ;; - ibm) IBM_TARGETS+=("$target") ;; no_std) NOSTD_TARGETS+=("$target") ;; wasm) WASM_TARGETS+=("$target") ;; esac - done < <(jq -r --arg group "$group" '.groups[$group][]' "$TARGET_MATRIX_JSON") + done < <( + jq -r --arg group "$group" ' + [.variants[] + | select(.dimensions.group == $group and .dimensions.operation != "amx") + | .dimensions.target] + | unique[] + ' "$TARGET_MATRIX_JSON" + ) } -load_target_group win load_target_group linux -load_target_group ibm load_target_group no_std load_target_group wasm diff --git a/scripts/test/fuzz-contracts.sh b/scripts/test/fuzz-contracts.sh new file mode 100755 index 00000000..f0e3150b --- /dev/null +++ b/scripts/test/fuzz-contracts.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +# Materialize and execute Cargo Rail-selected fuzz target groups. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +CATALOG="$REPO_ROOT/.config/fuzz-matrix.json" +# shellcheck source=../lib/common.sh +source "$SCRIPT_DIR/../lib/common.sh" + +usage() { + echo "Usage: $0 rows [PLAN READER] | selected ROWS | run ROW | list" >&2 + exit 2 +} + +validate_catalog() { + jq -e ' + .variant_catalog_version == 2 + and .work == "assurance.fuzz" + and (.variants | type == "array" and length > 0) + and ([.variants[].id] | length == (unique | length)) + and ([.variants[].dimensions.targets | split(",")[]] + | length == (unique | length)) + and all(.variants[]; + (.id | test("^[a-z][a-z0-9.-]*$")) + and (.dimensions | keys | sort) == ["duration_seconds", "name", "targets"] + and (.dimensions.name | type == "string" and length > 0) + and (.dimensions.targets | test("^[a-z0-9_]+(,[a-z0-9_]+)*$")) + and (.dimensions.duration_seconds | type == "number" and . > 0 and floor == .) + and (.external_paths | type == "array" and length > 0 and length == (unique | length)) + and all(.external_paths[]; type == "string" and length > 0) + ) + ' "$CATALOG" >/dev/null || { + echo "fuzz variant catalog is malformed" >&2 + return 2 + } + + local known_targets catalog_targets + known_targets=$(mktemp "${TMPDIR:-/tmp}/rscrypto-fuzz-targets.XXXXXX") + catalog_targets="$known_targets.catalog" + awk ' + /^\[\[bin\]\]$/ { in_bin = 1; next } + in_bin && /^name = "/ { + value = $0 + sub(/^name = "/, "", value) + sub(/".*$/, "", value) + print value + in_bin = 0 + } + ' "$REPO_ROOT/fuzz/Cargo.toml" "$REPO_ROOT"/fuzz-packages/*/Cargo.toml \ + | sort -u >"$known_targets" + + jq -r '.variants[].dimensions.targets | split(",")[]' "$CATALOG" \ + | sort -u >"$catalog_targets" + if ! cmp -s "$known_targets" "$catalog_targets"; then + echo "fuzz variant catalog and declared fuzz targets differ:" >&2 + comm -3 "$known_targets" "$catalog_targets" >&2 + rm -f "$known_targets" "$catalog_targets" + return 2 + fi + rm -f "$known_targets" "$catalog_targets" +} + +row_exists() { + jq -e --arg id "$1" 'any(.variants[]; .id == $id)' "$CATALOG" >/dev/null +} + +parse_rows() { + local value=$1 + local row seen=, + [[ -n "$value" ]] || { + echo "selected fuzz rows must not be empty" >&2 + return 2 + } + IFS=',' read -r -a FUZZ_ROWS <<<"$value" + for row in "${FUZZ_ROWS[@]}"; do + if [[ ! "$row" =~ ^[a-z][a-z0-9.-]*$ ]] || ! row_exists "$row"; then + echo "unknown fuzz row: ${row:-}" >&2 + return 2 + fi + [[ "$seen" != *",$row,"* ]] || { + echo "duplicate fuzz row: $row" >&2 + return 2 + } + seen+="$row," + done +} + +selected_rows() { + [[ $# -eq 0 || $# -eq 2 ]] || usage + if [[ $# -eq 2 ]]; then + [[ -f "$1" && -f "$2" ]] || usage + export RAIL_PLAN_FILE=$1 + export RAIL_PLAN_READER=$2 + fi + + local matrix + matrix=$(rail_variant_matrix assurance.fuzz) + if [[ "$matrix" == all ]]; then + jq -r '[.variants[].id] | join(",")' "$CATALOG" + else + jq -er '[.include[].id] | join(",")' <<<"$matrix" + fi +} + +run_selected() { + [[ $# -eq 1 ]] || usage + parse_rows "$1" + + local selection + selection=$(jq -cer --arg rows "$1" ' + ($rows | split(",")) as $wanted + | [.variants[] | select(.id as $id | $wanted | index($id))] + | { + duration: ([.[].dimensions.duration_seconds] | max), + targets: ([.[].dimensions.targets | split(",")[]] + | reduce .[] as $target ([]; + if index($target) then . else . + [$target] end) + | join(",")) + } + ' "$CATALOG") + local duration targets + duration=$(jq -r '.duration' <<<"$selection") + targets=$(jq -r '.targets' <<<"$selection") + RSCRYPTO_FUZZ_DURATION_SECS="$duration" "$SCRIPT_DIR/test-fuzz.sh" --targets "$targets" +} + +run_row() { + [[ $# -eq 1 ]] || usage + row_exists "$1" || { + echo "unknown fuzz row: $1" >&2 + exit 2 + } + run_selected "$1" +} + +validate_catalog +case "${1:-}" in + rows) + shift + selected_rows "$@" + ;; + selected) + shift + run_selected "$@" + ;; + run) + shift + run_row "$@" + ;; + list) + [[ $# -eq 1 ]] || usage + jq -r '.variants[] | [.id, .dimensions.targets, (.dimensions.duration_seconds | tostring), .dimensions.name] | @tsv' "$CATALOG" + ;; + *) usage ;; +esac diff --git a/scripts/test/miri-contracts.sh b/scripts/test/miri-contracts.sh new file mode 100755 index 00000000..e130b403 --- /dev/null +++ b/scripts/test/miri-contracts.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# Materialize and execute Cargo Rail-selected Miri proof modes. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CATALOG="$SCRIPT_DIR/../../.config/miri-matrix.json" +# shellcheck source=../lib/common.sh +source "$SCRIPT_DIR/../lib/common.sh" + +usage() { + echo "Usage: $0 rows [PLAN READER] | selected ROWS | run ROW | list" >&2 + exit 2 +} + +validate_catalog() { + jq -e ' + .variant_catalog_version == 2 + and .work == "assurance.miri" + and (.variants | type == "array" and length > 0) + and ([.variants[].id] | length == (unique | length)) + and ([.variants[].dimensions.mode] | length == (unique | length)) + and all(.variants[]; + (.id | test("^[a-z][a-z0-9.-]*$")) + and (.dimensions | keys | sort) == ["mode", "name"] + and (.dimensions.name | type == "string" and length > 0) + and (.dimensions.mode == "focused" or .dimensions.mode == "rsa") + and (.external_paths | type == "array" and length > 0 and length == (unique | length)) + and all(.external_paths[]; type == "string" and length > 0) + ) + ' "$CATALOG" >/dev/null || { + echo "Miri variant catalog is malformed" >&2 + return 2 + } +} + +row_exists() { + jq -e --arg id "$1" 'any(.variants[]; .id == $id)' "$CATALOG" >/dev/null +} + +parse_rows() { + local value=$1 + local row seen=, + [[ -n "$value" ]] || { + echo "selected Miri rows must not be empty" >&2 + return 2 + } + IFS=',' read -r -a MIRI_ROWS <<<"$value" + for row in "${MIRI_ROWS[@]}"; do + if [[ ! "$row" =~ ^[a-z][a-z0-9.-]*$ ]] || ! row_exists "$row"; then + echo "unknown Miri row: ${row:-}" >&2 + return 2 + fi + [[ "$seen" != *",$row,"* ]] || { + echo "duplicate Miri row: $row" >&2 + return 2 + } + seen+="$row," + done +} + +selected_rows() { + [[ $# -eq 0 || $# -eq 2 ]] || usage + if [[ $# -eq 2 ]]; then + [[ -f "$1" && -f "$2" ]] || usage + export RAIL_PLAN_FILE=$1 + export RAIL_PLAN_READER=$2 + fi + + local matrix + matrix=$(rail_variant_matrix assurance.miri) + if [[ "$matrix" == all ]]; then + jq -r '[.variants[].id] | join(",")' "$CATALOG" + else + jq -er '[.include[].id] | join(",")' <<<"$matrix" + fi +} + +run_row() { + [[ $# -eq 1 ]] || usage + local row=$1 + row_exists "$row" || { + echo "unknown Miri row: $row" >&2 + exit 2 + } + local mode + mode=$(jq -r --arg id "$row" '.variants[] | select(.id == $id) | .dimensions.mode' "$CATALOG") + "$SCRIPT_DIR/test-miri.sh" "--$mode" +} + +run_selected() { + [[ $# -eq 1 ]] || usage + parse_rows "$1" + local row + for row in "${MIRI_ROWS[@]}"; do + run_row "$row" + done +} + +validate_catalog +case "${1:-}" in + rows) + shift + selected_rows "$@" + ;; + selected) + shift + run_selected "$@" + ;; + run) + shift + run_row "$@" + ;; + list) + [[ $# -eq 1 ]] || usage + jq -r '.variants[] | [.id, .dimensions.mode, .dimensions.name] | @tsv' "$CATALOG" + ;; + *) usage ;; +esac diff --git a/scripts/test/test-feature-matrix.sh b/scripts/test/test-feature-matrix.sh deleted file mode 100755 index 083407b5..00000000 --- a/scripts/test/test-feature-matrix.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -# shellcheck source=../lib/common.sh -source "$SCRIPT_DIR/../lib/common.sh" -# shellcheck source=../lib/feature-profiles.sh -source "$SCRIPT_DIR/../lib/feature-profiles.sh" - -LOG_DIR=$(mktemp -d) -TARGET_DIR="$REPO_ROOT/target/feature-matrix" - -cleanup() { - rm -rf "$LOG_DIR" "$TARGET_DIR" -} -trap cleanup EXIT -rm -rf "$TARGET_DIR" - -cleanup_feature_artifacts() { - cargo clean --target-dir "$TARGET_DIR" -p rscrypto >/dev/null 2>&1 || true -} - -show_feature_matrix_disk() { - if [[ "${CI:-}" == "true" || -n "${GITHUB_ACTIONS:-}" ]]; then - df -h "$LOG_DIR" | sed 's/^/ /' - fi -} - -TOTAL=${#EXECUTABLE_FEATURE_SETS[@]} -STARTED_AT=$SECONDS - -# RISC-V: nightly rustc crashes (SIGABRT in glibc allocator) when linking -# per-feature test binaries on the RISE runner. Downgrade to cargo-check so -# we still verify every feature combination compiles; full test coverage is -# provided by the --all-features test job. -ARCH=$(uname -m) -if [[ "$ARCH" == "riscv64" ]]; then - CARGO_CMD=(cargo check --locked --workspace --lib --tests) - COMMAND_CLASS="cargo check" - echo "Compilation rscrypto feature matrix ($TOTAL profiles; riscv64: check-only)" -else - CARGO_CMD=(cargo test --locked --workspace --lib --tests) - COMMAND_CLASS="cargo test" - echo "Executable rscrypto feature matrix ($TOTAL profiles)" -fi - -for i in "${!EXECUTABLE_FEATURE_SETS[@]}"; do - feature_set=${EXECUTABLE_FEATURE_SETS[$i]} - profile=$((i + 1)) - profile_started_at=$SECONDS - log_path="$LOG_DIR/$(echo "$feature_set" | tr ',' '_').log" - step "[$profile/$TOTAL] $COMMAND_CLASS --no-default-features --features $feature_set" - # Isolate reduced-feature test builds from the workspace target dir. The - # commit lane runs full-feature and no_std checks first, and sharing the same - # restored target cache has produced flaky matrix failures in CI. - if ! CARGO_TARGET_DIR="$TARGET_DIR" "${CARGO_CMD[@]}" --no-default-features \ - --features "$feature_set" >"$log_path" 2>&1; then - fail - show_error "$log_path" - show_feature_matrix_disk - exit 1 - fi - cleanup_feature_artifacts - ok - echo " elapsed: $((SECONDS - profile_started_at))s" -done - -echo "${GREEN}✓${RESET} Feature matrix passed: $TOTAL/$TOTAL profiles in $((SECONDS - STARTED_AT))s" diff --git a/scripts/test/test-fuzz-asan.sh b/scripts/test/test-fuzz-asan.sh index 64ad9886..e5745dea 100755 --- a/scripts/test/test-fuzz-asan.sh +++ b/scripts/test/test-fuzz-asan.sh @@ -11,6 +11,7 @@ source "$SCRIPT_DIR/../lib/common.sh" source "$SCRIPT_DIR/../lib/fuzz-packages.sh" activate_nightly_toolchain +export CARGO_RAIL_CACHE=off PACKAGE_SCOPE="full" diff --git a/scripts/test/test-fuzz-scheduler-test.sh b/scripts/test/test-fuzz-scheduler-test.sh index 58a4f964..eff856a6 100755 --- a/scripts/test/test-fuzz-scheduler-test.sh +++ b/scripts/test/test-fuzz-scheduler-test.sh @@ -182,4 +182,41 @@ if env \ fail "scheduler accepted zero concurrency" fi +SELECTED_CAPTURE="$TMP_ROOT/selected-capture" +mkdir -p "$SELECTED_CAPTURE" +env \ + PATH="$BIN:$PATH" \ + RSCRYPTO_FUZZ_CAPTURE="$SELECTED_CAPTURE" \ + RSCRYPTO_FUZZ_DURATION_SECS=3 \ + RSCRYPTO_FUZZ_TARGET_CONCURRENCY=2 \ + RSCRYPTO_FUZZ_JOBS=1 \ + bash "$FIXTURE/scripts/test/test-fuzz.sh" --targets alpha,delta \ + >"$SELECTED_CAPTURE/output" 2>&1 \ + || fail "exact target selection failed" + +for target in alpha delta; do + [[ -f "$SELECTED_CAPTURE/$target.args" ]] || fail "exact selection omitted $target" + grep -Fxq -- '-max_total_time=3' "$SELECTED_CAPTURE/$target.args" \ + || fail "$target ignored the exact-selection duration" +done +for target in beta crash; do + [[ ! -e "$SELECTED_CAPTURE/$target.args" ]] || fail "exact selection ran unselected target $target" +done +grep -Fq 'Summary: 2 targets, 0 failed' "$SELECTED_CAPTURE/output" \ + || fail "exact selection did not aggregate only the requested targets" + +if env \ + PATH="$BIN:$PATH" \ + RSCRYPTO_FUZZ_CAPTURE="$SELECTED_CAPTURE" \ + bash "$FIXTURE/scripts/test/test-fuzz.sh" --targets alpha,alpha >/dev/null 2>&1; then + fail "exact selection accepted a duplicate target" +fi + +if env \ + PATH="$BIN:$PATH" \ + RSCRYPTO_FUZZ_CAPTURE="$SELECTED_CAPTURE" \ + bash "$FIXTURE/scripts/test/test-fuzz.sh" --targets absent >/dev/null 2>&1; then + fail "exact selection accepted an unknown target" +fi + echo "Fuzz scheduler regression tests passed" diff --git a/scripts/test/test-fuzz.sh b/scripts/test/test-fuzz.sh index efe06580..fd03b7f8 100755 --- a/scripts/test/test-fuzz.sh +++ b/scripts/test/test-fuzz.sh @@ -13,6 +13,7 @@ set -euo pipefail # ./scripts/test/test-fuzz.sh --full # Run the full harness only # ./scripts/test/test-fuzz.sh --scoped # Run all scoped packages # ./scripts/test/test-fuzz.sh --scoped-build # Build scoped packages only +# ./scripts/test/test-fuzz.sh --targets A,B # Run an exact target set # ./scripts/test/test-fuzz.sh # Run specific target # ./scripts/test/test-fuzz.sh --build # Build without running # ./scripts/test/test-fuzz.sh --list # List available targets @@ -29,6 +30,7 @@ source "$SCRIPT_DIR/../lib/common.sh" source "$SCRIPT_DIR/../lib/fuzz-packages.sh" activate_nightly_toolchain +export CARGO_RAIL_CACHE=off # Configuration (can be overridden via environment) DURATION_SECS=${RSCRYPTO_FUZZ_DURATION_SECS:-60} @@ -53,6 +55,7 @@ show_help() { echo " $0 --full Run full harness targets (${DURATION_SECS}s each)" echo " $0 --scoped Run scoped targets (${DURATION_SECS}s each)" echo " $0 --scoped-build Build scoped packages without running" + echo " $0 --targets A,B Run an exact comma-separated target set" echo " $0 Run specific target" echo " $0 --build [--full|--scoped|--all] Build selected fuzz packages" echo " $0 --list List available targets by package" @@ -226,38 +229,28 @@ run_target() { run_target_in_package "$package_dir" "$target" "$duration" } -run_scope() { - local scope="$1" +RUN_PACKAGE_DIRS=() +RUN_TARGETS=() + +run_batch() { + local label="$1" local duration="$2" - local package_dir local failed=0 local crashed="" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "Fuzz Testing ($scope)" + echo "Fuzz Testing ($label)" echo "Duration: ${duration}s per target" echo "Concurrent targets: $TARGET_CONCURRENCY" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" - fuzz_select_packages "$scope" - if [ ${#SELECTED_FUZZ_PACKAGES[@]} -eq 0 ]; then - echo "No fuzz packages selected for scope: $scope" + local total=${#RUN_TARGETS[@]} + if [ "$total" -eq 0 ]; then + echo "No fuzz targets selected" return 0 fi - local package_dirs=() - local targets=() - local target - for package_dir in "${SELECTED_FUZZ_PACKAGES[@]}"; do - while IFS= read -r target; do - [ -z "$target" ] && continue - package_dirs+=("$package_dir") - targets+=("$target") - done < <(fuzz_list_targets "$package_dir") - done - - local total=${#targets[@]} local run_log_dir run_log_dir=$(mktemp -d) local batch_start=0 @@ -273,7 +266,7 @@ run_scope() { local logs=() for ((index = batch_start; index < batch_end; index++)); do local log_path="$run_log_dir/$index.log" - run_target_in_package "${package_dirs[$index]}" "${targets[$index]}" "$duration" >"$log_path" 2>&1 & + run_target_in_package "${RUN_PACKAGE_DIRS[$index]}" "${RUN_TARGETS[$index]}" "$duration" >"$log_path" 2>&1 & pids+=("$!") logs+=("$log_path") done @@ -292,7 +285,7 @@ run_scope() { rm -f "${logs[$batch_index]}" if [ "$fuzz_status" -ne 0 ]; then failed=$((failed + 1)) - crashed="${crashed} $(fuzz_package_label "${package_dirs[$index]}")/${targets[$index]}\n" + crashed="${crashed} $(fuzz_package_label "${RUN_PACKAGE_DIRS[$index]}")/${RUN_TARGETS[$index]}\n" fi done @@ -305,16 +298,73 @@ run_scope() { echo "Summary: $total targets, $failed failed" if [ $failed -gt 0 ]; then echo -e "Crashed:\n$crashed" - exit 1 + return 1 else echo "All fuzz targets passed" fi } +run_scope() { + local scope="$1" + local duration="$2" + local package_dir target + + fuzz_select_packages "$scope" + if [ ${#SELECTED_FUZZ_PACKAGES[@]} -eq 0 ]; then + echo "No fuzz packages selected for scope: $scope" + return 0 + fi + + RUN_PACKAGE_DIRS=() + RUN_TARGETS=() + for package_dir in "${SELECTED_FUZZ_PACKAGES[@]}"; do + while IFS= read -r target; do + [ -z "$target" ] && continue + RUN_PACKAGE_DIRS+=("$package_dir") + RUN_TARGETS+=("$target") + done < <(fuzz_list_targets "$package_dir") + done + run_batch "$scope" "$duration" +} + +run_target_list() { + local value="$1" + local duration="$2" + local package_dir target seen=, + local -a requested=() + [[ -n "$value" ]] || { + echo "Selected fuzz targets must not be empty" >&2 + return 2 + } + IFS=',' read -r -a requested <<<"$value" + + RUN_PACKAGE_DIRS=() + RUN_TARGETS=() + for target in "${requested[@]}"; do + [[ "$target" =~ ^[a-z0-9_]+$ ]] || { + echo "Invalid fuzz target: ${target:-}" >&2 + return 2 + } + [[ "$seen" != *",$target,"* ]] || { + echo "Duplicate fuzz target: $target" >&2 + return 2 + } + seen+="$target," + package_dir=$(fuzz_find_target_package "$target" scoped-first) || { + echo "Unknown fuzz target: $target" >&2 + return 2 + } + RUN_PACKAGE_DIRS+=("$package_dir") + RUN_TARGETS+=("$target") + done + run_batch selected "$duration" +} + ACTION="default" PACKAGE_SCOPE="full" TARGET_SCOPE_OVERRIDE="" TARGET="" +TARGETS_CSV="" TARGET_DURATION="$DURATION_SECS" while [ $# -gt 0 ]; do @@ -354,7 +404,20 @@ while [ $# -gt 0 ]; do ACTION="build" PACKAGE_SCOPE="scoped" ;; + --targets) + shift + [[ $# -gt 0 ]] || { + echo "--targets requires a comma-separated value" >&2 + exit 2 + } + ACTION="selected" + TARGETS_CSV=$1 + ;; *) + [[ "$ACTION" != selected ]] || { + echo "--targets cannot be combined with positional targets" >&2 + exit 2 + } if [ -z "$TARGET" ]; then TARGET="$1" else @@ -384,6 +447,9 @@ case "$ACTION" in clean) clean_artifacts ;; + selected) + run_target_list "$TARGETS_CSV" "$TARGET_DURATION" + ;; run) if [ -n "$TARGET" ]; then run_target "$TARGET" "$TARGET_DURATION" diff --git a/scripts/test/test-rsa-linux-asm.sh b/scripts/test/test-rsa-linux-asm.sh new file mode 100755 index 00000000..1eded9bd --- /dev/null +++ b/scripts/test/test-rsa-linux-asm.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +fail() { + echo "RSA assembly evidence error: $*" >&2 + exit 1 +} + +[[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]] \ + || fail "requires a Linux x86-64 host" +flags=$(sed -n 's/^flags[[:space:]]*: //p' /proc/cpuinfo | head -n 1) +[[ " $flags " == *" bmi2 "* && " $flags " == *" adx "* ]] \ + || fail "requires BMI2 and ADX" + +test_name=auth::rsa::tests::x86_64_linux_rsa_montgomery_asm_matches_portable_across_supported_widths +for profile in debug release; do + args=(test --locked --features rsa,diag,getrandom --lib) + [[ "$profile" == debug ]] || args+=(--release) + listing=$(cargo "${args[@]}" -- --list) + [[ $(awk -v expected="$test_name: test" '$0 == expected { count++ } END { print count + 0 }' <<<"$listing") -eq 1 ]] \ + || fail "expected exactly one $test_name test" + cargo "${args[@]}" "$test_name" -- --exact --nocapture +done + +build_output=$(cargo test --locked --release --features rsa,diag \ + --test rsa_public_key --no-run --message-format=json) +binary=$(sed -n 's/.*"executable":"\([^"]*rsa_public_key-[^"]*\)".*/\1/p' <<<"$build_output" | tail -n 1) +[[ -n "$binary" && -x "$binary" ]] || fail "could not resolve optimized RSA test binary" +symbols=$(nm "$binary") +[[ "$symbols" == *rscrypto_rsa_bn_mulx4x_mont_x86_64_elf* ]] \ + || fail "optimized binary lacks the Montgomery multiply" +[[ "$symbols" == *rscrypto_rsa_bn_sqr8x_mont_x86_64_elf* ]] \ + || fail "optimized binary lacks the Montgomery square" + +test_name=public_operation_montgomery_candidates_match_current_path +[[ $("$binary" "$test_name" --list | awk -v expected="$test_name: test" '$0 == expected { count++ } END { print count + 0 }') -eq 1 ]] \ + || fail "expected exactly one $test_name test" +"$binary" "$test_name" --exact --nocapture diff --git a/scripts/test/test.sh b/scripts/test/test.sh index caef108d..d70e2936 100755 --- a/scripts/test/test.sh +++ b/scripts/test/test.sh @@ -65,11 +65,11 @@ if [[ "$scope_status" -eq 0 ]]; then echo "Testing $SCOPE_DESC" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" if [[ "$has_nextest" == true ]]; then - cargo nextest run --locked "${CARGO_ARGS[@]}" -P "$profile" --all-features \ + cargo nextest run --locked "${CARGO_ARGS[@]:+${CARGO_ARGS[@]}}" -P "$profile" --all-features \ --config-file .config/nextest.toml \ "${nextest_thread_args[@]:+${nextest_thread_args[@]}}" else - cargo test --locked "${CARGO_ARGS[@]}" --all-features --lib --tests + cargo test --locked "${CARGO_ARGS[@]:+${CARGO_ARGS[@]}}" --all-features --lib --tests fi else echo "No unit or integration test targets selected by Cargo Rail" @@ -93,4 +93,4 @@ fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Running doctests for $SCOPE_DESC" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -cargo test --locked "${CARGO_ARGS[@]}" --doc --all-features +cargo test --locked "${CARGO_ARGS[@]:+${CARGO_ARGS[@]}}" --doc --all-features diff --git a/src/platform/target_matrix.rs b/src/platform/target_matrix.rs index 07bbbfae..6ba2d4cb 100644 --- a/src/platform/target_matrix.rs +++ b/src/platform/target_matrix.rs @@ -24,10 +24,9 @@ fn matches_target_arch(target: &str, arch: Arch) -> bool { target.starts_with(prefix) } -/// Scan all quoted strings in the JSON manifest for a target triple matching -/// `arch`. The only quoted strings in the file are JSON keys (`"groups"`, -/// `"win"`, `"ci"`, etc.) and target triple values — keys never start with -/// an arch prefix like `x86_64-`, so false positives are impossible. +/// Scan quoted catalog strings for a target triple matching `arch`. +/// Repository validation owns the exact row schema; this embedded check keeps +/// runtime detection tied to a catalog that names the host architecture. #[inline] #[must_use] pub(super) fn manifest_has_arch(arch: Arch) -> bool { From decd9b987143bc9c0b8812efa1dc7604cde46388 Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Tue, 1 Sep 2026 17:56:41 -0400 Subject: [PATCH 02/18] ci: accept actionlint version report actionlint reports its semantic version followed by installation and compiler metadata. Compare the first line so the checksum-verified release passes while preserving exact version enforcement and reporting mismatches clearly. --- scripts/ci/install-actionlint.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/ci/install-actionlint.sh b/scripts/ci/install-actionlint.sh index b2d95036..92795d96 100755 --- a/scripts/ci/install-actionlint.sh +++ b/scripts/ci/install-actionlint.sh @@ -22,8 +22,10 @@ curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location \ printf '%s %s\n' "$sha256" "$download_dir/$archive" | sha256sum --check --status tar -xzf "$download_dir/$archive" -C "$download_dir" actionlint install -m 755 "$download_dir/actionlint" "$bin_dir/actionlint" -[[ "$("$bin_dir/actionlint" -version)" == "$version" ]] || { - echo "actionlint version check failed" >&2 +installed_version=$("$bin_dir/actionlint" -version) +installed_version=${installed_version%%$'\n'*} +[[ "$installed_version" == "$version" ]] || { + echo "actionlint version check failed: expected $version, got $installed_version" >&2 exit 1 } printf '%s\n' "$bin_dir" >>"${GITHUB_PATH:?GITHUB_PATH is required}" From 6495bd67407f54acb3cb70838617c7a8ae9866c5 Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Tue, 1 Sep 2026 18:26:46 -0400 Subject: [PATCH 03/18] ci: synchronize stable tool releases Advance every stale selected tool to its latest stable release and pin the installer action that carries Zizmor 1.30.0 metadata. Enforce one direct-archive version across supported Linux and macOS hosts while keeping source fallback disabled. --- .config/ci-tool-archives.tsv | 16 ++++++++-------- .github/workflows/ci.yaml | 4 ++-- .github/workflows/qualification.yaml | 4 ++-- scripts/ci/assurance-planning-test.sh | 3 +++ scripts/ci/install-tools.sh | 4 ++-- scripts/lib/ci-tool-integrity.sh | 12 +++++++++++- 6 files changed, 28 insertions(+), 15 deletions(-) diff --git a/.config/ci-tool-archives.tsv b/.config/ci-tool-archives.tsv index eaa18299..eac4017e 100644 --- a/.config/ci-tool-archives.tsv +++ b/.config/ci-tool-archives.tsv @@ -1,9 +1,9 @@ # tool version os architecture filename url sha256 -wasmtime v48.0.0 linux x86_64 wasmtime-v48.0.0-x86_64-linux.tar.xz https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.0/wasmtime-v48.0.0-x86_64-linux.tar.xz 1d23a692da51a4f825698f3f999da71f28bad19a96df5395fadc8d07f162dac3 -wasmtime v48.0.0 linux aarch64 wasmtime-v48.0.0-aarch64-linux.tar.xz https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.0/wasmtime-v48.0.0-aarch64-linux.tar.xz ea20f14785f908afcd07063ade8d89b82b15228d9e6be426ea12ebecbc9c0c8b -wasmtime v48.0.0 macos x86_64 wasmtime-v48.0.0-x86_64-macos.tar.xz https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.0/wasmtime-v48.0.0-x86_64-macos.tar.xz 3cf3c8d1f67184183acd61567e758046d40dddd169295dc9e7570b2b20505ac9 -wasmtime v48.0.0 macos aarch64 wasmtime-v48.0.0-aarch64-macos.tar.xz https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.0/wasmtime-v48.0.0-aarch64-macos.tar.xz b62f687c7b19dd7c04b44ba5d6170a92a2c99bfc190887348aee9323e3703e60 -wasm-tools 1.257.1 linux x86_64 wasm-tools-1.257.1-x86_64-linux.tar.gz https://github.com/bytecodealliance/wasm-tools/releases/download/v1.257.1/wasm-tools-1.257.1-x86_64-linux.tar.gz acea0a6f71b15f3b2d394f97453398f29099cec52df93bb25741a22561639b24 -wasm-tools 1.257.1 linux aarch64 wasm-tools-1.257.1-aarch64-linux.tar.gz https://github.com/bytecodealliance/wasm-tools/releases/download/v1.257.1/wasm-tools-1.257.1-aarch64-linux.tar.gz ff23fa2ed1bf46c590ebaff9ec585c941b820dd56384080466dabc61ed829178 -wasm-tools 1.257.1 macos x86_64 wasm-tools-1.257.1-x86_64-macos.tar.gz https://github.com/bytecodealliance/wasm-tools/releases/download/v1.257.1/wasm-tools-1.257.1-x86_64-macos.tar.gz e35977a05d9e04b8e20208db5d2cd67031d3f782be2da763296b6a8ba13527ce -wasm-tools 1.257.1 macos aarch64 wasm-tools-1.257.1-aarch64-macos.tar.gz https://github.com/bytecodealliance/wasm-tools/releases/download/v1.257.1/wasm-tools-1.257.1-aarch64-macos.tar.gz 1d1ced29aa8320733a7e27452434e66f9bcb61ce39a00ee63229594d71f8544d +wasmtime v48.0.1 linux x86_64 wasmtime-v48.0.1-x86_64-linux.tar.xz https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-linux.tar.xz 4c2e31b68ad99e0a519f225a261fda099eb15f056d4a24fdb3c2a46517bde1df +wasmtime v48.0.1 linux aarch64 wasmtime-v48.0.1-aarch64-linux.tar.xz https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-linux.tar.xz fdbebd838ed7b9cc4e2b63f6d7d855b33386fc388f3595f668bf394131dd072f +wasmtime v48.0.1 macos x86_64 wasmtime-v48.0.1-x86_64-macos.tar.xz https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-macos.tar.xz ce95a41b85adaf2c44f47cb68defb282bdb87d68ed96d1662295330fe542335e +wasmtime v48.0.1 macos aarch64 wasmtime-v48.0.1-aarch64-macos.tar.xz https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-macos.tar.xz 88cc08b395fbfb960b99f355a81224af975679b8a5f4b74a51d59e5e34b20dcd +wasm-tools 1.258.0 linux x86_64 wasm-tools-1.258.0-x86_64-linux.tar.gz https://github.com/bytecodealliance/wasm-tools/releases/download/v1.258.0/wasm-tools-1.258.0-x86_64-linux.tar.gz b52d14eb74a4852cc249369bd4480c2b2fdd876145f41db51ff52269ded240ce +wasm-tools 1.258.0 linux aarch64 wasm-tools-1.258.0-aarch64-linux.tar.gz https://github.com/bytecodealliance/wasm-tools/releases/download/v1.258.0/wasm-tools-1.258.0-aarch64-linux.tar.gz 86714f59d76218c22a1aa7e14bdc19e22facaec5c4a12a1f0be2f1594ce1ad26 +wasm-tools 1.258.0 macos x86_64 wasm-tools-1.258.0-x86_64-macos.tar.gz https://github.com/bytecodealliance/wasm-tools/releases/download/v1.258.0/wasm-tools-1.258.0-x86_64-macos.tar.gz 10621ef8537d9c37d1c3e4e6199ab6b0d4d2bf4862eb6815c4a4d4ebaec2db38 +wasm-tools 1.258.0 macos aarch64 wasm-tools-1.258.0-aarch64-macos.tar.gz https://github.com/bytecodealliance/wasm-tools/releases/download/v1.258.0/wasm-tools-1.258.0-aarch64-macos.tar.gz 64370d279234669fca407383fae96ce518c6628e0a4ab186dba590f8678c3646 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5ddd01f2..5e9f41bd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -108,7 +108,7 @@ jobs: - name: Install Zizmor if: steps.select.outputs.actions == 'true' - uses: taiki-e/install-action@6a1bd70eaac3c8bdf093356838d7ee09fda951cf # v2.85.5 + uses: taiki-e/install-action@0758d235715de2f3551eacc980d9ae8fce9342c3 # v2.87.3 with: tool: zizmor@1.30.0 fallback: none @@ -175,7 +175,7 @@ jobs: cache-secret-access-key: ${{ github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY || secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - name: Install test runner if: needs.plan.outputs.tests == 'true' - uses: taiki-e/install-action@6a1bd70eaac3c8bdf093356838d7ee09fda951cf # v2.85.5 + uses: taiki-e/install-action@0758d235715de2f3551eacc980d9ae8fce9342c3 # v2.87.3 with: tool: cargo-nextest@0.9.143 fallback: none diff --git a/.github/workflows/qualification.yaml b/.github/workflows/qualification.yaml index 5d8c4bda..6b07f90b 100644 --- a/.github/workflows/qualification.yaml +++ b/.github/workflows/qualification.yaml @@ -152,7 +152,7 @@ jobs: cache-access-key-id: ${{ github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID || secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} cache-secret-access-key: ${{ github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY || secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - name: Install test runner - uses: taiki-e/install-action@6a1bd70eaac3c8bdf093356838d7ee09fda951cf # v2.85.5 + uses: taiki-e/install-action@0758d235715de2f3551eacc980d9ae8fce9342c3 # v2.87.3 with: tool: cargo-nextest@0.9.143 fallback: none @@ -405,7 +405,7 @@ jobs: with: components: llvm-tools-preview - name: Install coverage tools - uses: taiki-e/install-action@6a1bd70eaac3c8bdf093356838d7ee09fda951cf # v2.85.5 + uses: taiki-e/install-action@0758d235715de2f3551eacc980d9ae8fce9342c3 # v2.87.3 with: tool: cargo-llvm-cov@0.9.0,cargo-nextest@0.9.143 fallback: none diff --git a/scripts/ci/assurance-planning-test.sh b/scripts/ci/assurance-planning-test.sh index 297054aa..58cef3fa 100755 --- a/scripts/ci/assurance-planning-test.sh +++ b/scripts/ci/assurance-planning-test.sh @@ -317,6 +317,9 @@ raise "Coverage is not bound to assurance.fuzz" unless coverage_runs.include?("s raise "Qualification lost total coverage" unless coverage_runs.include?("scripts/test/test-coverage.sh") coverage_installer = coverage.fetch("steps").find { |step| step["uses"]&.start_with?("taiki-e/install-action@") } raise "Coverage tools are not installed from a pinned action" unless coverage_installer +unless coverage_installer.dig("with", "tool") == "cargo-llvm-cov@0.9.0,cargo-nextest@0.9.143" + raise "Coverage installs tool versions that differ from the core test contract" +end raise "Coverage tool fallback must remain disabled" unless coverage_installer.dig("with", "fallback") == "none" zeroization = qualification.fetch("jobs").fetch("zeroization") diff --git a/scripts/ci/install-tools.sh b/scripts/ci/install-tools.sh index 87ae9ed0..af92d8a7 100755 --- a/scripts/ci/install-tools.sh +++ b/scripts/ci/install-tools.sh @@ -10,10 +10,10 @@ CARGO_DENY_VERSION=0.20.2 CARGO_AUDIT_VERSION=0.22.2 CARGO_FUZZ_VERSION=0.13.2 -OPAM_REPOSITORY_COMMIT=49f6d620cf20ae0168cfcbeb2c33932e06cb4b74 +OPAM_REPOSITORY_COMMIT=607f49d990590190e047dba24bd53b28e8195c7b OPAM_REPOSITORY_REMOTE=https://github.com/ocaml/opam-repository.git OCAML_COMPILER_PACKAGE=ocaml-base-compiler.5.2.1 -BINSEC_PACKAGE=binsec.0.11.1 +BINSEC_PACKAGE=binsec.0.11.3 BINSEC_DECODER_PACKAGE=unisim_archisec.0.0.14 BINSEC_SOLVER_PACKAGES=(bitwuzla.1.0.6 bitwuzla-cxx.0.9.0) diff --git a/scripts/lib/ci-tool-integrity.sh b/scripts/lib/ci-tool-integrity.sh index 22736e62..51c03fcc 100644 --- a/scripts/lib/ci-tool-integrity.sh +++ b/scripts/lib/ci-tool-integrity.sh @@ -102,9 +102,10 @@ ci_tool_resolve() { } ci_tool_validate_manifest() { - local tool + local tool expected_version version for tool in wasmtime wasm-tools; do local expected_platforms=(linux:x86_64 linux:aarch64 macos:x86_64 macos:aarch64) + expected_version="" local platform for platform in "${expected_platforms[@]}"; do @@ -117,6 +118,15 @@ ci_tool_validate_manifest() { "$CI_TOOL_ARCHIVES") [[ "$count" -eq 1 ]] \ || ci_tool_fail "expected one $tool archive for $expected_os/$expected_arch" || return + version=$(awk -F '\t' \ + -v tool="$tool" -v os="$expected_os" -v arch="$expected_arch" \ + '$1 == tool && $3 == os && $4 == arch { print $2 }' \ + "$CI_TOOL_ARCHIVES") + if [[ -z "$expected_version" ]]; then + expected_version=$version + elif [[ "$version" != "$expected_version" ]]; then + ci_tool_fail "$tool versions differ across supported hosts" || return + fi done done From 0d19951e75c253d810f10b906ffd5e3bfbb2475f Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Tue, 1 Sep 2026 18:30:54 -0400 Subject: [PATCH 04/18] ci: provision complete policy toolset Install the pinned prebuilt Just release beside Zizmor so the hosted Actions policy lane has the same runtime contract as the local entry point without source builds. --- .github/workflows/ci.yaml | 4 ++-- scripts/README.md | 6 +++--- scripts/ci/assurance-planning-test.sh | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5e9f41bd..7ba08949 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -106,11 +106,11 @@ jobs: if: steps.select.outputs.actions == 'true' run: scripts/ci/install-actionlint.sh - - name: Install Zizmor + - name: Install Actions policy tools if: steps.select.outputs.actions == 'true' uses: taiki-e/install-action@0758d235715de2f3551eacc980d9ae8fce9342c3 # v2.87.3 with: - tool: zizmor@1.30.0 + tool: just@1.58.0,zizmor@1.30.0 fallback: none - name: Run affected repository policy diff --git a/scripts/README.md b/scripts/README.md index 14ea722d..9948c213 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -201,9 +201,9 @@ macOS AArch64 rows own native runtime and backend-differential proof instead of duplicating interpreter and fuzz hosts. The selected Actions policy lane downloads the exact checksum-verified -actionlint release and the exact prebuilt Zizmor release, then runs the same -`scripts/ci/actions-policy.sh` entry point as `just check-actions`. No CI linter -is compiled from source. +actionlint release and the exact prebuilt Just and Zizmor releases, then runs +the same `scripts/ci/actions-policy.sh` entry point as `just check-actions`. +None of these tools is compiled from source. Qualification captures one `--all` plan, restores the complete feature contract, and materializes every platform catalog row as an independent retry diff --git a/scripts/ci/assurance-planning-test.sh b/scripts/ci/assurance-planning-test.sh index 58cef3fa..2528c336 100755 --- a/scripts/ci/assurance-planning-test.sh +++ b/scripts/ci/assurance-planning-test.sh @@ -211,11 +211,11 @@ select_run = plan_steps.find { |step| step["id"] == "select" }.fetch("run") raise "CI plan does not select Actions policy setup" unless select_run.include?('echo "actions=$(required_any policy.actions)"') actionlint_install = plan_steps.find { |step| step["run"] == "scripts/ci/install-actionlint.sh" } raise "CI does not install actionlint only for Actions policy" unless actionlint_install&.fetch("if") == "steps.select.outputs.actions == 'true'" -zizmor_install = plan_steps.find do |step| - step["uses"]&.start_with?("taiki-e/install-action@") && step.dig("with", "tool") == "zizmor@1.30.0" +policy_tools_install = plan_steps.find do |step| + step["uses"]&.start_with?("taiki-e/install-action@") && step.dig("with", "tool") == "just@1.58.0,zizmor@1.30.0" end -raise "CI does not install pinned Zizmor only for Actions policy" unless zizmor_install&.fetch("if") == "steps.select.outputs.actions == 'true'" -raise "CI permits Zizmor source fallback" unless zizmor_install.dig("with", "fallback") == "none" +raise "CI does not install pinned Just and Zizmor only for Actions policy" unless policy_tools_install&.fetch("if") == "steps.select.outputs.actions == 'true'" +raise "CI permits Actions policy tool source fallback" unless policy_tools_install.dig("with", "fallback") == "none" actions_policy = File.read(File.join(root, "scripts/ci/actions-policy.sh")) raise "Actions policy does not execute actionlint" unless actions_policy.match?(/^actionlint$/) From 1decb9d4e68c3f5c64fb7f0087e015dcab14c2dd Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Tue, 1 Sep 2026 18:37:58 -0400 Subject: [PATCH 05/18] ci: declare complete policy toolchain Install checksum-verified ripgrep with actionlint from the central direct-archive manifest so the hosted policy lane matches its local runtime contract without package-manager or source-build overhead. --- .config/ci-tool-archives.tsv | 2 ++ .github/workflows/ci.yaml | 6 ++-- scripts/README.md | 10 +++--- scripts/ci/assurance-planning-test.sh | 4 +-- scripts/ci/install-actionlint.sh | 31 ---------------- scripts/ci/install-actions-policy-tools.sh | 41 ++++++++++++++++++++++ scripts/lib/ci-tool-integrity.sh | 10 ++++-- 7 files changed, 60 insertions(+), 44 deletions(-) delete mode 100755 scripts/ci/install-actionlint.sh create mode 100755 scripts/ci/install-actions-policy-tools.sh diff --git a/.config/ci-tool-archives.tsv b/.config/ci-tool-archives.tsv index eac4017e..112e83dc 100644 --- a/.config/ci-tool-archives.tsv +++ b/.config/ci-tool-archives.tsv @@ -1,4 +1,6 @@ # tool version os architecture filename url sha256 +actionlint 1.7.12 linux x86_64 actionlint_1.7.12_linux_amd64.tar.gz https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 +ripgrep 15.2.0 linux x86_64 ripgrep-15.2.0-x86_64-unknown-linux-musl.tar.gz https://github.com/BurntSushi/ripgrep/releases/download/15.2.0/ripgrep-15.2.0-x86_64-unknown-linux-musl.tar.gz 33e15bcf1624b25cdd2a55813a47a2f95dbe126268203e76aa6a585d1e7b149c wasmtime v48.0.1 linux x86_64 wasmtime-v48.0.1-x86_64-linux.tar.xz https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-linux.tar.xz 4c2e31b68ad99e0a519f225a261fda099eb15f056d4a24fdb3c2a46517bde1df wasmtime v48.0.1 linux aarch64 wasmtime-v48.0.1-aarch64-linux.tar.xz https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-linux.tar.xz fdbebd838ed7b9cc4e2b63f6d7d855b33386fc388f3595f668bf394131dd072f wasmtime v48.0.1 macos x86_64 wasmtime-v48.0.1-x86_64-macos.tar.xz https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-macos.tar.xz ce95a41b85adaf2c44f47cb68defb282bdb87d68ed96d1662295330fe542335e diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7ba08949..d78ae482 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -102,11 +102,11 @@ jobs: echo "tests=$(required_any cargo.test)" } >>"$GITHUB_OUTPUT" - - name: Install actionlint + - name: Install actionlint and ripgrep if: steps.select.outputs.actions == 'true' - run: scripts/ci/install-actionlint.sh + run: scripts/ci/install-actions-policy-tools.sh - - name: Install Actions policy tools + - name: Install Just and Zizmor if: steps.select.outputs.actions == 'true' uses: taiki-e/install-action@0758d235715de2f3551eacc980d9ae8fce9342c3 # v2.87.3 with: diff --git a/scripts/README.md b/scripts/README.md index 9948c213..2ba298a9 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -123,7 +123,7 @@ claim in [`docs/secret-lifecycle.md`](../docs/secret-lifecycle.md). | `lib/targets.sh` | `scripts/ci/cross-targets.sh` | | `lib/target-matrix.sh` | `scripts/lib/targets.sh`, `scripts/ci/target-contracts.sh` | | `lib/toolchain.sh` | Toolchain setup, Miri/fuzz helpers, and cross-target check scripts | -| `lib/ci-tool-integrity.sh` | `ci/nostd-wasm-suite.sh`, `just check-actions` | +| `lib/ci-tool-integrity.sh` | `ci/install-actions-policy-tools.sh`, `ci/nostd-wasm-suite.sh`, `just check-actions` | ## Python boundary @@ -200,10 +200,10 @@ Miri remains on x86-64 because it forces portable execution; deep Linux and macOS AArch64 rows own native runtime and backend-differential proof instead of duplicating interpreter and fuzz hosts. -The selected Actions policy lane downloads the exact checksum-verified -actionlint release and the exact prebuilt Just and Zizmor releases, then runs -the same `scripts/ci/actions-policy.sh` entry point as `just check-actions`. -None of these tools is compiled from source. +The selected Actions policy lane downloads exact checksum-verified actionlint +and ripgrep releases plus exact prebuilt Just and Zizmor releases, then runs the +same `scripts/ci/actions-policy.sh` entry point as `just check-actions`. None of +these tools is compiled from source. Qualification captures one `--all` plan, restores the complete feature contract, and materializes every platform catalog row as an independent retry diff --git a/scripts/ci/assurance-planning-test.sh b/scripts/ci/assurance-planning-test.sh index 2528c336..47c696cb 100755 --- a/scripts/ci/assurance-planning-test.sh +++ b/scripts/ci/assurance-planning-test.sh @@ -209,8 +209,8 @@ end plan_steps = jobs.fetch("plan").fetch("steps") select_run = plan_steps.find { |step| step["id"] == "select" }.fetch("run") raise "CI plan does not select Actions policy setup" unless select_run.include?('echo "actions=$(required_any policy.actions)"') -actionlint_install = plan_steps.find { |step| step["run"] == "scripts/ci/install-actionlint.sh" } -raise "CI does not install actionlint only for Actions policy" unless actionlint_install&.fetch("if") == "steps.select.outputs.actions == 'true'" +direct_policy_tools_install = plan_steps.find { |step| step["run"] == "scripts/ci/install-actions-policy-tools.sh" } +raise "CI does not install direct policy tools only for Actions policy" unless direct_policy_tools_install&.fetch("if") == "steps.select.outputs.actions == 'true'" policy_tools_install = plan_steps.find do |step| step["uses"]&.start_with?("taiki-e/install-action@") && step.dig("with", "tool") == "just@1.58.0,zizmor@1.30.0" end diff --git a/scripts/ci/install-actionlint.sh b/scripts/ci/install-actionlint.sh deleted file mode 100755 index 92795d96..00000000 --- a/scripts/ci/install-actionlint.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -# Install the exact prebuilt actionlint release used by the Actions policy lane. - -set -euo pipefail - -version=1.7.12 -archive="actionlint_${version}_linux_amd64.tar.gz" -url="https://github.com/rhysd/actionlint/releases/download/v${version}/${archive}" -sha256=8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 -temp_root=${RUNNER_TEMP:?RUNNER_TEMP is required} -bin_dir="$temp_root/rscrypto-actionlint/bin" -download_dir=$(mktemp -d "$temp_root/rscrypto-actionlint-download.XXXXXX") -trap 'rm -rf "$download_dir"' EXIT - -[[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]] || { - echo "actionlint installer requires Linux x86-64" >&2 - exit 1 -} -mkdir -p "$bin_dir" -curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location \ - --retry 3 --retry-delay 2 --output "$download_dir/$archive" "$url" -printf '%s %s\n' "$sha256" "$download_dir/$archive" | sha256sum --check --status -tar -xzf "$download_dir/$archive" -C "$download_dir" actionlint -install -m 755 "$download_dir/actionlint" "$bin_dir/actionlint" -installed_version=$("$bin_dir/actionlint" -version) -installed_version=${installed_version%%$'\n'*} -[[ "$installed_version" == "$version" ]] || { - echo "actionlint version check failed: expected $version, got $installed_version" >&2 - exit 1 -} -printf '%s\n' "$bin_dir" >>"${GITHUB_PATH:?GITHUB_PATH is required}" diff --git a/scripts/ci/install-actions-policy-tools.sh b/scripts/ci/install-actions-policy-tools.sh new file mode 100755 index 00000000..c5d627a7 --- /dev/null +++ b/scripts/ci/install-actions-policy-tools.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Install exact prebuilt tools not provided by the Actions tool installer. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../lib/ci-tool-integrity.sh +source "$SCRIPT_DIR/../lib/ci-tool-integrity.sh" + +temp_root=${RUNNER_TEMP:?RUNNER_TEMP is required} +bin_dir="$temp_root/rscrypto-actions-policy/bin" +download_dir=$(mktemp -d "$temp_root/rscrypto-actions-policy-download.XXXXXX") +trap 'rm -rf "$download_dir"' EXIT +mkdir -p "$bin_dir" "$download_dir/actionlint" "$download_dir/ripgrep" + +ci_tool_download actionlint "$download_dir/actionlint" +actionlint_version=$CI_TOOL_VERSION +tar -xzf "$CI_TOOL_ARCHIVE_PATH" -C "$download_dir/actionlint" actionlint +install -m 755 "$download_dir/actionlint/actionlint" "$bin_dir/actionlint" + +ci_tool_download ripgrep "$download_dir/ripgrep" +ripgrep_version=$CI_TOOL_VERSION +ripgrep_root="ripgrep-${ripgrep_version}-x86_64-unknown-linux-musl" +tar -xzf "$CI_TOOL_ARCHIVE_PATH" -C "$download_dir/ripgrep" "$ripgrep_root/rg" +install -m 755 "$download_dir/ripgrep/$ripgrep_root/rg" "$bin_dir/rg" + +installed_actionlint_version=$("$bin_dir/actionlint" -version) +installed_actionlint_version=${installed_actionlint_version%%$'\n'*} +[[ "$installed_actionlint_version" == "$actionlint_version" ]] || { + echo "actionlint version check failed: expected $actionlint_version, got $installed_actionlint_version" >&2 + exit 1 +} + +installed_ripgrep_version=$("$bin_dir/rg" --version) +installed_ripgrep_version=${installed_ripgrep_version%%$'\n'*} +[[ "$installed_ripgrep_version" == "ripgrep $ripgrep_version" ]] || { + echo "ripgrep version check failed: expected $ripgrep_version, got $installed_ripgrep_version" >&2 + exit 1 +} + +printf '%s\n' "$bin_dir" >>"${GITHUB_PATH:?GITHUB_PATH is required}" diff --git a/scripts/lib/ci-tool-integrity.sh b/scripts/lib/ci-tool-integrity.sh index 51c03fcc..4a789401 100644 --- a/scripts/lib/ci-tool-integrity.sh +++ b/scripts/lib/ci-tool-integrity.sh @@ -33,7 +33,7 @@ ci_tool_validate_record() { local digest=$7 case "$tool" in - wasmtime | wasm-tools) ;; + actionlint | ripgrep | wasmtime | wasm-tools) ;; *) ci_tool_fail "unknown direct CI tool: $tool" || return ;; esac [[ "$version" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+[-+A-Za-z0-9.]*$ ]] \ @@ -103,8 +103,12 @@ ci_tool_resolve() { ci_tool_validate_manifest() { local tool expected_version version - for tool in wasmtime wasm-tools; do - local expected_platforms=(linux:x86_64 linux:aarch64 macos:x86_64 macos:aarch64) + for tool in actionlint ripgrep wasmtime wasm-tools; do + local expected_platforms + case "$tool" in + actionlint | ripgrep) expected_platforms=(linux:x86_64) ;; + wasmtime | wasm-tools) expected_platforms=(linux:x86_64 linux:aarch64 macos:x86_64 macos:aarch64) ;; + esac expected_version="" local platform From 718d1d42841b825a10cd78a85e10c3cd4e699740 Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Tue, 1 Sep 2026 18:40:30 -0400 Subject: [PATCH 06/18] ci: parse ripgrep release metadata Verify ripgrep's exact name and semantic version while accepting the revision suffix emitted by its official version report. --- scripts/ci/install-actions-policy-tools.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/ci/install-actions-policy-tools.sh b/scripts/ci/install-actions-policy-tools.sh index c5d627a7..8d7a6a40 100755 --- a/scripts/ci/install-actions-policy-tools.sh +++ b/scripts/ci/install-actions-policy-tools.sh @@ -31,10 +31,11 @@ installed_actionlint_version=${installed_actionlint_version%%$'\n'*} exit 1 } -installed_ripgrep_version=$("$bin_dir/rg" --version) -installed_ripgrep_version=${installed_ripgrep_version%%$'\n'*} -[[ "$installed_ripgrep_version" == "ripgrep $ripgrep_version" ]] || { - echo "ripgrep version check failed: expected $ripgrep_version, got $installed_ripgrep_version" >&2 +installed_ripgrep_report=$("$bin_dir/rg" --version) +installed_ripgrep_report=${installed_ripgrep_report%%$'\n'*} +read -r installed_ripgrep_name installed_ripgrep_version _ <<<"$installed_ripgrep_report" +[[ "$installed_ripgrep_name" == ripgrep && "$installed_ripgrep_version" == "$ripgrep_version" ]] || { + echo "ripgrep version check failed: expected $ripgrep_version, got $installed_ripgrep_report" >&2 exit 1 } From 1ea55235b0e66bf64d80a9d70ec21b9c5c6b30c1 Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Tue, 1 Sep 2026 18:48:46 -0400 Subject: [PATCH 07/18] test: isolate feature contract plans Poison ambient plan transport in the regression harness and strip it from each executor case so local checks reproduce the same Cargo Rail environment used by hosted policy jobs. --- scripts/ci/feature-contracts-test.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/ci/feature-contracts-test.sh b/scripts/ci/feature-contracts-test.sh index 9d5cae7b..3b50c8ea 100755 --- a/scripts/ci/feature-contracts-test.sh +++ b/scripts/ci/feature-contracts-test.sh @@ -11,6 +11,9 @@ EXECUTOR="$REPO_ROOT/scripts/check/feature-contracts.sh" source "$REPO_ROOT/scripts/lib/feature-profiles.sh" TMP_ROOT=$(mktemp -d) trap 'rm -rf "$TMP_ROOT"' EXIT +# Make ordinary local runs prove that executor cases cannot consume an ambient plan. +export RAIL_PLAN_FILE="$TMP_ROOT/ambient-plan-must-not-be-read" +export RAIL_PLAN_READER="$TMP_ROOT/ambient-reader-must-not-be-read" fail() { echo "feature-contract executor regression failure: $*" >&2 @@ -20,6 +23,7 @@ fail() { fake_bin="$TMP_ROOT/bin" command_log="$TMP_ROOT/commands.log" real_cargo=$(command -v cargo) +clean_plan_env=(env -u BASH_ENV -u RAIL_PLAN_FILE -u RAIL_PLAN_READER) mkdir -p "$fake_bin" for case_entry in "${RUNTIME_TEST_CASES[@]}"; do @@ -72,7 +76,7 @@ chmod +x "$fake_bin/cargo" run_executor() { local output=$1 shift - env -u BASH_ENV PATH="$fake_bin:$PATH" MOCK_LOG="$command_log" REAL_CARGO="$real_cargo" \ + "${clean_plan_env[@]}" PATH="$fake_bin:$PATH" MOCK_LOG="$command_log" REAL_CARGO="$real_cargo" \ "$EXECUTOR" "$@" >"$output" 2>&1 } @@ -128,7 +132,7 @@ raise SystemExit(2) EOF chmod +x "$fake_reader" selected_matrix="$TMP_ROOT/selected-matrix.json" -env -u BASH_ENV PATH="$fake_bin:$PATH" MOCK_LOG="$command_log" REAL_CARGO="$real_cargo" \ +"${clean_plan_env[@]}" PATH="$fake_bin:$PATH" MOCK_LOG="$command_log" REAL_CARGO="$real_cargo" \ RAIL_PLAN_FILE="$fake_plan" RAIL_PLAN_READER="$fake_reader" \ "$EXECUTOR" matrix >"$selected_matrix" jq -e ' @@ -203,7 +207,7 @@ if run_executor "$TMP_ROOT/invalid-shard.out" compile 0/2; then fi : >"$command_log" -if env -u BASH_ENV PATH="$fake_bin:$PATH" MOCK_LOG="$command_log" REAL_CARGO="$real_cargo" MOCK_ALIAS_DIVERGES=1 \ +if "${clean_plan_env[@]}" PATH="$fake_bin:$PATH" MOCK_LOG="$command_log" REAL_CARGO="$real_cargo" MOCK_ALIAS_DIVERGES=1 \ "$EXECUTOR" compile >"$TMP_ROOT/divergent-alias.out" 2>&1; then fail "divergent compile alias was accepted" fi From 6072f449613ec49060ac0ffc37a3e2f532b1f3ef Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Tue, 1 Sep 2026 18:51:56 -0400 Subject: [PATCH 08/18] test: isolate plan identity fixtures Poison and clear the transported plan identity and head alongside the fake plan paths so the hosted environment is fully reproduced by local policy checks. --- scripts/ci/feature-contracts-test.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/ci/feature-contracts-test.sh b/scripts/ci/feature-contracts-test.sh index 3b50c8ea..70fe3aed 100755 --- a/scripts/ci/feature-contracts-test.sh +++ b/scripts/ci/feature-contracts-test.sh @@ -14,6 +14,8 @@ trap 'rm -rf "$TMP_ROOT"' EXIT # Make ordinary local runs prove that executor cases cannot consume an ambient plan. export RAIL_PLAN_FILE="$TMP_ROOT/ambient-plan-must-not-be-read" export RAIL_PLAN_READER="$TMP_ROOT/ambient-reader-must-not-be-read" +export RAIL_PLAN_IDENTITY=ambient-plan-identity-must-not-be-read +export RAIL_PLAN_HEAD_COMMIT=ambient-plan-head-must-not-be-read fail() { echo "feature-contract executor regression failure: $*" >&2 @@ -23,7 +25,11 @@ fail() { fake_bin="$TMP_ROOT/bin" command_log="$TMP_ROOT/commands.log" real_cargo=$(command -v cargo) -clean_plan_env=(env -u BASH_ENV -u RAIL_PLAN_FILE -u RAIL_PLAN_READER) +clean_plan_env=( + env -u BASH_ENV + -u RAIL_PLAN_FILE -u RAIL_PLAN_READER + -u RAIL_PLAN_IDENTITY -u RAIL_PLAN_HEAD_COMMIT +) mkdir -p "$fake_bin" for case_entry in "${RUNTIME_TEST_CASES[@]}"; do From f8f9e23e7c6c06327179b3c4385b044c7dbbb6e4 Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Tue, 1 Sep 2026 18:57:43 -0400 Subject: [PATCH 09/18] test: isolate manual matrix output Poison GitHub's output file in the regression harness and clear it only for direct matrix capture so local policy checks reproduce hosted execution. --- scripts/ci/emit-manual-matrix-test.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/ci/emit-manual-matrix-test.sh b/scripts/ci/emit-manual-matrix-test.sh index efd2b5a6..828aa9fb 100755 --- a/scripts/ci/emit-manual-matrix-test.sh +++ b/scripts/ci/emit-manual-matrix-test.sh @@ -3,10 +3,11 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +export GITHUB_OUTPUT=/dev/null matrix="$({ cd "$REPO_ROOT" - GH_RUN_ID=1 CT_PLATFORMS=rise-riscv scripts/ci/emit-manual-matrix.sh ct + env -u GITHUB_OUTPUT GH_RUN_ID=1 CT_PLATFORMS=rise-riscv scripts/ci/emit-manual-matrix.sh ct })" jq -e ' @@ -20,13 +21,13 @@ jq -e ' ct_default="$({ cd "$REPO_ROOT" - GH_RUN_ID=1 CT_PLATFORMS=all scripts/ci/emit-manual-matrix.sh ct + env -u GITHUB_OUTPUT GH_RUN_ID=1 CT_PLATFORMS=all scripts/ci/emit-manual-matrix.sh ct })" jq -e 'length == 9 and any(.platform == "rise-riscv")' <<<"$ct_default" >/dev/null bench_default="$({ cd "$REPO_ROOT" - GH_RUN_ID=1 BENCH_PLATFORMS=all scripts/ci/emit-manual-matrix.sh bench + env -u GITHUB_OUTPUT GH_RUN_ID=1 BENCH_PLATFORMS=all scripts/ci/emit-manual-matrix.sh bench })" jq -e 'length == 9 and any(.platform == "rise-riscv")' <<<"$bench_default" >/dev/null From d63585d65a7971f26e35eccceb26ae71baf6fe35 Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Tue, 1 Sep 2026 19:05:02 -0400 Subject: [PATCH 10/18] ci: normalize target projection lines Strip carriage returns while reading and comparing target projections so Git Bash validates the same catalog as Unix hosts. --- scripts/lib/target-matrix.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/lib/target-matrix.sh b/scripts/lib/target-matrix.sh index b9dc357c..4d7dee1e 100755 --- a/scripts/lib/target-matrix.sh +++ b/scripts/lib/target-matrix.sh @@ -70,17 +70,20 @@ validate_manifest() { local matrix_targets matrix_targets="$(jq -r '[.variants[].dimensions.target] | unique[]' "$MANIFEST" | LC_ALL=C sort)" + matrix_targets=${matrix_targets//$'\r'/} local projection for projection in "$REPO_ROOT/.config/rail.toml" "$REPO_ROOT/deny.toml"; do local projected_targets projected_targets="$(awk ' + { sub(/\r$/, "") } /^targets = \[$/ { in_targets = 1; next } in_targets && /^\]$/ { exit } in_targets && match($0, /"[^"]+"/) { print substr($0, RSTART + 1, RLENGTH - 2) } ' "$projection" | LC_ALL=C sort)" + projected_targets=${projected_targets//$'\r'/} if [[ "$projected_targets" != "$matrix_targets" ]]; then echo "ERROR: target projection does not match .config/target-matrix.json: $projection" >&2 diff -u <(printf '%s\n' "$matrix_targets") <(printf '%s\n' "$projected_targets") >&2 || true From f571a682da2bc588d8e60d3ad899b2a6e462fa81 Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Tue, 1 Sep 2026 19:10:32 -0400 Subject: [PATCH 11/18] ci: trust Windows Arm64 Rust host Keep the exact aarch64-pc-windows-msvc host check while ignoring the architecture reported by the emulated x86-64 Git Bash process in the compile-only Arm64 lane. --- scripts/ci/native-platform.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/ci/native-platform.sh b/scripts/ci/native-platform.sh index f971832e..670e7902 100755 --- a/scripts/ci/native-platform.sh +++ b/scripts/ci/native-platform.sh @@ -40,7 +40,8 @@ require_host() { case "$platform" in linux-arm64 | macos-arm64) [[ "$(uname -m)" == aarch64 || "$(uname -m)" == arm64 ]] ;; macos-x64 | windows-x64) [[ "$(uname -m)" == x86_64 ]] ;; - windows-arm64) [[ "$(uname -m)" == aarch64 || "$(uname -m)" == arm64 ]] ;; + # Windows Arm64 runs an x86-64 Git Bash; the exact native Rust host above is authoritative. + windows-arm64) ;; ibm-s390x) [[ "$(uname -m)" == s390x ]] ;; ibm-power10) [[ "$(uname -m)" == ppc64le ]] ;; rise-riscv) [[ "$(uname -m)" == riscv64 ]] ;; From 35669f041c158e1f0c1e17c64645bb1e50e43de0 Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Tue, 1 Sep 2026 21:33:47 -0400 Subject: [PATCH 12/18] ci: close assurance coverage gaps Add plan-selected MSRV, Cargo graph, and minimum-feature example gates to CI and Qualification. Report bounded Cargo Rail cache effectiveness from every cache-enabled lane. Harden fuzz target discovery against SIGPIPE and give valid RSA seeds a realistic per-input watchdog. --- .config/rail.toml | 29 ++++++++++ .github/actions/rust/action.yaml | 17 +++++- .github/workflows/ci.yaml | 67 +++++++++++++++++++++--- .github/workflows/qualification.yaml | 51 ++++++++++++++++-- justfile | 4 ++ scripts/README.md | 12 +++-- scripts/check/affected.sh | 10 ++++ scripts/check/check-all.sh | 2 + scripts/check/msrv.sh | 22 ++++++++ scripts/check/policy.sh | 12 +++++ scripts/ci/assurance-planning-test.sh | 57 +++++++++++++++++++- scripts/ci/check-worktree-test.sh | 14 ++++- scripts/ci/remote-cache-recipes-test.sh | 19 +++++++ scripts/ci/report-cache.sh | 41 +++++++++++++++ scripts/lib/fuzz-packages.sh | 4 +- scripts/test/test-fuzz-scheduler-test.sh | 17 ++++++ scripts/test/test-fuzz.sh | 4 +- 17 files changed, 362 insertions(+), 20 deletions(-) create mode 100755 scripts/check/msrv.sh create mode 100755 scripts/ci/report-cache.sh diff --git a/.config/rail.toml b/.config/rail.toml index 06844542..590c65e0 100644 --- a/.config/rail.toml +++ b/.config/rail.toml @@ -164,6 +164,35 @@ paths = [ "tools/**", ] +[plan.work."contracts.cargo-graph"] +scope = "repository" +paths = [ + ".cargo/**", + ".config/rail.toml", + ".config/toolchains.toml", + "Cargo.lock", + "Cargo.toml", + "build.rs", + "fuzz/Cargo.lock", + "fuzz/Cargo.toml", + "fuzz/**/Cargo.lock", + "fuzz/**/Cargo.toml", + "fuzz-packages/**/Cargo.lock", + "fuzz-packages/**/Cargo.toml", + "rust-toolchain.toml", + "tools/**/Cargo.lock", + "tools/**/Cargo.toml", +] + +[plan.work."contracts.examples"] +scope = "repository" +paths = [ + "Cargo.lock", + "Cargo.toml", + "examples/**", + "scripts/test/test-examples.sh", +] + [plan.work."targets.platforms"] scope = "variants" config = ["targets"] diff --git a/.github/actions/rust/action.yaml b/.github/actions/rust/action.yaml index ca979b9c..9761536f 100644 --- a/.github/actions/rust/action.yaml +++ b/.github/actions/rust/action.yaml @@ -28,6 +28,9 @@ inputs: default: "" outputs: + cache-enabled: + description: Whether Cargo Rail compiler reuse was enabled. + value: ${{ steps.cache-policy.outputs.enabled }} toolchain: description: Exact installed toolchain. value: ${{ steps.resolve.outputs.toolchain }} @@ -56,6 +59,19 @@ runs: TOOLCHAIN: ${{ steps.resolve.outputs.toolchain }} run: scripts/ci/setup-toolchain.sh "$TOOLCHAIN" "$COMPONENTS" "$GITHUB_ENV" + - id: cache-policy + name: Resolve compiler cache policy + shell: bash + env: + CACHE_MODE: ${{ inputs.cache-mode }} + run: | + case "$CACHE_MODE" in + off) enabled=false ;; + read | read-write) enabled=true ;; + *) echo "invalid cache mode: $CACHE_MODE" >&2; exit 2 ;; + esac + echo "enabled=$enabled" >>"$GITHUB_OUTPUT" + - name: Authorize compiler cache if: inputs.cache-mode != 'off' shell: bash @@ -65,7 +81,6 @@ runs: CACHE_URL: ${{ inputs.cache-url }} SECRET_ACCESS_KEY: ${{ inputs.cache-secret-access-key }} run: | - case "$CACHE_MODE" in read|read-write) ;; *) echo "invalid cache mode: $CACHE_MODE" >&2; exit 2 ;; esac [[ -n "$CACHE_URL" ]] || { echo "cache URL is required" >&2; exit 2; } [[ -n "$ACCESS_KEY_ID" && -n "$SECRET_ACCESS_KEY" ]] || { echo "complete cache credentials are required" >&2 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d78ae482..04458179 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -29,6 +29,7 @@ jobs: artifact: ${{ steps.stage.outputs.artifact }} core: ${{ steps.select.outputs.core }} ct: ${{ steps.select.outputs.ct }} + examples: ${{ steps.select.outputs.examples }} features: ${{ steps.select.outputs.features }} feature-matrix: ${{ steps.select.outputs.feature-matrix }} fuzz: ${{ steps.select.outputs.fuzz }} @@ -37,6 +38,7 @@ jobs: identity: ${{ steps.rail.outputs.plan-identity }} miri: ${{ steps.select.outputs.miri }} miri-rows: ${{ steps.select.outputs.miri-rows }} + msrv: ${{ steps.select.outputs.msrv }} platform-matrix: ${{ steps.select.outputs.platform-matrix }} platforms: ${{ steps.select.outputs.platforms }} rsa: ${{ steps.select.outputs.rsa }} @@ -87,14 +89,16 @@ jobs: fuzz_rows=$(scripts/test/fuzz-contracts.sh rows "$PLAN" "$READER") { echo "actions=$(required_any policy.actions)" - echo "core=$(required_any cargo.fmt cargo.build cargo.clippy cargo.doc cargo.doctest cargo.test contracts.auxiliary)" + echo "core=$(required_any cargo.fmt cargo.build cargo.clippy cargo.doc cargo.doctest cargo.test contracts.auxiliary contracts.examples)" echo "ct=$(required_any assurance.ct)" + echo "examples=$(required_any cargo.build contracts.examples)" [[ "$feature_count" -gt 0 ]] && echo "features=true" || echo "features=false" echo "feature-matrix=$feature_matrix" [[ -n "$fuzz_rows" ]] && echo "fuzz=true" || echo "fuzz=false" echo "fuzz-rows=$fuzz_rows" [[ -n "$miri_rows" ]] && echo "miri=true" || echo "miri=false" echo "miri-rows=$miri_rows" + echo "msrv=$(required_any cargo.build)" echo "platform-matrix=$platform_matrix" [[ "$platform_count" -gt 0 ]] && echo "platforms=true" || echo "platforms=false" echo "rsa=$(required_any assurance.rsa)" @@ -166,7 +170,8 @@ jobs: artifact: ${{ needs.plan.outputs.artifact }} identity: ${{ needs.plan.outputs.identity }} head-commit: ${{ needs.plan.outputs.head-commit }} - - uses: $/.github/actions/rust + - id: rust + uses: $/.github/actions/rust with: components: clippy,rustfmt cache-url: ${{ vars.CARGO_RAIL_CACHE_URL }} @@ -187,6 +192,41 @@ jobs: env: RSCRYPTO_TEST_MODE: commit run: scripts/test/test.sh + - name: Run minimum-feature examples + if: needs.plan.outputs.examples == 'true' + run: scripts/test/test-examples.sh + - name: Report compiler cache + if: ${{ !cancelled() && steps.rust.outputs.cache-enabled == 'true' }} + run: scripts/ci/report-cache.sh + + msrv: + name: MSRV + needs: plan + if: needs.plan.outputs.msrv == 'true' + runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: $/.github/actions/plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - run: scripts/ci/require-work.sh cargo.build + - id: rust + uses: $/.github/actions/rust + with: + contract: msrv + cache-url: ${{ vars.CARGO_RAIL_CACHE_URL }} + cache-mode: ${{ vars.CARGO_RAIL_CACHE_URL != '' && github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && 'read-write' || vars.CARGO_RAIL_CACHE_URL != '' && secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && 'read' || 'off' }} + cache-access-key-id: ${{ github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID || secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} + cache-secret-access-key: ${{ github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY || secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} + - run: scripts/check/msrv.sh + - name: Report compiler cache + if: ${{ !cancelled() && steps.rust.outputs.cache-enabled == 'true' }} + run: scripts/ci/report-cache.sh features: name: Features (${{ matrix.domain }} ${{ matrix.shard }}) @@ -207,7 +247,8 @@ jobs: identity: ${{ needs.plan.outputs.identity }} head-commit: ${{ needs.plan.outputs.head-commit }} - run: scripts/ci/require-work.sh contracts.features - - uses: $/.github/actions/rust + - id: rust + uses: $/.github/actions/rust with: cache-url: ${{ vars.CARGO_RAIL_CACHE_URL }} cache-mode: ${{ vars.CARGO_RAIL_CACHE_URL != '' && github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && 'read-write' || vars.CARGO_RAIL_CACHE_URL != '' && secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && 'read' || 'off' }} @@ -218,6 +259,9 @@ jobs: FEATURE_PROFILES: ${{ matrix.profiles }} FEATURE_SHARD: ${{ matrix.shard }} run: scripts/check/feature-contracts.sh selected "$FEATURE_DOMAIN" "$FEATURE_SHARD" "$FEATURE_PROFILES" + - name: Report compiler cache + if: ${{ !cancelled() && steps.rust.outputs.cache-enabled == 'true' }} + run: scripts/ci/report-cache.sh platforms: name: ${{ matrix.name }} @@ -239,7 +283,8 @@ jobs: artifact: ${{ needs.plan.outputs.artifact }} identity: ${{ needs.plan.outputs.identity }} head-commit: ${{ needs.plan.outputs.head-commit }} - - uses: $/.github/actions/rust + - id: rust + uses: $/.github/actions/rust with: contract: ${{ matrix.contract }} components: ${{ matrix.components }} @@ -251,6 +296,9 @@ jobs: env: TARGET_ROW: ${{ matrix.id }} run: scripts/ci/target-contracts.sh run "$TARGET_ROW" shallow + - name: Report compiler cache + if: ${{ !cancelled() && steps.rust.outputs.cache-enabled == 'true' }} + run: scripts/ci/report-cache.sh miri: name: Miri @@ -361,17 +409,21 @@ jobs: identity: ${{ needs.plan.outputs.identity }} head-commit: ${{ needs.plan.outputs.head-commit }} - run: scripts/ci/require-work.sh assurance.rsa - - uses: $/.github/actions/rust + - id: rust + uses: $/.github/actions/rust with: cache-url: ${{ vars.CARGO_RAIL_CACHE_URL }} cache-mode: ${{ vars.CARGO_RAIL_CACHE_URL != '' && github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && 'read-write' || vars.CARGO_RAIL_CACHE_URL != '' && secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && 'read' || 'off' }} cache-access-key-id: ${{ github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID || secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} cache-secret-access-key: ${{ github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY || secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} - run: scripts/test/test-rsa-linux-asm.sh + - name: Report compiler cache + if: ${{ !cancelled() && steps.rust.outputs.cache-enabled == 'true' }} + run: scripts/ci/report-cache.sh complete: name: Complete - needs: [plan, core, features, platforms, miri, fuzz, supply-chain, ct, rsa] + needs: [plan, core, msrv, features, platforms, miri, fuzz, supply-chain, ct, rsa] if: ${{ !cancelled() && needs.plan.result != 'skipped' }} runs-on: ubuntu-latest timeout-minutes: 5 @@ -389,6 +441,8 @@ jobs: FUZZ_RESULT: ${{ needs.fuzz.result }} MIRI_REQUIRED: ${{ needs.plan.outputs.miri }} MIRI_RESULT: ${{ needs.miri.result }} + MSRV_REQUIRED: ${{ needs.plan.outputs.msrv }} + MSRV_RESULT: ${{ needs.msrv.result }} PLATFORM_REQUIRED: ${{ needs.plan.outputs.platforms }} PLATFORM_RESULT: ${{ needs.platforms.result }} PLAN_RESULT: ${{ needs.plan.result }} @@ -404,6 +458,7 @@ jobs: "$FEATURES_REQUIRED:$FEATURES_RESULT:features" \ "$FUZZ_REQUIRED:$FUZZ_RESULT:fuzz" \ "$MIRI_REQUIRED:$MIRI_RESULT:miri" \ + "$MSRV_REQUIRED:$MSRV_RESULT:msrv" \ "$PLATFORM_REQUIRED:$PLATFORM_RESULT:platforms" \ "$RSA_REQUIRED:$RSA_RESULT:rsa" \ "$SUPPLY_REQUIRED:$SUPPLY_RESULT:supply-chain" diff --git a/.github/workflows/qualification.yaml b/.github/workflows/qualification.yaml index 6b07f90b..e641a302 100644 --- a/.github/workflows/qualification.yaml +++ b/.github/workflows/qualification.yaml @@ -144,7 +144,8 @@ jobs: artifact: ${{ needs.plan.outputs.artifact }} identity: ${{ needs.plan.outputs.identity }} head-commit: ${{ needs.plan.outputs.head-commit }} - - uses: $/.github/actions/rust + - id: rust + uses: $/.github/actions/rust with: components: clippy,rustfmt cache-url: ${{ vars.CARGO_RAIL_CACHE_URL }} @@ -162,6 +163,39 @@ jobs: run: scripts/check/check.sh - name: Test run: scripts/test/test.sh + - name: Run minimum-feature examples + run: scripts/test/test-examples.sh + - name: Report compiler cache + if: ${{ !cancelled() && steps.rust.outputs.cache-enabled == 'true' }} + run: scripts/ci/report-cache.sh + + msrv: + name: MSRV + needs: plan + runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: $/.github/actions/plan + with: + artifact: ${{ needs.plan.outputs.artifact }} + identity: ${{ needs.plan.outputs.identity }} + head-commit: ${{ needs.plan.outputs.head-commit }} + - run: scripts/ci/require-work.sh cargo.build + - id: rust + uses: $/.github/actions/rust + with: + contract: msrv + cache-url: ${{ vars.CARGO_RAIL_CACHE_URL }} + cache-mode: ${{ vars.CARGO_RAIL_CACHE_URL != '' && github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && 'read-write' || vars.CARGO_RAIL_CACHE_URL != '' && secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && 'read' || 'off' }} + cache-access-key-id: ${{ github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID || secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} + cache-secret-access-key: ${{ github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY || secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} + - run: scripts/check/msrv.sh + - name: Report compiler cache + if: ${{ !cancelled() && steps.rust.outputs.cache-enabled == 'true' }} + run: scripts/ci/report-cache.sh features: name: Features (${{ matrix.domain }} ${{ matrix.shard }}) @@ -182,7 +216,8 @@ jobs: identity: ${{ needs.plan.outputs.identity }} head-commit: ${{ needs.plan.outputs.head-commit }} - run: scripts/ci/require-work.sh contracts.features - - uses: $/.github/actions/rust + - id: rust + uses: $/.github/actions/rust with: cache-url: ${{ vars.CARGO_RAIL_CACHE_URL }} cache-mode: ${{ vars.CARGO_RAIL_CACHE_URL != '' && github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && 'read-write' || vars.CARGO_RAIL_CACHE_URL != '' && secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && 'read' || 'off' }} @@ -193,6 +228,9 @@ jobs: FEATURE_PROFILES: ${{ matrix.profiles }} FEATURE_SHARD: ${{ matrix.shard }} run: scripts/check/feature-contracts.sh selected "$FEATURE_DOMAIN" "$FEATURE_SHARD" "$FEATURE_PROFILES" + - name: Report compiler cache + if: ${{ !cancelled() && steps.rust.outputs.cache-enabled == 'true' }} + run: scripts/ci/report-cache.sh platforms: name: ${{ matrix.name }} @@ -213,7 +251,8 @@ jobs: artifact: ${{ needs.plan.outputs.artifact }} identity: ${{ needs.plan.outputs.identity }} head-commit: ${{ needs.plan.outputs.head-commit }} - - uses: $/.github/actions/rust + - id: rust + uses: $/.github/actions/rust with: contract: ${{ matrix.contract }} components: ${{ matrix.components }} @@ -225,6 +264,9 @@ jobs: env: TARGET_ROW: ${{ matrix.id }} run: scripts/ci/target-contracts.sh run "$TARGET_ROW" deep + - name: Report compiler cache + if: ${{ !cancelled() && steps.rust.outputs.cache-enabled == 'true' }} + run: scripts/ci/report-cache.sh miri: name: Miri @@ -451,7 +493,7 @@ jobs: complete: name: Complete (${{ needs.plan.outputs.mode }}) - needs: [plan, core, features, platforms, miri, fuzz, mlkem, rsa-leakage, rsa-asm, supply-chain, coverage, zeroization, ct] + needs: [plan, core, msrv, features, platforms, miri, fuzz, mlkem, rsa-leakage, rsa-asm, supply-chain, coverage, zeroization, ct] if: ${{ !cancelled() }} runs-on: ubuntu-latest timeout-minutes: 5 @@ -463,6 +505,7 @@ jobs: for lane in \ "plan=${{ needs.plan.result }}" \ "core=${{ needs.core.result }}" \ + "msrv=${{ needs.msrv.result }}" \ "features=${{ needs.features.result }}" \ "platforms=${{ needs.platforms.result }}" \ "miri=${{ needs.miri.result }}" \ diff --git a/justfile b/justfile index 7d0f9021..00200a20 100644 --- a/justfile +++ b/justfile @@ -82,6 +82,10 @@ validate *args="": check-all: @scripts/check/check-all.sh +# Check the public library contract with the Cargo.toml minimum Rust version. +msrv: + @scripts/check/msrv.sh + # Compile and test feature contracts; optionally select a domain and shard. feature-contracts *args="": @scripts/check/feature-contracts.sh {{ args }} diff --git a/scripts/README.md b/scripts/README.md index 2ba298a9..cdc35760 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -12,6 +12,7 @@ the recipes reported by `just --list`. | `check/policy.sh` | `check/affected.sh`, `ci.yaml`, `qualification.yaml` | | `check/check.sh` | `check/affected.sh`, `check/check-all.sh`, `ci.yaml`, `qualification.yaml` | | `check/check-all.sh` | `just check-all` | +| `check/msrv.sh` | `just msrv`, `scripts/check/check-all.sh`, `ci.yaml`, `qualification.yaml` | | `check/feature-contracts.sh` | `just feature-contracts`, `scripts/check/affected.sh`, `scripts/check/check-all.sh`, `ci.yaml`, `qualification.yaml` | | `check/asm-ledger.sh` | `scripts/check/policy.sh` | | `check/rsa-asm-provenance.sh` | `check/asm-ledger.sh`; direct `--archive PATH` reconstructs the three pinned RSA snapshots offline | @@ -24,7 +25,7 @@ the recipes reported by `just --list`. | `ci/check-locked-cargo-test.sh` | `scripts/ci/actions-policy.sh` | | `ci/target-contracts.sh` | `just target-contract`, `scripts/check/check-all.sh`, `ci.yaml`, `qualification.yaml` | | `test/test.sh` | `just test` (`--all` for the full workspace), `ci.yaml`, `qualification.yaml` | -| `test/test-examples.sh` | `just test-examples` | +| `test/test-examples.sh` | `just test-examples`, `scripts/check/affected.sh`, `scripts/check/check-all.sh`, `ci.yaml`, `qualification.yaml` | | `test/miri-contracts.sh` | `just miri-contract`, `scripts/check/affected.sh`, `ci.yaml`, `qualification.yaml` | | `test/test-miri.sh` | `just test-miri`, `test/miri-contracts.sh` | | `test/fuzz-contracts.sh` | `just fuzz-contract`, `scripts/check/affected.sh`, `ci.yaml` | @@ -41,6 +42,7 @@ the recipes reported by `just --list`. | `ci/actions-policy.sh` | `scripts/check/policy.sh` | | `ci/check-action-pins-test.sh` | `just check-actions` | | `ci/remote-cache-recipes-test.sh` | `just check-actions` | +| `ci/report-cache.sh` | Cache-enabled jobs in `ci.yaml` and `qualification.yaml` | | `ci/feature-contracts-test.sh` | `just check-actions`; proves unique compile graphs, focused runtime scopes, and disjoint deterministic shards | | `ci/feature-planning-test.sh` | `just check-actions`; proves exact algorithm groups, full feature-policy selection, and fail-closed unattributed inputs | | `ci/activate-plan.sh` | `.github/actions/plan/action.yaml`; validates and exports one transported plan | @@ -228,12 +230,16 @@ Qualification enables reuse for host, feature-contract, and supported native platform rows; release preflight is read-only. Cross targets, Clippy, rustdoc, doctests, Miri, fuzzing, CT, benchmarks, macOS x86-64, and donated hosts stay cold because the released cache deliberately bypasses or cannot install on -those classes. +those classes. Every cache-enabled CI and Qualification job emits Cargo Rail's +bounded post-run hit, miss, bypass, failure, and local/remote-origin counters; +telemetry failure warns without making acceleration a correctness gate. Local and remote development use the same affected commands: `just plan`, `just check`, `just test`, and `just validate`. `just check` runs selected compile feature contracts; `just validate` adds selected runtime contracts and -shares one saved plan across policy, checks, feature contracts, and tests. +minimum-feature examples, then shares one saved plan across policy, checks, +feature contracts, and tests. `just msrv` reproduces the compiler-floor job; +`just check-all` includes MSRV and minimum-feature example execution. `just feature-contracts [compile|runtime] [N/M]` reproduces any CI shard. `just target-contract ROW [shallow|deep]` reproduces any independently executable platform row, locally or through `ssh-just`. diff --git a/scripts/check/affected.sh b/scripts/check/affected.sh index a7ea48a2..0e65205e 100755 --- a/scripts/check/affected.sh +++ b/scripts/check/affected.sh @@ -47,6 +47,16 @@ fi if [[ "$with_tests" == true ]]; then "$SCRIPT_DIR/../test/test.sh" + examples_required=false + if rail_work_required cargo.build || rail_work_required contracts.examples; then + examples_required=true + fi + if [[ "$examples_required" == true ]]; then + "$SCRIPT_DIR/../test/test-examples.sh" + else + echo "Examples: not required by Cargo Rail" + fi + miri_rows=$("$SCRIPT_DIR/../test/miri-contracts.sh" rows) if [[ -n "$miri_rows" ]]; then "$SCRIPT_DIR/../test/miri-contracts.sh" selected "$miri_rows" diff --git a/scripts/check/check-all.sh b/scripts/check/check-all.sh index 5f40e2f5..ed725e06 100755 --- a/scripts/check/check-all.sh +++ b/scripts/check/check-all.sh @@ -17,7 +17,9 @@ echo "" "$SCRIPT_DIR/policy.sh" --all "$SCRIPT_DIR/check.sh" --all +"$SCRIPT_DIR/msrv.sh" "$SCRIPT_DIR/feature-contracts.sh" all +"$SCRIPT_DIR/../test/test-examples.sh" "$SCRIPT_DIR/zeroize-evidence.sh" rows=() diff --git a/scripts/check/msrv.sh b/scripts/check/msrv.sh new file mode 100755 index 00000000..d0284d31 --- /dev/null +++ b/scripts/check/msrv.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Compile the public library contract with the exact Cargo.toml MSRV. + +set -euo pipefail + +[[ $# -eq 0 ]] || { echo "Usage: $0" >&2; exit 2; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +toolchain=$("$SCRIPT_DIR/../lib/toolchain.sh" --msrv) + +if ! rustup run "$toolchain" rustc --version >/dev/null 2>&1; then + echo "MSRV toolchain $toolchain is not installed" >&2 + echo "Install it with: rustup toolchain install $toolchain --profile minimal --no-self-update" >&2 + exit 1 +fi + +cd "$REPO_ROOT" +export RUSTUP_TOOLCHAIN="$toolchain" +rustc --version --verbose +cargo check --locked --workspace --lib --no-default-features +cargo check --locked --workspace --lib --all-features diff --git a/scripts/check/policy.sh b/scripts/check/policy.sh index ba4d22cd..6f7dd55f 100755 --- a/scripts/check/policy.sh +++ b/scripts/check/policy.sh @@ -47,6 +47,18 @@ else skip "Actions policy" "not required by Cargo Rail" fi +if work_required contracts.cargo-graph; then + step "Checking Cargo graph consistency" + if ! cargo rail unify --check >"$LOG_DIR/cargo-graph.log" 2>&1; then + fail + show_error "$LOG_DIR/cargo-graph.log" + exit 1 + fi + ok +else + skip "Cargo graph consistency" "not required by Cargo Rail" +fi + if work_required policy.repository; then step "Checking assembly ledger" if ! "$SCRIPT_DIR/asm-ledger.sh" >"$LOG_DIR/asm-ledger.log" 2>&1; then diff --git a/scripts/ci/assurance-planning-test.sh b/scripts/ci/assurance-planning-test.sh index 47c696cb..b941dfcb 100755 --- a/scripts/ci/assurance-planning-test.sh +++ b/scripts/ci/assurance-planning-test.sh @@ -65,6 +65,14 @@ assert_decisions() { "$plan" >/dev/null || fail "unexpected CT/RSA decisions in $plan" } +assert_state() { + local plan=$1 + local work=$2 + local expected=$3 + jq -e --arg work "$work" --arg expected "$expected" '.work[$work].state == $expected' "$plan" >/dev/null \ + || fail "$work should be $expected in $plan" +} + assert_variants() { local plan=$1 local work=$2 @@ -149,9 +157,16 @@ assert_variants "$unattributed_plan" assurance.fuzz all manifest_plan="$TMP_ROOT/manifest.json" plan_path Cargo.toml manifest "$manifest_plan" assert_decisions "$manifest_plan" required required +assert_state "$manifest_plan" contracts.cargo-graph required +assert_state "$manifest_plan" contracts.examples required assert_variants "$manifest_plan" assurance.miri all assert_variants "$manifest_plan" assurance.fuzz all +example_plan="$TMP_ROOT/example.json" +plan_path examples/aead_seal_open.rs example "$example_plan" +assert_state "$example_plan" contracts.cargo-graph skipped +assert_state "$example_plan" contracts.examples required + all_plan="$TMP_ROOT/all.json" cargo rail plan --quiet --from "$base_commit" --to "$base_commit" --all --json >"$all_plan" expected_all_work=$( @@ -168,6 +183,8 @@ expected_all_work=$( cargo.package \ cargo.test \ contracts.auxiliary \ + contracts.cargo-graph \ + contracts.examples \ contracts.features \ dependencies.auxiliary \ dependency-policy \ @@ -184,12 +201,16 @@ actual_all_work=$(jq -r '.required[]' "$all_plan" | sort) lock_plan="$TMP_ROOT/lock.json" plan_path Cargo.lock lock "$lock_plan" assert_decisions "$lock_plan" skipped skipped +assert_state "$lock_plan" contracts.cargo-graph required +assert_state "$lock_plan" contracts.examples required assert_variants "$lock_plan" assurance.miri skipped assert_variants "$lock_plan" assurance.fuzz skipped docs_plan="$TMP_ROOT/docs.json" plan_path docs/features.md docs "$docs_plan" assert_decisions "$docs_plan" skipped skipped +assert_state "$docs_plan" contracts.cargo-graph skipped +assert_state "$docs_plan" contracts.examples skipped ruby - "$REPO_ROOT" <<'RUBY' require "json" @@ -202,6 +223,8 @@ plan_outputs = jobs.fetch("plan").fetch("outputs") raise "missing CT plan output" unless plan_outputs.key?("ct") raise "missing RSA plan output" unless plan_outputs.key?("rsa") raise "missing test plan output" unless plan_outputs.key?("tests") +raise "missing examples plan output" unless plan_outputs.key?("examples") +raise "missing MSRV plan output" unless plan_outputs.key?("msrv") %w[fuzz fuzz-rows miri miri-rows].each do |output| raise "missing #{output} plan output" unless plan_outputs.key?(output) end @@ -209,6 +232,8 @@ end plan_steps = jobs.fetch("plan").fetch("steps") select_run = plan_steps.find { |step| step["id"] == "select" }.fetch("run") raise "CI plan does not select Actions policy setup" unless select_run.include?('echo "actions=$(required_any policy.actions)"') +raise "CI plan does not select minimum-feature examples" unless select_run.include?('echo "examples=$(required_any cargo.build contracts.examples)"') +raise "CI plan does not select MSRV from Cargo build impact" unless select_run.include?('echo "msrv=$(required_any cargo.build)"') direct_policy_tools_install = plan_steps.find { |step| step["run"] == "scripts/ci/install-actions-policy-tools.sh" } raise "CI does not install direct policy tools only for Actions policy" unless direct_policy_tools_install&.fetch("if") == "steps.select.outputs.actions == 'true'" policy_tools_install = plan_steps.find do |step| @@ -235,11 +260,35 @@ raise "Miri job bypasses selected rows" unless miri_runs.include?('scripts/test/ raise "Fuzz job bypasses its plan decision" unless fuzz_runs.include?("scripts/ci/require-work.sh assurance.fuzz") raise "Fuzz job bypasses selected rows" unless fuzz_runs.include?('scripts/test/fuzz-contracts.sh selected "$FUZZ_ROWS"') +core_runs = jobs.fetch("core").fetch("steps").map { |step| step["run"] }.compact +raise "CI core lost minimum-feature examples" unless core_runs.include?("scripts/test/test-examples.sh") +msrv = jobs.fetch("msrv") +msrv_runs = msrv.fetch("steps").map { |step| step["run"] }.compact +raise "CI MSRV bypasses Cargo build selection" unless msrv_runs.include?("scripts/ci/require-work.sh cargo.build") +raise "CI MSRV bypasses its repository command" unless msrv_runs.include?("scripts/check/msrv.sh") +msrv_rust = msrv.fetch("steps").find { |step| step["uses"] == "$/.github/actions/rust" } +raise "CI MSRV does not install the declared contract" unless msrv_rust&.dig("with", "contract") == "msrv" + +policy = File.read(File.join(root, "scripts/check/policy.sh")) +raise "repository policy does not own Cargo graph consistency" unless policy.include?("work_required contracts.cargo-graph") && policy.include?("cargo rail unify --check") + +rust_action = YAML.safe_load(File.read(File.join(root, ".github/actions/rust/action.yaml")), aliases: true) +raise "Rust action does not expose cache activation" unless rust_action.fetch("outputs").key?("cache-enabled") + +%w[core msrv features platforms rsa].each do |job_name| + steps = jobs.fetch(job_name).fetch("steps") + rust = steps.find { |step| step["id"] == "rust" && step["uses"] == "$/.github/actions/rust" } + report = steps.find { |step| step["run"] == "scripts/ci/report-cache.sh" } + raise "CI #{job_name} cache setup has no stable output identity" unless rust + raise "CI #{job_name} lost post-run cache telemetry" unless report +end + needs = jobs.fetch("complete").fetch("needs") raise "Complete omits CT" unless needs.include?("ct") raise "Complete omits RSA" unless needs.include?("rsa") raise "Complete omits Miri" unless needs.include?("miri") raise "Complete omits fuzz" unless needs.include?("fuzz") +raise "Complete omits MSRV" unless needs.include?("msrv") qualification = YAML.safe_load(File.read(File.join(root, ".github/workflows/qualification.yaml")), aliases: true) ct_call = qualification.fetch("jobs").fetch("ct").fetch("with") @@ -284,6 +333,12 @@ qualification_fuzz = qualification.fetch("jobs").fetch("fuzz").fetch("steps").ma raise "Qualification lost exhaustive fuzzing" unless qualification_fuzz.include?("scripts/test/test-fuzz.sh --all") raise "Qualification lost exhaustive ASan replay" unless qualification_fuzz.include?("scripts/test/test-fuzz-asan.sh --all") +qualification_core_runs = qualification.fetch("jobs").fetch("core").fetch("steps").map { |step| step["run"] }.compact +raise "Qualification lost minimum-feature examples" unless qualification_core_runs.include?("scripts/test/test-examples.sh") +qualification_msrv = qualification.fetch("jobs").fetch("msrv") +qualification_msrv_runs = qualification_msrv.fetch("steps").map { |step| step["run"] }.compact +raise "Qualification lost MSRV execution" unless qualification_msrv_runs.include?("scripts/check/msrv.sh") + qualification_platforms = qualification.fetch("jobs").fetch("platforms").fetch("steps").map { |step| step["run"] }.compact raise "Qualification lost deep native platform proof" unless qualification_platforms.include?('scripts/ci/target-contracts.sh run "$TARGET_ROW" deep') @@ -330,7 +385,7 @@ zeroization_rust = zeroization.fetch("steps").find { |step| step["uses"] == "$/. raise "Zeroization must not enable compiler reuse" unless zeroization_rust && !zeroization_rust.key?("with") qualification_needs = qualification.fetch("jobs").fetch("complete").fetch("needs") -%w[coverage zeroization].each do |job| +%w[coverage msrv zeroization].each do |job| raise "Qualification Complete omits #{job}" unless qualification_needs.include?(job) end diff --git a/scripts/ci/check-worktree-test.sh b/scripts/ci/check-worktree-test.sh index a41aa993..9762abe8 100755 --- a/scripts/ci/check-worktree-test.sh +++ b/scripts/ci/check-worktree-test.sh @@ -63,6 +63,16 @@ cat >"$fixture/scripts/check/zeroize-evidence.sh" <<'EOF' exit 0 EOF +cat >"$fixture/scripts/check/msrv.sh" <<'EOF' +#!/usr/bin/env bash +exit 0 +EOF + +cat >"$fixture/scripts/test/test-examples.sh" <<'EOF' +#!/usr/bin/env bash +exit 0 +EOF + cat >"$fixture/scripts/check/affected.sh" <<'EOF' #!/usr/bin/env bash set -euo pipefail @@ -83,9 +93,11 @@ chmod +x \ "$fixture/scripts/ci/target-contracts.sh" \ "$fixture/scripts/lib/python.sh" \ "$fixture/scripts/check/affected.sh" \ + "$fixture/scripts/check/msrv.sh" \ "$fixture/scripts/check/policy.sh" \ "$fixture/scripts/check/rsa-asm-provenance.sh" \ - "$fixture/scripts/check/zeroize-evidence.sh" + "$fixture/scripts/check/zeroize-evidence.sh" \ + "$fixture/scripts/test/test-examples.sh" cat >"$fake_bin/cargo" <<'EOF' #!/usr/bin/env bash diff --git a/scripts/ci/remote-cache-recipes-test.sh b/scripts/ci/remote-cache-recipes-test.sh index e5132858..71cfe27e 100755 --- a/scripts/ci/remote-cache-recipes-test.sh +++ b/scripts/ci/remote-cache-recipes-test.sh @@ -17,6 +17,9 @@ cat >"$TMP_ROOT/bin/cargo" <<'SH' #!/usr/bin/env bash set -euo pipefail printf '%s\n' "$*" >>"$MOCK_CARGO_LOG" +if [[ "$*" == "rail cache status --scope local --format json" ]]; then + printf '%s\n' "${MOCK_CACHE_STATUS:-}" +fi if [[ ${MOCK_FAIL_APPLY:-0} == 1 && "$*" == "rail cache setup --remote "* ]]; then exit 40 fi @@ -45,6 +48,22 @@ just --justfile "$REPO_ROOT/justfile" cache-status [[ $(<"$MOCK_CARGO_LOG") == 'rail cache status --scope local --format json' ]] \ || fail "status recipe did not request local JSON telemetry" +: >"$MOCK_CARGO_LOG" +cache_report=$( + MOCK_CACHE_STATUS='{"result":"success","status":{"installation":{"healthy":true,"usage":{"hits":7,"misses":3,"failures":0,"bypasses":2,"early_bypasses":1}},"local":{"present":true,"cross_workspace":true,"cache":{"bytes":123,"results":10,"native_local_origins":4,"native_remote_origins":6}},"remote":{"provider":"cloudflare-r2","mode":"read","activation":"direct_transport_selected"}}}' \ + "$REPO_ROOT/scripts/ci/report-cache.sh" +) +jq -e ' + .healthy == true + and .usage.hits == 7 + and .usage.misses == 3 + and .local.native_remote_origins == 6 + and .remote.provider == "cloudflare-r2" + and .remote.mode == "read" +' <<<"$cache_report" >/dev/null || fail "cache report lost bounded effectiveness telemetry" +[[ $(<"$MOCK_CARGO_LOG") == 'rail cache status --scope local --format json' ]] \ + || fail "cache report did not use Cargo Rail's local status authority" + : >"$MOCK_CARGO_LOG" if MOCK_FAIL_PREVIEW=1 just --justfile "$REPO_ROOT/justfile" rail-cache-setup --max-size 10GiB \ >/dev/null 2>&1; then diff --git a/scripts/ci/report-cache.sh b/scripts/ci/report-cache.sh new file mode 100755 index 00000000..bde3d974 --- /dev/null +++ b/scripts/ci/report-cache.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Emit bounded, credential-free Cargo Rail compiler-cache telemetry. + +set -euo pipefail + +[[ $# -eq 0 ]] || { echo "Usage: $0" >&2; exit 2; } + +status="" +if ! status=$(cargo rail cache status --scope local --format json); then + echo "::warning::Cargo Rail cache status is unavailable" + exit 0 +fi + +if ! jq -e '.result == "success" and .status.installation.healthy == true' <<<"$status" >/dev/null; then + echo "::warning::Cargo Rail cache status is unhealthy or incompatible" + exit 0 +fi + +jq '{ + healthy: .status.installation.healthy, + usage: .status.installation.usage | { + hits, + misses, + failures, + bypasses, + early_bypasses + }, + local: .status.local | { + present, + cross_workspace, + bytes: .cache.bytes, + results: .cache.results, + native_local_origins: .cache.native_local_origins, + native_remote_origins: .cache.native_remote_origins + }, + remote: .status.remote | { + provider, + mode, + activation + } +}' <<<"$status" diff --git a/scripts/lib/fuzz-packages.sh b/scripts/lib/fuzz-packages.sh index 155a5ced..f2a4a7f5 100644 --- a/scripts/lib/fuzz-packages.sh +++ b/scripts/lib/fuzz-packages.sh @@ -99,7 +99,7 @@ fuzz_find_target_package() { case "$search_order" in full) for package_dir in "${FUZZ_FULL_PACKAGES[@]}"; do - if fuzz_list_targets "$package_dir" | grep -Fxq "$target"; then + if fuzz_list_targets "$package_dir" | grep -Fx "$target" >/dev/null; then echo "$package_dir" return 0 fi @@ -107,7 +107,7 @@ fuzz_find_target_package() { ;; scoped) for package_dir in "${FUZZ_SCOPED_PACKAGES[@]}"; do - if fuzz_list_targets "$package_dir" | grep -Fxq "$target"; then + if fuzz_list_targets "$package_dir" | grep -Fx "$target" >/dev/null; then echo "$package_dir" return 0 fi diff --git a/scripts/test/test-fuzz-scheduler-test.sh b/scripts/test/test-fuzz-scheduler-test.sh index eff856a6..24ee7b51 100755 --- a/scripts/test/test-fuzz-scheduler-test.sh +++ b/scripts/test/test-fuzz-scheduler-test.sh @@ -94,6 +94,11 @@ done case "$subcommand" in list) printf '%s\n' alpha beta crash delta + if [[ "${RSCRYPTO_FUZZ_LONG_LIST:-0}" == "1" ]]; then + for index in {1..10000}; do + printf 'filler_%s\n' "$index" + done + fi ;; run) printf '%s\n' "${args[@]}" >"$RSCRYPTO_FUZZ_CAPTURE/$target.args" @@ -205,6 +210,18 @@ done grep -Fq 'Summary: 2 targets, 0 failed' "$SELECTED_CAPTURE/output" \ || fail "exact selection did not aggregate only the requested targets" +LONG_LIST_CAPTURE="$TMP_ROOT/long-list-capture" +mkdir -p "$LONG_LIST_CAPTURE" +env \ + PATH="$BIN:$PATH" \ + RSCRYPTO_FUZZ_CAPTURE="$LONG_LIST_CAPTURE" \ + RSCRYPTO_FUZZ_DURATION_SECS=1 \ + RSCRYPTO_FUZZ_LONG_LIST=1 \ + RSCRYPTO_FUZZ_TARGET_CONCURRENCY=1 \ + bash "$FIXTURE/scripts/test/test-fuzz.sh" --targets alpha \ + >"$LONG_LIST_CAPTURE/output" 2>&1 \ + || fail "target discovery failed when cargo-fuzz produced more than one pipe buffer" + if env \ PATH="$BIN:$PATH" \ RSCRYPTO_FUZZ_CAPTURE="$SELECTED_CAPTURE" \ diff --git a/scripts/test/test-fuzz.sh b/scripts/test/test-fuzz.sh index fd03b7f8..269e4c0d 100755 --- a/scripts/test/test-fuzz.sh +++ b/scripts/test/test-fuzz.sh @@ -34,7 +34,7 @@ export CARGO_RAIL_CACHE=off # Configuration (can be overridden via environment) DURATION_SECS=${RSCRYPTO_FUZZ_DURATION_SECS:-60} -TIMEOUT=${RSCRYPTO_FUZZ_TIMEOUT_SECS:-10} +TIMEOUT=${RSCRYPTO_FUZZ_TIMEOUT_SECS:-30} RSS_LIMIT=${RSCRYPTO_FUZZ_RSS_LIMIT_MB:-2048} MAX_LEN=${RSCRYPTO_FUZZ_MAX_LEN:-65536} JOBS=${RSCRYPTO_FUZZ_JOBS:-1} @@ -63,7 +63,7 @@ show_help() { echo "" echo "Environment variables:" echo " RSCRYPTO_FUZZ_DURATION_SECS Duration per target (default: 60)" - echo " RSCRYPTO_FUZZ_TIMEOUT_SECS Timeout per test case (default: 10)" + echo " RSCRYPTO_FUZZ_TIMEOUT_SECS Timeout per test case (default: 30)" echo " RSCRYPTO_FUZZ_RSS_LIMIT_MB Memory limit in MB (default: 2048)" echo " RSCRYPTO_FUZZ_MAX_LEN Max input length (default: 65536)" echo " RSCRYPTO_FUZZ_JOBS LibFuzzer workers per target (default: 1)" From 8a865d954c4f2b09c40132ce5b78bda0e136d930 Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Tue, 1 Sep 2026 22:28:58 -0400 Subject: [PATCH 13/18] ci: isolate named runtime feature contracts Run each runtime profile independently and use Nextest for the three full-suite baselines while retaining two compile shards. Align CI and Qualification display names with the proof each job provides. --- .config/target-matrix.json | 34 ++++++++++++++-------------- .github/workflows/ci.yaml | 18 ++++++++++----- .github/workflows/qualification.yaml | 14 ++++++++---- scripts/README.md | 2 +- scripts/check/feature-contracts.sh | 32 +++++++++++++++++++++++--- scripts/ci/feature-contracts-test.sh | 25 ++++++++++++-------- scripts/lib/feature-profiles.sh | 11 +++++---- 7 files changed, 90 insertions(+), 46 deletions(-) diff --git a/.config/target-matrix.json b/.config/target-matrix.json index 42c2c761..a4de3ce2 100644 --- a/.config/target-matrix.json +++ b/.config/target-matrix.json @@ -5,7 +5,7 @@ { "id": "aarch64-apple-darwin", "dimensions": { - "name": "macOS AArch64 runtime", + "name": "macOS AArch64 Runtime", "target": "aarch64-apple-darwin", "group": "macos", "operation": "native", @@ -35,7 +35,7 @@ { "id": "aarch64-pc-windows-msvc", "dimensions": { - "name": "Windows AArch64 compile", + "name": "Windows AArch64 Compile", "target": "aarch64-pc-windows-msvc", "group": "win", "operation": "native", @@ -59,7 +59,7 @@ { "id": "aarch64-unknown-linux-gnu", "dimensions": { - "name": "Linux AArch64 runtime", + "name": "Linux AArch64 Runtime", "target": "aarch64-unknown-linux-gnu", "group": "linux", "operation": "native", @@ -89,7 +89,7 @@ { "id": "aarch64-unknown-linux-musl", "dimensions": { - "name": "Linux AArch64 musl compile", + "name": "Linux AArch64 musl Compile", "target": "aarch64-unknown-linux-musl", "group": "linux", "operation": "cross", @@ -113,7 +113,7 @@ { "id": "aarch64-unknown-none", "dimensions": { - "name": "Bare-metal AArch64 compile", + "name": "Bare-metal AArch64 Compile", "target": "aarch64-unknown-none", "group": "no_std", "operation": "cross", @@ -142,7 +142,7 @@ { "id": "powerpc64le-unknown-linux-gnu", "dimensions": { - "name": "IBM POWER10 runtime", + "name": "IBM POWER10 Runtime", "target": "powerpc64le-unknown-linux-gnu", "group": "ibm", "operation": "native", @@ -172,7 +172,7 @@ { "id": "riscv32imac-unknown-none-elf", "dimensions": { - "name": "Bare-metal RISC-V 32 compile", + "name": "Bare-metal RISC-V 32 Compile", "target": "riscv32imac-unknown-none-elf", "group": "no_std", "operation": "cross", @@ -201,7 +201,7 @@ { "id": "riscv64gc-unknown-linux-gnu", "dimensions": { - "name": "RISE RISC-V runtime", + "name": "RISC-V Runtime", "target": "riscv64gc-unknown-linux-gnu", "group": "linux", "operation": "native", @@ -231,7 +231,7 @@ { "id": "s390x-unknown-linux-gnu", "dimensions": { - "name": "IBM Z runtime", + "name": "IBM Z Runtime", "target": "s390x-unknown-linux-gnu", "group": "ibm", "operation": "native", @@ -261,7 +261,7 @@ { "id": "thumbv6m-none-eabi", "dimensions": { - "name": "Bare-metal Armv6-M compile", + "name": "Bare-metal Armv6-M Compile", "target": "thumbv6m-none-eabi", "group": "no_std", "operation": "cross", @@ -289,7 +289,7 @@ { "id": "wasm32-unknown-unknown", "dimensions": { - "name": "WebAssembly compile", + "name": "WebAssembly Compile", "target": "wasm32-unknown-unknown", "group": "wasm", "operation": "cross", @@ -319,7 +319,7 @@ { "id": "wasm32-wasip1", "dimensions": { - "name": "WASI compile and emulated runtime", + "name": "WASI Compile and Emulated Runtime", "target": "wasm32-wasip1", "group": "wasm", "operation": "cross", @@ -373,7 +373,7 @@ { "id": "x86-64-apple-darwin", "dimensions": { - "name": "macOS x86-64 runtime", + "name": "macOS x86-64 Runtime", "target": "x86_64-apple-darwin", "group": "macos", "operation": "native", @@ -403,7 +403,7 @@ { "id": "x86-64-pc-windows-msvc", "dimensions": { - "name": "Windows x86-64 runtime", + "name": "Windows x86-64 Runtime", "target": "x86_64-pc-windows-msvc", "group": "win", "operation": "native", @@ -432,7 +432,7 @@ { "id": "x86-64-unknown-linux-gnu", "dimensions": { - "name": "Linux x86-64 runtime (core owner)", + "name": "Linux x86-64 Runtime (Core Owner)", "target": "x86_64-unknown-linux-gnu", "group": "linux", "operation": "core", @@ -456,7 +456,7 @@ { "id": "x86-64-unknown-linux-musl", "dimensions": { - "name": "Linux x86-64 musl compile", + "name": "Linux x86-64 musl Compile", "target": "x86_64-unknown-linux-musl", "group": "linux", "operation": "cross", @@ -480,7 +480,7 @@ { "id": "x86-64-unknown-none", "dimensions": { - "name": "Bare-metal x86-64 compile", + "name": "Bare-metal x86-64 Compile", "target": "x86_64-unknown-none", "group": "no_std", "operation": "cross", diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 04458179..ae085763 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,7 +21,7 @@ env: jobs: plan: - name: Plan and policy + name: Plan if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: ubuntu-latest timeout-minutes: 10 @@ -156,7 +156,7 @@ jobs: if-no-files-found: error core: - name: Rust + name: Core needs: plan if: needs.plan.outputs.core == 'true' runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci @@ -229,7 +229,7 @@ jobs: run: scripts/ci/report-cache.sh features: - name: Features (${{ matrix.domain }} ${{ matrix.shard }}) + name: Feature Contracts (${{ matrix.label }}) needs: plan if: needs.plan.outputs.features == 'true' strategy: @@ -254,6 +254,12 @@ jobs: cache-mode: ${{ vars.CARGO_RAIL_CACHE_URL != '' && github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && 'read-write' || vars.CARGO_RAIL_CACHE_URL != '' && secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && 'read' || 'off' }} cache-access-key-id: ${{ github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID || secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} cache-secret-access-key: ${{ github.event_name == 'push' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY || secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} + - name: Install test runner + if: matrix.test_runner == 'nextest' + uses: taiki-e/install-action@0758d235715de2f3551eacc980d9ae8fce9342c3 # v2.87.3 + with: + tool: cargo-nextest@0.9.143 + fallback: none - env: FEATURE_DOMAIN: ${{ matrix.domain }} FEATURE_PROFILES: ${{ matrix.profiles }} @@ -352,7 +358,7 @@ jobs: run: scripts/test/fuzz-contracts.sh selected "$FUZZ_ROWS" supply-chain: - name: Supply chain + name: Supply Chain needs: plan if: needs.plan.outputs.supply-chain == 'true' runs-on: ubuntu-latest @@ -373,7 +379,7 @@ jobs: - run: cargo audit --ignore RUSTSEC-2023-0071 ct: - name: Constant-time structure + name: Constant-Time Gate needs: plan if: needs.plan.outputs.ct == 'true' runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci @@ -394,7 +400,7 @@ jobs: - run: scripts/ct/structural.sh rsa: - name: RSA assembly + name: RSA Gate needs: plan if: needs.plan.outputs.rsa == 'true' runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci diff --git a/.github/workflows/qualification.yaml b/.github/workflows/qualification.yaml index e641a302..02cca86c 100644 --- a/.github/workflows/qualification.yaml +++ b/.github/workflows/qualification.yaml @@ -131,7 +131,7 @@ jobs: if-no-files-found: error core: - name: Rust + name: Core needs: plan runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci timeout-minutes: 45 @@ -198,7 +198,7 @@ jobs: run: scripts/ci/report-cache.sh features: - name: Features (${{ matrix.domain }} ${{ matrix.shard }}) + name: Feature Contracts (${{ matrix.label }}) needs: plan strategy: fail-fast: false @@ -223,6 +223,12 @@ jobs: cache-mode: ${{ vars.CARGO_RAIL_CACHE_URL != '' && github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && 'read-write' || vars.CARGO_RAIL_CACHE_URL != '' && secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY != '' && 'read' || 'off' }} cache-access-key-id: ${{ github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID || secrets.CARGO_RAIL_R2_READ_ACCESS_KEY_ID }} cache-secret-access-key: ${{ github.ref == 'refs/heads/main' && secrets.CARGO_RAIL_R2_WRITE_ACCESS_KEY_ID != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY != '' && secrets.CARGO_RAIL_R2_WRITE_SECRET_ACCESS_KEY || secrets.CARGO_RAIL_R2_READ_SECRET_ACCESS_KEY }} + - name: Install test runner + if: matrix.test_runner == 'nextest' + uses: taiki-e/install-action@0758d235715de2f3551eacc980d9ae8fce9342c3 # v2.87.3 + with: + tool: cargo-nextest@0.9.143 + fallback: none - env: FEATURE_DOMAIN: ${{ matrix.domain }} FEATURE_PROFILES: ${{ matrix.profiles }} @@ -390,7 +396,7 @@ jobs: if-no-files-found: error rsa-asm: - name: RSA assembly + name: RSA Gate needs: plan runs-on: runs-on=${{ github.run_id }}/runner=linux-x64-ci timeout-minutes: 45 @@ -408,7 +414,7 @@ jobs: - run: scripts/test/test-rsa-linux-asm.sh supply-chain: - name: Supply chain + name: Supply Chain needs: plan runs-on: ubuntu-latest timeout-minutes: 45 diff --git a/scripts/README.md b/scripts/README.md index cdc35760..58390ae0 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -178,7 +178,7 @@ repository policy, so a workflow-only change does not start a second runner. Built-in Cargo work starts the single warm host-Rust job; `contracts.features` uses `.config/feature-matrix.json` to select algorithm or capability groups, resolves every affected compile profile from Cargo's feature graph, and packs -only those profiles into at most two compile and three runtime shards. +only those profiles into at most two compile shards and one job per selected runtime profile. Manifest, catalog, shared-surface, or unattributed inputs widen to the complete 59-compile/9-runtime contract. `targets.platforms` independently materializes only affected platform proof rows; built-in `dependency-policy` starts the diff --git a/scripts/check/feature-contracts.sh b/scripts/check/feature-contracts.sh index 560ec8e5..08d5383c 100755 --- a/scripts/check/feature-contracts.sh +++ b/scripts/check/feature-contracts.sh @@ -370,6 +370,12 @@ runtime_args() { local target=$2 local filter=$3 + if [[ "$target" == all ]] && command -v cargo-nextest >/dev/null 2>&1; then + RUNTIME_ARGS=(cargo nextest run --locked --workspace --no-default-features + --features "$feature_set" --config-file .config/nextest.toml -P commit) + return 0 + fi + RUNTIME_ARGS=(cargo test --locked --workspace --no-default-features --features "$feature_set") case "$target" in all) RUNTIME_ARGS+=(--lib --tests) ;; @@ -380,6 +386,19 @@ runtime_args() { return 0 } +runtime_test_runner() { + local profile_id=$1 + local case_entry case_profile case_target case_filter + for case_entry in "${RUNTIME_TEST_CASES[@]}"; do + IFS='|' read -r case_profile case_target case_filter <<<"$case_entry" + if [[ "$case_profile" == "$profile_id" && "$case_target" == all ]]; then + printf 'nextest\n' + return + fi + done + printf 'cargo\n' +} + run_runtime_contracts() { local total=${#RUNTIME_PROFILE_IDS[@]} local selected=0 started_at=$SECONDS @@ -462,7 +481,7 @@ print_matrix() { return 2 } - local id domain count shard index planned_id profiles separator="" + local id domain count shard index label planned_id profiles separator="" test_runner local -a planned=() if jq -e 'any(.include[]; .full == true)' <<<"$selected_rows" >/dev/null; then for id in "${COMPILE_PROFILE_IDS[@]}" "${RUNTIME_PROFILE_IDS_CANONICAL[@]}"; do @@ -552,8 +571,15 @@ print_matrix() { done fi [[ -n "$profiles" ]] || continue - printf '%s{"domain":"%s","shard":"%s/%s","profiles":"%s"}' \ - "$separator" "$domain" "$shard" "$count" "$profiles" + if [[ "$domain" == compile ]]; then + label="Compile $shard/$count" + test_runner=cargo + else + label="Runtime ${profiles#runtime.}" + test_runner=$(runtime_test_runner "${profiles#runtime.}") + fi + printf '%s{"domain":"%s","shard":"%s/%s","profiles":"%s","label":"%s","test_runner":"%s"}' \ + "$separator" "$domain" "$shard" "$count" "$profiles" "$label" "$test_runner" separator=, done done diff --git a/scripts/ci/feature-contracts-test.sh b/scripts/ci/feature-contracts-test.sh index 70fe3aed..c166e39f 100755 --- a/scripts/ci/feature-contracts-test.sh +++ b/scripts/ci/feature-contracts-test.sh @@ -78,6 +78,8 @@ if [[ "${1:-}" == metadata ]]; then fi EOF chmod +x "$fake_bin/cargo" +touch "$fake_bin/cargo-nextest" +chmod +x "$fake_bin/cargo-nextest" run_executor() { local output=$1 @@ -96,11 +98,14 @@ grep -Fq 'runtime (9 profiles)' "$list_output" || fail "runtime catalog count ch matrix_output="$TMP_ROOT/matrix.json" run_executor "$matrix_output" matrix jq -e ' - (.include | length) == 5 + (.include | length) == 11 and ([.include[].domain] | map(select(. == "compile")) | length) == 2 - and ([.include[].domain] | map(select(. == "runtime")) | length) == 3 + and ([.include[].domain] | map(select(. == "runtime")) | length) == 9 and all(.include[]; (.profiles | type == "string" and length > 0)) -' "$matrix_output" >/dev/null || fail "executor matrix is not the five bounded shards" + and all(.include[]; (.label | type == "string" and length > 0)) + and ([.include[] | select(.test_runner == "nextest")] | length) == 3 + and all(.include[]; .test_runner == "cargo" or .test_runner == "nextest") +' "$matrix_output" >/dev/null || fail "executor matrix does not name every bounded shard" compile_matrix_count=$(jq -r ' [.include[] | select(.domain == "compile") | .profiles | split(",")[]] | unique | length @@ -174,21 +179,21 @@ if grep -Fq 'cargo clean' "$command_log"; then fi : >"$command_log" -run_executor "$TMP_ROOT/runtime-1.out" runtime 1/3 -run_executor "$TMP_ROOT/runtime-2.out" runtime 2/3 -run_executor "$TMP_ROOT/runtime-3.out" runtime 3/3 -runtime_count=$(grep -c '^cargo test ' "$command_log") +for ((shard = 1; shard <= FEATURE_RUNTIME_SHARDS; shard++)); do + run_executor "$TMP_ROOT/runtime-$shard.out" runtime "$shard/$FEATURE_RUNTIME_SHARDS" +done +runtime_count=$(grep -Ec '^cargo (test|nextest run) ' "$command_log") expected_runtime_count=${#RUNTIME_TEST_CASES[@]} [[ "$runtime_count" -eq "$expected_runtime_count" ]] \ || fail "expected $expected_runtime_count focused runtime commands, found $runtime_count" -runtime_unique=$(grep '^cargo test ' "$command_log" | LC_ALL=C sort -u | wc -l | tr -d ' ') +runtime_unique=$(grep -E '^cargo (test|nextest run) ' "$command_log" | LC_ALL=C sort -u | wc -l | tr -d ' ') [[ "$runtime_unique" -eq "$expected_runtime_count" ]] \ || fail "runtime shards overlap or omit a test case" profile_count=$(cat "$TMP_ROOT"/runtime-*.out | grep -c '^ profile ') [[ "$profile_count" -eq 9 ]] || fail "runtime shards did not execute nine profiles exactly once" -full_count=$(grep '^cargo test ' "$command_log" | grep -c -- '--lib --tests') -[[ "$full_count" -eq 3 ]] || fail "expected three complete behavior baselines, found $full_count" +nextest_count=$(grep -c '^cargo nextest run ' "$command_log") +[[ "$nextest_count" -eq 3 ]] || fail "expected three parallel behavior baselines, found $nextest_count" grep '^cargo test ' "$command_log" \ | grep -F -- '--features std,full,serde --test serde_roundtrip' >/dev/null \ || fail "public Serde delta is not focused" diff --git a/scripts/lib/feature-profiles.sh b/scripts/lib/feature-profiles.sh index 642c81b4..98cdaa73 100644 --- a/scripts/lib/feature-profiles.sh +++ b/scripts/lib/feature-profiles.sh @@ -3,7 +3,6 @@ # Feature profile arrays are caller-visible outputs for sourced matrix scripts. FEATURE_COMPILE_SHARDS=2 -FEATURE_RUNTIME_SHARDS=3 # Unique resolved compile graphs. COMPILE_FEATURE_ALIASES retains named roots # that Cargo currently resolves identically to one canonical entry. The @@ -75,10 +74,9 @@ COMPILE_FEATURE_ALIASES=( "auth|alloc,auth" ) -# Runtime contracts are ordered so modulo-three sharding places one broad -# behavior baseline in each shard. Focused scopes exercise only the capability -# delta; `all` retains the complete library and integration suite where the -# feature changes the whole execution surface. +# Runtime contracts run one profile per CI job. Focused scopes exercise only +# the capability delta; `all` retains the complete library and integration +# suite where the feature changes the whole execution surface. RUNTIME_PROFILE_IDS=( "no-std-full" "std-full" @@ -103,6 +101,9 @@ RUNTIME_FEATURE_SETS=( "std,full,serde-secrets" ) +# One independently named CI job per runtime profile. +FEATURE_RUNTIME_SHARDS=${#RUNTIME_PROFILE_IDS[@]} + # profile|target|optional libtest substring. `lib` selects unit tests and `all` # selects the complete library/integration suite. Focused cases cover every # test gated by the named capability without rerunning unrelated vectors. From 29df1e16d6a25f2ecadef5bd67ce82253ad79a23 Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Tue, 1 Sep 2026 22:44:34 -0400 Subject: [PATCH 14/18] ci: keep Windows Arm64 on the cache cold path Cargo Rail Action v8.2.0 loses its installer outputs on hosted Windows ARM. Preserve compile coverage by disabling cache acceleration for that row until upstream output transport is proven. --- .config/target-matrix.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/target-matrix.json b/.config/target-matrix.json index a4de3ce2..1305130c 100644 --- a/.config/target-matrix.json +++ b/.config/target-matrix.json @@ -46,7 +46,7 @@ "components": "clippy", "timeout_minutes": 30, "verify_plan": false, - "cache": true, + "cache": false, "compile": "hosted", "runtime": "none", "performance": false, From 9d8928cdd8f0f59e0b4f935ca881cafc26726d60 Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Wed, 2 Sep 2026 00:51:50 -0400 Subject: [PATCH 15/18] test: accelerate cryptographic suites with light optimization Keep Miri and coverage at opt-level 0 so optimization cannot narrow their evidence. --- Cargo.toml | 3 +++ scripts/ci/assurance-planning-test.sh | 13 +++++++++++++ scripts/test/test-coverage.sh | 1 + scripts/test/test-miri.sh | 1 + 4 files changed, 18 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 2f4685c5..c1a07758 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -439,6 +439,9 @@ lto = false incremental = false # Preserve reproducible full codegen and Cargo Rail compiler-result reuse. codegen-units = 256 +[profile.test] +opt-level = 1 + [profile.release] opt-level = 3 debug = false diff --git a/scripts/ci/assurance-planning-test.sh b/scripts/ci/assurance-planning-test.sh index b941dfcb..a4957781 100755 --- a/scripts/ci/assurance-planning-test.sh +++ b/scripts/ci/assurance-planning-test.sh @@ -260,6 +260,19 @@ raise "Miri job bypasses selected rows" unless miri_runs.include?('scripts/test/ raise "Fuzz job bypasses its plan decision" unless fuzz_runs.include?("scripts/ci/require-work.sh assurance.fuzz") raise "Fuzz job bypasses selected rows" unless fuzz_runs.include?('scripts/test/fuzz-contracts.sh selected "$FUZZ_ROWS"') +manifest = File.read(File.join(root, "Cargo.toml")) +unless manifest.match?(/^\[profile\.test\]\nopt-level = 1$/) + raise "ordinary test execution lost its optimized Cargo profile" +end +miri_script = File.read(File.join(root, "scripts/test/test-miri.sh")) +unless miri_script.include?("export CARGO_PROFILE_TEST_OPT_LEVEL=0") + raise "Miri no longer forces unoptimized test MIR" +end +coverage_script = File.read(File.join(root, "scripts/test/test-coverage.sh")) +unless coverage_script.include?("export CARGO_PROFILE_TEST_OPT_LEVEL=0") + raise "coverage no longer forces unoptimized test code" +end + core_runs = jobs.fetch("core").fetch("steps").map { |step| step["run"] }.compact raise "CI core lost minimum-feature examples" unless core_runs.include?("scripts/test/test-examples.sh") msrv = jobs.fetch("msrv") diff --git a/scripts/test/test-coverage.sh b/scripts/test/test-coverage.sh index 56be41e3..9f3cb7ab 100755 --- a/scripts/test/test-coverage.sh +++ b/scripts/test/test-coverage.sh @@ -50,6 +50,7 @@ if [[ $# -gt 1 ]]; then fi apply_ci_resource_profile +export CARGO_PROFILE_TEST_OPT_LEVEL=0 export RSCRYPTO_TEST_MODE=${RSCRYPTO_TEST_MODE:-${CARGO_RAIL_TEST_MODE:-local}} export CARGO_RAIL_TEST_MODE=${CARGO_RAIL_TEST_MODE:-$RSCRYPTO_TEST_MODE} diff --git a/scripts/test/test-miri.sh b/scripts/test/test-miri.sh index cc055040..9bf77e87 100755 --- a/scripts/test/test-miri.sh +++ b/scripts/test/test-miri.sh @@ -35,6 +35,7 @@ echo "" activate_nightly_toolchain export CARGO_RAIL_CACHE=off +export CARGO_PROFILE_TEST_OPT_LEVEL=0 # Miri cannot execute SIMD/CLMUL kernels directly. Force CRC families onto their # portable tiers so the lane spends time validating real pointer/length logic From 72d5f244a88cce8a1b26f93e2c09f5b7d25e2108 Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Wed, 2 Sep 2026 01:39:01 -0400 Subject: [PATCH 16/18] checksum: avoid function address identity in dispatch test --- src/checksum/kernel_table.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/checksum/kernel_table.rs b/src/checksum/kernel_table.rs index c8dce155..fc2302b8 100644 --- a/src/checksum/kernel_table.rs +++ b/src/checksum/kernel_table.rs @@ -3068,10 +3068,6 @@ mod tests { assert_eq!(table.select_names(4096).crc64_xz_name, "x86_64/vpclmul-2way"); assert_eq!(table.select_names(4097).crc64_xz_name, "x86_64/vpclmul-4x512"); assert_eq!(table.select_names(4097).crc64_nvme_name, "x86_64/vpclmul-4way"); - assert!(core::ptr::fn_addr_eq( - table.select_fns(4097).crc64_xz, - crate::checksum::crc64::kernels::x86_64::XZ_VPCLMUL_4X512 - )); } #[test] From 086f9a7ed17fba21b84e2121992f1bcb41e1623e Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Wed, 2 Sep 2026 01:39:10 -0400 Subject: [PATCH 17/18] ci: adapt fuzz concurrency to runner capacity --- .github/workflows/ci.yaml | 1 - .github/workflows/qualification.yaml | 1 - scripts/test/test-fuzz-scheduler-test.sh | 22 ++++++++++++++++++++- scripts/test/test-fuzz.sh | 25 ++++++++++++++++++++++-- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ae085763..8fb41d8d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -354,7 +354,6 @@ jobs: - env: FUZZ_ROWS: ${{ needs.plan.outputs.fuzz-rows }} RSCRYPTO_FUZZ_JOBS: 1 - RSCRYPTO_FUZZ_TARGET_CONCURRENCY: 2 run: scripts/test/fuzz-contracts.sh selected "$FUZZ_ROWS" supply-chain: diff --git a/.github/workflows/qualification.yaml b/.github/workflows/qualification.yaml index 02cca86c..2814c5ba 100644 --- a/.github/workflows/qualification.yaml +++ b/.github/workflows/qualification.yaml @@ -324,7 +324,6 @@ jobs: env: RSCRYPTO_FUZZ_DURATION_SECS: 60 RSCRYPTO_FUZZ_JOBS: 1 - RSCRYPTO_FUZZ_TARGET_CONCURRENCY: 2 run: scripts/test/test-fuzz.sh --all - name: Replay corpora under ASan run: scripts/test/test-fuzz-asan.sh --all diff --git a/scripts/test/test-fuzz-scheduler-test.sh b/scripts/test/test-fuzz-scheduler-test.sh index 24ee7b51..18dded6e 100755 --- a/scripts/test/test-fuzz-scheduler-test.sh +++ b/scripts/test/test-fuzz-scheduler-test.sh @@ -38,6 +38,13 @@ set -euo pipefail echo "host: x86_64-unknown-linux-gnu" EOF +cat >"$BIN/getconf" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +[[ "${1:-}" == "_NPROCESSORS_ONLN" ]] || exit 1 +printf '%s\n' "${RSCRYPTO_TEST_PROCESSORS:-8}" +EOF + cat >"$BIN/cargo" <<'EOF' #!/usr/bin/env bash set -euo pipefail @@ -145,7 +152,7 @@ case "$subcommand" in esac EOF -chmod +x "$BIN/cargo" "$BIN/rustc" +chmod +x "$BIN/cargo" "$BIN/getconf" "$BIN/rustc" status=0 env \ @@ -210,6 +217,19 @@ done grep -Fq 'Summary: 2 targets, 0 failed' "$SELECTED_CAPTURE/output" \ || fail "exact selection did not aggregate only the requested targets" +CONSTRAINED_CAPTURE="$TMP_ROOT/constrained-capture" +mkdir -p "$CONSTRAINED_CAPTURE" +env \ + PATH="$BIN:$PATH" \ + RSCRYPTO_FUZZ_CAPTURE="$CONSTRAINED_CAPTURE" \ + RSCRYPTO_FUZZ_DURATION_SECS=1 \ + RSCRYPTO_TEST_PROCESSORS=2 \ + bash "$FIXTURE/scripts/test/test-fuzz.sh" --targets alpha,delta \ + >"$CONSTRAINED_CAPTURE/output" 2>&1 \ + || fail "automatic constrained-runner scheduling failed" +[[ $(<"$CONSTRAINED_CAPTURE/maximum") == "1" ]] \ + || fail "automatic scheduling oversubscribed a two-processor runner" + LONG_LIST_CAPTURE="$TMP_ROOT/long-list-capture" mkdir -p "$LONG_LIST_CAPTURE" env \ diff --git a/scripts/test/test-fuzz.sh b/scripts/test/test-fuzz.sh index 269e4c0d..968a77e9 100755 --- a/scripts/test/test-fuzz.sh +++ b/scripts/test/test-fuzz.sh @@ -38,7 +38,28 @@ TIMEOUT=${RSCRYPTO_FUZZ_TIMEOUT_SECS:-30} RSS_LIMIT=${RSCRYPTO_FUZZ_RSS_LIMIT_MB:-2048} MAX_LEN=${RSCRYPTO_FUZZ_MAX_LEN:-65536} JOBS=${RSCRYPTO_FUZZ_JOBS:-1} -TARGET_CONCURRENCY=${RSCRYPTO_FUZZ_TARGET_CONCURRENCY:-2} + +default_target_concurrency() { + local processor_count="" + + if command -v getconf >/dev/null 2>&1; then + processor_count=$(getconf _NPROCESSORS_ONLN 2>/dev/null || true) + fi + if [[ ! "$processor_count" =~ ^[1-9][0-9]*$ ]] && command -v sysctl >/dev/null 2>&1; then + processor_count=$(sysctl -n hw.logicalcpu 2>/dev/null || true) + fi + if [[ ! "$processor_count" =~ ^[1-9][0-9]*$ ]]; then + processor_count=${NUMBER_OF_PROCESSORS:-} + fi + + if [[ "$processor_count" =~ ^[1-9][0-9]*$ ]] && ((processor_count >= 4)); then + printf '2' + else + printf '1' + fi +} + +TARGET_CONCURRENCY=${RSCRYPTO_FUZZ_TARGET_CONCURRENCY:-$(default_target_concurrency)} # Skip if commit mode (fuzzing takes too long) if [ "$RSCRYPTO_TEST_MODE" = "commit" ]; then @@ -67,7 +88,7 @@ show_help() { echo " RSCRYPTO_FUZZ_RSS_LIMIT_MB Memory limit in MB (default: 2048)" echo " RSCRYPTO_FUZZ_MAX_LEN Max input length (default: 65536)" echo " RSCRYPTO_FUZZ_JOBS LibFuzzer workers per target (default: 1)" - echo " RSCRYPTO_FUZZ_TARGET_CONCURRENCY Independent targets to run concurrently (default: 2)" + echo " RSCRYPTO_FUZZ_TARGET_CONCURRENCY Independent targets to run concurrently (default: 1 below 4 CPUs, otherwise 2)" echo " RSCRYPTO_FUZZ_TARGET_DIR Shared cargo target dir (default: fuzz/target)" } From d95561d0ea964fa97491ebcb3f959261366016d2 Mon Sep 17 00:00:00 2001 From: LoadingALIAS Date: Wed, 2 Sep 2026 11:31:51 -0400 Subject: [PATCH 18/18] ci: enforce every declared feature build Gate Linux AEAD helpers and FFI declarations to their owning features, then require an independent compile contract for every Cargo feature. --- .changes/keep-standalone-aead-feature-c4b6.md | 5 ++ scripts/check/feature-contracts.sh | 54 ++++++++++++++++++- scripts/ci/feature-contracts-test.sh | 14 +++-- scripts/lib/feature-profiles.sh | 24 +++++++++ src/aead/aes/x86_64/asm.rs | 8 ++- src/aead/poly1305.rs | 14 +++-- 6 files changed, 105 insertions(+), 14 deletions(-) create mode 100644 .changes/keep-standalone-aead-feature-c4b6.md diff --git a/.changes/keep-standalone-aead-feature-c4b6.md b/.changes/keep-standalone-aead-feature-c4b6.md new file mode 100644 index 00000000..6dc30ccc --- /dev/null +++ b/.changes/keep-standalone-aead-feature-c4b6.md @@ -0,0 +1,5 @@ +--- +"rscrypto" = "patch" +--- + +Keep standalone AEAD feature builds lint-clean on Linux. diff --git a/scripts/check/feature-contracts.sh b/scripts/check/feature-contracts.sh index 08d5383c..ed2c6429 100755 --- a/scripts/check/feature-contracts.sh +++ b/scripts/check/feature-contracts.sh @@ -30,6 +30,18 @@ compile_profile_id() { fi } +has_independent_compile_contract() { + local feature=$1 + local feature_set alias + for feature_set in "${COMPILE_FEATURE_SETS[@]}"; do + [[ "$feature_set" == "$feature" ]] && return 0 + done + for alias in "${COMPILE_FEATURE_ALIASES[@]}"; do + [[ "${alias#*|}" == "$feature" ]] && return 0 + done + return 1 +} + profile_is_known() { local needle=$1 local item @@ -58,7 +70,7 @@ validate_profiles() { return 2 } - local i j + local i j feature_set alias_entry canonical alias canonical_known for i in "${!COMPILE_FEATURE_SETS[@]}"; do for ((j = i + 1; j < ${#COMPILE_FEATURE_SETS[@]}; j++)); do [[ "${COMPILE_FEATURE_SETS[$i]}" != "${COMPILE_FEATURE_SETS[$j]}" ]] || { @@ -67,6 +79,33 @@ validate_profiles() { } done done + for i in "${!COMPILE_FEATURE_ALIASES[@]}"; do + alias_entry=${COMPILE_FEATURE_ALIASES[$i]} + canonical=${alias_entry%%|*} + alias=${alias_entry#*|} + [[ -n "$canonical" && -n "$alias" && "$canonical" != "$alias_entry" ]] || { + echo "malformed compile alias: $alias_entry" >&2 + return 2 + } + canonical_known=false + for feature_set in "${COMPILE_FEATURE_SETS[@]}"; do + [[ "$feature_set" == "$canonical" ]] && canonical_known=true + [[ "$feature_set" != "$alias" ]] || { + echo "compile alias is also a unique graph: $alias" >&2 + return 2 + } + done + [[ "$canonical_known" == true ]] || { + echo "compile alias names unknown canonical graph: $canonical" >&2 + return 2 + } + for ((j = i + 1; j < ${#COMPILE_FEATURE_ALIASES[@]}; j++)); do + [[ "$alias" != "${COMPILE_FEATURE_ALIASES[$j]#*|}" ]] || { + echo "duplicate compile alias: $alias" >&2 + return 2 + } + done + done for i in "${!RUNTIME_PROFILE_IDS[@]}"; do [[ -n "${RUNTIME_PROFILE_IDS[$i]}" \ && -n "${RUNTIME_FEATURE_SETS[$i]}" ]] || { @@ -188,11 +227,22 @@ validate_variant_catalog() { echo "feature variant catalog names unknown Cargo feature $feature_root" >&2 return 2 } + has_independent_compile_contract "$feature_root" || { + echo "feature variant catalog root lacks an independent compile contract: $feature_root" >&2 + return 2 + } done < <(jq -r ' .variants[].dimensions.feature_roots | select(length > 0) | split(",")[] ' "$FEATURE_CATALOG") + + while IFS= read -r feature_root; do + has_independent_compile_contract "$feature_root" || { + echo "Cargo feature lacks an independent compile contract: $feature_root" >&2 + return 2 + } + done < <(jq -r 'keys[]' <<<"$FEATURE_GRAPH") } parse_shard() { @@ -611,12 +661,12 @@ case "$domain" in esac validate_profiles +validate_variant_catalog if [[ "$domain" == list ]]; then list_contracts exit 0 fi if [[ "$domain" == matrix ]]; then - validate_variant_catalog print_matrix exit 0 fi diff --git a/scripts/ci/feature-contracts-test.sh b/scripts/ci/feature-contracts-test.sh index c166e39f..5d87e734 100755 --- a/scripts/ci/feature-contracts-test.sh +++ b/scripts/ci/feature-contracts-test.sh @@ -90,7 +90,7 @@ run_executor() { list_output="$TMP_ROOT/list.out" run_executor "$list_output" list -grep -Fq 'compile (59 unique graphs, 60 named contracts)' "$list_output" \ +grep -Fq 'compile (82 unique graphs, 84 named contracts)' "$list_output" \ || fail "compile catalog counts changed" grep -Fq 'alias: alloc,auth' "$list_output" || fail "compile alias disappeared" grep -Fq 'runtime (9 profiles)' "$list_output" || fail "runtime catalog count changed" @@ -110,7 +110,7 @@ compile_matrix_count=$(jq -r ' [.include[] | select(.domain == "compile") | .profiles | split(",")[]] | unique | length ' "$matrix_output") -[[ "$compile_matrix_count" -eq 59 ]] || fail "full matrix omitted compile profiles" +[[ "$compile_matrix_count" -eq 82 ]] || fail "full matrix omitted compile profiles" runtime_matrix_count=$(jq -r ' [.include[] | select(.domain == "runtime") | .profiles | split(",")[]] | unique | length @@ -169,11 +169,15 @@ fi run_executor "$TMP_ROOT/compile-1.out" compile 1/2 run_executor "$TMP_ROOT/compile-2.out" compile 2/2 compile_count=$(grep -c '^cargo check ' "$command_log") -[[ "$compile_count" -eq 59 ]] || fail "expected 59 compile commands, found $compile_count" +[[ "$compile_count" -eq 82 ]] || fail "expected 82 compile commands, found $compile_count" compile_unique=$(grep '^cargo check ' "$command_log" | LC_ALL=C sort -u | wc -l | tr -d ' ') -[[ "$compile_unique" -eq 59 ]] || fail "compile shards overlap or omit a unique graph" +[[ "$compile_unique" -eq 82 ]] || fail "compile shards overlap or omit a unique graph" metadata_count=$(grep -c '^cargo metadata ' "$command_log") -[[ "$metadata_count" -eq 2 ]] || fail "the one compile alias was not verified exactly once" +[[ "$metadata_count" -eq 4 ]] || fail "the two compile aliases were not verified exactly once" +grep -F -- '--features aes-gcm-siv' "$command_log" >/dev/null \ + || fail "AES-GCM-SIV lost its independent compile contract" +grep -F -- '--features xchacha20poly1305' "$command_log" >/dev/null \ + || fail "XChaCha20-Poly1305 lost its independent compile contract" if grep -Fq 'cargo clean' "$command_log"; then fail "feature execution still deletes Cargo artifacts" fi diff --git a/scripts/lib/feature-profiles.sh b/scripts/lib/feature-profiles.sh index 98cdaa73..2dbc945c 100644 --- a/scripts/lib/feature-profiles.sh +++ b/scripts/lib/feature-profiles.sh @@ -9,12 +9,16 @@ FEATURE_COMPILE_SHARDS=2 # executor verifies that equivalence before sharing the proof. COMPILE_FEATURE_SETS=( "" + "default" "alloc" + "std" "crc16" "crc24" "crc32" "crc64" "diag" + "getrandom" + "portable-only" "crc32,diag" "crc64,diag" "alloc,crc32" @@ -23,25 +27,44 @@ COMPILE_FEATURE_SETS=( "websocket-sha1" "blake2b" "blake2s" + "blake3" + "ascon-hash" "xxh3" + "rapidhash" "hmac" "hmac-sha3" "kmac" "hkdf" "pbkdf2" "poly1305" + "ecdsa-p256" + "ecdsa-p384" + "ecdsa" + "ed25519" "rsa" "rsa,getrandom" "x25519" + "ml-kem" + "argon2" + "scrypt" + "phc-strings" + "password-hashing" + "aes-gcm" + "aes-gcm-siv" "chacha20poly1305" + "xchacha20poly1305" "aes-siv" "aegis256" "ascon-aead" + "serde" + "serde-secrets" "checksums" "alloc,checksums" "std,checksums" "std,checksums,diag" "hashes" + "crypto-hashes" + "fast-hashes" "alloc,hashes" "std,hashes" "std,hashes,diag" @@ -72,6 +95,7 @@ COMPILE_FEATURE_SETS=( # canonical|alias. Keep one entry per retained named contract. COMPILE_FEATURE_ALIASES=( "auth|alloc,auth" + "std,parallel|parallel" ) # Runtime contracts run one profile per CI job. Focused scopes exercise only diff --git a/src/aead/aes/x86_64/asm.rs b/src/aead/aes/x86_64/asm.rs index 24a09024..66ccad7c 100644 --- a/src/aead/aes/x86_64/asm.rs +++ b/src/aead/aes/x86_64/asm.rs @@ -5,6 +5,7 @@ use core::arch::global_asm; global_asm!(include_str!("asm/rscrypto_aes_gcm_x86_64_linux.s")); #[repr(C)] +#[cfg(feature = "aes-gcm")] pub(super) struct AesGcmX86State { acc_lo: u64, acc_hi: u64, @@ -13,6 +14,7 @@ pub(super) struct AesGcmX86State { pub(super) processed: usize, } +#[cfg(feature = "aes-gcm")] impl AesGcmX86State { #[inline] pub(super) fn new(acc: u128, ctr: u32) -> Self { @@ -31,6 +33,7 @@ impl AesGcmX86State { } } +#[cfg(feature = "aes-gcm")] unsafe extern "C" { pub(super) fn rscrypto_aes128_gcm_seal_16x_vaes512_x86_64_linux( round_keys: *const u8, @@ -139,8 +142,10 @@ unsafe extern "C" { h_powers_rev_128: *const u128, state: *mut AesGcmX86State, ); +} - #[cfg(feature = "aes-gcm-siv")] +#[cfg(feature = "aes-gcm-siv")] +unsafe extern "C" { pub(super) fn rscrypto_aes128_gcmsiv_ctr_16x_vaes512_x86_64_linux( round_keys: *const u8, initial_counter: *const u8, @@ -148,7 +153,6 @@ unsafe extern "C" { len: usize, ) -> usize; - #[cfg(feature = "aes-gcm-siv")] pub(super) fn rscrypto_aes256_gcmsiv_ctr_16x_vaes512_x86_64_linux( round_keys: *const u8, initial_counter: *const u8, diff --git a/src/aead/poly1305.rs b/src/aead/poly1305.rs index 08e0345b..21fe8d41 100644 --- a/src/aead/poly1305.rs +++ b/src/aead/poly1305.rs @@ -993,8 +993,10 @@ pub(crate) fn authenticate_aead( #[cfg(any( test, - target_arch = "x86_64", - all(target_arch = "powerpc64", target_endian = "little") + all( + feature = "chacha20poly1305", + any(target_arch = "x86_64", all(target_arch = "powerpc64", target_endian = "little")) + ) ))] fn authenticate_aead_portable_blocks( aad: &[u8], @@ -1016,14 +1018,16 @@ fn authenticate_aead_portable_blocks( #[cfg(any( test, - target_arch = "x86_64", - all(target_arch = "powerpc64", target_endian = "little") + all( + feature = "chacha20poly1305", + any(target_arch = "x86_64", all(target_arch = "powerpc64", target_endian = "little")) + ) ))] pub(crate) fn authenticate_aead_empty_text_portable(aad: &[u8], key: &[u8; 32]) -> [u8; 16] { authenticate_aead_portable_blocks(aad, &[], key, super::AeadByteLengths::from_usize(aad.len(), 0)) } -#[cfg(all(target_arch = "powerpc64", target_endian = "little"))] +#[cfg(all(feature = "chacha20poly1305", target_arch = "powerpc64", target_endian = "little"))] pub(crate) fn authenticate_aead_short_text_portable(aad: &[u8], ciphertext: &[u8], key: &[u8; 32]) -> [u8; 16] { authenticate_aead_portable_blocks( aad,