From 32048048ac75ad818bf2d354d733330003aef6ec Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 17 May 2026 19:37:31 -0600 Subject: [PATCH 01/12] Harden dependency checks --- .github/dependabot.yml | 16 + .github/workflows/cuda-build-check.yml | 13 +- .../workflows/dependency-integrity-check.yml | 45 +++ .github/workflows/dependency-review.yml | 75 +++++ .github/workflows/go-test.yml | 7 +- .github/workflows/go-version-consistency.yml | 3 + .github/workflows/julia-release.yml | 19 +- .github/workflows/julia-test.yml | 7 +- .github/workflows/julia-update-hash.yml | 3 + .../workflows/julia-version-consistency.yml | 3 + .github/workflows/python-release.yml | 27 +- .github/workflows/python-test.yml | 7 +- .../workflows/python-version-consistency.yml | 3 + .github/workflows/rust-test.yml | 21 +- .../workflows/rust-version-consistency.yml | 3 + .github/workflows/selene-plugins.yml | 11 +- .github/workflows/test-docs-examples.yml | 13 +- .pre-commit-config.yaml | 12 + Cargo.lock | 18 +- Cargo.toml | 2 +- Justfile | 77 ++--- crates/pecos-cli/src/cli/cuda_cmd.rs | 8 +- crates/pecos-cli/src/cli/python_cmd.rs | 2 +- crates/pecos-cli/src/cli/rust_cmd.rs | 14 +- crates/pecos-cli/src/cli/setup_cmd.rs | 4 +- crates/pecos-qis/Cargo.toml | 4 +- scripts/dependency-integrity-check.sh | 281 ++++++++++++++++++ scripts/native_bench/bench_pecos/Cargo.lock | 90 +++++- scripts/native_bench/run.sh | 2 +- scripts/native_bench/run_gpu.sh | 2 +- scripts/run.sh | 26 +- scripts/test_rebuild_edge_cases.sh | 12 +- scripts/test_rebuild_system.sh | 8 +- 33 files changed, 705 insertions(+), 133 deletions(-) create mode 100644 .github/workflows/dependency-integrity-check.yml create mode 100644 .github/workflows/dependency-review.yml create mode 100755 scripts/dependency-integrity-check.sh diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7c7821c86..1d84933c0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -19,6 +19,22 @@ updates: - dependency-name: "*" update-types: ["version-update:semver-patch", "version-update:semver-minor", "version-update:semver-major"] + # Rust (Cargo) - native benchmark security updates only + - package-ecosystem: "cargo" + directory: "/scripts/native_bench/bench_pecos" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + groups: + native-bench-rust-security: + applies-to: security-updates + patterns: ["*"] + allow: + - dependency-type: "all" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch", "version-update:semver-minor", "version-update:semver-major"] + # Python (pip) - security updates only - package-ecosystem: "pip" directory: "/" diff --git a/.github/workflows/cuda-build-check.yml b/.github/workflows/cuda-build-check.yml index 66069b2f5..1f47eccf1 100644 --- a/.github/workflows/cuda-build-check.yml +++ b/.github/workflows/cuda-build-check.yml @@ -4,6 +4,9 @@ name: CUDA Build Check +permissions: + contents: read + env: RUSTFLAGS: -C debuginfo=0 RUST_BACKTRACE: 1 @@ -68,28 +71,28 @@ jobs: - name: Check pecos-cuquantum-sys compiles (stub mode) run: | echo "Building pecos-cuquantum-sys (will use stubs since cuQuantum SDK is not installed)..." - cargo check -p pecos-cuquantum-sys + cargo check --locked -p pecos-cuquantum-sys - name: Check pecos-cuquantum compiles (stub mode) run: | echo "Building pecos-cuquantum (will use stubs since cuQuantum SDK is not installed)..." - cargo check -p pecos-cuquantum + cargo check --locked -p pecos-cuquantum - name: Check pecos-rslib-cuda compiles (stub mode) run: | echo "Building pecos-rslib-cuda Python bindings..." - cargo check -p pecos-rslib-cuda + cargo check --locked -p pecos-rslib-cuda - name: Run clippy on CUDA crates run: | echo "Running clippy on CUDA crates..." - cargo clippy -p pecos-cuquantum-sys -p pecos-cuquantum -p pecos-rslib-cuda -- -D warnings + cargo clippy --locked -p pecos-cuquantum-sys -p pecos-cuquantum -p pecos-rslib-cuda -- -D warnings - name: Verify stub mode detection run: | echo "Verifying that build correctly detects stub mode..." # The crates should compile but is_cuquantum_available() should return false - cargo build -p pecos-cuquantum --release + cargo build --locked -p pecos-cuquantum --release # Note: We can't actually run the binary to test is_cuquantum_available() # because it requires the cuQuantum runtime, but the build succeeding # in stub mode means the detection is working diff --git a/.github/workflows/dependency-integrity-check.yml b/.github/workflows/dependency-integrity-check.yml new file mode 100644 index 000000000..5deadbc6a --- /dev/null +++ b/.github/workflows/dependency-integrity-check.yml @@ -0,0 +1,45 @@ +name: Dependency Integrity Check + +on: + push: + branches: [ "main", "master", "development", "dev" ] + pull_request: + branches: [ "main", "master", "development", "dev" ] + schedule: + - cron: "17 9 * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + dependency-integrity-check: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: "0.11.14" + enable-cache: true + + - name: Set up Rust + run: rustup show + + - name: Run dependency integrity checks + run: ./scripts/dependency-integrity-check.sh diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 000000000..7bdd8bd52 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,75 @@ +name: Dependency Review + +on: + push: + branches: [ "**" ] + paths: + - 'Cargo.toml' + - 'Cargo.lock' + - 'scripts/native_bench/bench_pecos/Cargo.toml' + - 'scripts/native_bench/bench_pecos/Cargo.lock' + - 'pyproject.toml' + - 'python/**/pyproject.toml' + - 'uv.lock' + - 'requirements*.txt' + - '**/requirements*.txt' + - 'package.json' + - 'package-lock.json' + - 'pnpm-lock.yaml' + - 'yarn.lock' + - 'bun.lock' + - 'bun.lockb' + - '.github/dependabot.yml' + - '.github/workflows/dependency-review.yml' + pull_request: + branches: [ "main", "master", "development", "dev" ] + paths: + - 'Cargo.toml' + - 'Cargo.lock' + - 'scripts/native_bench/bench_pecos/Cargo.toml' + - 'scripts/native_bench/bench_pecos/Cargo.lock' + - 'pyproject.toml' + - 'python/**/pyproject.toml' + - 'uv.lock' + - 'requirements*.txt' + - '**/requirements*.txt' + - 'package.json' + - 'package-lock.json' + - 'pnpm-lock.yaml' + - 'yarn.lock' + - 'bun.lock' + - 'bun.lockb' + - '.github/dependabot.yml' + - '.github/workflows/dependency-review.yml' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + dependency-review: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + + - name: Review dependency changes in pull request + if: github.event_name == 'pull_request' + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high + + - name: Review dependency changes in push + if: github.event_name == 'push' && github.event.before != '0000000000000000000000000000000000000000' + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high + base-ref: ${{ github.event.before }} + head-ref: ${{ github.sha }} + + - name: Skip dependency review for first push to a branch + if: github.event_name == 'push' && github.event.before == '0000000000000000000000000000000000000000' + run: echo "No previous commit exists for this pushed ref; dependency review will run on subsequent pushes." diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index af18009e3..a4c9ffe63 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -11,6 +11,9 @@ name: Go test +permissions: + contents: read + env: TRIGGER_ON_PR_PUSH: true # Set to true to enable triggers on PR pushes RUSTFLAGS: -C debuginfo=0 @@ -71,7 +74,7 @@ jobs: shell: pwsh run: | cd go/pecos-go-ffi - cargo build --release + cargo build --locked --release - name: Build Rust FFI library (Unix) if: runner.os != 'Windows' @@ -93,7 +96,7 @@ jobs: fi cd go/pecos-go-ffi - cargo build --release + cargo build --locked --release - name: Run Go tests (Unix) if: runner.os != 'Windows' diff --git a/.github/workflows/go-version-consistency.yml b/.github/workflows/go-version-consistency.yml index be7a9b1f7..ce41dc57e 100644 --- a/.github/workflows/go-version-consistency.yml +++ b/.github/workflows/go-version-consistency.yml @@ -11,6 +11,9 @@ name: Go version consistency +permissions: + contents: read + on: push: branches: [ "main", "master", "development", "dev" ] diff --git a/.github/workflows/julia-release.yml b/.github/workflows/julia-release.yml index 02d064ca9..238e988a5 100644 --- a/.github/workflows/julia-release.yml +++ b/.github/workflows/julia-release.yml @@ -11,6 +11,9 @@ name: Julia Artifacts +permissions: + contents: read + env: TRIGGER_ON_PR_PUSH: true # Set to true to enable triggers on PR pushes @@ -107,34 +110,34 @@ jobs: if: runner.os != 'Windows' run: | echo "Installing LLVM using pecos..." - cargo run -p pecos-cli --release -- install llvm + cargo run --locked -p pecos-cli --release -- install llvm echo "Setting LLVM environment variables..." - export PECOS_LLVM=$(cargo run -p pecos-cli --release -- llvm find 2>/dev/null) + export PECOS_LLVM=$(cargo run --locked -p pecos-cli --release -- llvm find 2>/dev/null) export LLVM_SYS_140_PREFIX="$PECOS_LLVM" echo "PECOS_LLVM=$PECOS_LLVM" >> $GITHUB_ENV echo "LLVM_SYS_140_PREFIX=$LLVM_SYS_140_PREFIX" >> $GITHUB_ENV echo "Verifying LLVM installation..." - cargo run -p pecos-cli --release -- llvm check + cargo run --locked -p pecos-cli --release -- llvm check - name: Install LLVM 14.0.6 using pecos-llvm (Windows) if: runner.os == 'Windows' shell: pwsh run: | Write-Host "Installing LLVM using pecos..." - cargo run -p pecos-cli --release -- install llvm + cargo run --locked -p pecos-cli --release -- install llvm Write-Host "Setting LLVM environment variables..." - $env:PECOS_LLVM = (cargo run -p pecos-cli --release -- llvm find 2>$null) + $env:PECOS_LLVM = (cargo run --locked -p pecos-cli --release -- llvm find 2>$null) $env:LLVM_SYS_140_PREFIX = $env:PECOS_LLVM "PECOS_LLVM=$env:PECOS_LLVM" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "LLVM_SYS_140_PREFIX=$env:LLVM_SYS_140_PREFIX" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append Write-Host "Verifying LLVM installation..." - cargo run -p pecos-cli --release -- llvm check + cargo run --locked -p pecos-cli --release -- llvm check - name: Install Rust target run: | @@ -152,7 +155,7 @@ jobs: shell: pwsh run: | cd julia/pecos-julia-ffi - cargo build --release + cargo build --locked --release # Create artifact directory New-Item -ItemType Directory -Force -Path ..\..\artifacts @@ -181,7 +184,7 @@ jobs: fi # Native build for all platforms (no cross-compilation needed with native ARM64 runners) - cargo build --release + cargo build --locked --release target_dir="../../target/release" # Create artifact directory diff --git a/.github/workflows/julia-test.yml b/.github/workflows/julia-test.yml index cb19fca93..40cac1dd9 100644 --- a/.github/workflows/julia-test.yml +++ b/.github/workflows/julia-test.yml @@ -11,6 +11,9 @@ name: Julia test +permissions: + contents: read + env: TRIGGER_ON_PR_PUSH: true # Set to true to enable triggers on PR pushes RUSTFLAGS: -C debuginfo=0 @@ -107,7 +110,7 @@ jobs: shell: pwsh run: | cd julia/pecos-julia-ffi - cargo build --release + cargo build --locked --release - name: Build Rust FFI library (Unix) if: runner.os != 'Windows' @@ -129,7 +132,7 @@ jobs: fi cd julia/pecos-julia-ffi - cargo build --release + cargo build --locked --release - name: Run Julia tests run: | diff --git a/.github/workflows/julia-update-hash.yml b/.github/workflows/julia-update-hash.yml index 8f57d5226..36017a19c 100644 --- a/.github/workflows/julia-update-hash.yml +++ b/.github/workflows/julia-update-hash.yml @@ -16,6 +16,9 @@ on: tags: - 'jl-*' +permissions: + contents: read + jobs: update-build-hash: runs-on: ubuntu-latest diff --git a/.github/workflows/julia-version-consistency.yml b/.github/workflows/julia-version-consistency.yml index 317ed206f..64d901841 100644 --- a/.github/workflows/julia-version-consistency.yml +++ b/.github/workflows/julia-version-consistency.yml @@ -11,6 +11,9 @@ name: Julia version consistency +permissions: + contents: read + on: push: branches: [ "main", "master", "development", "dev" ] diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml index e5cbb1a29..a85f8a7d0 100644 --- a/.github/workflows/python-release.yml +++ b/.github/workflows/python-release.yml @@ -1,5 +1,8 @@ name: Python Artifacts +permissions: + contents: read + env: TRIGGER_ON_PR_PUSH: true # Set to true to enable triggers on PR pushes # Must match pecos_build::cmake::CMAKE_VERSION. The MWPF decoder feature @@ -196,7 +199,7 @@ jobs: LLVM_SYS_140_PREFIX=$HOME/.pecos/deps/llvm-14 CMAKE=$HOME/.pecos/deps/cmake-${{ env.PECOS_CMAKE_VERSION }}/bin/cmake CUDA_PATH=/usr/local/cuda-12.6 - MATURIN_PEP517_ARGS=--features=extension-module,mwpf + MATURIN_PEP517_ARGS="--locked --features=extension-module,mwpf" CIBW_BEFORE_ALL_LINUX: | curl -sSf https://sh.rustup.rs | sh -s -- -y source $HOME/.cargo/env @@ -217,8 +220,8 @@ jobs: else echo "Skipping CUDA installation (GPU support not enabled for this build)" fi - cargo run --release -p pecos-cli -- install llvm --force - cargo run --release -p pecos-cli -- install cmake --force + cargo run --locked --release -p pecos-cli -- install llvm --force + cargo run --locked --release -p pecos-cli -- install cmake --force CIBW_REPAIR_WHEEL_COMMAND_LINUX: > auditwheel repair -w {dest_dir} {wheel} && pipx run abi3audit --strict --report {wheel} @@ -229,13 +232,13 @@ jobs: CMAKE=$HOME/.pecos/deps/cmake-${{ env.PECOS_CMAKE_VERSION }}/CMake.app/Contents/bin/cmake MACOSX_DEPLOYMENT_TARGET=13.2 SDKROOT=$(xcrun --show-sdk-path) - MATURIN_PEP517_ARGS=--features=extension-module,mwpf + MATURIN_PEP517_ARGS="--locked --features=extension-module,mwpf" CIBW_BEFORE_ALL_MACOS: | curl -sSf https://sh.rustup.rs | sh -s -- -y source $HOME/.cargo/env rustup update - cargo run --release -p pecos-cli -- install llvm --force - cargo run --release -p pecos-cli -- install cmake --force + cargo run --locked --release -p pecos-cli -- install llvm --force + cargo run --locked --release -p pecos-cli -- install cmake --force # Create a codesign wrapper that strips DYLD_LIBRARY_PATH to prevent # crashes on macOS 15 when bundled libc++ conflicts with system libc++ mkdir -p $HOME/.pecos/bin @@ -249,12 +252,12 @@ jobs: PATH="C:\\Users\\runneradmin\\.pecos\\deps\\llvm-14\\bin;C:\\Users\\runneradmin\\.pecos\\deps\\cmake-${{ env.PECOS_CMAKE_VERSION }}\\bin;$PATH" LLVM_SYS_140_PREFIX="C:\\Users\\runneradmin\\.pecos\\deps\\llvm-14" CMAKE="C:\\Users\\runneradmin\\.pecos\\deps\\cmake-${{ env.PECOS_CMAKE_VERSION }}\\bin\\cmake.exe" - MATURIN_PEP517_ARGS=--features=extension-module,mwpf + MATURIN_PEP517_ARGS="--locked --features=extension-module,mwpf" CIBW_BEFORE_ALL_WINDOWS: > echo "=== Installing LLVM using pecos ===" && rustup update && - cargo run --release -p pecos-cli -- install llvm --force && - cargo run --release -p pecos-cli -- install cmake --force && + cargo run --locked --release -p pecos-cli -- install llvm --force && + cargo run --locked --release -p pecos-cli -- install cmake --force && echo "=== Checking LLVM installation ===" && (test -d "C:\\Users\\runneradmin\\.pecos\\deps\\llvm-14" && echo "LLVM directory exists") || (echo "ERROR: LLVM directory not found!" && exit 1) # Install delvewheel and patch it to ignore ext-ms-win-* API sets @@ -293,7 +296,7 @@ jobs: curl -sSf https://sh.rustup.rs | sh -s -- -y source $HOME/.cargo/env dnf install libffi-devel -y - cargo run --release -p pecos-cli -- install llvm --force + cargo run --locked --release -p pecos-cli -- install llvm --force CIBW_REPAIR_WHEEL_COMMAND_LINUX: > auditwheel repair -w {dest_dir} {wheel} && pipx run abi3audit --strict --report {wheel} @@ -306,7 +309,7 @@ jobs: source $HOME/.cargo/env 2>/dev/null || { curl -sSf https://sh.rustup.rs | sh -s -- -y && source $HOME/.cargo/env; } if [ ! -d "$HOME/.pecos/deps/llvm-14/bin" ]; then rustup update - cargo run --release -p pecos-cli -- install llvm --force + cargo run --locked --release -p pecos-cli -- install llvm --force else echo "LLVM already installed from pecos-rslib build, skipping" fi @@ -321,7 +324,7 @@ jobs: LLVM_SYS_140_PREFIX="C:\\Users\\runneradmin\\.pecos\\deps\\llvm-14" CIBW_BEFORE_ALL_WINDOWS: > rustup update && - if not exist "C:\Users\runneradmin\.pecos\deps\llvm-14\bin" (cargo run --release -p pecos-cli -- install llvm --force) else (echo LLVM already installed from pecos-rslib build) + if not exist "C:\Users\runneradmin\.pecos\deps\llvm-14\bin" (cargo run --locked --release -p pecos-cli -- install llvm --force) else (echo LLVM already installed from pecos-rslib build) CIBW_BEFORE_BUILD_WINDOWS: > pip install delvewheel && python -c "import delvewheel._dll_list as d,inspect,re as r;p=inspect.getfile(d);c=open(p).read();n=chr(10);open(p,'w').write(c.replace(r\"re.compile('api-.*'),\",r\"re.compile('api-.*'),\"+n+r\" re.compile('ext-.*'),\")) if 'ext-.*' not in c else None" diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 65133f0f8..95d8c004b 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -1,5 +1,8 @@ name: Python test / linting +permissions: + contents: read + env: TRIGGER_ON_PR_PUSH: true # Set to true to enable triggers on PR pushes RUSTFLAGS: -C debuginfo=0 @@ -138,7 +141,7 @@ jobs: run: just ci-env - name: Install PECOS CLI - run: cargo install --path crates/pecos-cli --force + run: cargo install --locked --path crates/pecos-cli --force # macOS: prevent Homebrew library path issues - name: Configure macOS environment @@ -230,4 +233,4 @@ jobs: # PECOS workaround) and are out of scope for this contract lane. - name: Vanilla cargo check (publishable crates, no just/bootstrap) shell: pwsh - run: cargo check -p pecos-build -p pecos-core + run: cargo check --locked -p pecos-build -p pecos-core diff --git a/.github/workflows/python-version-consistency.yml b/.github/workflows/python-version-consistency.yml index 16bd806e9..d9db9b53b 100644 --- a/.github/workflows/python-version-consistency.yml +++ b/.github/workflows/python-version-consistency.yml @@ -1,5 +1,8 @@ name: Python Version Consistency Check +permissions: + contents: read + on: push: paths: diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml index 4b028f79e..d1187f16c 100644 --- a/.github/workflows/rust-test.yml +++ b/.github/workflows/rust-test.yml @@ -1,5 +1,8 @@ name: Rust test / linting +permissions: + contents: read + env: TRIGGER_ON_PR_PUSH: true # Set to true to enable triggers on PR pushes RUSTFLAGS: -C debuginfo=0 @@ -91,7 +94,7 @@ jobs: run: cargo fmt --all -- --check - name: Run clippy - run: cargo clippy --workspace --all-targets --all-features -- -D warnings + run: cargo clippy --locked --workspace --all-targets --all-features -- -D warnings rust-lint-no-llvm: runs-on: ubuntu-latest @@ -121,10 +124,10 @@ jobs: run: | for crate in pecos-core pecos-engines pecos-simulators pecos-programs pecos-random pecos-qasm pecos-phir-json pecos-qis-ffi-types; do echo "Testing $crate..." - cd "crates/$crate" && cargo clippy --all-targets -- -D warnings && cd ../.. + cd "crates/$crate" && cargo clippy --locked --all-targets -- -D warnings && cd ../.. done echo "Testing pecos without default features..." - cd crates/pecos && cargo clippy --all-targets --no-default-features -- -D warnings && cd ../.. + cd crates/pecos && cargo clippy --locked --all-targets --no-default-features -- -D warnings && cd ../.. pre-commit: runs-on: ubuntu-latest @@ -215,18 +218,18 @@ jobs: - name: Compile tests (macOS) if: matrix.os == 'macos-latest' - run: cargo build -p pecos-cli --release + run: cargo build --locked -p pecos-cli --release - name: Compile tests (Linux) if: matrix.os == 'ubuntu-latest' run: | # Use `pecos rust test` for runtime hardware detection (cuQuantum, GPU) # Dry-run: just compile, the run step below does the actual test - cargo build -p pecos-cli --release + cargo build --locked -p pecos-cli --release - name: Compile tests (Windows) if: runner.os == 'Windows' - run: cargo build -p pecos-cli --release + run: cargo build --locked -p pecos-cli --release - name: Run tests (macOS) if: matrix.os == 'macos-latest' @@ -235,13 +238,13 @@ jobs: LIBRARY_PATH: /usr/lib run: | unset LD_LIBRARY_PATH DYLD_LIBRARY_PATH DYLD_FALLBACK_LIBRARY_PATH PKG_CONFIG_PATH - cargo run -p pecos-cli --release -- rust test + cargo run --locked -p pecos-cli --release -- rust test - name: Run tests (Linux) if: matrix.os == 'ubuntu-latest' run: | - cargo run -p pecos-cli --release -- rust test + cargo run --locked -p pecos-cli --release -- rust test - name: Run tests (Windows) if: runner.os == 'Windows' - run: cargo run -p pecos-cli --release -- rust test + run: cargo run --locked -p pecos-cli --release -- rust test diff --git a/.github/workflows/rust-version-consistency.yml b/.github/workflows/rust-version-consistency.yml index d6f3238e1..5ffb0e845 100644 --- a/.github/workflows/rust-version-consistency.yml +++ b/.github/workflows/rust-version-consistency.yml @@ -1,5 +1,8 @@ name: Rust Version Consistency Check +permissions: + contents: read + # bump on: diff --git a/.github/workflows/selene-plugins.yml b/.github/workflows/selene-plugins.yml index dab9a5d6c..2b493af3d 100644 --- a/.github/workflows/selene-plugins.yml +++ b/.github/workflows/selene-plugins.yml @@ -1,5 +1,8 @@ name: Selene Plugins +permissions: + contents: read + env: CARGO_TERM_COLOR: always RUSTFLAGS: -C debuginfo=0 @@ -81,7 +84,7 @@ jobs: PLUGIN=$(basename "$DIR") CARGO_ARGS="$CARGO_ARGS -p $PLUGIN" done - cargo build --release $CARGO_ARGS + cargo build --locked --release $CARGO_ARGS for DIR in python/selene-plugins/pecos-selene-*/; do PLUGIN=$(basename "$DIR") @@ -98,7 +101,7 @@ jobs: run: | $plugins = Get-ChildItem -Directory python/selene-plugins/pecos-selene-* | Select-Object -ExpandProperty Name $cargoArgs = $plugins | ForEach-Object { "-p"; $_ } - cargo build --release @cargoArgs + cargo build --locked --release @cargoArgs foreach ($plugin in $plugins) { $libName = $plugin -replace '-', '_' @@ -163,13 +166,13 @@ jobs: - name: Build Rust library (Unix) if: runner.os != 'Windows' run: | - cargo build --release -p ${{ matrix.plugin.name }} + cargo build --locked --release -p ${{ matrix.plugin.name }} - name: Build Rust library (Windows) if: runner.os == 'Windows' shell: pwsh run: | - cargo build --release -p ${{ matrix.plugin.name }} + cargo build --locked --release -p ${{ matrix.plugin.name }} - name: Copy library to Python package (Unix) if: runner.os != 'Windows' diff --git a/.github/workflows/test-docs-examples.yml b/.github/workflows/test-docs-examples.yml index 0c3d64bf0..45da3d5ad 100644 --- a/.github/workflows/test-docs-examples.yml +++ b/.github/workflows/test-docs-examples.yml @@ -1,5 +1,8 @@ name: Documentation Tests & Build +permissions: + contents: read + on: push: branches: [ main, master, development, dev ] @@ -70,14 +73,14 @@ jobs: - name: Install dependencies and build run: | - uv lock --project . - uv sync --project . --all-packages - cargo install --path crates/pecos-cli --force + uv lock --check --project . + uv sync --locked --project . --all-packages + cargo install --locked --path crates/pecos-cli --force pecos python build --profile debug - name: Test working documentation examples - run: uv run python scripts/docs/test_working_examples.py + run: uv run --frozen python scripts/docs/test_working_examples.py - name: Build documentation if: success() - run: uv run mkdocs build + run: uv run --frozen mkdocs build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 74772a041..8d6185478 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,6 +18,9 @@ repos: - id: check-yaml args: [--unsafe] - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: detect-private-key # Python-specific - id: check-ast - id: check-docstring-first @@ -47,3 +50,12 @@ repos: args: [--line-length=120] additional_dependencies: - black==25.9.0 + + - repo: local + hooks: + - id: dependency-integrity-check + name: dependency integrity check + entry: scripts/dependency-integrity-check.sh + language: system + pass_filenames: false + always_run: true diff --git a/Cargo.lock b/Cargo.lock index 17df3a742..a9e67cf54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -532,7 +532,7 @@ checksum = "38c99613cb3cd7429889a08dfcf651721ca971c86afa30798461f8eee994de47" [[package]] name = "bp" version = "0.1.0" -source = "git+https://github.com/yuewuo/mwpf?tag=v0.2.12#b8444428f999457208c4b0956f3f1c745a0ec2d5" +source = "git+https://github.com/yuewuo/mwpf?rev=b8444428f999457208c4b0956f3f1c745a0ec2d5#b8444428f999457208c4b0956f3f1c745a0ec2d5" dependencies = [ "float-cmp", "rand 0.8.6", @@ -2273,7 +2273,7 @@ dependencies = [ [[package]] name = "heapz" version = "1.1.4" -source = "git+https://github.com/yuewuo/mwpf?tag=v0.2.12#b8444428f999457208c4b0956f3f1c745a0ec2d5" +source = "git+https://github.com/yuewuo/mwpf?rev=b8444428f999457208c4b0956f3f1c745a0ec2d5#b8444428f999457208c4b0956f3f1c745a0ec2d5" [[package]] name = "heck" @@ -2326,7 +2326,7 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" [[package]] name = "highs" version = "1.6.1" -source = "git+https://github.com/yuewuo/mwpf?tag=v0.2.12#b8444428f999457208c4b0956f3f1c745a0ec2d5" +source = "git+https://github.com/yuewuo/mwpf?rev=b8444428f999457208c4b0956f3f1c745a0ec2d5#b8444428f999457208c4b0956f3f1c745a0ec2d5" dependencies = [ "highs-sys", "log", @@ -3270,7 +3270,7 @@ checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" [[package]] name = "mwpf" version = "0.2.12" -source = "git+https://github.com/yuewuo/mwpf?tag=v0.2.12#b8444428f999457208c4b0956f3f1c745a0ec2d5" +source = "git+https://github.com/yuewuo/mwpf?rev=b8444428f999457208c4b0956f3f1c745a0ec2d5#b8444428f999457208c4b0956f3f1c745a0ec2d5" dependencies = [ "base64", "bp", @@ -4676,7 +4676,7 @@ dependencies = [ [[package]] name = "pheap" version = "0.3.0" -source = "git+https://github.com/yuewuo/mwpf?tag=v0.2.12#b8444428f999457208c4b0956f3f1c745a0ec2d5" +source = "git+https://github.com/yuewuo/mwpf?rev=b8444428f999457208c4b0956f3f1c745a0ec2d5#b8444428f999457208c4b0956f3f1c745a0ec2d5" dependencies = [ "num-traits", ] @@ -5964,7 +5964,7 @@ dependencies = [ [[package]] name = "selene-core" version = "0.2.2" -source = "git+https://github.com/Quantinuum/selene.git?rev=01300ee#01300ee5d4825e2dfc6500941d0540c3ff06988a" +source = "git+https://github.com/Quantinuum/selene.git?rev=01300ee5d4825e2dfc6500941d0540c3ff06988a#01300ee5d4825e2dfc6500941d0540c3ff06988a" dependencies = [ "anyhow", "delegate", @@ -5977,7 +5977,7 @@ dependencies = [ [[package]] name = "selene-simple-runtime" version = "0.2.6" -source = "git+https://github.com/Quantinuum/selene.git?rev=01300ee#01300ee5d4825e2dfc6500941d0540c3ff06988a" +source = "git+https://github.com/Quantinuum/selene.git?rev=01300ee5d4825e2dfc6500941d0540c3ff06988a#01300ee5d4825e2dfc6500941d0540c3ff06988a" dependencies = [ "anyhow", "selene-core 0.2.2", @@ -5986,7 +5986,7 @@ dependencies = [ [[package]] name = "selene-soft-rz-runtime" version = "0.2.6" -source = "git+https://github.com/Quantinuum/selene.git?rev=01300ee#01300ee5d4825e2dfc6500941d0540c3ff06988a" +source = "git+https://github.com/Quantinuum/selene.git?rev=01300ee5d4825e2dfc6500941d0540c3ff06988a#01300ee5d4825e2dfc6500941d0540c3ff06988a" dependencies = [ "anyhow", "selene-core 0.2.2", @@ -6237,7 +6237,7 @@ dependencies = [ [[package]] name = "slp" version = "0.2.0" -source = "git+https://github.com/yuewuo/mwpf?tag=v0.2.12#b8444428f999457208c4b0956f3f1c745a0ec2d5" +source = "git+https://github.com/yuewuo/mwpf?rev=b8444428f999457208c4b0956f3f1c745a0ec2d5#b8444428f999457208c4b0956f3f1c745a0ec2d5" dependencies = [ "num-bigint", "num-rational", diff --git a/Cargo.toml b/Cargo.toml index 977c66a2f..c3e1faff0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -165,7 +165,7 @@ quizx = "0.3" # --- Decoder libraries --- fusion-blossom = "0.2" -mwpf = { git = "https://github.com/yuewuo/mwpf", tag = "v0.2.12", default-features = false, features = [ +mwpf = { git = "https://github.com/yuewuo/mwpf", rev = "b8444428f999457208c4b0956f3f1c745a0ec2d5", default-features = false, features = [ "f64_weight", ] } relay-bp = "0.2" diff --git a/Justfile b/Justfile index 57702d3a0..346adb80e 100644 --- a/Justfile +++ b/Justfile @@ -28,7 +28,7 @@ default: set shell := ["bash", "-cu"] # PECOS CLI - must be installed (run 'just install-cli' first) -pecos := "cargo run -p pecos-cli --" +pecos := "cargo run --locked -p pecos-cli --" # ============================================================================= # Getting Started @@ -38,7 +38,7 @@ pecos := "cargo run -p pecos-cli --" [group('setup')] install-cli: _msvc-bootstrap @echo "Installing PECOS CLI..." - cargo install --path crates/pecos-cli --force + cargo install --locked --path crates/pecos-cli --force @echo "" @echo "Done! You can now run: just build" @@ -89,10 +89,10 @@ doctor: _msvc-bootstrap else fail "uv" "not found (see: https://docs.astral.sh/uv/)" fi - PECOS_VER=$(uv run python -c "import pecos; print(pecos.__version__)" 2>/dev/null) \ + PECOS_VER=$(uv run --frozen python -c "import pecos; print(pecos.__version__)" 2>/dev/null) \ && ok "import pecos" "v$PECOS_VER" \ || fail "import pecos" "failed (run: just build)" - RSLIB_VER=$(uv run python -c "import pecos_rslib; print(pecos_rslib.__version__)" 2>/dev/null) \ + RSLIB_VER=$(uv run --frozen python -c "import pecos_rslib; print(pecos_rslib.__version__)" 2>/dev/null) \ && ok "pecos_rslib" "v$RSLIB_VER" \ || fail "pecos_rslib" "native library failed to load (run: just build)" echo "" @@ -144,6 +144,11 @@ doctor: _msvc-bootstrap sys-info: _msvc-bootstrap {{pecos}} sys-info +# Check lockfiles, CI posture, and current package-worm indicators +[group('security')] +dependency-integrity-check: + ./scripts/dependency-integrity-check.sh + # List installed and cached dependencies [group('setup')] list-deps: _msvc-bootstrap @@ -193,12 +198,12 @@ pytest *args: #!/usr/bin/env bash set -euo pipefail if [ -n "{{args}}" ]; then - uv run pytest {{args}} + uv run --frozen pytest {{args}} else - uv run pytest python/pecos-rslib/tests -m "not performance" - uv run --group numpy-compat pytest python/pecos-rslib/tests -m "numpy and not performance" - uv run pytest python/quantum-pecos/tests -m "not optional_dependency and not slow" - uv run pytest python/selene-plugins + uv run --frozen pytest python/pecos-rslib/tests -m "not performance" + uv run --frozen --group numpy-compat pytest python/pecos-rslib/tests -m "numpy and not performance" + uv run --frozen pytest python/quantum-pecos/tests -m "not optional_dependency and not slow" + uv run --frozen pytest python/selene-plugins fi # Run Rust tests (CUDA-aware; mode: dev/debug, release, native) @@ -251,9 +256,9 @@ lint mode="fix": _msvc-bootstrap (validate-lint-mode mode) python-workspace-chec echo "==> Checking Rust formatting..." cargo fmt --all -- --check echo "==> Running clippy..." - cargo clippy --workspace --all-targets $CLIPPY_FEATURES -- -D warnings + cargo clippy --locked --workspace --all-targets $CLIPPY_FEATURES -- -D warnings echo "==> Running pre-commit..." - uv run pre-commit run --all-files + uv run --frozen pre-commit run --all-files if command -v julia >/dev/null 2>&1; then echo "==> Checking Julia formatting..." just julia-fmt-check @@ -267,9 +272,9 @@ lint mode="fix": _msvc-bootstrap (validate-lint-mode mode) python-workspace-chec else echo "==> Fixing Rust formatting and clippy..." cargo fmt --all - cargo clippy --workspace --all-targets $CLIPPY_FEATURES --fix --allow-staged --allow-dirty -- -D warnings + cargo clippy --locked --workspace --all-targets $CLIPPY_FEATURES --fix --allow-staged --allow-dirty -- -D warnings echo "==> Running pre-commit..." - uv run pre-commit run --all-files || true + uv run --frozen pre-commit run --all-files || true if command -v julia >/dev/null 2>&1; then echo "==> Fixing Julia formatting..." just julia-fmt @@ -283,12 +288,12 @@ lint mode="fix": _msvc-bootstrap (validate-lint-mode mode) python-workspace-chec # Run cargo check [group('lint')] check: _msvc-bootstrap - cargo check --workspace --all-targets + cargo check --locked --workspace --all-targets # Check Python workspace metadata [group('lint')] python-workspace-check: - @uv run python scripts/check_python_workspace.py + @uv run --frozen python scripts/check_python_workspace.py # Run cargo clippy (CUDA-aware: uses --all-features only when CUDA is available) [group('lint')] @@ -297,10 +302,10 @@ clippy: _msvc-bootstrap set -euo pipefail if command -v nvcc >/dev/null 2>&1 || [ -n "${CUDA_PATH:-}" ] || [ -d /usr/local/cuda ]; then echo "(CUDA detected -- clippy with all features)" - cargo clippy --workspace --all-targets --all-features -- -D warnings + cargo clippy --locked --workspace --all-targets --all-features -- -D warnings else echo "(No CUDA -- clippy with default features)" - cargo clippy --workspace --all-targets -- -D warnings + cargo clippy --locked --workspace --all-targets -- -D warnings fi # Check Rust formatting @@ -345,7 +350,7 @@ bench profile="release" features="" pattern="": _msvc-bootstrap (validate-bench- # Dev Workflows # ============================================================================= -# Dev cycle: build + test (lang: all, rust, python, julia, go) +# Fast dev cycle: build + test only (lang: all, rust, python, julia, go) [group('dev')] dev lang="all": (validate-dev-lang lang) #!/usr/bin/env bash @@ -377,7 +382,7 @@ dev lang="all": (validate-dev-lang lang) ;; esac -# Clean build + test + lint check (run before opening a PR) +# Pre-PR gate: clean build + test + lint + dependency integrity [group('dev')] check-all: clean (build "release") (test "release") (lint "check") @@ -408,7 +413,7 @@ clean *target: fi # macOS bash 3.2: ${arr[@]+"${arr[@]}"} expands to nothing when arr is empty/unset # under `set -u` (which otherwise trips on empty @-expansion). - uv run python scripts/clean.py ${ARGS[@]+"${ARGS[@]}"} + uv run --frozen python scripts/clean.py ${ARGS[@]+"${ARGS[@]}"} # ============================================================================= # Documentation @@ -417,18 +422,18 @@ clean *target: # Serve documentation locally (port: default 8000) [group('docs')] docs port="8000": (validate-port port) - uv run mkdocs serve -a "127.0.0.1:{{port}}" + uv run --frozen mkdocs serve -a "127.0.0.1:{{port}}" # Build documentation [group('docs')] docs-build: - uv run mkdocs build --clean + uv run --frozen mkdocs build --clean # Test Python code examples in documentation [group('docs')] docs-test: - uv run python scripts/docs/generate_doc_tests.py - uv run pytest python/quantum-pecos/tests/docs/generated -v -k "not rust" -m "not slow" + uv run --frozen python scripts/docs/generate_doc_tests.py + uv run --frozen pytest python/quantum-pecos/tests/docs/generated -v -k "not rust" -m "not slow" # ============================================================================= # Deps Management (prefer `just setup` or `pecos install `) @@ -487,9 +492,9 @@ julia-build profile="release" rustflags="": _msvc-bootstrap (validate-profile "j export RUSTFLAGS="${RUSTFLAGS:-} -C target-cpu=native" fi case "$PROFILE" in - native) cargo build --profile native -p pecos-julia-ffi ;; - release) cargo build --release -p pecos-julia-ffi ;; - dev|debug) cargo build -p pecos-julia-ffi ;; + native) cargo build --locked --profile native -p pecos-julia-ffi ;; + release) cargo build --locked --release -p pecos-julia-ffi ;; + dev|debug) cargo build --locked -p pecos-julia-ffi ;; *) echo "Unknown profile: $PROFILE"; exit 1 ;; esac @@ -555,9 +560,9 @@ go-build profile="release" rustflags="": _msvc-bootstrap (validate-profile "go-b export RUSTFLAGS="${RUSTFLAGS:-} -C target-cpu=native" fi case "$PROFILE" in - native) cargo build --profile native -p pecos-go-ffi ;; - release) cargo build --release -p pecos-go-ffi ;; - dev|debug) cargo build -p pecos-go-ffi ;; + native) cargo build --locked --profile native -p pecos-go-ffi ;; + release) cargo build --locked --release -p pecos-go-ffi ;; + dev|debug) cargo build --locked -p pecos-go-ffi ;; *) echo "Unknown profile: $PROFILE"; exit 1 ;; esac @@ -612,18 +617,18 @@ go-lint profile="release": (validate-profile "go-lint" profile) (go-build profil # Run performance tests with release build [group('test')] pytest-perf: build-release - uv run --group numpy-compat pytest python/pecos-rslib/tests -m "performance" -v + uv run --frozen --group numpy-compat pytest python/pecos-rslib/tests -m "performance" -v # Run tests for optional dependencies [group('test')] pytest-dep: - uv run pytest python/pecos-rslib/tests -m "optional_dependency" - uv run pytest python/quantum-pecos/tests -m "optional_dependency" + uv run --frozen pytest python/pecos-rslib/tests -m "optional_dependency" + uv run --frozen pytest python/quantum-pecos/tests -m "optional_dependency" # Run the slower integration lane (excluded from the default fast lane) [group('test')] pytest-slow: - uv run pytest python/quantum-pecos/tests -m "slow and not optional_dependency" + uv run --frozen pytest python/quantum-pecos/tests -m "slow and not optional_dependency" @@ -772,7 +777,7 @@ sync-deps: exit 0 fi echo "Python deps incomplete, running uv sync..." - SYNC_ARGS=(--project . --all-packages) + SYNC_ARGS=(--project . --all-packages --locked) # Include CUDA Python packages (cupy, cuquantum, pytket-cutensornet) when # the toolkit is installed AND an NVIDIA GPU is present. Pure Rust users # and machines without a GPU skip this -- mirrors `pecos python build`. @@ -854,7 +859,7 @@ build-selene profile="release": CARGO_PKG_ARGS+=(-p "$(basename "$DIR")") done if [ ${#CARGO_PKG_ARGS[@]} -gt 0 ]; then - cargo build ${CARGO_PROFILE_FLAGS[@]+"${CARGO_PROFILE_FLAGS[@]}"} "${CARGO_PKG_ARGS[@]}" + cargo build --locked ${CARGO_PROFILE_FLAGS[@]+"${CARGO_PROFILE_FLAGS[@]}"} "${CARGO_PKG_ARGS[@]}" fi else echo "Selene plugins: cargo output up to date ($PROFILE)" diff --git a/crates/pecos-cli/src/cli/cuda_cmd.rs b/crates/pecos-cli/src/cli/cuda_cmd.rs index 64e7807df..e06fa8cd8 100644 --- a/crates/pecos-cli/src/cli/cuda_cmd.rs +++ b/crates/pecos-cli/src/cli/cuda_cmd.rs @@ -218,7 +218,7 @@ fn run_validate(path: Option) -> Result<()> { } /// CLI entry point for `pecos cuda setup-python`. Validates the toolkit is -/// present, then runs `uv sync --group cuda` and prints next-step hints. +/// present, then runs `uv sync --locked --group cuda` and prints next-step hints. fn run_setup_python() -> Result<()> { if find_cuda().is_none() { eprintln!("Error: CUDA toolkit not found."); @@ -239,7 +239,7 @@ fn run_setup_python() -> Result<()> { Ok(()) } -/// Run `uv sync --group cuda` to install Python CUDA packages. +/// Run `uv sync --locked --group cuda` to install Python CUDA packages. /// /// Reusable from other CLI commands (e.g. `pecos setup`) once they've already /// confirmed the user wants this. Does NOT validate toolkit presence -- caller @@ -249,7 +249,7 @@ pub(super) fn install_cuda_python_packages() -> Result<()> { println!(); let status = Command::new("uv") - .args(["sync", "--group", "cuda"]) + .args(["sync", "--locked", "--group", "cuda"]) .status(); match status { @@ -263,7 +263,7 @@ pub(super) fn install_cuda_python_packages() -> Result<()> { eprintln!("Failed to install CUDA Python packages."); eprintln!(); eprintln!("You may need to install manually:"); - eprintln!(" uv sync --group cuda"); + eprintln!(" uv sync --locked --group cuda"); Err(Error::Cuda( "Failed to install CUDA Python packages".to_string(), )) diff --git a/crates/pecos-cli/src/cli/python_cmd.rs b/crates/pecos-cli/src/cli/python_cmd.rs index 23e521d77..022f70dbd 100644 --- a/crates/pecos-cli/src/cli/python_cmd.rs +++ b/crates/pecos-cli/src/cli/python_cmd.rs @@ -190,7 +190,7 @@ fn run_build(profile: &str, rustflags: Option<&str>, cuda: bool) -> Result<()> { let maturin = venv_bin.join("maturin"); let mut cmd = Command::new(&maturin); - cmd.args(["develop", "--uv"]); + cmd.args(["develop", "--uv", "--locked"]); cmd.args(cargo_profile_flag); // Maturin's CLI --features REPLACES (not merges with) the features list // in pyproject.toml's [tool.maturin], so any time we pass extra features diff --git a/crates/pecos-cli/src/cli/rust_cmd.rs b/crates/pecos-cli/src/cli/rust_cmd.rs index d71512995..8ddda43e0 100644 --- a/crates/pecos-cli/src/cli/rust_cmd.rs +++ b/crates/pecos-cli/src/cli/rust_cmd.rs @@ -213,7 +213,19 @@ fn run_cargo_command(args: &[&str]) -> bool { /// `run_test` to inject `-C target-cpu=native` for the native profile. fn run_cargo_command_with_rustflags(args: &[&str], rustflags: Option<&str>) -> bool { let mut cmd = Command::new("cargo"); - cmd.args(args); + let mut locked_args = Vec::with_capacity(args.len() + 1); + if let Some((subcommand, rest)) = args.split_first() { + locked_args.push(*subcommand); + if matches!(*subcommand, "build" | "check" | "clippy" | "run" | "test") + && !args + .iter() + .any(|arg| matches!(*arg, "--locked" | "--frozen" | "--offline")) + { + locked_args.push("--locked"); + } + locked_args.extend(rest.iter().copied()); + } + cmd.args(&locked_args); for (key, value) in super::env_cmd::collect_env() { cmd.env(key, value); } diff --git a/crates/pecos-cli/src/cli/setup_cmd.rs b/crates/pecos-cli/src/cli/setup_cmd.rs index 4e2cf26cd..c24311310 100644 --- a/crates/pecos-cli/src/cli/setup_cmd.rs +++ b/crates/pecos-cli/src/cli/setup_cmd.rs @@ -131,7 +131,7 @@ fn print_status_summary(skip_llvm: bool, skip_cuda: bool, skip_cmake: bool) { if super::cuda_cmd::cuda_python_packages_installed() { println!(" cupy: installed (CUDA Python packages synced)"); } else { - println!(" cupy: not installed (~500 MB via `uv sync --group cuda`)"); + println!(" cupy: not installed (~500 MB via `uv sync --locked --group cuda`)"); } } @@ -353,7 +353,7 @@ fn setup_cuda_python(mode: PromptMode) -> Result<()> { } if confirm( - "Install CUDA Python packages? (cupy, cuquantum, pytket-cutensornet via `uv sync --group cuda`)", + "Install CUDA Python packages? (cupy, cuquantum, pytket-cutensornet via `uv sync --locked --group cuda`)", true, // default yes when CUDA toolkit + NVIDIA GPU are present mode, ) { diff --git a/crates/pecos-qis/Cargo.toml b/crates/pecos-qis/Cargo.toml index f45a9b899..d58b89ef1 100644 --- a/crates/pecos-qis/Cargo.toml +++ b/crates/pecos-qis/Cargo.toml @@ -60,12 +60,12 @@ optional = true [dependencies.selene-simple-runtime] git = "https://github.com/Quantinuum/selene.git" -rev = "01300ee" +rev = "01300ee5d4825e2dfc6500941d0540c3ff06988a" optional = true [dependencies.selene-soft-rz-runtime] git = "https://github.com/Quantinuum/selene.git" -rev = "01300ee" +rev = "01300ee5d4825e2dfc6500941d0540c3ff06988a" optional = true [build-dependencies] diff --git a/scripts/dependency-integrity-check.sh b/scripts/dependency-integrity-check.sh new file mode 100755 index 000000000..4906594ad --- /dev/null +++ b/scripts/dependency-integrity-check.sh @@ -0,0 +1,281 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +failures=0 + +section() { + printf '\n==> %s\n' "$1" +} + +fail() { + printf 'ERROR: %s\n' "$1" >&2 + failures=$((failures + 1)) +} + +warn() { + printf 'WARN: %s\n' "$1" >&2 +} + +require_tool() { + if ! command -v "$1" >/dev/null 2>&1; then + fail "$1 is required for dependency integrity checks" + return 1 + fi +} + +collect_files() { + rg --files "$@" +} + +KNOWN_BAD_PACKAGE_RE='(mistralai|guardrails-ai|lightning|@tanstack/|@mistralai/|@uipath/|@opensearch-project/|@squawk/|@tallyui/|@beproduct/|@draftauth/|@dirigible-ai/|@ml-toolkit-ts/|@supersurkhet/|agentwork-cli|cmux-agent-mcp|cross-stitch|git-branch-selector|git-git-git|nextmove-mcp|safe-action|ts-dna|wot-api|finch-rust|sha-rust|finch_cli_rust|finch-rst|sha-rst)' +SHAI_HULUD_IOC_RE='(shai[-_ ]?hulud|router_init\.js|router_runtime\.js|setup\.mjs|setup_bun\.js|bun_environment\.js|transformers\.pyz|git-tanstack\.com|api\.masscan\.cloud|getsession\.org|filev2\.getsession|gh-token-monitor|IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner|shai-hulud-workflow)' + +RG_EXCLUDES=( + --hidden + --glob '!.git/**' + --glob '!target/**' + --glob '!.venv/**' + --glob '!.ruff_cache/**' + --glob '!scripts/dependency-integrity-check.sh' +) + +section "Tooling" +require_tool rg || true +require_tool cargo || true +require_tool uv || true + +section "Known affected package names" +lockfiles=() +while IFS= read -r file; do + lockfiles+=("$file") +done < <(collect_files \ + -g 'Cargo.lock' \ + -g 'uv.lock' \ + -g 'pylock.toml' \ + -g 'requirements*.txt' \ + -g 'package-lock.json' \ + -g 'npm-shrinkwrap.json' \ + -g 'pnpm-lock.yaml' \ + -g 'yarn.lock' \ + -g 'bun.lock' \ + -g 'bun.lockb') + +manifests=() +while IFS= read -r file; do + manifests+=("$file") +done < <(collect_files \ + -g 'Cargo.toml' \ + -g 'pyproject.toml' \ + -g 'requirements*.txt' \ + -g 'package.json' \ + -g 'pnpm-workspace.yaml' \ + -g 'bunfig.toml') + +package_files=("${lockfiles[@]}" "${manifests[@]}") +if ((${#package_files[@]} == 0)); then + fail "no supported package manifests or lockfiles found" +else + if rg -n -i "$KNOWN_BAD_PACKAGE_RE" "${package_files[@]}"; then + fail "known Shai-Hulud-affected package name found in package files" + else + echo "No current Shai-Hulud package-name hits in package manifests or lockfiles." + fi +fi + +section "Repository IoCs" +if rg -n -i "${RG_EXCLUDES[@]}" "$SHAI_HULUD_IOC_RE" .; then + fail "Shai-Hulud indicator found in repository contents" +else + echo "No current Shai-Hulud payload or persistence indicators found." +fi + +section "npm lock discipline" +npm_manifests=() +while IFS= read -r file; do + npm_manifests+=("$file") +done < <(collect_files -g 'package.json') +npm_locks=() +while IFS= read -r file; do + npm_locks+=("$file") +done < <(collect_files \ + -g 'package-lock.json' \ + -g 'npm-shrinkwrap.json' \ + -g 'pnpm-lock.yaml' \ + -g 'yarn.lock' \ + -g 'bun.lock' \ + -g 'bun.lockb') + +if ((${#npm_manifests[@]} > 0 && ${#npm_locks[@]} == 0)); then + printf '%s\n' "${npm_manifests[@]}" + fail "npm package manifests exist without a committed lockfile" +elif ((${#npm_manifests[@]} == 0)); then + echo "No npm package manifests found." +else + echo "npm manifests have a committed lockfile." +fi + +section "Cargo lock discipline" +cargo_failures_before=$failures +cargo_locks=() +while IFS= read -r file; do + cargo_locks+=("$file") +done < <(collect_files -g 'Cargo.lock') + +if ((${#cargo_locks[@]} == 0)); then + fail "no Cargo.lock files found" +else + for lockfile in "${cargo_locks[@]}"; do + manifest="$(dirname "$lockfile")/Cargo.toml" + if [[ ! -f "$manifest" ]]; then + fail "$lockfile has no adjacent Cargo.toml" + continue + fi + if ! cargo metadata --locked --manifest-path "$manifest" --format-version 1 >/dev/null; then + fail "$lockfile is missing or not current with $manifest" + fi + done + if ((failures == cargo_failures_before)); then + echo "Cargo lockfiles are current." + fi +fi + +section "Cargo git dependency pins" +cargo_manifests=() +while IFS= read -r file; do + cargo_manifests+=("$file") +done < <(collect_files -g 'Cargo.toml') + +if ((${#cargo_manifests[@]} > 0)); then + if rg -n --pcre2 '^\s*(tag|branch)\s*=' "${cargo_manifests[@]}"; then + fail "Cargo git dependencies must use full immutable rev pins, not tag/branch" + fi + if rg -n --pcre2 '^\s*rev\s*=\s*"[0-9a-f]{1,39}"' "${cargo_manifests[@]}"; then + fail "Cargo git dependency rev pins must use full 40-character commit SHAs" + fi +fi + +if rg -n --pcre2 'git\+.*[?&](tag|branch)=' Cargo.lock >/dev/null 2>&1; then + rg -n --pcre2 'git\+.*[?&](tag|branch)=' Cargo.lock || true + fail "Cargo.lock contains git sources resolved from mutable tag/branch refs" +elif rg -n 'git\+' Cargo.lock >/dev/null 2>&1; then + echo "Cargo git sources are pinned by commit." +else + echo "No Cargo git sources found." +fi + +section "uv lock discipline" +export UV_CACHE_DIR="${UV_CACHE_DIR:-$ROOT/target/uv-cache}" +if ! uv lock --check --project .; then + fail "uv.lock is missing or not current with pyproject.toml" +else + echo "uv.lock is current." +fi + +section "GitHub Actions trigger posture" +if rg -n "pull_request_target|workflow_run" .github/workflows >/dev/null 2>&1; then + rg -n "pull_request_target|workflow_run" .github/workflows || true + fail "privileged workflow trigger found; review before running untrusted code" +else + echo "No pull_request_target or workflow_run triggers found." +fi + +section "Dependency review coverage" +if [[ ! -f .github/dependabot.yml && ! -f .github/dependabot.yaml ]]; then + fail "Dependabot configuration is missing" +else + echo "Dependabot configuration is present." +fi + +dependency_review_workflow="" +if [[ -f .github/workflows/dependency-review.yml ]]; then + dependency_review_workflow=".github/workflows/dependency-review.yml" +elif [[ -f .github/workflows/dependency-review.yaml ]]; then + dependency_review_workflow=".github/workflows/dependency-review.yaml" +fi + +if [[ -z "$dependency_review_workflow" ]]; then + fail "GitHub dependency review workflow is missing" +else + echo "GitHub dependency review workflow is present." + if ! rg -q '^\s*push:\s*$' "$dependency_review_workflow"; then + fail "GitHub dependency review workflow must run on push" + fi +fi + +section "GitHub Actions lock enforcement" +if rg -n --pcre2 '^\s*(run:\s*)?cargo (build|check|clippy|run|install)(?! --locked)' .github/workflows; then + fail "workflow Cargo build/check/run/install commands must use --locked" +else + echo "Workflow Cargo build/check/run/install commands use --locked." +fi + +if rg -n --pcre2 '^\s*(run:\s*)?uv sync(?!.*--locked)' .github/workflows; then + fail "workflow uv sync commands must use --locked" +else + echo "Workflow uv sync commands use --locked." +fi + +if rg -n --pcre2 '^\s*(run:\s*)?uv lock(?!.*--check)' .github/workflows; then + fail "workflows must not regenerate uv.lock; use uv lock --check" +else + echo "Workflows validate uv.lock instead of regenerating it." +fi + +if rg -n --pcre2 '^\s*(run:\s*)?uv run(?! --frozen)' .github/workflows; then + fail "workflow uv run commands must use --frozen" +else + echo "Workflow uv run commands use --frozen." +fi + +section "Writable workflow permissions" +workflow_files=() +while IFS= read -r file; do + workflow_files+=("$file") +done < <(collect_files .github/workflows -g '*.yml' -g '*.yaml') + +missing_top_level_permissions=() +for file in "${workflow_files[@]}"; do + if ! rg -q '^permissions:\s*$' "$file"; then + missing_top_level_permissions+=("$file") + fi +done + +if ((${#missing_top_level_permissions[@]} > 0)); then + printf '%s\n' "${missing_top_level_permissions[@]}" + fail "workflow files must declare top-level read-only permissions" +fi + +writable_permissions="$(rg -n '^\s*(contents|packages|id-token|pull-requests|actions|security-events): write\s*$' .github/workflows || true)" +unexpected_writable_permissions="$( + printf '%s\n' "$writable_permissions" | awk -F: ' + $1 == ".github/workflows/julia-update-hash.yml" && + $0 ~ /^[^:]+:[0-9]+:[[:space:]]+(contents|pull-requests): write[[:space:]]*$/ { next } + NF { print } + ' +)" + +if [[ -n "$unexpected_writable_permissions" ]]; then + printf '%s\n' "$unexpected_writable_permissions" + fail "unexpected writable workflow permission found" +elif [[ -n "$writable_permissions" ]]; then + echo "Only expected write permissions found in the tag-only Julia hash updater." +else + echo "No writable workflow permissions found." +fi + +section "External binary download verification" +if rg -n 'sha256: None|checksum not available|does not publish SHA256' crates/pecos-build/src; then + warn "some external binary installers cannot verify upstream checksums; prefer preinstalled dependencies in CI/release lanes" +else + echo "External binary download paths have checksum verification." +fi + +if ((failures > 0)); then + printf '\nDependency integrity check failed with %d issue(s).\n' "$failures" >&2 + exit 1 +fi + +printf '\nDependency integrity check passed.\n' diff --git a/scripts/native_bench/bench_pecos/Cargo.lock b/scripts/native_bench/bench_pecos/Cargo.lock index 23fa1c190..b69313378 100644 --- a/scripts/native_bench/bench_pecos/Cargo.lock +++ b/scripts/native_bench/bench_pecos/Cargo.lock @@ -97,6 +97,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -567,6 +576,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -1993,7 +2013,9 @@ dependencies = [ "tar", "thiserror 2.0.18", "toml", + "toml_edit", "xz2", + "zip", ] [[package]] @@ -3138,12 +3160,18 @@ dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 1.0.1", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -3153,15 +3181,33 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + [[package]] name = "toml_parser" version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.1", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "toml_writer" version = "1.1.1+spec-1.1.0" @@ -3999,6 +4045,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "1.0.1" @@ -4236,8 +4291,37 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap", + "memchr", + "thiserror 2.0.18", + "zopfli", +] + [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/scripts/native_bench/run.sh b/scripts/native_bench/run.sh index 72ccf62e4..2de773e0f 100755 --- a/scripts/native_bench/run.sh +++ b/scripts/native_bench/run.sh @@ -167,7 +167,7 @@ echo "" echo "--- Building PECOS standalone benchmark (Rust, Release, -C target-cpu=native) ---" PECOS_BENCH_DIR="$SCRIPT_DIR/bench_pecos" -(cd "$PECOS_BENCH_DIR" && RUSTFLAGS="${RUSTFLAGS:-} -C target-cpu=native" cargo build --release 2>&1 | tail -3) +(cd "$PECOS_BENCH_DIR" && RUSTFLAGS="${RUSTFLAGS:-} -C target-cpu=native" cargo build --locked --release 2>&1 | tail -3) PECOS_BIN="$PECOS_BENCH_DIR/target/release/bench_pecos" echo "PECOS benchmark built." echo "" diff --git a/scripts/native_bench/run_gpu.sh b/scripts/native_bench/run_gpu.sh index 3f743e979..62f8c6aaf 100755 --- a/scripts/native_bench/run_gpu.sh +++ b/scripts/native_bench/run_gpu.sh @@ -218,7 +218,7 @@ echo "" echo "--- Building PECOS GPU benchmark (Rust, wgpu + cuQuantum) ---" PECOS_BENCH_DIR="$SCRIPT_DIR/bench_pecos" (cd "$PECOS_BENCH_DIR" && RUSTFLAGS="${RUSTFLAGS:-} -C target-cpu=native" \ - cargo build --release --features gpu,cuquantum 2>&1 | tail -5) + cargo build --locked --release --features gpu,cuquantum 2>&1 | tail -5) PECOS_BIN="$PECOS_BENCH_DIR/target/release/bench_pecos" echo "PECOS GPU benchmark built." echo "" diff --git a/scripts/run.sh b/scripts/run.sh index 5629370d5..f0709dc21 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -14,19 +14,19 @@ cd "$PROJECT_ROOT" make clean build test -cargo run --bin pecos run examples/phir/bell.phir.json -s 10 -w 2 -p 0.2 -cargo run --bin pecos run examples/llvm/bell.ll -s 10 -w 2 -p 0.2 -cargo run --bin pecos run examples/phir/bell.phir.json -s 10 -w 1 -cargo run --bin pecos run examples/llvm/bell.ll -s 10 -w 1 -cargo run --bin pecos run examples/phir/bell.phir.json -s 10 -w 10 -cargo run --bin pecos run examples/llvm/bell.ll -s 10 -w 10 -cargo run --example replaying_rng --package pecos-core -cargo run --example bell_state_replay --package pecos-simulators -cargo run --example run_noisy_circ -cargo run --example biased_measurement_example -cargo run --example compare_noise_models -cargo run --example run_noisy_circ_with_general -cargo run --example general_noise_test +cargo run --locked --bin pecos run examples/phir/bell.phir.json -s 10 -w 2 -p 0.2 +cargo run --locked --bin pecos run examples/llvm/bell.ll -s 10 -w 2 -p 0.2 +cargo run --locked --bin pecos run examples/phir/bell.phir.json -s 10 -w 1 +cargo run --locked --bin pecos run examples/llvm/bell.ll -s 10 -w 1 +cargo run --locked --bin pecos run examples/phir/bell.phir.json -s 10 -w 10 +cargo run --locked --bin pecos run examples/llvm/bell.ll -s 10 -w 10 +cargo run --locked --example replaying_rng --package pecos-core +cargo run --locked --example bell_state_replay --package pecos-simulators +cargo run --locked --example run_noisy_circ +cargo run --locked --example biased_measurement_example +cargo run --locked --example compare_noise_models +cargo run --locked --example run_noisy_circ_with_general +cargo run --locked --example general_noise_test .venv/bin/python python/pecos-rslib/examples/bell_state_example.py .venv/bin/python python/pecos-rslib/examples/bell_state_simulator.py diff --git a/scripts/test_rebuild_edge_cases.sh b/scripts/test_rebuild_edge_cases.sh index 90f1f1070..a63d71fc3 100755 --- a/scripts/test_rebuild_edge_cases.sh +++ b/scripts/test_rebuild_edge_cases.sh @@ -132,7 +132,7 @@ test_corrupted_marker() { # Try to build cd "$PROJECT_ROOT" - if cargo build -p pecos-llvm-runtime --quiet 2>/dev/null; then + if cargo build --locked -p pecos-llvm-runtime --quiet 2>/dev/null; then log_info "Build succeeded despite corrupted marker" else log_error "Build failed with corrupted marker" @@ -186,7 +186,7 @@ test_permission_issues() { # Try to build (should handle gracefully) cd "$PROJECT_ROOT" - if cargo build -p pecos-llvm-runtime --quiet 2>&1 | grep -q "permission"; then + if cargo build --locked -p pecos-llvm-runtime --quiet 2>&1 | grep -q "permission"; then log_info "Permission error handled gracefully" chmod 755 "$MARKER_DIR" return 0 @@ -214,7 +214,7 @@ test_symlink_handling() { # Run build cd "$PROJECT_ROOT" - if cargo build -p pecos-llvm-runtime --quiet; then + if cargo build --locked -p pecos-llvm-runtime --quiet; then log_info "Build works with symlinked runtime library" # Check if marker was created (it shouldn't be if symlink is valid) @@ -255,7 +255,7 @@ test_cargo_home_variations() { log_info "Testing with CARGO_HOME=$CARGO_HOME" cd "$PROJECT_ROOT" - if cargo build -p pecos-llvm-runtime --quiet 2>&1; then + if cargo build --locked -p pecos-llvm-runtime --quiet 2>&1; then # Check if marker path is created in custom location local CUSTOM_MARKER="$CARGO_HOME/pecos-llvm-runtime/.needs_rebuild" if [[ -f "$CUSTOM_MARKER" ]]; then @@ -303,7 +303,7 @@ test_filesystem_full() { export CARGO_HOME="$MOUNT_POINT" cd "$PROJECT_ROOT" - if cargo build -p pecos-llvm-runtime --quiet 2>&1 | grep -q "space"; then + if cargo build --locked -p pecos-llvm-runtime --quiet 2>&1 | grep -q "space"; then log_info "Filesystem full error handled" else log_info "Build handled full filesystem scenario" @@ -324,7 +324,7 @@ main() { # Build CLI first log_info "Building PECOS CLI..." cd "$PROJECT_ROOT" - cargo build -p pecos-cli --quiet || { + cargo build --locked -p pecos-cli --quiet || { log_error "Failed to build PECOS CLI" exit 1 } diff --git a/scripts/test_rebuild_system.sh b/scripts/test_rebuild_system.sh index 56f14ba7c..a165127a2 100755 --- a/scripts/test_rebuild_system.sh +++ b/scripts/test_rebuild_system.sh @@ -99,7 +99,7 @@ test_marker_creation() { cd "$PROJECT_ROOT" # Force a rebuild by cleaning first cargo clean -p pecos-llvm-runtime --quiet - cargo build -p pecos-llvm-runtime --quiet + cargo build --locked -p pecos-llvm-runtime --quiet if check_file "$MARKER_FILE"; then log_info "Marker created for missing library" @@ -120,7 +120,7 @@ test_marker_creation() { # Case 2: Up-to-date library rm -f "$MARKER_FILE" log_info "Running cargo build with up-to-date library..." - cargo build -p pecos-llvm-runtime --quiet + cargo build --locked -p pecos-llvm-runtime --quiet if [[ -f "$MARKER_FILE" ]]; then log_error "Marker created when library is up-to-date" @@ -242,7 +242,7 @@ test_source_change_flow() { # Run cargo build log_info "Running cargo build after source change..." cd "$PROJECT_ROOT" - cargo build -p pecos-llvm-runtime --quiet + cargo build --locked -p pecos-llvm-runtime --quiet if check_file "$MARKER_FILE"; then log_info "Marker created after source change" @@ -290,7 +290,7 @@ main() { # Build the CLI first log_info "Building PECOS CLI..." cd "$PROJECT_ROOT" - cargo build -p pecos-cli --quiet || { + cargo build --locked -p pecos-cli --quiet || { log_error "Failed to build PECOS CLI" exit 1 } From 1e813be40e0eee21aebbad82f0549138ddb098f8 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 17 May 2026 19:53:42 -0600 Subject: [PATCH 02/12] Harden CI automation --- .github/workflows/cuda-build-check.yml | 2 +- .../workflows/dependency-integrity-check.yml | 1 + .github/workflows/github-actions-security.yml | 53 +++++++++++++++++++ .github/workflows/go-test.yml | 1 + .github/workflows/julia-test.yml | 1 + .github/workflows/python-test.yml | 11 +++- .github/workflows/rust-test.yml | 24 +++++++-- .github/workflows/selene-plugins.yml | 6 ++- .github/workflows/test-docs-examples.yml | 11 +++- scripts/dependency-integrity-check.sh | 39 ++++++++++++++ 10 files changed, 139 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/github-actions-security.yml diff --git a/.github/workflows/cuda-build-check.yml b/.github/workflows/cuda-build-check.yml index 1f47eccf1..02504fc1a 100644 --- a/.github/workflows/cuda-build-check.yml +++ b/.github/workflows/cuda-build-check.yml @@ -57,7 +57,7 @@ jobs: with: cache-bin: false prefix-key: v1-rust-no-bin - save-if: ${{ github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev' }} + save-if: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} - name: Install CUDA Toolkit uses: Jimver/cuda-toolkit@v0.2.35 diff --git a/.github/workflows/dependency-integrity-check.yml b/.github/workflows/dependency-integrity-check.yml index 5deadbc6a..8b1270ce2 100644 --- a/.github/workflows/dependency-integrity-check.yml +++ b/.github/workflows/dependency-integrity-check.yml @@ -37,6 +37,7 @@ jobs: with: version: "0.11.14" enable-cache: true + save-cache: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} - name: Set up Rust run: rustup show diff --git a/.github/workflows/github-actions-security.yml b/.github/workflows/github-actions-security.yml new file mode 100644 index 000000000..4eef20925 --- /dev/null +++ b/.github/workflows/github-actions-security.yml @@ -0,0 +1,53 @@ +name: GitHub Actions Security Check + +on: + push: + branches: [ "main", "master", "development", "dev" ] + paths: + - '.github/workflows/**' + - '.github/actions/**' + - '.github/dependabot.yml' + - 'action.yml' + - 'action.yaml' + pull_request: + branches: [ "main", "master", "development", "dev" ] + paths: + - '.github/workflows/**' + - '.github/actions/**' + - '.github/dependabot.yml' + - 'action.yml' + - 'action.yaml' + schedule: + - cron: "31 8 * * 1" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + zizmor: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + actions: read + steps: + - name: Checkout repository + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + with: + persist-credentials: false + + - name: Run GitHub Actions security analysis + continue-on-error: true + uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0 + with: + advanced-security: false + annotations: true + min-severity: high + min-confidence: medium + inputs: | + .github/workflows + .github/dependabot.yml diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index a4c9ffe63..adbefca87 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -59,6 +59,7 @@ jobs: with: cache-bin: false prefix-key: v1-rust-no-bin + save-if: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} workspaces: go/pecos-go-ffi - name: Bootstrap MSVC for the Cargo build path (Windows) diff --git a/.github/workflows/julia-test.yml b/.github/workflows/julia-test.yml index 40cac1dd9..9d1e18b65 100644 --- a/.github/workflows/julia-test.yml +++ b/.github/workflows/julia-test.yml @@ -95,6 +95,7 @@ jobs: with: cache-bin: false prefix-key: v1-rust-no-bin + save-if: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} workspaces: julia/pecos-julia-ffi - name: Bootstrap MSVC for the Cargo build path (Windows) diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 95d8c004b..a1e13b19d 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -90,6 +90,7 @@ jobs: with: version: "0.11.14" enable-cache: true + save-cache: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} - name: Set up Rust (Windows) if: runner.os == 'Windows' @@ -120,6 +121,7 @@ jobs: with: cache-bin: false prefix-key: v1-rust-no-bin + save-if: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} workspaces: | python/pecos-rslib python/pecos-rslib-llvm @@ -127,7 +129,7 @@ jobs: # Cache LLVM installation (fixed version, only varies by OS) - name: Cache LLVM ${{ env.LLVM_VERSION }} id: cache-llvm - uses: actions/cache@v5 + uses: actions/cache/restore@v5 with: path: ~/.pecos/deps/llvm-14 key: llvm-${{ env.LLVM_VERSION }}-${{ runner.os }}-${{ runner.arch }}-v2 @@ -140,6 +142,13 @@ jobs: - name: Ensure LLVM ${{ env.LLVM_VERSION }} run: just ci-env + - name: Save LLVM ${{ env.LLVM_VERSION }} cache + if: steps.cache-llvm.outputs.cache-hit != 'true' && github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') + uses: actions/cache/save@v5 + with: + path: ~/.pecos/deps/llvm-14 + key: ${{ steps.cache-llvm.outputs.cache-primary-key }} + - name: Install PECOS CLI run: cargo install --locked --path crates/pecos-cli --force diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml index d1187f16c..f83ad76af 100644 --- a/.github/workflows/rust-test.yml +++ b/.github/workflows/rust-test.yml @@ -69,7 +69,7 @@ jobs: with: cache-bin: false prefix-key: v1-rust-no-bin - save-if: ${{ github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev' }} + save-if: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} - name: Install CUDA Toolkit uses: Jimver/cuda-toolkit@v0.2.35 @@ -82,7 +82,7 @@ jobs: - name: Cache LLVM ${{ env.LLVM_VERSION }} id: cache-llvm - uses: actions/cache@v5 + uses: actions/cache/restore@v5 with: path: ~/.pecos/deps/llvm-14 key: llvm-${{ env.LLVM_VERSION }}-${{ runner.os }}-${{ runner.arch }}-v2 @@ -90,6 +90,13 @@ jobs: - name: Ensure LLVM ${{ env.LLVM_VERSION }} run: just ci-env + - name: Save LLVM ${{ env.LLVM_VERSION }} cache + if: steps.cache-llvm.outputs.cache-hit != 'true' && github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') + uses: actions/cache/save@v5 + with: + path: ~/.pecos/deps/llvm-14 + key: ${{ steps.cache-llvm.outputs.cache-primary-key }} + - name: Check formatting run: cargo fmt --all -- --check @@ -115,7 +122,7 @@ jobs: with: cache-bin: false prefix-key: v1-rust-no-bin - save-if: ${{ github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev' }} + save-if: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} - name: Check formatting run: cargo fmt --all -- --check @@ -185,11 +192,11 @@ jobs: with: cache-bin: false prefix-key: v1-rust-no-bin - save-if: ${{ github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev' }} + save-if: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} - name: Cache LLVM ${{ env.LLVM_VERSION }} id: cache-llvm - uses: actions/cache@v5 + uses: actions/cache/restore@v5 with: path: ~/.pecos/deps/llvm-14 key: llvm-${{ env.LLVM_VERSION }}-${{ runner.os }}-${{ runner.arch }}-v2 @@ -206,6 +213,13 @@ jobs: - name: Ensure LLVM ${{ env.LLVM_VERSION }} run: just ci-env + - name: Save LLVM ${{ env.LLVM_VERSION }} cache + if: steps.cache-llvm.outputs.cache-hit != 'true' && github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') + uses: actions/cache/save@v5 + with: + path: ~/.pecos/deps/llvm-14 + key: ${{ steps.cache-llvm.outputs.cache-primary-key }} + - name: Install CUDA Toolkit (Linux) if: matrix.os == 'ubuntu-latest' uses: Jimver/cuda-toolkit@v0.2.35 diff --git a/.github/workflows/selene-plugins.yml b/.github/workflows/selene-plugins.yml index 2b493af3d..899723f3f 100644 --- a/.github/workflows/selene-plugins.yml +++ b/.github/workflows/selene-plugins.yml @@ -57,6 +57,7 @@ jobs: with: version: "0.11.14" enable-cache: true + save-cache: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} - name: Set up Rust run: rustup show @@ -67,7 +68,7 @@ jobs: cache-bin: false key: ${{ matrix.os }} prefix-key: v1-rust-no-bin - save-if: ${{ github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev' }} + save-if: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} - name: Build and install Selene plugins (Unix) if: runner.os != 'Windows' @@ -151,6 +152,7 @@ jobs: with: version: "0.11.14" enable-cache: true + save-cache: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} - name: Set up Rust run: rustup show @@ -161,7 +163,7 @@ jobs: cache-bin: false key: ${{ matrix.os }}-${{ matrix.plugin.name }} prefix-key: v1-rust-no-bin - save-if: ${{ github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev' }} + save-if: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} - name: Build Rust library (Unix) if: runner.os != 'Windows' diff --git a/.github/workflows/test-docs-examples.yml b/.github/workflows/test-docs-examples.yml index 45da3d5ad..6361b301e 100644 --- a/.github/workflows/test-docs-examples.yml +++ b/.github/workflows/test-docs-examples.yml @@ -41,6 +41,7 @@ jobs: with: version: "0.11.14" enable-cache: true + save-cache: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} - name: Set up Rust run: | @@ -57,13 +58,14 @@ jobs: with: cache-bin: false prefix-key: v1-rust-no-bin + save-if: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} workspaces: | python/pecos-rslib python/pecos-rslib-llvm - name: Cache LLVM ${{ env.LLVM_VERSION }} id: cache-llvm - uses: actions/cache@v5 + uses: actions/cache/restore@v5 with: path: ~/.pecos/deps/llvm-14 key: llvm-${{ env.LLVM_VERSION }}-${{ runner.os }}-${{ runner.arch }}-v2 @@ -71,6 +73,13 @@ jobs: - name: Ensure LLVM ${{ env.LLVM_VERSION }} run: just ci-env + - name: Save LLVM ${{ env.LLVM_VERSION }} cache + if: steps.cache-llvm.outputs.cache-hit != 'true' && github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') + uses: actions/cache/save@v5 + with: + path: ~/.pecos/deps/llvm-14 + key: ${{ steps.cache-llvm.outputs.cache-primary-key }} + - name: Install dependencies and build run: | uv lock --check --project . diff --git a/scripts/dependency-integrity-check.sh b/scripts/dependency-integrity-check.sh index 4906594ad..c21d350ad 100755 --- a/scripts/dependency-integrity-check.sh +++ b/scripts/dependency-integrity-check.sh @@ -205,6 +205,45 @@ else fi fi +if [[ ! -f .github/workflows/github-actions-security.yml && ! -f .github/workflows/github-actions-security.yaml ]]; then + fail "GitHub Actions security analysis workflow is missing" +else + echo "GitHub Actions security analysis workflow is present." +fi + +section "GitHub Actions cache write posture" +cache_policy_failures=() +while IFS=: read -r file line _; do + if ! sed -n "${line},$((line + 16))p" "$file" | rg -q "save-if:.*github\.event_name == 'push'.*github\.ref_name == 'main'"; then + cache_policy_failures+=("$file:$line rust-cache save-if must be restricted to trusted branch pushes") + fi +done < <(rg -n 'uses:\s+Swatinem/rust-cache@' .github/workflows || true) + +while IFS=: read -r file line _; do + setup_uv_block="$(sed -n "${line},$((line + 16))p" "$file")" + if printf '%s\n' "$setup_uv_block" | rg -q 'enable-cache:\s*true' && + ! printf '%s\n' "$setup_uv_block" | rg -q "save-cache:.*github\.event_name == 'push'.*github\.ref_name == 'main'"; then + cache_policy_failures+=("$file:$line setup-uv save-cache must be restricted to trusted branch pushes") + fi +done < <(rg -n 'uses:\s+astral-sh/setup-uv@' .github/workflows || true) + +while IFS=: read -r file line _; do + cache_policy_failures+=("$file:$line use actions/cache/restore plus an explicitly gated actions/cache/save step") +done < <(rg -n 'uses:\s+actions/cache@' .github/workflows || true) + +while IFS=: read -r file line _; do + if ! sed -n "$((line - 2)),$((line + 2))p" "$file" | rg -q "if:.*github\.event_name == 'push'.*github\.ref_name == 'main'"; then + cache_policy_failures+=("$file:$line actions/cache/save must be restricted to trusted branch pushes") + fi +done < <(rg -n 'uses:\s+actions/cache/save@' .github/workflows || true) + +if ((${#cache_policy_failures[@]} > 0)); then + printf '%s\n' "${cache_policy_failures[@]}" + fail "cache writers must not save reusable caches from PR or untrusted branch runs" +else + echo "Cache saves are restricted to trusted branch pushes." +fi + section "GitHub Actions lock enforcement" if rg -n --pcre2 '^\s*(run:\s*)?cargo (build|check|clippy|run|install)(?! --locked)' .github/workflows; then fail "workflow Cargo build/check/run/install commands must use --locked" From 01691bb81cdde54f60b3a0781ad2d8137a3d548b Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 17 May 2026 20:08:07 -0600 Subject: [PATCH 03/12] Add security automation --- .github/workflows/codeql.yml | 66 +++++++++ .github/workflows/cuda-build-check.yml | 7 +- .github/workflows/github-actions-security.yml | 5 +- .github/workflows/julia-release.yml | 63 +++++---- .github/workflows/julia-update-hash.yml | 10 +- .github/workflows/osv-scanner.yml | 25 ++++ .github/workflows/python-release.yml | 28 ++-- .github/workflows/python-test.yml | 14 +- .github/workflows/rust-test.yml | 37 +++-- .github/workflows/test-docs-examples.yml | 3 +- .github/zizmor.yml | 8 ++ julia/PECOS.jl/deps/build_tarballs.jl | 8 +- scripts/ci/ensure-rust.sh | 48 +++++++ scripts/ci/unsafe-allowlist.txt | 21 +++ scripts/dependency-integrity-check.sh | 130 +++++++++++++++++- 15 files changed, 397 insertions(+), 76 deletions(-) create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/osv-scanner.yml create mode 100644 .github/zizmor.yml create mode 100644 scripts/ci/ensure-rust.sh create mode 100644 scripts/ci/unsafe-allowlist.txt diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..2784c00c2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,66 @@ +name: CodeQL + +permissions: + actions: read + contents: read + security-events: write + +on: + push: + branches: [ "main", "master", "development", "dev" ] + pull_request: + branches: [ "main", "master", "development", "dev" ] + schedule: + - cron: "29 4 * * 1" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze ${{ matrix.language }} + runs-on: ubuntu-latest + timeout-minutes: 360 + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: "" + - language: c-cpp + build-mode: none + - language: go + build-mode: autobuild + - language: python + build-mode: "" + - language: rust + build-mode: none + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Initialize CodeQL + if: matrix.build-mode == '' + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + + - name: Initialize CodeQL with build mode + if: matrix.build-mode != '' + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + + - name: Autobuild + if: matrix.build-mode == 'autobuild' + uses: github/codeql-action/autobuild@v4 + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/cuda-build-check.yml b/.github/workflows/cuda-build-check.yml index 02504fc1a..f2789ebcd 100644 --- a/.github/workflows/cuda-build-check.yml +++ b/.github/workflows/cuda-build-check.yml @@ -45,12 +45,13 @@ jobs: - name: Install Rust run: | - curl https://sh.rustup.rs -sSf | sh -s -- -y - echo "$HOME/.cargo/bin" >> $GITHUB_PATH + bash scripts/ci/ensure-rust.sh stable minimal export PATH="$HOME/.cargo/bin:$PATH" - name: Set up Rust - run: rustup override set stable && rustup update + run: | + rustup override set stable + rustup show - name: Cache Rust uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/github-actions-security.yml b/.github/workflows/github-actions-security.yml index 4eef20925..cd9a7b30c 100644 --- a/.github/workflows/github-actions-security.yml +++ b/.github/workflows/github-actions-security.yml @@ -7,6 +7,8 @@ on: - '.github/workflows/**' - '.github/actions/**' - '.github/dependabot.yml' + - '.github/zizmor.yml' + - '.github/zizmor.yaml' - 'action.yml' - 'action.yaml' pull_request: @@ -15,6 +17,8 @@ on: - '.github/workflows/**' - '.github/actions/**' - '.github/dependabot.yml' + - '.github/zizmor.yml' + - '.github/zizmor.yaml' - 'action.yml' - 'action.yaml' schedule: @@ -41,7 +45,6 @@ jobs: persist-credentials: false - name: Run GitHub Actions security analysis - continue-on-error: true uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0 with: advanced-security: false diff --git a/.github/workflows/julia-release.yml b/.github/workflows/julia-release.yml index 238e988a5..6741e5c50 100644 --- a/.github/workflows/julia-release.yml +++ b/.github/workflows/julia-release.yml @@ -62,15 +62,20 @@ jobs: steps: - name: Check if should run on PR push id: check + env: + EVENT_NAME: ${{ github.event_name }} + REF: ${{ github.ref }} + REF_NAME: ${{ github.ref_name }} + TRIGGER_ON_PR_PUSH_VALUE: ${{ env.TRIGGER_ON_PR_PUSH }} run: | # Always run on tag pushes (jl-* tags trigger releases) - if [ "${{ github.event_name }}" = "push" ] && [[ "${{ github.ref }}" == refs/tags/jl-* ]]; then + if [ "$EVENT_NAME" = "push" ] && [[ "$REF" == refs/tags/jl-* ]]; then echo "run=true" >> $GITHUB_OUTPUT # Always run on pushes to main branches - elif [ "${{ github.event_name }}" = "push" ] && [[ "${{ github.ref_name }}" =~ ^(main|master|dev|development)$ ]]; then + elif [ "$EVENT_NAME" = "push" ] && [[ "$REF_NAME" =~ ^(main|master|dev|development)$ ]]; then echo "run=true" >> $GITHUB_OUTPUT # For PRs, check the TRIGGER_ON_PR_PUSH setting - elif [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ env.TRIGGER_ON_PR_PUSH }}" = "true" ]; then + elif [ "$EVENT_NAME" = "pull_request" ] && [ "$TRIGGER_ON_PR_PUSH_VALUE" = "true" ]; then echo "run=true" >> $GITHUB_OUTPUT else echo "run=false" >> $GITHUB_OUTPUT @@ -509,18 +514,18 @@ jobs: find release-bundle -type f | sort - name: Generate Artifacts.toml + env: + RELEASE_REF_NAME: ${{ github.ref_name }} run: | cd release-bundle # Create Artifacts.toml with actual checksums - cat > ../julia/PECOS.jl/Artifacts.toml << 'EOF' - # Auto-generated by julia-release workflow - # For release: ${{ github.ref_name }} - - [PECOS_julia] - git-tree-sha1 = "0000000000000000000000000000000000000000" # Tree hash will be computed on first use - - EOF + { + printf '%s\n' '# Auto-generated by julia-release workflow' + printf '# For release: %s\n\n' "$RELEASE_REF_NAME" + printf '%s\n' '[PECOS_julia]' + printf '%s\n\n' 'git-tree-sha1 = "0000000000000000000000000000000000000000" # Tree hash will be computed on first use' + } > ../julia/PECOS.jl/Artifacts.toml # Add download entries for each platform for tarball in binaries/*.tar.gz; do @@ -529,12 +534,11 @@ jobs: platform=$(echo "$filename" | sed 's/pecos_julia-//; s/.tar.gz//') sha256=$(sha256sum "$tarball" | cut -d' ' -f1) - cat >> ../julia/PECOS.jl/Artifacts.toml << EOF - [[PECOS_julia.download]] - url = "https://github.com/PECOS-packages/PECOS/releases/download/${{ github.ref_name }}/$filename" - sha256 = "$sha256" - - EOF + { + printf '%s\n' '[[PECOS_julia.download]]' + printf 'url = "https://github.com/PECOS-packages/PECOS/releases/download/%s/%s"\n' "$RELEASE_REF_NAME" "$filename" + printf 'sha256 = "%s"\n\n' "$sha256" + } >> ../julia/PECOS.jl/Artifacts.toml fi done @@ -542,6 +546,9 @@ jobs: cat ../julia/PECOS.jl/Artifacts.toml - name: Create submission instructions + env: + RELEASE_SHA: ${{ github.sha }} + RELEASE_REF_NAME: ${{ github.ref_name }} run: | cat > release-bundle/SUBMISSION_INSTRUCTIONS.md << 'EOF' # Julia Package Submission Instructions @@ -591,12 +598,14 @@ jobs: - Windows x86_64 ## Version Info - - - Version: 0.1.0 - - Commit: ${{ github.sha }} - - Branch: ${{ github.ref_name }} - - Date: $(date -u +%Y-%m-%d) EOF + { + printf '\n' + printf '%s\n' '- Version: 0.1.0' + printf -- '- Commit: %s\n' "$RELEASE_SHA" + printf -- '- Branch: %s\n' "$RELEASE_REF_NAME" + printf -- '- Date: %s\n' "$(date -u +%Y-%m-%d)" + } >> release-bundle/SUBMISSION_INSTRUCTIONS.md - name: Create tarball of entire bundle run: | @@ -613,9 +622,11 @@ jobs: - name: Determine if pre-release id: check-prerelease if: startsWith(github.ref, 'refs/tags/jl-') + env: + RELEASE_REF_NAME: ${{ github.ref_name }} run: | # Extract version from tag (remove jl- prefix) - VERSION="${{ github.ref_name }}" + VERSION="$RELEASE_REF_NAME" VERSION="${VERSION#jl-}" # Check if it contains a hyphen (pre-release indicator) @@ -650,6 +661,8 @@ jobs: - name: Commit Artifacts.toml to branch if: startsWith(github.ref, 'refs/tags/jl-') + env: + RELEASE_REF_NAME: ${{ github.ref_name }} run: | # Configure git git config --local user.email "action@github.com" @@ -657,7 +670,7 @@ jobs: # Add and commit the Artifacts.toml git add julia/PECOS.jl/Artifacts.toml - git commit -m "Add Artifacts.toml for ${{ github.ref_name }}" || echo "No changes to commit" + git commit -m "Add Artifacts.toml for $RELEASE_REF_NAME" || echo "No changes to commit" # Push to the tag's branch - git push origin HEAD:${{ github.ref_name }}-artifacts || echo "Push failed" + git push origin "HEAD:${RELEASE_REF_NAME}-artifacts" || echo "Push failed" diff --git a/.github/workflows/julia-update-hash.yml b/.github/workflows/julia-update-hash.yml index 36017a19c..4f6f53163 100644 --- a/.github/workflows/julia-update-hash.yml +++ b/.github/workflows/julia-update-hash.yml @@ -34,16 +34,20 @@ jobs: - name: Get commit SHA id: get-sha + env: + REF_NAME: ${{ github.ref_name }} run: | # Get the commit SHA that the tag points to - COMMIT_SHA=$(git rev-list -n 1 ${{ github.ref_name }}) + COMMIT_SHA=$(git rev-list -n 1 "$REF_NAME") echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT - echo "Tag ${{ github.ref_name }} points to commit: $COMMIT_SHA" + echo "Tag $REF_NAME points to commit: $COMMIT_SHA" - name: Update build_tarballs.jl + env: + COMMIT_SHA: ${{ steps.get-sha.outputs.commit_sha }} run: | # Update the commit reference in build_tarballs.jl - sed -i 's/get(ENV, "PECOS_BUILD_COMMIT", "main")/"${{ steps.get-sha.outputs.commit_sha }}"/' \ + sed -i -E "s/get\(ENV, \"PECOS_BUILD_COMMIT\", \"[^\"]+\"\)/\"$COMMIT_SHA\"/" \ julia/PECOS.jl/deps/build_tarballs.jl # Show the change diff --git a/.github/workflows/osv-scanner.yml b/.github/workflows/osv-scanner.yml new file mode 100644 index 000000000..378b3fde1 --- /dev/null +++ b/.github/workflows/osv-scanner.yml @@ -0,0 +1,25 @@ +name: OSV Scanner + +permissions: + actions: read + contents: read + security-events: write + +on: + push: + branches: [ "main", "master", "development", "dev" ] + schedule: + - cron: "41 4 * * 2" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + scan: + uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.3.8 + with: + scan-args: |- + --recursive + ./ diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml index a85f8a7d0..63cc2384e 100644 --- a/.github/workflows/python-release.yml +++ b/.github/workflows/python-release.yml @@ -55,15 +55,20 @@ jobs: steps: - name: Check if should run on PR push id: check + env: + EVENT_NAME: ${{ github.event_name }} + REF: ${{ github.ref }} + REF_NAME: ${{ github.ref_name }} + TRIGGER_ON_PR_PUSH_VALUE: ${{ env.TRIGGER_ON_PR_PUSH }} run: | # Always run on tag pushes (py-* tags trigger releases) - if [ "${{ github.event_name }}" = "push" ] && [[ "${{ github.ref }}" == refs/tags/py-* ]]; then + if [ "$EVENT_NAME" = "push" ] && [[ "$REF" == refs/tags/py-* ]]; then echo "run=true" >> $GITHUB_OUTPUT # Always run on pushes to main branches - elif [ "${{ github.event_name }}" = "push" ] && [[ "${{ github.ref_name }}" =~ ^(main|master|dev|development)$ ]]; then + elif [ "$EVENT_NAME" = "push" ] && [[ "$REF_NAME" =~ ^(main|master|dev|development)$ ]]; then echo "run=true" >> $GITHUB_OUTPUT # For PRs, check the TRIGGER_ON_PR_PUSH setting - elif [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ env.TRIGGER_ON_PR_PUSH }}" = "true" ]; then + elif [ "$EVENT_NAME" = "pull_request" ] && [ "$TRIGGER_ON_PR_PUSH_VALUE" = "true" ]; then echo "run=true" >> $GITHUB_OUTPUT else echo "run=false" >> $GITHUB_OUTPUT @@ -201,8 +206,8 @@ jobs: CUDA_PATH=/usr/local/cuda-12.6 MATURIN_PEP517_ARGS="--locked --features=extension-module,mwpf" CIBW_BEFORE_ALL_LINUX: | - curl -sSf https://sh.rustup.rs | sh -s -- -y - source $HOME/.cargo/env + bash scripts/ci/ensure-rust.sh stable minimal + export PATH=$HOME/.cargo/bin:$PATH dnf install libffi-devel -y # Install CUDA Toolkit for GPU support on x86_64 (compile-time only, no GPU needed) if [ "${{ matrix.install_cuda }}" = "true" ]; then @@ -234,8 +239,8 @@ jobs: SDKROOT=$(xcrun --show-sdk-path) MATURIN_PEP517_ARGS="--locked --features=extension-module,mwpf" CIBW_BEFORE_ALL_MACOS: | - curl -sSf https://sh.rustup.rs | sh -s -- -y - source $HOME/.cargo/env + bash scripts/ci/ensure-rust.sh stable minimal + export PATH=$HOME/.cargo/bin:$PATH rustup update cargo run --locked --release -p pecos-cli -- install llvm --force cargo run --locked --release -p pecos-cli -- install cmake --force @@ -293,8 +298,8 @@ jobs: PATH=$HOME/.cargo/bin:$HOME/.pecos/deps/llvm-14/bin:$PATH LLVM_SYS_140_PREFIX=$HOME/.pecos/deps/llvm-14 CIBW_BEFORE_ALL_LINUX: | - curl -sSf https://sh.rustup.rs | sh -s -- -y - source $HOME/.cargo/env + bash scripts/ci/ensure-rust.sh stable minimal + export PATH=$HOME/.cargo/bin:$PATH dnf install libffi-devel -y cargo run --locked --release -p pecos-cli -- install llvm --force CIBW_REPAIR_WHEEL_COMMAND_LINUX: > @@ -306,7 +311,10 @@ jobs: MACOSX_DEPLOYMENT_TARGET=13.2 SDKROOT=$(xcrun --show-sdk-path) CIBW_BEFORE_ALL_MACOS: | - source $HOME/.cargo/env 2>/dev/null || { curl -sSf https://sh.rustup.rs | sh -s -- -y && source $HOME/.cargo/env; } + if ! command -v cargo >/dev/null 2>&1; then + bash scripts/ci/ensure-rust.sh stable minimal + export PATH=$HOME/.cargo/bin:$PATH + fi if [ ! -d "$HOME/.pecos/deps/llvm-14/bin" ]; then rustup update cargo run --locked --release -p pecos-cli -- install llvm --force diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index a1e13b19d..3f6a1fccc 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -96,8 +96,9 @@ jobs: if: runner.os == 'Windows' shell: pwsh run: | - curl -sSf -o rustup-init.exe https://win.rustup.rs - ./rustup-init.exe -y --default-toolchain stable --default-host x86_64-pc-windows-msvc --profile minimal + rustup set default-host x86_64-pc-windows-msvc + rustup toolchain install stable --profile minimal + rustup default stable "$HOME\.cargo\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append $env:Path += ";$HOME\.cargo\bin" rustup show @@ -105,8 +106,7 @@ jobs: - name: Set up Rust (Unix) if: runner.os != 'Windows' run: | - curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable --profile minimal - echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + bash scripts/ci/ensure-rust.sh stable minimal export PATH="$HOME/.cargo/bin:$PATH" rustup show @@ -223,9 +223,11 @@ jobs: - name: Set up Rust (MSVC) shell: pwsh run: | - curl.exe -sSf -o rustup-init.exe https://win.rustup.rs - ./rustup-init.exe -y --default-toolchain stable --default-host x86_64-pc-windows-msvc --profile minimal + rustup set default-host x86_64-pc-windows-msvc + rustup toolchain install stable --profile minimal + rustup default stable "$HOME\.cargo\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + rustup show - name: Assert no generated .cargo/config.toml (fail closed) shell: pwsh diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml index f83ad76af..ae96ac9fa 100644 --- a/.github/workflows/rust-test.yml +++ b/.github/workflows/rust-test.yml @@ -52,14 +52,12 @@ jobs: sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc sudo apt-get clean - - name: Install Rust (for local testing) + - name: Set up Rust run: | - curl https://sh.rustup.rs -sSf | sh -s -- -y - echo "$HOME/.cargo/bin" >> $GITHUB_PATH + bash scripts/ci/ensure-rust.sh stable minimal export PATH="$HOME/.cargo/bin:$PATH" - - - name: Set up Rust - run: rustup override set stable && rustup update + rustup override set stable + rustup show - name: Install just uses: extractions/setup-just@v4 @@ -108,14 +106,12 @@ jobs: steps: - uses: actions/checkout@v6 - - name: Install Rust (for local testing) + - name: Set up Rust run: | - curl https://sh.rustup.rs -sSf | sh -s -- -y - echo "$HOME/.cargo/bin" >> $GITHUB_PATH + bash scripts/ci/ensure-rust.sh stable minimal export PATH="$HOME/.cargo/bin:$PATH" - - - name: Set up Rust - run: rustup override set stable && rustup update + rustup override set stable + rustup show - name: Cache Rust uses: Swatinem/rust-cache@v2 @@ -166,23 +162,22 @@ jobs: sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc sudo apt-get clean - - name: Install Rust (for local testing) + - name: Set up Rust (Windows) if: runner.os == 'Windows' run: | - curl -sSf -o rustup-init.exe https://win.rustup.rs - ./rustup-init.exe -y --default-toolchain stable --default-host x86_64-pc-windows-msvc --profile minimal + rustup set default-host x86_64-pc-windows-msvc + rustup toolchain install stable --profile minimal + rustup default stable echo "$HOME\.cargo\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append $env:Path += ";$HOME\.cargo\bin" + rustup show - - name: Install Rust (for local testing) + - name: Set up Rust (Unix) if: runner.os != 'Windows' run: | - curl https://sh.rustup.rs -sSf | sh -s -- -y - echo "$HOME/.cargo/bin" >> $GITHUB_PATH + bash scripts/ci/ensure-rust.sh stable minimal export PATH="$HOME/.cargo/bin:$PATH" - - - name: Set up Rust - run: rustup show + rustup show - name: Install just uses: extractions/setup-just@v4 diff --git a/.github/workflows/test-docs-examples.yml b/.github/workflows/test-docs-examples.yml index 6361b301e..758610321 100644 --- a/.github/workflows/test-docs-examples.yml +++ b/.github/workflows/test-docs-examples.yml @@ -45,8 +45,7 @@ jobs: - name: Set up Rust run: | - curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable --profile minimal - echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + bash scripts/ci/ensure-rust.sh stable minimal export PATH="$HOME/.cargo/bin:$PATH" rustup show diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 000000000..43cefe622 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,8 @@ +rules: + unpinned-uses: + config: + policies: + # Existing workflows use version tags for most actions. Keep this + # blocking scan focused on executable workflow risks for now; full + # SHA pinning can be promoted separately. + "*": ref-pin diff --git a/julia/PECOS.jl/deps/build_tarballs.jl b/julia/PECOS.jl/deps/build_tarballs.jl index 8c157af89..4373aafdb 100644 --- a/julia/PECOS.jl/deps/build_tarballs.jl +++ b/julia/PECOS.jl/deps/build_tarballs.jl @@ -43,9 +43,9 @@ if [[ "${target}" == *-mingw* ]]; then ./rustup-init.exe -y --profile minimal --default-toolchain stable export PATH="$HOME/.cargo/bin:$PATH" else - # Unix-like: Use rustup - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal - source $HOME/.cargo/env + # Unix-like: use rustup-init with its SHA-256 sidecar instead of curl-pipe-shell. + bash scripts/ci/ensure-rust.sh stable minimal + export PATH="$HOME/.cargo/bin:$PATH" fi # Verify Rust installation @@ -61,7 +61,7 @@ if [[ -n "${CARGO_BUILD_TARGET}" ]]; then fi # BinaryBuilder handles --target automatically with CARGO_BUILD_TARGET -cargo build --release +cargo build --locked --release # Find and install the built library cd target/release diff --git a/scripts/ci/ensure-rust.sh b/scripts/ci/ensure-rust.sh new file mode 100644 index 000000000..66c851e2d --- /dev/null +++ b/scripts/ci/ensure-rust.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +toolchain="${1:-stable}" +profile="${2:-minimal}" + +if command -v rustup >/dev/null 2>&1; then + rustup toolchain install "$toolchain" --profile "$profile" +else + case "$(uname -s)-$(uname -m)" in + Linux-x86_64) + target="x86_64-unknown-linux-gnu" + ;; + Linux-aarch64 | Linux-arm64) + target="aarch64-unknown-linux-gnu" + ;; + Darwin-x86_64) + target="x86_64-apple-darwin" + ;; + Darwin-arm64 | Darwin-aarch64) + target="aarch64-apple-darwin" + ;; + *) + echo "Unsupported platform for rustup bootstrap: $(uname -s)-$(uname -m)" >&2 + exit 1 + ;; + esac + + tmp_dir="$(mktemp -d)" + trap 'rm -rf "$tmp_dir"' EXIT + + base_url="https://static.rust-lang.org/rustup/dist/${target}" + curl --proto '=https' --tlsv1.2 -fsSLo "$tmp_dir/rustup-init" "$base_url/rustup-init" + curl --proto '=https' --tlsv1.2 -fsSLo "$tmp_dir/rustup-init.sha256" "$base_url/rustup-init.sha256" + + if command -v sha256sum >/dev/null 2>&1; then + (cd "$tmp_dir" && sha256sum -c rustup-init.sha256) + else + (cd "$tmp_dir" && shasum -a 256 -c rustup-init.sha256) + fi + + chmod +x "$tmp_dir/rustup-init" + "$tmp_dir/rustup-init" -y --profile "$profile" --default-toolchain "$toolchain" --no-modify-path +fi + +if [[ -n "${GITHUB_PATH:-}" ]]; then + echo "$HOME/.cargo/bin" >>"$GITHUB_PATH" +fi diff --git a/scripts/ci/unsafe-allowlist.txt b/scripts/ci/unsafe-allowlist.txt new file mode 100644 index 000000000..4282e3f9f --- /dev/null +++ b/scripts/ci/unsafe-allowlist.txt @@ -0,0 +1,21 @@ +# Crates that currently contain Rust unsafe code or native FFI boundaries. +# New entries should be intentional and reviewed as native/FFI boundary changes. +crates/pecos-chromobius +crates/pecos-core +crates/pecos-cppsparsestab +crates/pecos-cuquantum +crates/pecos-cuquantum-sys +crates/pecos-foreign +crates/pecos-gpu-sims +crates/pecos-ldpc-decoders +crates/pecos-llvm +crates/pecos-pymatching +crates/pecos-qis +crates/pecos-qis-ffi +crates/pecos-simulators +crates/pecos-tesseract +exp/pecos-experimental +go/pecos-go-ffi +julia/pecos-julia-ffi +python/pecos-rslib +python/pecos-rslib-llvm diff --git a/scripts/dependency-integrity-check.sh b/scripts/dependency-integrity-check.sh index c21d350ad..24ccca1c8 100755 --- a/scripts/dependency-integrity-check.sh +++ b/scripts/dependency-integrity-check.sh @@ -26,6 +26,18 @@ require_tool() { fi } +list_contains() { + local needle="$1" + shift + local item + for item in "$@"; do + if [[ "$item" == "$needle" ]]; then + return 0 + fi + done + return 1 +} + collect_files() { rg --files "$@" } @@ -166,6 +178,76 @@ else echo "No Cargo git sources found." fi +section "Rust unsafe boundary allowlist" +unsafe_allowlist_file="scripts/ci/unsafe-allowlist.txt" +if [[ ! -f "$unsafe_allowlist_file" ]]; then + fail "$unsafe_allowlist_file is missing" +else + unsafe_tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/pecos-unsafe.XXXXXX")" + trap 'rm -rf "$unsafe_tmp_dir"' EXIT + allowed_unsafe_roots_file="$unsafe_tmp_dir/allowed" + actual_unsafe_roots_file="$unsafe_tmp_dir/actual" + unsafe_files_without_manifest_file="$unsafe_tmp_dir/no-manifest" + + sed 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' "$unsafe_allowlist_file" | sort -u >"$allowed_unsafe_roots_file" + : >"$actual_unsafe_roots_file" + : >"$unsafe_files_without_manifest_file" + + while IFS= read -r unsafe_file; do + dir="$(dirname "$unsafe_file")" + root="" + while [[ "$dir" != "." && "$dir" != "/" ]]; do + if [[ -f "$dir/Cargo.toml" ]]; then + root="$dir" + break + fi + dir="$(dirname "$dir")" + done + + if [[ -z "$root" ]]; then + printf '%s\n' "$unsafe_file" >>"$unsafe_files_without_manifest_file" + else + printf '%s\n' "$root" >>"$actual_unsafe_roots_file" + fi + done < <( + rg -l --pcre2 '\bunsafe\b' \ + crates python go julia exp \ + --glob '*.rs' \ + --glob '*.c' \ + --glob '*.cpp' \ + --glob '*.h' \ + --glob '*.hpp' \ + --glob '!crates/pecos-pymatching/tests/pymatching/**' \ + || true + ) + + sort -u "$actual_unsafe_roots_file" >"$actual_unsafe_roots_file.sorted" + mv "$actual_unsafe_roots_file.sorted" "$actual_unsafe_roots_file" + sort -u "$unsafe_files_without_manifest_file" >"$unsafe_files_without_manifest_file.sorted" + mv "$unsafe_files_without_manifest_file.sorted" "$unsafe_files_without_manifest_file" + + unexpected_unsafe_roots="$(comm -23 "$actual_unsafe_roots_file" "$allowed_unsafe_roots_file" || true)" + stale_unsafe_roots="$(comm -13 "$actual_unsafe_roots_file" "$allowed_unsafe_roots_file" || true)" + + if [[ -s "$unsafe_files_without_manifest_file" ]]; then + cat "$unsafe_files_without_manifest_file" + fail "unsafe usage found outside a Cargo package" + fi + if [[ -n "$unexpected_unsafe_roots" ]]; then + printf '%s\n' "$unexpected_unsafe_roots" + fail "new unsafe/FFI crate roots must be added to $unsafe_allowlist_file" + fi + if [[ -n "$stale_unsafe_roots" ]]; then + printf '%s\n' "$stale_unsafe_roots" + fail "stale unsafe/FFI allowlist entries should be removed" + fi + if [[ ! -s "$unsafe_files_without_manifest_file" && + -z "$unexpected_unsafe_roots" && + -z "$stale_unsafe_roots" ]]; then + echo "Unsafe/FFI crate roots match the reviewed allowlist." + fi +fi + section "uv lock discipline" export UV_CACHE_DIR="${UV_CACHE_DIR:-$ROOT/target/uv-cache}" if ! uv lock --check --project .; then @@ -182,6 +264,20 @@ else echo "No pull_request_target or workflow_run triggers found." fi +section "Remote shell bootstrap posture" +remote_shell_bootstraps="$( + rg -n --pcre2 '(curl|wget)[^\n|]*\|[^\n]*(sh|bash)' \ + .github/workflows \ + julia/PECOS.jl/deps/build_tarballs.jl \ + || true +)" +if [[ -n "$remote_shell_bootstraps" ]]; then + printf '%s\n' "$remote_shell_bootstraps" + fail "workflow and release build scripts must not pipe remote downloads into a shell" +else + echo "Workflow and release build scripts avoid curl-pipe-shell bootstraps." +fi + section "Dependency review coverage" if [[ ! -f .github/dependabot.yml && ! -f .github/dependabot.yaml ]]; then fail "Dependabot configuration is missing" @@ -205,10 +301,38 @@ else fi fi -if [[ ! -f .github/workflows/github-actions-security.yml && ! -f .github/workflows/github-actions-security.yaml ]]; then +actions_security_workflow="" +if [[ -f .github/workflows/github-actions-security.yml ]]; then + actions_security_workflow=".github/workflows/github-actions-security.yml" +elif [[ -f .github/workflows/github-actions-security.yaml ]]; then + actions_security_workflow=".github/workflows/github-actions-security.yaml" +fi + +if [[ -z "$actions_security_workflow" ]]; then fail "GitHub Actions security analysis workflow is missing" else echo "GitHub Actions security analysis workflow is present." + if rg -q 'continue-on-error:\s*true' "$actions_security_workflow"; then + fail "GitHub Actions security analysis workflow must be blocking" + fi +fi + +if [[ ! -f .github/zizmor.yml && ! -f .github/zizmor.yaml ]]; then + fail "GitHub Actions security analysis configuration is missing" +else + echo "GitHub Actions security analysis configuration is present." +fi + +if [[ ! -f .github/workflows/codeql.yml && ! -f .github/workflows/codeql.yaml ]]; then + fail "CodeQL code scanning workflow is missing" +else + echo "CodeQL code scanning workflow is present." +fi + +if [[ ! -f .github/workflows/osv-scanner.yml && ! -f .github/workflows/osv-scanner.yaml ]]; then + fail "OSV dependency vulnerability scanning workflow is missing" +else + echo "OSV dependency vulnerability scanning workflow is present." fi section "GitHub Actions cache write posture" @@ -292,6 +416,10 @@ unexpected_writable_permissions="$( printf '%s\n' "$writable_permissions" | awk -F: ' $1 == ".github/workflows/julia-update-hash.yml" && $0 ~ /^[^:]+:[0-9]+:[[:space:]]+(contents|pull-requests): write[[:space:]]*$/ { next } + $1 == ".github/workflows/codeql.yml" && + $0 ~ /^[^:]+:[0-9]+:[[:space:]]+security-events: write[[:space:]]*$/ { next } + $1 == ".github/workflows/osv-scanner.yml" && + $0 ~ /^[^:]+:[0-9]+:[[:space:]]+security-events: write[[:space:]]*$/ { next } NF { print } ' )" From 616114ca402f75faf23a46299141e368bd4cb219 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 17 May 2026 20:26:07 -0600 Subject: [PATCH 04/12] Fix integrity CI tools --- .github/workflows/dependency-integrity-check.yml | 5 +++++ .github/workflows/rust-test.yml | 4 ++++ scripts/dependency-integrity-check.sh | 5 +++++ 3 files changed, 14 insertions(+) diff --git a/.github/workflows/dependency-integrity-check.yml b/.github/workflows/dependency-integrity-check.yml index 8b1270ce2..83afadd6c 100644 --- a/.github/workflows/dependency-integrity-check.yml +++ b/.github/workflows/dependency-integrity-check.yml @@ -39,6 +39,11 @@ jobs: enable-cache: true save-cache: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} + - name: Install integrity check tools + run: | + sudo apt-get update + sudo apt-get install -y ripgrep + - name: Set up Rust run: rustup show diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml index ae96ac9fa..90ce11e8c 100644 --- a/.github/workflows/rust-test.yml +++ b/.github/workflows/rust-test.yml @@ -140,6 +140,10 @@ jobs: uses: actions/setup-python@v6 with: python-version: '3.x' + - name: Install integrity check tools + run: | + sudo apt-get update + sudo apt-get install -y ripgrep - name: Install pre-commit run: pip install pre-commit - name: Run pre-commit diff --git a/scripts/dependency-integrity-check.sh b/scripts/dependency-integrity-check.sh index 24ccca1c8..e04704bbf 100755 --- a/scripts/dependency-integrity-check.sh +++ b/scripts/dependency-integrity-check.sh @@ -55,9 +55,14 @@ RG_EXCLUDES=( ) section "Tooling" +tooling_failures_before=$failures require_tool rg || true require_tool cargo || true require_tool uv || true +if ((failures > tooling_failures_before)); then + printf '\nDependency integrity check failed: required tooling is missing.\n' >&2 + exit 1 +fi section "Known affected package names" lockfiles=() From e9e09418563163c8665cba992945818666fbc4e9 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 17 May 2026 20:40:34 -0600 Subject: [PATCH 05/12] Fix precommit CI tools --- .github/workflows/rust-test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml index 90ce11e8c..556443c3b 100644 --- a/.github/workflows/rust-test.yml +++ b/.github/workflows/rust-test.yml @@ -140,6 +140,10 @@ jobs: uses: actions/setup-python@v6 with: python-version: '3.x' + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: "0.11.14" - name: Install integrity check tools run: | sudo apt-get update From 2502a6c1e68dc6488e9ffaae9b4cd6887e0316c3 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 17 May 2026 21:56:19 -0600 Subject: [PATCH 06/12] Fix Python CI tools --- .github/workflows/python-test.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 3f6a1fccc..9ae42185a 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -205,6 +205,29 @@ jobs: if: runner.os == 'Linux' && matrix.python-version == '3.14' run: just pytest-slow + - name: Install integrity check tools + run: | + if command -v rg >/dev/null 2>&1; then + exit 0 + fi + + case "$RUNNER_OS" in + Linux) + sudo apt-get update + sudo apt-get install -y ripgrep + ;; + macOS) + HOMEBREW_NO_AUTO_UPDATE=1 brew install ripgrep + ;; + Windows) + cargo install --locked --version 14.1.1 ripgrep + ;; + *) + echo "Unsupported runner OS for ripgrep install: $RUNNER_OS" >&2 + exit 1 + ;; + esac + - name: Run linting run: just lint check From d22ac57a1c52b37c2bc63e140782f801fe76b12f Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 17 May 2026 23:30:29 -0600 Subject: [PATCH 07/12] Fix Windows integrity check --- scripts/dependency-integrity-check.sh | 48 ++++++++++++++++++++------- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/scripts/dependency-integrity-check.sh b/scripts/dependency-integrity-check.sh index e04704bbf..5dd12b50f 100755 --- a/scripts/dependency-integrity-check.sh +++ b/scripts/dependency-integrity-check.sh @@ -166,16 +166,16 @@ while IFS= read -r file; do done < <(collect_files -g 'Cargo.toml') if ((${#cargo_manifests[@]} > 0)); then - if rg -n --pcre2 '^\s*(tag|branch)\s*=' "${cargo_manifests[@]}"; then + if rg -n '^[[:space:]]*(tag|branch)[[:space:]]*=' "${cargo_manifests[@]}"; then fail "Cargo git dependencies must use full immutable rev pins, not tag/branch" fi - if rg -n --pcre2 '^\s*rev\s*=\s*"[0-9a-f]{1,39}"' "${cargo_manifests[@]}"; then + if rg -n '^[[:space:]]*rev[[:space:]]*=[[:space:]]*"[0-9a-f]{1,39}"' "${cargo_manifests[@]}"; then fail "Cargo git dependency rev pins must use full 40-character commit SHAs" fi fi -if rg -n --pcre2 'git\+.*[?&](tag|branch)=' Cargo.lock >/dev/null 2>&1; then - rg -n --pcre2 'git\+.*[?&](tag|branch)=' Cargo.lock || true +if rg -n 'git\+.*[?&](tag|branch)=' Cargo.lock >/dev/null 2>&1; then + rg -n 'git\+.*[?&](tag|branch)=' Cargo.lock || true fail "Cargo.lock contains git sources resolved from mutable tag/branch refs" elif rg -n 'git\+' Cargo.lock >/dev/null 2>&1; then echo "Cargo git sources are pinned by commit." @@ -215,7 +215,7 @@ else printf '%s\n' "$root" >>"$actual_unsafe_roots_file" fi done < <( - rg -l --pcre2 '\bunsafe\b' \ + rg -l '\bunsafe\b' \ crates python go julia exp \ --glob '*.rs' \ --glob '*.c' \ @@ -271,7 +271,7 @@ fi section "Remote shell bootstrap posture" remote_shell_bootstraps="$( - rg -n --pcre2 '(curl|wget)[^\n|]*\|[^\n]*(sh|bash)' \ + rg -n '(curl|wget)[^\n|]*\|[^\n]*(sh|bash)' \ .github/workflows \ julia/PECOS.jl/deps/build_tarballs.jl \ || true @@ -374,25 +374,49 @@ else fi section "GitHub Actions lock enforcement" -if rg -n --pcre2 '^\s*(run:\s*)?cargo (build|check|clippy|run|install)(?! --locked)' .github/workflows; then +cargo_workflow_commands="$( + rg -n '^[[:space:]]*(run:[[:space:]]*)?cargo (build|check|clippy|run|install)([[:space:]]|$)' .github/workflows | + rg -v '^[^:]+:[0-9]+:[[:space:]]*(run:[[:space:]]*)?cargo (build|check|clippy|run|install)[[:space:]]+--locked([[:space:]]|$)' || + true +)" +if [[ -n "$cargo_workflow_commands" ]]; then + printf '%s\n' "$cargo_workflow_commands" fail "workflow Cargo build/check/run/install commands must use --locked" else echo "Workflow Cargo build/check/run/install commands use --locked." fi -if rg -n --pcre2 '^\s*(run:\s*)?uv sync(?!.*--locked)' .github/workflows; then +uv_sync_without_lock="$( + rg -n '^[[:space:]]*(run:[[:space:]]*)?uv sync([[:space:]]|$)' .github/workflows | + rg -v -- '--locked' || + true +)" +if [[ -n "$uv_sync_without_lock" ]]; then + printf '%s\n' "$uv_sync_without_lock" fail "workflow uv sync commands must use --locked" else echo "Workflow uv sync commands use --locked." fi -if rg -n --pcre2 '^\s*(run:\s*)?uv lock(?!.*--check)' .github/workflows; then +uv_lock_without_check="$( + rg -n '^[[:space:]]*(run:[[:space:]]*)?uv lock([[:space:]]|$)' .github/workflows | + rg -v -- '--check' || + true +)" +if [[ -n "$uv_lock_without_check" ]]; then + printf '%s\n' "$uv_lock_without_check" fail "workflows must not regenerate uv.lock; use uv lock --check" else echo "Workflows validate uv.lock instead of regenerating it." fi -if rg -n --pcre2 '^\s*(run:\s*)?uv run(?! --frozen)' .github/workflows; then +uv_run_without_frozen="$( + rg -n '^[[:space:]]*(run:[[:space:]]*)?uv run([[:space:]]|$)' .github/workflows | + rg -v '^[^:]+:[0-9]+:[[:space:]]*(run:[[:space:]]*)?uv run[[:space:]]+--frozen([[:space:]]|$)' || + true +)" +if [[ -n "$uv_run_without_frozen" ]]; then + printf '%s\n' "$uv_run_without_frozen" fail "workflow uv run commands must use --frozen" else echo "Workflow uv run commands use --frozen." @@ -416,7 +440,7 @@ if ((${#missing_top_level_permissions[@]} > 0)); then fail "workflow files must declare top-level read-only permissions" fi -writable_permissions="$(rg -n '^\s*(contents|packages|id-token|pull-requests|actions|security-events): write\s*$' .github/workflows || true)" +writable_permissions="$(rg -n '^[[:space:]]*(contents|packages|id-token|pull-requests|actions|security-events): write[[:space:]]*$' .github/workflows | sed 's#\\#/#g' || true)" unexpected_writable_permissions="$( printf '%s\n' "$writable_permissions" | awk -F: ' $1 == ".github/workflows/julia-update-hash.yml" && @@ -433,7 +457,7 @@ if [[ -n "$unexpected_writable_permissions" ]]; then printf '%s\n' "$unexpected_writable_permissions" fail "unexpected writable workflow permission found" elif [[ -n "$writable_permissions" ]]; then - echo "Only expected write permissions found in the tag-only Julia hash updater." + echo "Only expected write permissions found." else echo "No writable workflow permissions found." fi From b61256c14b56b9db3a36112c141a9dcaa7abf204 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Mon, 18 May 2026 00:48:30 -0600 Subject: [PATCH 08/12] Fix Windows path checks --- scripts/dependency-integrity-check.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/dependency-integrity-check.sh b/scripts/dependency-integrity-check.sh index 5dd12b50f..111c0fd9b 100755 --- a/scripts/dependency-integrity-check.sh +++ b/scripts/dependency-integrity-check.sh @@ -26,6 +26,10 @@ require_tool() { fi } +normalize_path() { + printf '%s\n' "${1//\\//}" +} + list_contains() { local needle="$1" shift @@ -210,9 +214,9 @@ else done if [[ -z "$root" ]]; then - printf '%s\n' "$unsafe_file" >>"$unsafe_files_without_manifest_file" + normalize_path "$unsafe_file" >>"$unsafe_files_without_manifest_file" else - printf '%s\n' "$root" >>"$actual_unsafe_roots_file" + normalize_path "$root" >>"$actual_unsafe_roots_file" fi done < <( rg -l '\bunsafe\b' \ From 33267a550f0ffa0dca30d8f9d1e16b59d865ab71 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Mon, 18 May 2026 16:10:42 -0600 Subject: [PATCH 09/12] Harden CI security --- .github/workflows/cargo-deny.yml | 49 +++++++++++++++++++ .github/workflows/codeql.yml | 12 +++-- .github/workflows/cuda-build-check.yml | 8 +-- .../workflows/dependency-integrity-check.yml | 8 +-- .github/workflows/dependency-review.yml | 8 +-- .github/workflows/go-test.yml | 8 +-- .github/workflows/go-version-consistency.yml | 4 +- .github/workflows/julia-release.yml | 29 ++++++----- .github/workflows/julia-test.yml | 8 +-- .github/workflows/julia-update-hash.yml | 5 +- .../workflows/julia-version-consistency.yml | 4 +- .github/workflows/osv-scanner.yml | 2 +- .github/workflows/python-release.yml | 46 +++++++++-------- .github/workflows/python-test.yml | 22 +++++---- .../workflows/python-version-consistency.yml | 6 ++- .github/workflows/rust-test.yml | 42 +++++++++------- .../workflows/rust-version-consistency.yml | 4 +- .github/workflows/selene-plugins.yml | 26 +++++----- .github/workflows/test-docs-examples.yml | 16 +++--- .github/zizmor.yml | 5 +- SECURITY.md | 23 +++++++++ deny.toml | 23 +++++++++ scripts/dependency-integrity-check.sh | 44 ++++++++++++++++- 23 files changed, 291 insertions(+), 111 deletions(-) create mode 100644 .github/workflows/cargo-deny.yml create mode 100644 SECURITY.md create mode 100644 deny.toml diff --git a/.github/workflows/cargo-deny.yml b/.github/workflows/cargo-deny.yml new file mode 100644 index 000000000..b760b9e6d --- /dev/null +++ b/.github/workflows/cargo-deny.yml @@ -0,0 +1,49 @@ +name: Cargo Deny + +permissions: + contents: read + +on: + push: + branches: [ "main", "master", "development", "dev" ] + paths: + - 'Cargo.toml' + - 'Cargo.lock' + - '**/Cargo.toml' + - '**/Cargo.lock' + - 'deny.toml' + - '.github/workflows/cargo-deny.yml' + pull_request: + branches: [ "main", "master", "development", "dev" ] + paths: + - 'Cargo.toml' + - 'Cargo.lock' + - '**/Cargo.toml' + - '**/Cargo.lock' + - 'deny.toml' + - '.github/workflows/cargo-deny.yml' + schedule: + - cron: "17 5 * * 3" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + cargo-deny: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + + - name: Install cargo-deny + run: cargo install --locked --version 0.19.6 cargo-deny + + - name: Check root Rust workspace + run: cargo deny --locked --all-features check advisories bans sources + + - name: Check native benchmark crate + run: cargo deny --manifest-path scripts/native_bench/bench_pecos/Cargo.toml --locked --all-features check advisories bans sources diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2784c00c2..ba6b5d1bc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,26 +41,28 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Initialize CodeQL if: matrix.build-mode == '' - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4 with: languages: ${{ matrix.language }} - name: Initialize CodeQL with build mode if: matrix.build-mode != '' - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Autobuild if: matrix.build-mode == 'autobuild' - uses: github/codeql-action/autobuild@v4 + uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4 - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/cuda-build-check.yml b/.github/workflows/cuda-build-check.yml index f2789ebcd..ddc9218d4 100644 --- a/.github/workflows/cuda-build-check.yml +++ b/.github/workflows/cuda-build-check.yml @@ -35,7 +35,9 @@ jobs: cuda-build-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Free disk space run: | @@ -54,14 +56,14 @@ jobs: rustup show - name: Cache Rust - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: cache-bin: false prefix-key: v1-rust-no-bin save-if: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} - name: Install CUDA Toolkit - uses: Jimver/cuda-toolkit@v0.2.35 + uses: Jimver/cuda-toolkit@3d45d157f327c09c04b50ee6ccdea2d9d017ec76 # v0.2.35 id: cuda-toolkit with: cuda: '12.6.3' diff --git a/.github/workflows/dependency-integrity-check.yml b/.github/workflows/dependency-integrity-check.yml index 83afadd6c..eda2a020f 100644 --- a/.github/workflows/dependency-integrity-check.yml +++ b/.github/workflows/dependency-integrity-check.yml @@ -25,15 +25,17 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: "3.12" - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 with: version: "0.11.14" enable-cache: true diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 7bdd8bd52..632fa5e74 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -54,17 +54,19 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Review dependency changes in pull request if: github.event_name == 'pull_request' - uses: actions/dependency-review-action@v4 + uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4 with: fail-on-severity: high - name: Review dependency changes in push if: github.event_name == 'push' && github.event.before != '0000000000000000000000000000000000000000' - uses: actions/dependency-review-action@v4 + uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4 with: fail-on-severity: high base-ref: ${{ github.event.before }} diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index adbefca87..1aac9918b 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -44,10 +44,12 @@ jobs: go-version: ["stable"] # Latest stable (experimental bindings) steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@v6 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version: ${{ matrix.go-version }} @@ -55,7 +57,7 @@ jobs: run: rustup show - name: Cache Rust - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: cache-bin: false prefix-key: v1-rust-no-bin diff --git a/.github/workflows/go-version-consistency.yml b/.github/workflows/go-version-consistency.yml index ce41dc57e..ef0b82b0f 100644 --- a/.github/workflows/go-version-consistency.yml +++ b/.github/workflows/go-version-consistency.yml @@ -25,7 +25,9 @@ jobs: check-go-versions: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Check Go package consistency run: | diff --git a/.github/workflows/julia-release.yml b/.github/workflows/julia-release.yml index 6741e5c50..e7b702aaf 100644 --- a/.github/workflows/julia-release.yml +++ b/.github/workflows/julia-release.yml @@ -104,8 +104,9 @@ jobs: architecture: x86_64 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: + persist-credentials: false ref: ${{ inputs.sha || github.sha }} - name: Set up Rust @@ -237,7 +238,7 @@ jobs: ls -la *.tar.gz - name: Upload binary - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: julia-binary-${{ matrix.os }}-${{ matrix.architecture }} path: pecos_julia-*.tar.gz @@ -266,17 +267,18 @@ jobs: os: macos-latest architecture: aarch64 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: + persist-credentials: false ref: ${{ inputs.sha || github.sha }} - name: Set up Julia ${{ matrix.julia-version }} - uses: julia-actions/setup-julia@v3 + uses: julia-actions/setup-julia@fa02766e078afaaf09b14210362cee14137e6a32 # v3 with: version: ${{ matrix.julia-version }} - name: Download binary - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: name: julia-binary-${{ matrix.platform.os }}-${{ matrix.platform.architecture }} path: ./julia-binary @@ -344,12 +346,13 @@ jobs: needs.test_binaries.result == 'success' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: + persist-credentials: false ref: ${{ inputs.sha || github.sha }} - name: Set up Julia - uses: julia-actions/setup-julia@v3 + uses: julia-actions/setup-julia@fa02766e078afaaf09b14210362cee14137e6a32 # v3 with: version: '1.10' @@ -360,7 +363,7 @@ jobs: # Download artifacts into temp directory first - name: Download all binaries - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: path: temp-artifacts/ @@ -460,7 +463,7 @@ jobs: EOF - name: Upload distribution bundle - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: pecos-julia-distribution path: dist/ @@ -472,7 +475,7 @@ jobs: needs.test_binaries.result == 'success' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ inputs.sha || github.sha }} @@ -483,7 +486,7 @@ jobs: # Download artifacts - name: Download all binaries - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: path: temp-artifacts/ @@ -613,7 +616,7 @@ jobs: ls -lh pecos-julia-release-bundle-*.tar.gz - name: Upload release bundle - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: pecos-julia-release-bundle path: pecos-julia-release-bundle-*.tar.gz @@ -640,7 +643,7 @@ jobs: - name: Create GitHub Release and Upload Binaries if: startsWith(github.ref, 'refs/tags/jl-') - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: files: release-bundle/binaries/*.tar.gz body: | diff --git a/.github/workflows/julia-test.yml b/.github/workflows/julia-test.yml index 9d1e18b65..21f751277 100644 --- a/.github/workflows/julia-test.yml +++ b/.github/workflows/julia-test.yml @@ -44,10 +44,12 @@ jobs: julia-version: ["1"] # Latest stable (experimental bindings) steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Set up Julia ${{ matrix.julia-version }} - uses: julia-actions/setup-julia@v3 + uses: julia-actions/setup-julia@fa02766e078afaaf09b14210362cee14137e6a32 # v3 with: version: ${{ matrix.julia-version }} @@ -91,7 +93,7 @@ jobs: # cargo run -p pecos-cli --release -- llvm check - name: Cache Rust - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: cache-bin: false prefix-key: v1-rust-no-bin diff --git a/.github/workflows/julia-update-hash.yml b/.github/workflows/julia-update-hash.yml index 4f6f53163..631abfdbd 100644 --- a/.github/workflows/julia-update-hash.yml +++ b/.github/workflows/julia-update-hash.yml @@ -27,8 +27,9 @@ jobs: pull-requests: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: + persist-credentials: false fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} @@ -55,7 +56,7 @@ jobs: grep -n "GitSource" julia/PECOS.jl/deps/build_tarballs.jl - name: Create Pull Request - uses: peter-evans/create-pull-request@v8 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: "Update Julia build hash for ${{ github.ref_name }}" diff --git a/.github/workflows/julia-version-consistency.yml b/.github/workflows/julia-version-consistency.yml index 64d901841..0d93445b1 100644 --- a/.github/workflows/julia-version-consistency.yml +++ b/.github/workflows/julia-version-consistency.yml @@ -25,7 +25,9 @@ jobs: check-julia-versions: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Check Julia version consistency run: | diff --git a/.github/workflows/osv-scanner.yml b/.github/workflows/osv-scanner.yml index 378b3fde1..f17217c51 100644 --- a/.github/workflows/osv-scanner.yml +++ b/.github/workflows/osv-scanner.yml @@ -18,7 +18,7 @@ concurrency: jobs: scan: - uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.3.8 + uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@9a498708959aeaef5ef730655706c5a1df1edbc2 # v2.3.8 with: scan-args: |- --recursive diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml index 63cc2384e..21c3a30e5 100644 --- a/.github/workflows/python-release.yml +++ b/.github/workflows/python-release.yml @@ -122,8 +122,9 @@ jobs: install_cuda: true steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: + persist-credentials: false ref: ${{ inputs.sha || github.sha }} submodules: recursive @@ -138,7 +139,7 @@ jobs: # See: https://github.com/Jimver/cuda-toolkit/issues/382 - name: Install CUDA Toolkit (Windows) if: runner.os == 'Windows' && matrix.install_cuda - uses: Jimver/cuda-toolkit@v0.2.35 + uses: Jimver/cuda-toolkit@3d45d157f327c09c04b50ee6ccdea2d9d017ec76 # v0.2.35 with: cuda: '12.5.1' method: 'local' @@ -186,7 +187,7 @@ jobs: Write-Host "Configured MSVC linker: $linkPath" - name: Build wheels - uses: pypa/cibuildwheel@v3.3.1 + uses: pypa/cibuildwheel@298ed2fb2c105540f5ed055e8a6ad78d82dd3a7e # v3.3.1 with: package-dir: python/pecos-rslib output-dir: wheelhouse @@ -277,14 +278,14 @@ jobs: pipx run abi3audit --strict --report {wheel} - name: Upload pecos-rslib wheels - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: wheel-pecos-rslib-${{ matrix.os }}-${{ matrix.architecture }} path: ./wheelhouse/*.whl # Build pecos-rslib-llvm wheel (reuses LLVM already installed by pecos-rslib build) - name: Build pecos-rslib-llvm wheels - uses: pypa/cibuildwheel@v3.3.1 + uses: pypa/cibuildwheel@298ed2fb2c105540f5ed055e8a6ad78d82dd3a7e # v3.3.1 with: package-dir: python/pecos-rslib-llvm output-dir: wheelhouse-llvm @@ -341,7 +342,7 @@ jobs: pipx run abi3audit --strict --report {wheel} - name: Upload pecos-rslib-llvm wheels - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: wheel-pecos-rslib-llvm-${{ matrix.os }}-${{ matrix.architecture }} path: ./wheelhouse-llvm/*.whl @@ -370,17 +371,18 @@ jobs: os: macos-15 architecture: aarch64 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: + persist-credentials: false ref: ${{ inputs.sha || github.sha }} - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: ${{ matrix.python-version }} - name: Download abi3 wheel - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: name: wheel-pecos-rslib-${{ matrix.platform.os }}-${{ matrix.platform.architecture }} path: ./pecos-rslib-wheel @@ -432,23 +434,24 @@ jobs: needs.build_wheelspecos_rslib.result == 'success' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: + persist-credentials: false ref: ${{ inputs.sha || github.sha }} - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: '3.10' - name: Download pecos-rslib wheel - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: name: wheel-pecos-rslib-ubuntu-latest-x86_64 path: ./pecos-rslib-wheel - name: Download pecos-rslib-llvm wheel - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: name: wheel-pecos-rslib-llvm-ubuntu-latest-x86_64 path: ./pecos-rslib-llvm-wheel @@ -472,7 +475,7 @@ jobs: python -c 'import pecos; print(pecos.__version__)' - name: Upload quantum-pecos SDist - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: sdist-quantum-pecos path: python/quantum-pecos/dist/*.tar.gz @@ -485,23 +488,24 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: + persist-credentials: false ref: ${{ inputs.sha || github.sha }} - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: '3.10' - name: Download pecos-rslib wheel - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: name: wheel-pecos-rslib-ubuntu-latest-x86_64 path: ./pecos-rslib-wheel - name: Download pecos-rslib-llvm wheel - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: name: wheel-pecos-rslib-llvm-ubuntu-latest-x86_64 path: ./pecos-rslib-llvm-wheel @@ -525,7 +529,7 @@ jobs: python -c 'import pecos; print(pecos.__version__)' - name: Upload quantum-pecos wheel - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: wheel-quantum-pecos path: python/quantum-pecos/dist/*.whl @@ -546,7 +550,7 @@ jobs: # Download artifacts into temp directory first - name: Download all artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: path: temp-artifacts/ @@ -597,7 +601,7 @@ jobs: - name: Upload distribution bundle - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: pecos-distribution path: dist/ diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 9ae42185a..4a1bf2cc1 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -52,7 +52,7 @@ jobs: steps: - name: Free Disk Space (Ubuntu) if: runner.os == 'Linux' - uses: jlumbroso/free-disk-space@v1.3.1 + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 with: android: true dotnet: true @@ -71,10 +71,12 @@ jobs: Remove-Item -Path "C:\hostedtoolcache\CodeQL" -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path "C:\ProgramData\chocolatey" -Recurse -Force -ErrorAction SilentlyContinue - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: ${{ matrix.python-version }} @@ -86,7 +88,7 @@ jobs: # local Windows dev gets the exact same path via the same just prereq. - name: Install the latest version of uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 with: version: "0.11.14" enable-cache: true @@ -114,10 +116,10 @@ jobs: run: cmake --version - name: Install just - uses: extractions/setup-just@v4 + uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4 - name: Cache Rust - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: cache-bin: false prefix-key: v1-rust-no-bin @@ -129,7 +131,7 @@ jobs: # Cache LLVM installation (fixed version, only varies by OS) - name: Cache LLVM ${{ env.LLVM_VERSION }} id: cache-llvm - uses: actions/cache/restore@v5 + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: ~/.pecos/deps/llvm-14 key: llvm-${{ env.LLVM_VERSION }}-${{ runner.os }}-${{ runner.arch }}-v2 @@ -144,7 +146,7 @@ jobs: - name: Save LLVM ${{ env.LLVM_VERSION }} cache if: steps.cache-llvm.outputs.cache-hit != 'true' && github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') - uses: actions/cache/save@v5 + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: ~/.pecos/deps/llvm-14 key: ${{ steps.cache-llvm.outputs.cache-primary-key }} @@ -241,7 +243,9 @@ jobs: runs-on: windows-2022 timeout-minutes: 45 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Set up Rust (MSVC) shell: pwsh diff --git a/.github/workflows/python-version-consistency.yml b/.github/workflows/python-version-consistency.yml index d9db9b53b..0be9e9e37 100644 --- a/.github/workflows/python-version-consistency.yml +++ b/.github/workflows/python-version-consistency.yml @@ -22,10 +22,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: "3.11" diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml index 556443c3b..8468187ba 100644 --- a/.github/workflows/rust-test.yml +++ b/.github/workflows/rust-test.yml @@ -45,7 +45,9 @@ jobs: rust-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Free disk space run: | @@ -60,17 +62,17 @@ jobs: rustup show - name: Install just - uses: extractions/setup-just@v4 + uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4 - name: Cache Rust - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: cache-bin: false prefix-key: v1-rust-no-bin save-if: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') }} - name: Install CUDA Toolkit - uses: Jimver/cuda-toolkit@v0.2.35 + uses: Jimver/cuda-toolkit@3d45d157f327c09c04b50ee6ccdea2d9d017ec76 # v0.2.35 id: cuda-toolkit with: cuda: '12.6.3' @@ -80,7 +82,7 @@ jobs: - name: Cache LLVM ${{ env.LLVM_VERSION }} id: cache-llvm - uses: actions/cache/restore@v5 + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: ~/.pecos/deps/llvm-14 key: llvm-${{ env.LLVM_VERSION }}-${{ runner.os }}-${{ runner.arch }}-v2 @@ -90,7 +92,7 @@ jobs: - name: Save LLVM ${{ env.LLVM_VERSION }} cache if: steps.cache-llvm.outputs.cache-hit != 'true' && github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') - uses: actions/cache/save@v5 + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: ~/.pecos/deps/llvm-14 key: ${{ steps.cache-llvm.outputs.cache-primary-key }} @@ -104,7 +106,9 @@ jobs: rust-lint-no-llvm: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Set up Rust run: | @@ -114,7 +118,7 @@ jobs: rustup show - name: Cache Rust - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: cache-bin: false prefix-key: v1-rust-no-bin @@ -135,13 +139,15 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: '3.x' - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 with: version: "0.11.14" - name: Install integrity check tools @@ -162,7 +168,9 @@ jobs: os: [ubuntu-latest, macos-latest, windows-2022] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Free disk space (Linux) if: matrix.os == 'ubuntu-latest' @@ -188,10 +196,10 @@ jobs: rustup show - name: Install just - uses: extractions/setup-just@v4 + uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4 - name: Cache Rust - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: cache-bin: false prefix-key: v1-rust-no-bin @@ -199,7 +207,7 @@ jobs: - name: Cache LLVM ${{ env.LLVM_VERSION }} id: cache-llvm - uses: actions/cache/restore@v5 + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: ~/.pecos/deps/llvm-14 key: llvm-${{ env.LLVM_VERSION }}-${{ runner.os }}-${{ runner.arch }}-v2 @@ -218,14 +226,14 @@ jobs: - name: Save LLVM ${{ env.LLVM_VERSION }} cache if: steps.cache-llvm.outputs.cache-hit != 'true' && github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') - uses: actions/cache/save@v5 + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: ~/.pecos/deps/llvm-14 key: ${{ steps.cache-llvm.outputs.cache-primary-key }} - name: Install CUDA Toolkit (Linux) if: matrix.os == 'ubuntu-latest' - uses: Jimver/cuda-toolkit@v0.2.35 + uses: Jimver/cuda-toolkit@3d45d157f327c09c04b50ee6ccdea2d9d017ec76 # v0.2.35 id: cuda-toolkit-test with: cuda: '12.6.3' diff --git a/.github/workflows/rust-version-consistency.yml b/.github/workflows/rust-version-consistency.yml index 5ffb0e845..8c105f45a 100644 --- a/.github/workflows/rust-version-consistency.yml +++ b/.github/workflows/rust-version-consistency.yml @@ -20,7 +20,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Check Rust crate configurations run: | diff --git a/.github/workflows/selene-plugins.yml b/.github/workflows/selene-plugins.yml index 899723f3f..7a230965c 100644 --- a/.github/workflows/selene-plugins.yml +++ b/.github/workflows/selene-plugins.yml @@ -45,15 +45,17 @@ jobs: os: [ubuntu-latest, macos-latest, macos-15-intel, windows-2022] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: "3.12" - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 with: version: "0.11.14" enable-cache: true @@ -63,7 +65,7 @@ jobs: run: rustup show - name: Cache Rust - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: cache-bin: false key: ${{ matrix.os }} @@ -140,15 +142,17 @@ jobs: package: pecos_selene_mast steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: "3.12" - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 with: version: "0.11.14" enable-cache: true @@ -158,7 +162,7 @@ jobs: run: rustup show - name: Cache Rust - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: cache-bin: false key: ${{ matrix.os }}-${{ matrix.plugin.name }} @@ -203,7 +207,7 @@ jobs: python -m build --wheel --outdir dist - name: Upload wheel - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: wheel-${{ matrix.plugin.name }}-${{ matrix.os }} path: python/selene-plugins/${{ matrix.plugin.name }}/dist/*.whl @@ -214,7 +218,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download all wheel artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: path: wheels/ pattern: wheel-* @@ -229,7 +233,7 @@ jobs: echo "Total wheels: $(ls -1 wheels/*.whl 2>/dev/null | wc -l)" - name: Upload combined wheels - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: selene-plugin-wheels path: wheels/*.whl diff --git a/.github/workflows/test-docs-examples.yml b/.github/workflows/test-docs-examples.yml index 758610321..7ccecdd45 100644 --- a/.github/workflows/test-docs-examples.yml +++ b/.github/workflows/test-docs-examples.yml @@ -29,15 +29,17 @@ jobs: name: Test and build documentation runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: '3.10' - name: Install the latest version of uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 with: version: "0.11.14" enable-cache: true @@ -50,10 +52,10 @@ jobs: rustup show - name: Install just - uses: extractions/setup-just@v4 + uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4 - name: Cache Rust - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: cache-bin: false prefix-key: v1-rust-no-bin @@ -64,7 +66,7 @@ jobs: - name: Cache LLVM ${{ env.LLVM_VERSION }} id: cache-llvm - uses: actions/cache/restore@v5 + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: ~/.pecos/deps/llvm-14 key: llvm-${{ env.LLVM_VERSION }}-${{ runner.os }}-${{ runner.arch }}-v2 @@ -74,7 +76,7 @@ jobs: - name: Save LLVM ${{ env.LLVM_VERSION }} cache if: steps.cache-llvm.outputs.cache-hit != 'true' && github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'development' || github.ref_name == 'dev') - uses: actions/cache/save@v5 + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: ~/.pecos/deps/llvm-14 key: ${{ steps.cache-llvm.outputs.cache-primary-key }} diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 43cefe622..e7920e8cf 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -2,7 +2,4 @@ rules: unpinned-uses: config: policies: - # Existing workflows use version tags for most actions. Keep this - # blocking scan focused on executable workflow risks for now; full - # SHA pinning can be promoted separately. - "*": ref-pin + "*": hash-pin diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..3cc5776e1 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +## Reporting a Vulnerability + +Please do not file public issues for suspected vulnerabilities. + +Use GitHub's private vulnerability reporting for this repository when available. If private reporting is not available to you, open a minimal public issue asking for a private security contact and do not include exploit details, tokens, logs, or proof-of-concept code. + +Include enough detail for maintainers to reproduce and assess the issue privately: + +- affected package, crate, workflow, or component +- affected versions, commits, or release artifacts +- impact and prerequisites +- reproduction steps or a minimal proof of concept +- any suspected dependency or CI provenance issue + +## Dependency and CI Incidents + +For suspected dependency compromise, malicious package activity, leaked credentials, or CI tampering, include the exact package name, version, registry, lockfile entry, workflow run, or artifact involved. Maintainers should treat these reports as potentially active incidents until dependencies, generated artifacts, and GitHub Actions tokens have been checked. + +## Supported Versions + +Security fixes are prioritized for the default branch and currently maintained release lines. Older development snapshots may receive fixes only when the affected code is still supported or the risk carries forward into supported releases. diff --git a/deny.toml b/deny.toml new file mode 100644 index 000000000..2d0348f6c --- /dev/null +++ b/deny.toml @@ -0,0 +1,23 @@ +[advisories] +yanked = "deny" +unmaintained = "none" +unsound = "none" + +[bans] +multiple-versions = "allow" +wildcards = "allow" +allow-wildcard-paths = true +highlight = "simplest-path" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +required-git-spec = "rev" +allow-registry = [ + "https://github.com/rust-lang/crates.io-index", +] +allow-git = [ + "https://github.com/Quantinuum/selene", + "https://github.com/yuewuo/mwpf", +] +unused-allowed-source = "allow" diff --git a/scripts/dependency-integrity-check.sh b/scripts/dependency-integrity-check.sh index 111c0fd9b..aaa1b50f8 100755 --- a/scripts/dependency-integrity-check.sh +++ b/scripts/dependency-integrity-check.sh @@ -273,6 +273,26 @@ else echo "No pull_request_target or workflow_run triggers found." fi +section "GitHub Actions action pinning" +unpinned_actions="$( + rg -n 'uses:[[:space:]]+[^[:space:]#]+@[^[:space:]#]+' .github/workflows .github/actions | + awk -F: '{ + line = $0 + sub(/^[^:]+:[0-9]+:/, "", line) + if (line ~ /uses:[[:space:]]+[^[:space:]#]+@[0-9a-f]{40}([[:space:]#]|$)/) { + next + } + print + }' || + true +)" +if [[ -n "$unpinned_actions" ]]; then + printf '%s\n' "$unpinned_actions" + fail "GitHub Actions uses entries must be pinned to immutable commit SHAs" +else + echo "GitHub Actions uses entries are pinned to commit SHAs." +fi + section "Remote shell bootstrap posture" remote_shell_bootstraps="$( rg -n '(curl|wget)[^\n|]*\|[^\n]*(sh|bash)' \ @@ -326,10 +346,20 @@ else fi fi -if [[ ! -f .github/zizmor.yml && ! -f .github/zizmor.yaml ]]; then +zizmor_config="" +if [[ -f .github/zizmor.yml ]]; then + zizmor_config=".github/zizmor.yml" +elif [[ -f .github/zizmor.yaml ]]; then + zizmor_config=".github/zizmor.yaml" +fi + +if [[ -z "$zizmor_config" ]]; then fail "GitHub Actions security analysis configuration is missing" else echo "GitHub Actions security analysis configuration is present." + if ! rg -q 'hash-pin' "$zizmor_config"; then + fail "GitHub Actions security analysis must enforce hash-pinned actions" + fi fi if [[ ! -f .github/workflows/codeql.yml && ! -f .github/workflows/codeql.yaml ]]; then @@ -344,6 +374,18 @@ else echo "OSV dependency vulnerability scanning workflow is present." fi +if [[ ! -f deny.toml ]]; then + fail "cargo-deny policy is missing" +else + echo "cargo-deny policy is present." +fi + +if [[ ! -f .github/workflows/cargo-deny.yml && ! -f .github/workflows/cargo-deny.yaml ]]; then + fail "cargo-deny workflow is missing" +else + echo "cargo-deny workflow is present." +fi + section "GitHub Actions cache write posture" cache_policy_failures=() while IFS=: read -r file line _; do From 6d7413f2822fd1b5e68c4e0dc8c86eedddf40794 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Mon, 18 May 2026 19:00:01 -0600 Subject: [PATCH 10/12] Fix Windows TCP test --- .../tests/guppy/test_selene_tcp_stream.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/python/quantum-pecos/tests/guppy/test_selene_tcp_stream.py b/python/quantum-pecos/tests/guppy/test_selene_tcp_stream.py index b93d6fedd..605bfdb2a 100644 --- a/python/quantum-pecos/tests/guppy/test_selene_tcp_stream.py +++ b/python/quantum-pecos/tests/guppy/test_selene_tcp_stream.py @@ -16,6 +16,12 @@ from selene_sim.result_handling import ResultStream, TCPStream +def _unused_tcp_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + return int(sock.getsockname()[1]) + + class TestSeleneTCPStream: """Test Selene's TCP stream functionality.""" @@ -123,10 +129,10 @@ def test_result_stream_wrapper(self) -> None: def test_tcp_stream_configuration_options(self) -> None: """Test different configuration options for TCPStream.""" # Test with specific port - specific_port = 55555 + specific_port = _unused_tcp_port() try: with TCPStream( - host="localhost", + host="127.0.0.1", port=specific_port, logfile=None, shot_offset=10, @@ -141,7 +147,7 @@ def test_tcp_stream_configuration_options(self) -> None: assert hasattr(stream, "shot_increment") or True, "Stream tracks shot increment" except OSError as e: - # Port might be in use + # The selected port can still be claimed between discovery and bind. if "address already in use" in str(e).lower(): pytest.skip(f"Port {specific_port} already in use") raise From 87a56fcad61e63acbbc6e05aa7be531aaca78670 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Mon, 18 May 2026 19:23:23 -0600 Subject: [PATCH 11/12] Remove LM dependency --- Cargo.lock | 13 - Cargo.toml | 1 - Justfile | 23 +- crates/pecos-num/Cargo.toml | 3 - crates/pecos-num/README.md | 2 +- crates/pecos-num/src/curve_fit.rs | 499 +++++++++++++++----- crates/pecos-num/src/lib.rs | 2 +- python/pecos-rslib/src/num_bindings.rs | 2 +- scripts/native_bench/bench_pecos/Cargo.lock | 22 - 9 files changed, 397 insertions(+), 170 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9e67cf54..ffffaffbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3019,18 +3019,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" -[[package]] -name = "levenberg-marquardt" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be7a65739a815308eef33a6d8c78e435a7317305d5b0af0c8c465a2d7ac6fc6" -dependencies = [ - "cfg-if", - "nalgebra", - "num-traits", - "rustc_version", -] - [[package]] name = "libbz2-rs-sys" version = "0.2.3" @@ -4178,7 +4166,6 @@ dependencies = [ name = "pecos-num" version = "0.2.0-dev.0" dependencies = [ - "levenberg-marquardt", "log", "nalgebra", "ndarray 0.17.2", diff --git a/Cargo.toml b/Cargo.toml index c3e1faff0..1e2c2a3b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,7 +90,6 @@ pest_derive = "2" regex = "1" # --- Numerical computing --- -levenberg-marquardt = "0.15" nalgebra = "0.34" num = "0.4" num-complex = "0.4" diff --git a/Justfile b/Justfile index 346adb80e..470aee204 100644 --- a/Justfile +++ b/Justfile @@ -15,6 +15,7 @@ default: @echo " just test # Run all tests" @echo " just dev # Build + test (daily workflow)" @echo " just lint # Check formatting and linting" + @echo " just security-check # Check dependency/security policy" @echo " just doctor # Diagnose environment problems" @echo "" @echo "All commands:" @@ -149,6 +150,24 @@ sys-info: _msvc-bootstrap dependency-integrity-check: ./scripts/dependency-integrity-check.sh +# Run all local dependency/security policy checks +[group('security')] +security-check: dependency-integrity-check cargo-deny + +# Run cargo-deny against every Rust lockfile covered by CI +[group('security')] +cargo-deny: cargo-deny-workspace cargo-deny-native-bench + +# Check the root Rust workspace with cargo-deny +[group('security')] +cargo-deny-workspace: + cargo deny --locked --all-features check advisories bans sources + +# Check the standalone native benchmark crate with cargo-deny +[group('security')] +cargo-deny-native-bench: + cargo deny --manifest-path scripts/native_bench/bench_pecos/Cargo.toml --locked --all-features check advisories bans sources + # List installed and cached dependencies [group('setup')] list-deps: _msvc-bootstrap @@ -382,9 +401,9 @@ dev lang="all": (validate-dev-lang lang) ;; esac -# Pre-PR gate: clean build + test + lint + dependency integrity +# Pre-PR gate: clean build + test + lint + dependency/security checks [group('dev')] -check-all: clean (build "release") (test "release") (lint "check") +check-all: clean (build "release") (test "release") (lint "check") security-check # Clean build artifacts (or: just clean cache/deps/selene/all/dry-run; multiple OK, e.g. just clean selene deps) [group('clean')] diff --git a/crates/pecos-num/Cargo.toml b/crates/pecos-num/Cargo.toml index 5566e1fdd..b34dab9ae 100644 --- a/crates/pecos-num/Cargo.toml +++ b/crates/pecos-num/Cargo.toml @@ -15,9 +15,6 @@ description = "Numerical computing support for PECOS" # Linear algebra (for polynomial fitting, curve fitting, matrix operations) nalgebra.workspace = true -# Non-linear curve fitting (Levenberg-Marquardt algorithm) -levenberg-marquardt.workspace = true - # Array interface (for API compatibility and return types) ndarray.workspace = true diff --git a/crates/pecos-num/README.md b/crates/pecos-num/README.md index 4734f7ba4..79fd62d71 100644 --- a/crates/pecos-num/README.md +++ b/crates/pecos-num/README.md @@ -9,7 +9,7 @@ Provides numerical computing functionality including scipy/numpy-like operations ## Features - Root finding (Brent's method, Newton-Raphson) -- Curve fitting (Levenberg-Marquardt) +- Curve fitting (damped least-squares) - Polynomial fitting and evaluation - Statistical functions (mean, std) - Mathematical functions (cos, sin, exp, sqrt) diff --git a/crates/pecos-num/src/curve_fit.rs b/crates/pecos-num/src/curve_fit.rs index 0f34dbd4c..3d63ff51d 100644 --- a/crates/pecos-num/src/curve_fit.rs +++ b/crates/pecos-num/src/curve_fit.rs @@ -12,19 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Non-linear curve fitting using Levenberg-Marquardt algorithm. +//! Non-linear curve fitting using a damped least-squares algorithm. //! -//! Rust implementation of `scipy.optimize.curve_fit` -//! using the well-tested `levenberg-marquardt` crate. -//! -//! Note: We use `levenberg-marquardt` instead of Peroxide's optimizer because -//! Peroxide requires AD (automatic differentiation) types, while `scipy.optimize.curve_fit` -//! uses simple float functions. The levenberg-marquardt crate provides a better API match. +//! Rust implementation of `scipy.optimize.curve_fit` for simple float model +//! functions. -use levenberg_marquardt::{LeastSquaresProblem, LevenbergMarquardt}; -use nalgebra::{DMatrix, DVector, Dyn, Owned}; +use nalgebra::{DMatrix, DVector}; use ndarray::{Array1, Array2, ArrayView1}; +const DIFF_STEP: f64 = 1e-8; +const MIN_LAMBDA: f64 = 1e-15; +const MAX_LAMBDA: f64 = 1e15; +const MAX_DAMPING_ATTEMPTS: usize = 16; + /// Error type for curve fitting operations. #[derive(Debug, Clone)] pub enum CurveFitError { @@ -48,73 +48,6 @@ impl std::fmt::Display for CurveFitError { impl std::error::Error for CurveFitError {} -/// Problem struct for Levenberg-Marquardt optimization. -struct CurveFitProblem -where - F: Fn(f64, &[f64]) -> f64, -{ - func: F, - xdata: Vec, - ydata: Vec, - params: DVector, -} - -impl LeastSquaresProblem for CurveFitProblem -where - F: Fn(f64, &[f64]) -> f64, -{ - type ParameterStorage = Owned; - type ResidualStorage = Owned; - type JacobianStorage = Owned; - - fn set_params(&mut self, p: &DVector) { - self.params.copy_from(p); - } - - fn params(&self) -> DVector { - self.params.clone() - } - - fn residuals(&self) -> Option> { - let n = self.xdata.len(); - let mut residuals = DVector::zeros(n); - let param_slice = self.params.as_slice(); - - for (i, (&x, &y)) in self.xdata.iter().zip(self.ydata.iter()).enumerate() { - residuals[i] = (self.func)(x, param_slice) - y; - } - - Some(residuals) - } - - fn jacobian(&self) -> Option> { - let n = self.xdata.len(); - let n_params = self.params.len(); - let mut jacobian = DMatrix::zeros(n, n_params); - - let eps = 1e-8; - let residuals = self.residuals()?; - let param_slice = self.params.as_slice(); - - for j in 0..n_params { - let step = eps * (1.0 + param_slice[j].abs()).max(eps); - - // Create perturbed parameters - let mut params_plus = self.params.clone(); - params_plus[j] += step; - let params_plus_slice = params_plus.as_slice(); - - // Compute residuals with perturbed parameters - for (i, &x) in self.xdata.iter().enumerate() { - let residual_plus = (self.func)(x, params_plus_slice) - self.ydata[i]; - jacobian[(i, j)] = (residual_plus - residuals[i]) / step; - } - } - - Some(jacobian) - } -} - /// Options for curve fitting. #[derive(Debug, Clone)] pub struct CurveFitOptions { @@ -124,7 +57,7 @@ pub struct CurveFitOptions { pub xtol: f64, /// Tolerance for cost changes pub ftol: f64, - /// Initial damping parameter (ignored, using crate defaults) + /// Initial damping parameter pub lambda: f64, } @@ -152,12 +85,242 @@ pub struct CurveFitResult { pub cost: f64, } -// No problem struct needed - we'll use a closure directly +struct SolverState { + params: DVector, + jacobian: DMatrix, + cost: f64, + nfev: usize, +} + +fn all_finite(values: &DVector) -> bool { + values.iter().all(|value| value.is_finite()) +} + +fn max_abs(values: &DVector) -> f64 { + values + .iter() + .fold(0.0_f64, |max_value, value| max_value.max(value.abs())) +} + +fn evaluate_residuals( + func: &F, + xdata: &[f64], + ydata: &[f64], + params: &DVector, +) -> Result, CurveFitError> +where + F: Fn(f64, &[f64]) -> f64, +{ + let mut residuals = DVector::zeros(xdata.len()); + let param_slice = params.as_slice(); + + for (i, (&x, &y)) in xdata.iter().zip(ydata.iter()).enumerate() { + let value = func(x, param_slice); + let residual = value - y; + if !residual.is_finite() { + return Err(CurveFitError::NumericalIssue { + message: "Model function returned a non-finite residual".to_string(), + }); + } + residuals[i] = residual; + } + + Ok(residuals) +} + +fn finite_difference_jacobian( + func: &F, + xdata: &[f64], + ydata: &[f64], + params: &DVector, + residuals: &DVector, + nfev: &mut usize, +) -> Result, CurveFitError> +where + F: Fn(f64, &[f64]) -> f64, +{ + let n = xdata.len(); + let n_params = params.len(); + let mut jacobian = DMatrix::zeros(n, n_params); + + for j in 0..n_params { + let step = DIFF_STEP * (1.0 + params[j].abs()); + if !step.is_finite() || step <= 0.0 { + return Err(CurveFitError::NumericalIssue { + message: "Failed to choose a finite-difference step".to_string(), + }); + } + + let mut params_plus = params.clone(); + params_plus[j] += step; + let residuals_plus = evaluate_residuals(func, xdata, ydata, ¶ms_plus)?; + *nfev += 1; + + for i in 0..n { + jacobian[(i, j)] = (residuals_plus[i] - residuals[i]) / step; + } + } + + Ok(jacobian) +} + +fn solve_damped_step( + jt_j: &DMatrix, + gradient: &DVector, + lambda: f64, +) -> Option> { + let mut lhs = jt_j.clone(); + for i in 0..lhs.nrows() { + lhs[(i, i)] += lambda * jt_j[(i, i)].abs().max(1.0); + } + + let rhs = -gradient; + + if let Some(cholesky) = lhs.clone().cholesky() { + let delta = cholesky.solve(&rhs); + if all_finite(&delta) { + return Some(delta); + } + } -/// Fit a non-linear function to data using Levenberg-Marquardt. + if let Some(delta) = lhs.clone().lu().solve(&rhs) + && all_finite(&delta) + { + return Some(delta); + } + + match lhs.svd(true, true).solve(&rhs, 1e-12) { + Ok(delta) if all_finite(&delta) => Some(delta), + _ => None, + } +} + +fn increase_lambda(lambda: f64) -> f64 { + (lambda * 10.0).min(MAX_LAMBDA) +} + +fn decrease_lambda(lambda: f64) -> f64 { + (lambda * 0.1).max(MIN_LAMBDA) +} + +fn solve_least_squares( + func: &F, + xdata: &[f64], + ydata: &[f64], + p0: DVector, + opts: &CurveFitOptions, +) -> Result +where + F: Fn(f64, &[f64]) -> f64, +{ + let maxfev = opts.maxfev.max(1); + let xtol = if opts.xtol.is_finite() && opts.xtol > 0.0 { + opts.xtol + } else { + CurveFitOptions::default().xtol + }; + let ftol = if opts.ftol.is_finite() && opts.ftol > 0.0 { + opts.ftol + } else { + CurveFitOptions::default().ftol + }; + let mut lambda = if opts.lambda.is_finite() && opts.lambda > 0.0 { + opts.lambda + } else { + CurveFitOptions::default().lambda + } + .clamp(MIN_LAMBDA, MAX_LAMBDA); + + let mut params = p0; + let mut residuals = evaluate_residuals(func, xdata, ydata, ¶ms)?; + let mut cost = residuals.dot(&residuals); + let mut nfev = 1; + + while nfev < maxfev { + let jacobian = + finite_difference_jacobian(func, xdata, ydata, ¶ms, &residuals, &mut nfev)?; + let jt = jacobian.transpose(); + let jt_j = &jt * &jacobian; + let gradient = &jt * &residuals; + let gradient_norm = max_abs(&gradient); + + if gradient_norm <= ftol || cost <= ftol * ftol { + return Ok(SolverState { + params, + jacobian, + cost, + nfev, + }); + } + + let mut accepted = false; + + for _ in 0..MAX_DAMPING_ATTEMPTS { + if nfev >= maxfev { + break; + } + + let Some(delta) = solve_damped_step(&jt_j, &gradient, lambda) else { + lambda = increase_lambda(lambda); + continue; + }; + + let step_norm = delta.norm(); + let step_converged = step_norm <= xtol * (xtol + params.norm()); + + let trial_params = ¶ms + δ + if !all_finite(&trial_params) { + lambda = increase_lambda(lambda); + continue; + } + + let trial_residuals = evaluate_residuals(func, xdata, ydata, &trial_params)?; + nfev += 1; + let trial_cost = trial_residuals.dot(&trial_residuals); + + if trial_cost.is_finite() && trial_cost < cost { + params = trial_params; + residuals = trial_residuals; + cost = trial_cost; + lambda = decrease_lambda(lambda); + accepted = true; + + if step_converged || cost <= ftol * ftol { + let jacobian = finite_difference_jacobian( + func, xdata, ydata, ¶ms, &residuals, &mut nfev, + )?; + return Ok(SolverState { + params, + jacobian, + cost, + nfev, + }); + } + + break; + } + + lambda = increase_lambda(lambda); + } + + if !accepted { + return Err(CurveFitError::ConvergenceError { + message: format!( + "Optimization did not converge: no improving step found after {nfev} evaluations" + ), + }); + } + } + + Err(CurveFitError::ConvergenceError { + message: format!("Optimization did not converge within {maxfev} evaluations"), + }) +} + +/// Fit a non-linear function to data using damped least-squares. /// -/// This is a Rust implementation of `scipy.optimize.curve_fit` using the -/// `levenberg-marquardt` crate for robust, well-tested optimization. +/// This is a Rust implementation of `scipy.optimize.curve_fit` for simple +/// float model functions. /// /// # Arguments /// @@ -230,52 +393,25 @@ where let opts = options.unwrap_or_default(); - // Create problem for Levenberg-Marquardt - let problem = CurveFitProblem { - func, - xdata: xdata.to_vec(), - ydata: ydata.to_vec(), - params: DVector::from_vec(p0.to_vec()), - }; - - // Run Levenberg-Marquardt optimization - let (result, report) = LevenbergMarquardt::new() - .with_stepbound(100.0) - .with_patience(opts.maxfev) - .minimize(problem); - - // Check convergence - if !report.termination.was_successful() { - return Err(CurveFitError::ConvergenceError { - message: format!("Optimization did not converge: {:?}", report.termination), - }); - } - - // Get final parameters and residuals - let final_params = result.params(); - let final_residuals = result - .residuals() - .ok_or_else(|| CurveFitError::NumericalIssue { - message: "Failed to compute final residuals".to_string(), - })?; - let cost = final_residuals.dot(&final_residuals); - - // Get Jacobian at solution - let jacobian = result - .jacobian() - .ok_or_else(|| CurveFitError::NumericalIssue { - message: "Failed to compute Jacobian".to_string(), - })?; + let xdata_vec = xdata.to_vec(); + let ydata_vec = ydata.to_vec(); + let state = solve_least_squares( + &func, + &xdata_vec, + &ydata_vec, + DVector::from_vec(p0.to_vec()), + &opts, + )?; // Compute covariance matrix: (J^T * J)^-1 * variance - let jt_j = jacobian.transpose() * &jacobian; + let jt_j = state.jacobian.transpose() * &state.jacobian; let pcov = match jt_j.svd(true, true).pseudo_inverse(1e-15) { Ok(inv) => { - let n_params = final_params.len(); + let n_params = state.params.len(); // Cast to f64 is safe for reasonable dataset sizes (< 2^53 points) #[allow(clippy::cast_precision_loss)] let dof = (n as f64 - n_params as f64).max(1.0); - let variance = cost / dof; + let variance = state.cost / dof; let cov_mat = inv * variance; // Convert to ndarray @@ -292,22 +428,22 @@ where log::debug!( "curve_fit: converged after {} evaluations with cost={:.6e}", - report.number_of_evaluations, - cost + state.nfev, + state.cost ); Ok(CurveFitResult { - params: Array1::from_vec(final_params.as_slice().to_vec()), + params: Array1::from_vec(state.params.as_slice().to_vec()), pcov, - nfev: report.number_of_evaluations, - cost, + nfev: state.nfev, + cost: state.cost, }) } #[cfg(test)] mod tests { use super::*; - use ndarray::array; + use ndarray::{Array1, array}; #[test] fn test_curve_fit_linear() { @@ -399,6 +535,117 @@ mod tests { ); } + #[test] + fn test_curve_fit_gaussian() { + fn gaussian(x: f64, params: &[f64]) -> f64 { + let amp = params[0]; + let mu = params[1]; + let sigma = params[2]; + amp * (-((x - mu).powi(2)) / (2.0 * sigma * sigma)).exp() + } + + let xdata = Array1::linspace(-5.0, 5.0, 50); + let ydata = xdata.mapv(|x| gaussian(x, &[2.0, 1.0, 1.5])); + let p0 = array![1.0, 0.0, 1.0]; + let options = CurveFitOptions { + maxfev: 5000, + ..CurveFitOptions::default() + }; + + let result = curve_fit( + gaussian, + xdata.view(), + ydata.view(), + p0.view(), + Some(options), + ) + .unwrap(); + + assert!( + (result.params[0] - 2.0).abs() < 1e-5, + "amplitude should be 2.0, got {}", + result.params[0] + ); + assert!( + (result.params[1] - 1.0).abs() < 1e-5, + "mean should be 1.0, got {}", + result.params[1] + ); + assert!( + (result.params[2] - 1.5).abs() < 1e-5, + "sigma should be 1.5, got {}", + result.params[2] + ); + } + + #[test] + fn test_curve_fit_sine() { + fn sine(x: f64, params: &[f64]) -> f64 { + params[0] * (2.0 * std::f64::consts::PI * params[1] * x + params[2]).sin() + } + + let xdata = Array1::linspace(0.0, 2.0, 100); + let ydata = xdata.mapv(|x| sine(x, &[1.5, 2.0, 0.5])); + let p0 = array![1.0, 2.0, 0.0]; + let options = CurveFitOptions { + maxfev: 5000, + ..CurveFitOptions::default() + }; + + let result = curve_fit(sine, xdata.view(), ydata.view(), p0.view(), Some(options)).unwrap(); + + assert!( + (result.params[0] - 1.5).abs() < 1e-5, + "amplitude should be 1.5, got {}", + result.params[0] + ); + assert!( + (result.params[1] - 2.0).abs() < 1e-5, + "frequency should be 2.0, got {}", + result.params[1] + ); + assert!( + (result.params[2] - 0.5).abs() < 1e-5, + "phase should be 0.5, got {}", + result.params[2] + ); + } + + #[test] + fn test_curve_fit_non_finite_residual() { + fn non_finite(x: f64, params: &[f64]) -> f64 { + params[0] / x + } + + let xdata = array![0.0, 1.0, 2.0]; + let ydata = array![1.0, 2.0, 3.0]; + let p0 = array![1.0]; + + let result = curve_fit(non_finite, xdata.view(), ydata.view(), p0.view(), None); + assert!(matches!(result, Err(CurveFitError::NumericalIssue { .. }))); + } + + #[test] + fn test_curve_fit_maxfev_too_small() { + fn linear(x: f64, params: &[f64]) -> f64 { + params[0] * x + params[1] + } + + let xdata = array![0.0, 1.0, 2.0, 3.0]; + let ydata = array![1.0, 3.0, 5.0, 7.0]; + let p0 = array![1.0, 0.0]; + let options = CurveFitOptions { + maxfev: 1, + ..CurveFitOptions::default() + }; + + let result = curve_fit(linear, xdata.view(), ydata.view(), p0.view(), Some(options)); + assert!(matches!( + result, + Err(CurveFitError::ConvergenceError { .. }) + )); + } + #[test] fn test_curve_fit_insufficient_data() { fn linear(x: f64, params: &[f64]) -> f64 { diff --git a/crates/pecos-num/src/lib.rs b/crates/pecos-num/src/lib.rs index e95d1dbd3..3976769be 100644 --- a/crates/pecos-num/src/lib.rs +++ b/crates/pecos-num/src/lib.rs @@ -22,7 +22,7 @@ //! - Array operations (diag, linspace) //! - Random number generation (numpy.random drop-in replacements) //! - Root finding algorithms (Brent's method, Newton-Raphson) -//! - Curve fitting (Levenberg-Marquardt, polynomial fitting) +//! - Curve fitting (damped least-squares, polynomial fitting) //! - Graph data structures ([`Graph`](graph::Graph), [`DiGraph`](digraph::DiGraph), [`DAG`](dag::DAG)) //! - Graph algorithms (MWPM matching, shortest paths, topological sort) //! - Performance improvements over scipy/numpy diff --git a/python/pecos-rslib/src/num_bindings.rs b/python/pecos-rslib/src/num_bindings.rs index 54aa23b9a..526e34e14 100644 --- a/python/pecos-rslib/src/num_bindings.rs +++ b/python/pecos-rslib/src/num_bindings.rs @@ -357,7 +357,7 @@ impl Poly1d { } } -/// Fit a non-linear function to data using Levenberg-Marquardt. +/// Fit a non-linear function to data using damped least-squares. /// /// This is a drop-in replacement for `scipy.optimize.curve_fit`. /// diff --git a/scripts/native_bench/bench_pecos/Cargo.lock b/scripts/native_bench/bench_pecos/Cargo.lock index b69313378..5030ea8a4 100644 --- a/scripts/native_bench/bench_pecos/Cargo.lock +++ b/scripts/native_bench/bench_pecos/Cargo.lock @@ -1523,18 +1523,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" -[[package]] -name = "levenberg-marquardt" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be7a65739a815308eef33a6d8c78e435a7317305d5b0af0c8c465a2d7ac6fc6" -dependencies = [ - "cfg-if", - "nalgebra", - "num-traits", - "rustc_version", -] - [[package]] name = "libbz2-rs-sys" version = "0.2.2" @@ -2081,7 +2069,6 @@ dependencies = [ name = "pecos-num" version = "0.2.0-dev.0" dependencies = [ - "levenberg-marquardt", "log", "nalgebra", "ndarray 0.17.2", @@ -2602,15 +2589,6 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustix" version = "1.1.4" From 26fe132a5bb7f58d577451d279ef1c869a0b1808 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Mon, 18 May 2026 19:43:26 -0600 Subject: [PATCH 12/12] Document security checks --- docs/development/DEVELOPMENT.md | 36 +++++++++++++++++++++++++++++++-- docs/development/dev-tools.md | 30 +++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/docs/development/DEVELOPMENT.md b/docs/development/DEVELOPMENT.md index 8ecee016e..750b79a05 100644 --- a/docs/development/DEVELOPMENT.md +++ b/docs/development/DEVELOPMENT.md @@ -108,15 +108,47 @@ For developers who want to contribute or modify PECOS: just lint ``` -11. To deactivate your development venv: +11. Run dependency and security policy checks when touching dependency manifests, lockfiles, GitHub Actions workflows, or security policy: + ```sh + just security-check + ``` + + For Rust-only dependency changes, `just cargo-deny` runs the same `cargo-deny` checks that CI applies to the root workspace and the standalone native benchmark crate. + +12. To deactivate your development venv: ```sh deactivate ``` -Before pull requests are merged, they must pass linting and the test. +Before pull requests are merged, they must pass linting, tests, and dependency/security checks. The local pre-PR gate is: + +```sh +just check-all +``` Note: For the Rust side of the project, you can use `cargo` to run tests, benchmarks, formatting, etc. +## Dependency and Security Checks + +Use the Justfile recipes below so local checks match CI: + +| Command | When to run | What it checks | +|---------|-------------|----------------| +| `just security-check` | Dependency, lockfile, GitHub Actions, cache, or security-policy changes | Runs the dependency integrity script and both `cargo-deny` checks | +| `just cargo-deny` | Rust dependency or Cargo lockfile changes | Checks advisories, banned dependency patterns, and allowed dependency sources | +| `just cargo-deny-workspace` | Root workspace Rust dependency changes | Runs `cargo-deny` on the root Rust workspace | +| `just cargo-deny-native-bench` | Native benchmark crate dependency changes | Runs `cargo-deny` on `scripts/native_bench/bench_pecos/Cargo.toml` | +| `just dependency-integrity-check` | CI workflow, lockfile policy, action pinning, or cache posture changes | Checks lock discipline, action pinning, cache write posture, dependency review coverage, and package-worm indicators | +| `just check-all` | Before opening or updating a PR with broad changes | Runs clean, release build, release tests, lint, and dependency/security checks | + +`cargo-deny` is not installed by `uv sync`. To run the Rust dependency policy checks locally, install the same version used by CI: + +```sh +cargo install --locked --version 0.19.6 cargo-deny +``` + +The first `cargo-deny` run may update the local advisory database under `~/.cargo`. CI runs these checks on every relevant Cargo manifest, lockfile, `deny.toml`, or cargo-deny workflow change, and also on the scheduled security lane. + ## Cleaning Build Artifacts Clean commands are cross-platform (Windows, macOS, Linux): diff --git a/docs/development/dev-tools.md b/docs/development/dev-tools.md index 26811ac47..774dacfae 100644 --- a/docs/development/dev-tools.md +++ b/docs/development/dev-tools.md @@ -85,6 +85,10 @@ just fmt # Check Rust formatting just fmt-fix # Fix Rust formatting just clippy # Run clippy +# Dependency and security policy +just security-check # Dependency integrity + cargo-deny checks +just cargo-deny # Rust dependency policy checks + # Cleaning just clean # Clean build artifacts ``` @@ -153,6 +157,32 @@ pecos deps sync Syncs crate-level `pecos.toml` manifests from the workspace-level manifest. +## Dependency and Security Policy + +Run these recipes when changing dependencies, lockfiles, CI workflows, action references, cache behavior, or security policy: + +```bash +just security-check # Full local dependency/security policy check +just dependency-integrity-check # Lock discipline, CI posture, action pins, cache posture +just cargo-deny # Both Rust cargo-deny checks covered by CI +just cargo-deny-workspace # Root Rust workspace only +just cargo-deny-native-bench # Standalone native benchmark crate only +``` + +`cargo-deny` checks the resolved Rust dependency graph against `deny.toml`. In this repo it checks: + +- `advisories`: known Rust security advisories +- `bans`: disallowed crates or dependency patterns +- `sources`: approved registries and git sources + +Install the same `cargo-deny` version used by CI before running these locally: + +```bash +cargo install --locked --version 0.19.6 cargo-deny +``` + +Use `just check-all` before a broad PR; it runs the build, tests, lint gate, and `just security-check`. + ## Environment Variables | Variable | Description | Default |