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/.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..112e83dc 100644 --- a/.config/ci-tool-archives.tsv +++ b/.config/ci-tool-archives.tsv @@ -1,10 +1,11 @@ # 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 -codecov v11.3.1 linux x86_64 codecov https://cli.codecov.io/v11.3.1/linux/codecov ca1d64196d2d34771084afe76ea657d581bf628e31d993ff8e52ea09cc88a56d +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 +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/.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..590c65e0 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,230 @@ 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."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" -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..1305130c 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": false, + "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": "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..9761536f --- /dev/null +++ b/.github/actions/rust/action.yaml @@ -0,0 +1,114 @@ +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: + 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 }} + +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" + + - 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 + 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: | + [[ -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..8fb41d8d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,168 +8,470 @@ 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 + 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 }} + examples: ${{ steps.select.outputs.examples }} + 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 }} + msrv: ${{ steps.select.outputs.msrv }} + 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 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)" + echo "supply-chain=$(required_any dependency-policy dependencies.auxiliary)" + echo "tests=$(required_any cargo.test)" + } >>"$GITHUB_OUTPUT" + + - name: Install actionlint and ripgrep + if: steps.select.outputs.actions == 'true' + run: scripts/ci/install-actions-policy-tools.sh + + - name: Install Just and Zizmor + if: steps.select.outputs.actions == 'true' + uses: taiki-e/install-action@0758d235715de2f3551eacc980d9ae8fce9342c3 # v2.87.3 + with: + tool: just@1.58.0,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: Core + 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 }} + - id: rust + 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@0758d235715de2f3551eacc980d9ae8fce9342c3 # v2.87.3 + 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 + - 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: Feature Contracts (${{ matrix.label }}) + 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 + - 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 }} + - 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 }} + 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 - 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 }} + 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 }} + - id: rust + 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 + - name: Report compiler cache + if: ${{ !cancelled() && steps.rust.outputs.cache-enabled == 'true' }} + run: scripts/ci/report-cache.sh + + 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 + 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 Gate + 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 Gate + 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 + - 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: [rail-plan, suite, cache-seed, dependency-smoke] - if: ${{ !cancelled() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }} + 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 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 }} + 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 }} + 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" \ + "$MSRV_REQUIRED:$MSRV_RESULT:msrv" \ + "$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..2814c5ba --- /dev/null +++ b/.github/workflows/qualification.yaml @@ -0,0 +1,528 @@ +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: Core + 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 }} + - id: rust + 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@0758d235715de2f3551eacc980d9ae8fce9342c3 # v2.87.3 + 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 + - 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: Feature Contracts (${{ matrix.label }}) + 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 + - 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' }} + 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 }} + 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 }} + 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 }} + - id: rust + 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 + - name: Report compiler cache + if: ${{ !cancelled() && steps.rust.outputs.cache-enabled == 'true' }} + run: scripts/ci/report-cache.sh + + 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 + 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 Gate + 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@0758d235715de2f3551eacc980d9ae8fce9342c3 # v2.87.3 + 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, msrv, 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 }}" \ + "msrv=${{ needs.msrv.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/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/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..00200a20 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,38 @@ 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 +# 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 }} + +# 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 +111,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 +123,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 +197,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..58390ae0 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -8,67 +8,60 @@ 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/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 | | `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`, `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` | +| `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/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 | +| `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 +74,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 +121,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/install-actions-policy-tools.sh`, `ci/nostd-wasm-suite.sh`, `just check-actions` | ## Python boundary @@ -148,24 +137,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 +145,113 @@ 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 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 +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 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 +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. 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 +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`. +`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..0e65205e --- /dev/null +++ b/scripts/check/affected.sh @@ -0,0 +1,73 @@ +#!/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" + + 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" + 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..ed725e06 100755 --- a/scripts/check/check-all.sh +++ b/scripts/check/check-all.sh @@ -1,181 +1,60 @@ #!/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/msrv.sh" +"$SCRIPT_DIR/feature-contracts.sh" all +"$SCRIPT_DIR/../test/test-examples.sh" "$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..ed2c6429 --- /dev/null +++ b/scripts/check/feature-contracts.sh @@ -0,0 +1,690 @@ +#!/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 +} + +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 + 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 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]}" ]] || { + echo "duplicate compile feature root: ${COMPILE_FEATURE_SETS[$i]:-}" >&2 + return 2 + } + 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]}" ]] || { + 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 + } + 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() { + 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 + + 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) ;; + lib) RUNTIME_ARGS+=(--lib) ;; + *) RUNTIME_ARGS+=(--test "$target") ;; + esac + [[ -n "$filter" ]] && RUNTIME_ARGS+=(-- "$filter") + 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 + 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 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 + 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 + 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 + 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 +validate_variant_catalog +if [[ "$domain" == list ]]; then + list_contracts + exit 0 +fi +if [[ "$domain" == matrix ]]; then + 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/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 new file mode 100755 index 00000000..6f7dd55f --- /dev/null +++ b/scripts/check/policy.sh @@ -0,0 +1,130 @@ +#!/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 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 + 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..a4957781 --- /dev/null +++ b/scripts/ci/assurance-planning-test.sh @@ -0,0 +1,426 @@ +#!/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_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 + 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_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=$( + 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.cargo-graph \ + contracts.examples \ + 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_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" +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") +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 + +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| + 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 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$/) +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"') + +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") +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") +%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_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') + +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 +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") +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 msrv 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..9762abe8 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,15 +63,41 @@ 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 +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/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 @@ -109,7 +136,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 +174,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..828aa9fb 100755 --- a/scripts/ci/emit-manual-matrix-test.sh +++ b/scripts/ci/emit-manual-matrix-test.sh @@ -3,37 +3,33 @@ 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 ' 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 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 == 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 + env -u GITHUB_OUTPUT 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..5d87e734 --- /dev/null +++ b/scripts/ci/feature-contracts-test.sh @@ -0,0 +1,232 @@ +#!/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 +# 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 + exit 1 +} + +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 + -u RAIL_PLAN_IDENTITY -u RAIL_PLAN_HEAD_COMMIT +) +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" +touch "$fake_bin/cargo-nextest" +chmod +x "$fake_bin/cargo-nextest" + +run_executor() { + local output=$1 + shift + "${clean_plan_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 (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" + +matrix_output="$TMP_ROOT/matrix.json" +run_executor "$matrix_output" matrix +jq -e ' + (.include | length) == 11 + and ([.include[].domain] | map(select(. == "compile")) | length) == 2 + and ([.include[].domain] | map(select(. == "runtime")) | length) == 9 + and all(.include[]; (.profiles | type == "string" and length > 0)) + 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 +' "$matrix_output") +[[ "$compile_matrix_count" -eq 82 ]] || 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" +"${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 ' + ([.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 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 82 ]] || fail "compile shards overlap or omit a unique graph" +metadata_count=$(grep -c '^cargo metadata ' "$command_log") +[[ "$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 + +: >"$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 -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" + +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" +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 "${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 +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-actions-policy-tools.sh b/scripts/ci/install-actions-policy-tools.sh new file mode 100755 index 00000000..8d7a6a40 --- /dev/null +++ b/scripts/ci/install-actions-policy-tools.sh @@ -0,0 +1,42 @@ +#!/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_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 +} + +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..af92d8a7 100755 --- a/scripts/ci/install-tools.sh +++ b/scripts/ci/install-tools.sh @@ -1,31 +1,19 @@ #!/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_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) @@ -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..670e7902 --- /dev/null +++ b/scripts/ci/native-platform.sh @@ -0,0 +1,117 @@ +#!/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 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 ]] ;; + 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..71cfe27e 100755 --- a/scripts/ci/remote-cache-recipes-test.sh +++ b/scripts/ci/remote-cache-recipes-test.sh @@ -17,10 +17,13 @@ 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 [[ "$*" == "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 -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 +36,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 +49,35 @@ 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" +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 + 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/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/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..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 | codecov) ;; + 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.]*$ ]] \ @@ -102,16 +102,14 @@ ci_tool_resolve() { } ci_tool_validate_manifest() { - local tool - for tool in wasmtime wasm-tools codecov; do + local tool expected_version version + for tool in actionlint ripgrep wasmtime wasm-tools; do + local expected_platforms 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) - ;; + 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 for platform in "${expected_platforms[@]}"; do @@ -124,6 +122,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 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..2dbc945c 100644 --- a/scripts/lib/feature-profiles.sh +++ b/scripts/lib/feature-profiles.sh @@ -2,16 +2,23 @@ # 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 + +# 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=( "" + "default" "alloc" + "std" "crc16" "crc24" "crc32" "crc64" "diag" + "getrandom" + "portable-only" "crc32,diag" "crc64,diag" "alloc,crc32" @@ -20,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" @@ -47,7 +73,6 @@ COMPILE_FEATURE_SETS=( "signatures" "key-exchange" "auth" - "alloc,auth" "std,auth" "std,password-hashing,getrandom" "aead" @@ -67,18 +92,79 @@ 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" + "std,parallel|parallel" +) + +# 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" + "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" +) + +# 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. +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/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/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..4d7dee1e 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,28 +27,63 @@ 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)" + 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 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-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-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..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 @@ -94,6 +101,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" @@ -140,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 \ @@ -182,4 +194,66 @@ 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" + +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 \ + 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" \ + 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..968a77e9 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,14 +30,36 @@ 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} -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} -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 @@ -53,6 +76,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" @@ -60,11 +84,11 @@ 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)" - 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)" } @@ -226,38 +250,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 +287,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 +306,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 +319,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 +425,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 +468,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-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 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/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, 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] 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 {