diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8ae9df08..0f476bcb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,12 +7,23 @@ version: 2 updates: + # All Rust/Cargo directories are grouped into a single entry so that + # when a dependency is updated, Dependabot creates ONE PR that bumps it + # across the root workspace AND every binding, preventing version skew. - package-ecosystem: "cargo" - directory: "/" # Location of package manifests + directories: + - "/" + - "/bindings/ffi" + - "/bindings/java" + - "/bindings/python" + - "/bindings/ruby" + - "/bindings/wasm" schedule: interval: "weekly" + commit-message: + prefix: "build(deps)" groups: - per-dependency: + rust-dependencies: patterns: - "*" # Ignore vendored mimalloc crates; updates are managed manually. @@ -20,82 +31,12 @@ updates: - dependency-name: "regorus-mimalloc" - dependency-name: "regorus-mimalloc-sys" - - package-ecosystem: "cargo" - directory: "/bindings/ffi" - schedule: - interval: "weekly" - groups: - per-dependency: - patterns: - - "*" - - - package-ecosystem: "cargo" - directory: "/bindings/java" - schedule: - interval: "weekly" - groups: - per-dependency: - patterns: - - "*" - - - package-ecosystem: "cargo" - directory: "/bindings/python" - schedule: - interval: "weekly" - groups: - per-dependency: - patterns: - - "*" - - - package-ecosystem: "cargo" - directory: "/bindings/ruby" - schedule: - interval: "weekly" - groups: - per-dependency: - patterns: - - "*" - - - package-ecosystem: "cargo" - directory: "/bindings/ruby/ext/regorusrb" - schedule: - interval: "weekly" - groups: - per-dependency: - patterns: - - "*" - - - package-ecosystem: "cargo" - directory: "/bindings/wasm" - schedule: - interval: "weekly" - groups: - per-dependency: - patterns: - - "*" - - - package-ecosystem: "cargo" - directory: "/tests/ensure_no_std" - schedule: - interval: "weekly" - groups: - per-dependency: - patterns: - - "*" - - - package-ecosystem: "cargo" - directory: "/xtask" - schedule: - interval: "weekly" - groups: - per-dependency: - patterns: - - "*" - - package-ecosystem: "gomod" directory: "/bindings/go" schedule: interval: "weekly" + commit-message: + prefix: "build(deps)" groups: per-dependency: patterns: @@ -105,6 +46,8 @@ updates: directory: "/bindings/java" schedule: interval: "weekly" + commit-message: + prefix: "build(deps)" groups: per-dependency: patterns: @@ -114,6 +57,8 @@ updates: directory: "/bindings/csharp" schedule: interval: "weekly" + commit-message: + prefix: "build(deps)" groups: per-dependency: patterns: @@ -123,6 +68,8 @@ updates: directory: "/bindings/python" schedule: interval: "weekly" + commit-message: + prefix: "build(deps)" groups: per-dependency: patterns: @@ -132,6 +79,8 @@ updates: directory: "/bindings/ruby" schedule: interval: "weekly" + commit-message: + prefix: "build(deps)" groups: per-dependency: patterns: @@ -141,7 +90,9 @@ updates: directory: "/" schedule: interval: "weekly" + commit-message: + prefix: "ci(deps)" groups: - per-dependency: + github-actions: patterns: - "*" diff --git a/.github/workflows/dependabot-refresh-cargo-lockfiles.yml b/.github/workflows/dependabot-refresh-cargo-lockfiles.yml new file mode 100644 index 00000000..4bdbae33 --- /dev/null +++ b/.github/workflows/dependabot-refresh-cargo-lockfiles.yml @@ -0,0 +1,107 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# +name: dependabot/refresh-cargo-lockfiles + +on: + pull_request_target: + types: [opened, synchronize, reopened] + branches: ["main"] + +permissions: + contents: write + +env: + CARGO_TERM_COLOR: always + +jobs: + refresh-cargo-lockfiles: + if: >- + github.actor == 'dependabot[bot]' && + github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + fetch-depth: 0 + + - name: Setup Rust toolchain + run: | + rustup override set 1.92.0 + cargo --version + rustc --version + + - name: Refresh affected Cargo lockfiles + shell: bash + run: | + set -euo pipefail + + base_sha="${{ github.event.pull_request.base.sha }}" + head_sha="${{ github.event.pull_request.head.sha }}" + + mapfile -t changed_files < <(git diff --name-only "$base_sha" "$head_sha" -- '**/Cargo.toml' '**/Cargo.lock') + + if [ "${#changed_files[@]}" -eq 0 ]; then + echo "No Cargo manifest or lockfile changes detected." + exit 0 + fi + + declare -A manifests=() + for path in "${changed_files[@]}"; do + case "$path" in + bindings/ffi/*) + manifests["bindings/ffi/Cargo.toml"]=1 + ;; + bindings/java/*) + manifests["bindings/java/Cargo.toml"]=1 + ;; + bindings/python/*) + manifests["bindings/python/Cargo.toml"]=1 + ;; + bindings/ruby/*) + manifests["bindings/ruby/Cargo.toml"]=1 + ;; + bindings/wasm/*) + manifests["bindings/wasm/Cargo.toml"]=1 + ;; + *) + manifests["Cargo.toml"]=1 + ;; + esac + done + + for manifest in "${!manifests[@]}"; do + echo "Refreshing lockfile for $manifest" + cargo metadata --format-version 1 --all-features --manifest-path "$manifest" > /dev/null + done + + if [[ -n "${manifests[Cargo.toml]+x}" ]]; then + echo "Refreshing lockfile for tests/ensure_no_std/Cargo.toml (thumbv7m-none-eabi)" + cargo metadata --format-version 1 \ + --manifest-path tests/ensure_no_std/Cargo.toml \ + --filter-platform thumbv7m-none-eabi > /dev/null + fi + + - name: Commit lockfile refresh + shell: bash + run: | + set -euo pipefail + + git add Cargo.lock \ + bindings/ffi/Cargo.lock \ + bindings/java/Cargo.lock \ + bindings/python/Cargo.lock \ + bindings/ruby/Cargo.lock \ + bindings/wasm/Cargo.lock + + if git diff --cached --quiet; then + echo "No Cargo lockfile changes required." + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "build(deps): refresh Cargo lockfiles" + git push diff --git a/.github/workflows/feature-matrix.yml b/.github/workflows/feature-matrix.yml new file mode 100644 index 00000000..df9de6c8 --- /dev/null +++ b/.github/workflows/feature-matrix.yml @@ -0,0 +1,82 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# +# Thorough weekly test of non-default feature combinations. +# Catches regressions from dependency updates and feature-gating issues +# that the fast PR CI checks (cargo check only) would miss at runtime. +name: tests/feature-matrix + +on: + workflow_dispatch: + schedule: + # Run at 3:42 AM UTC every Saturday. + - cron: "42 3 * * 6" + +env: + CARGO_TERM_COLOR: always + +jobs: + feature-matrix: + name: ${{ matrix.name }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + # Bare minimum: validates that the core interpreter works + # without any builtins or optional subsystems. + - name: minimal (std + arc) + features: std,arc + + # Common library usage pattern (issue #595): consumer enables + # std + arc + rvm and relies on indexmap/std propagation. + - name: library (std + arc + rvm) + features: std,arc,rvm + + # New default after removing mimalloc from full-opa. + # Ensures all builtins compile without the allocator. + - name: full-opa (no mimalloc) + features: std,arc,full-opa + + # Binding-style usage: full-opa with the vendored allocator. + # Mirrors how ffi/java/python/ruby bindings are built. + - name: full-opa + allocator + features: std,arc,full-opa,allocator-memory-limits + + # Selective builtins without full-opa: validates that popular + # features can be cherry-picked independently. + - name: cherry-picked builtins + features: std,arc,rvm,regex,time,semver,cache + + # Observability features only: coverage + cache without the + # heavier builtins (regex, time, etc.). + - name: observability + features: std,arc,rvm,coverage,cache + + # Azure Policy adds jsonschema + dashmap; test it compiles + # and runs on top of full-opa. + - name: azure-policy + features: std,arc,full-opa,azure_policy + + # Azure RBAC adds regex + time + net on top of full-opa. + - name: azure-rbac + features: std,arc,full-opa,azure-rbac + + # no_std with the OPA-compatible feature set: exercises the + # spin_no_std codepath and absence of std-only dependencies. + - name: no_std + features: arc,opa-no-std + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup Rust toolchain + uses: ./.github/actions/toolchains/rust + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus-features + - name: Fetch dependencies + run: cargo fetch --locked + - name: Build + run: cargo build --no-default-features --features "${{ matrix.features }}" --frozen + - name: Test + run: cargo test --no-default-features --features "${{ matrix.features }}" --frozen diff --git a/Cargo.toml b/Cargo.toml index 2dbe5d3c..a75c9479 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ cache = ["dep:lru"] rvm = ["dep:postcard", "dep:indexmap"] semver = ["dep:semver"] allocator-memory-limits = ["std", "mimalloc", "mimalloc/allocator-memory-limits"] -std = ["rand/std", "rand/std_rng", "serde_json/std", "msvc_spectre_libs", "dep:parking_lot" ] +std = ["rand/std", "rand/std_rng", "serde_json/std", "indexmap?/std", "msvc_spectre_libs", "dep:parking_lot" ] time = ["dep:chrono", "dep:chrono-tz"] uuid = ["dep:uuid"] urlquery = ["dep:url"] @@ -57,8 +57,6 @@ full-opa = [ "hex", "http", "jsonschema", - "allocator-memory-limits", - "mimalloc", "net", "opa-runtime", "regex", diff --git a/benches/rvm_benchmark.rs b/benches/rvm_benchmark.rs index 2604be06..ce5092bf 100644 --- a/benches/rvm_benchmark.rs +++ b/benches/rvm_benchmark.rs @@ -57,6 +57,7 @@ use regorus::{Engine, Rc, Value}; // hot path (memory_check, execution_timer_tick, instruction-limit compare). // --------------------------------------------------------------------------- +#[cfg(feature = "allocator-memory-limits")] const MEMORY_LIMIT_BYTES: u64 = 256 * 1024 * 1024; const TIME_LIMIT: Duration = Duration::from_secs(30); const TIMER_CHECK_INTERVAL: NonZeroU32 = NonZeroU32::new(16).unwrap(); @@ -360,6 +361,7 @@ fn compile_all_programs() -> Vec { /// Apply or remove production-style limits based on a boolean flag. fn configure_limits(vm: &mut RegoVM, limits: bool) { if limits { + #[cfg(feature = "allocator-memory-limits")] regorus::set_global_memory_limit(Some(MEMORY_LIMIT_BYTES)); vm.set_execution_timer_config(Some(ExecutionTimerConfig { limit: TIME_LIMIT, @@ -367,6 +369,7 @@ fn configure_limits(vm: &mut RegoVM, limits: bool) { })); vm.set_max_instructions(INSTRUCTION_LIMIT); } else { + #[cfg(feature = "allocator-memory-limits")] regorus::set_global_memory_limit(None); vm.set_execution_timer_config(None); vm.set_max_instructions(usize::MAX); diff --git a/bindings/java/Cargo.toml b/bindings/java/Cargo.toml index 07104487..a32284b8 100644 --- a/bindings/java/Cargo.toml +++ b/bindings/java/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["interpreter", "opa", "policy-as-code", "rego"] crate-type = ["cdylib"] [features] -default = ["ast", "cache", "coverage", "regorus/std", "regorus/full-opa"] +default = ["ast", "cache", "coverage", "regorus/std", "regorus/full-opa", "regorus/allocator-memory-limits"] coverage = ["regorus/coverage"] ast = ["regorus/ast"] cache = ["regorus/cache"] diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index 87f2c944..f265c66b 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -15,7 +15,7 @@ keywords = ["interpreter", "opa", "policy-as-code", "rego"] crate-type = ["cdylib"] [features] -default = ["ast", "cache", "coverage", "regorus/std", "regorus/full-opa"] +default = ["ast", "cache", "coverage", "regorus/std", "regorus/full-opa", "regorus/allocator-memory-limits"] ast = ["regorus/ast"] cache = ["regorus/cache"] coverage = ["regorus/coverage"] diff --git a/bindings/ruby/Gemfile b/bindings/ruby/Gemfile index 0b5a592e..7cd75951 100644 --- a/bindings/ruby/Gemfile +++ b/bindings/ruby/Gemfile @@ -11,6 +11,6 @@ gem "minitest", "~> 6.0" gem "rake", "~> 13.3" gem "rake-compiler", "~> 1.3" gem "rake-compiler-dock", "~> 1.11" -gem "rubocop", "~> 1.85", require: false +gem "rubocop", "~> 1.86", require: false gem "rubocop-minitest", "~> 0.39.1", require: false gem "rubocop-rake", "~> 0.7.1", require: false diff --git a/bindings/ruby/Gemfile.lock b/bindings/ruby/Gemfile.lock index 1b9f061e..29df353b 100644 --- a/bindings/ruby/Gemfile.lock +++ b/bindings/ruby/Gemfile.lock @@ -7,19 +7,11 @@ PATH GEM remote: https://rubygems.org/ specs: - addressable (2.8.9) - public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) - bigdecimal (4.0.1) drb (2.2.3) - json (2.18.1) - json-schema (6.1.0) - addressable (~> 2.8) - bigdecimal (>= 3.1, < 5) + json (2.19.3) language_server-protocol (3.17.0.5) lint_roller (1.1.0) - mcp (0.8.0) - json-schema (>= 4.1) minitest (6.0.2) drb (~> 2.0) prism (~> 1.5) @@ -28,7 +20,6 @@ GEM ast (~> 2.4.1) racc prism (1.9.0) - public_suffix (7.0.5) racc (1.8.1) rainbow (3.1.1) rake (13.3.1) @@ -38,11 +29,10 @@ GEM rb_sys (0.9.124) rake-compiler-dock (= 1.11.0) regexp_parser (2.11.3) - rubocop (1.85.0) + rubocop (1.86.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) - mcp (~> 0.6) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) @@ -50,7 +40,7 @@ GEM rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.49.0) + rubocop-ast (1.49.1) parser (>= 3.3.7.2) prism (~> 1.7) rubocop-minitest (0.39.1) @@ -75,7 +65,7 @@ DEPENDENCIES rake-compiler (~> 1.3) rake-compiler-dock (~> 1.11) regorusrb! - rubocop (~> 1.85) + rubocop (~> 1.86) rubocop-minitest (~> 0.39.1) rubocop-rake (~> 0.7.1) diff --git a/bindings/ruby/ext/regorusrb/Cargo.toml b/bindings/ruby/ext/regorusrb/Cargo.toml index b2f18518..db89b357 100644 --- a/bindings/ruby/ext/regorusrb/Cargo.toml +++ b/bindings/ruby/ext/regorusrb/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["cdylib"] path = "src/lib.rs" [features] -default = ["ast", "cache", "coverage", "regorus/std", "regorus/full-opa"] +default = ["ast", "cache", "coverage", "regorus/std", "regorus/full-opa", "regorus/allocator-memory-limits"] ast = ["regorus/ast"] cache = ["regorus/cache"] coverage = ["regorus/coverage"] diff --git a/xtask/src/tasks/ci/mod.rs b/xtask/src/tasks/ci/mod.rs index 7250a0db..78349097 100644 --- a/xtask/src/tasks/ci/mod.rs +++ b/xtask/src/tasks/ci/mod.rs @@ -242,6 +242,10 @@ fn run_ci_suite(config: CiSuiteConfig) -> Result<()> { )?; } + // Verify that important feature subsets compile correctly. + // These catch issues like #595 where non-default combinations fail. + check_feature_combinations(&workspace, config.release, config.frozen)?; + Ok(()) } @@ -364,3 +368,48 @@ fn base_cargo_args( } args } + +/// Verify that various feature subsets compile. +/// +/// Library consumers may pick non-default feature combinations. Running +/// `cargo check` for each combination is fast and catches regressions like +/// issue #595 (indexmap/std not propagated) early. +/// +/// The weekly `feature-matrix.yml` workflow runs a superset of these with +/// full `cargo test`; these PR checks are intentionally `cargo check` only +/// to keep CI fast. +fn check_feature_combinations(workspace: &Path, release: bool, frozen: bool) -> Result<()> { + let combos: &[&str] = &[ + // Issue #595: library consumer with std + arc + rvm but no full-opa. + // Validates indexmap/std propagation via the weak-dep syntax. + "std,arc,rvm", + // full-opa without mimalloc: the new default after removing the + // vendored allocator from full-opa. All builtins, no allocator. + "std,arc,full-opa", + // Binding-style: full-opa plus the explicit allocator opt-in. + // Mirrors how ffi/java/python/ruby crates are configured. + "std,arc,full-opa,allocator-memory-limits", + // no_std codepath: exercises spin_no_std + absence of std deps. + "arc,opa-no-std", + // Cherry-picked builtins: popular features without full-opa to + // ensure individual feature gates compose correctly. + "std,arc,rvm,coverage,cache,regex,time", + ]; + + for features in combos { + let label = format!( + "cargo check --no-default-features --features {} (ci)", + features + ); + run_ci_cargo_step( + workspace, + "check", + release, + frozen, + Some(features), + &["--no-default-features"], + &label, + )?; + } + Ok(()) +}