Skip to content
35 changes: 13 additions & 22 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CI

on:
push:
branches: [main, master]
branches: [main]
pull_request:
branches: [main, master]
branches: [main]

env:
CARGO_TERM_COLOR: always
Expand All @@ -17,29 +17,19 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable, 1.70.0]
rust: [stable, "1.70.0"]

steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-action@stable
uses: dtolnay/rust-toolchain@master

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify action refs that are not pinned to 40-char SHAs.
rg -nP '^\s*-\s*uses:\s*[^@]+@(?!(?:[0-9a-fA-F]{40})$).+$' .github/workflows/*.yml

Repository: absmartly/rust-sdk

Length of output: 576


🏁 Script executed:

sed -n '20,35p' .github/workflows/ci.yml

Repository: absmartly/rust-sdk

Length of output: 420


🏁 Script executed:

cat -n .github/workflows/ci.yml

Repository: absmartly/rust-sdk

Length of output: 2017


🏁 Script executed:

# Check if there are any commit SHAs already pinned in the file
rg -nP '@[0-9a-fA-F]{40}' .github/workflows/ci.yml

Repository: absmartly/rust-sdk

Length of output: 44


Pin GitHub Actions to immutable commit SHAs.

Using moving refs (@master, @stable, @v2) allows upstream changes to alter CI behaviour without review and weakens supply-chain guarantees.

Proposed hardening diff
-      - name: Install Rust
-        uses: dtolnay/rust-toolchain@master
+      - name: Install Rust
+        uses: dtolnay/rust-toolchain@<full_commit_sha>

-      - uses: Swatinem/rust-cache@v2
+      - uses: Swatinem/rust-cache@<full_commit_sha>

-      - name: Install Rust
-        uses: dtolnay/rust-toolchain@stable
+      - name: Install Rust
+        uses: dtolnay/rust-toolchain@<full_commit_sha>

-      - uses: Swatinem/rust-cache@v2
+      - uses: Swatinem/rust-cache@<full_commit_sha>

-      - name: Install Rust
-        uses: dtolnay/rust-toolchain@stable
+      - name: Install Rust
+        uses: dtolnay/rust-toolchain@<full_commit_sha>

-      - uses: Swatinem/rust-cache@v2
+      - uses: Swatinem/rust-cache@<full_commit_sha>

Also applies to: 30-30, 44-44, 48-48, 63-63, 65-65

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml at line 26, Replace all GitHub Action moving refs
in .github/workflows/ci.yml (e.g., the uses: dtolnay/rust-toolchain@master line
and the other uses: entries flagged at lines 30, 44, 48, 63, 65) with immutable
commit SHAs; locate each uses: "<owner>/<repo>@<ref>" occurrence that uses
branches or tags like `@master`, `@stable`, or `@v2` and pin them to the corresponding
commit SHA from the action's repository (update the ref to @<full-commit-sha>),
ensuring every uses: entry in the workflow is changed to a specific commit hash
and not a moving ref.

with:
toolchain: ${{ matrix.rust }}

- name: Cache cargo registry
uses: actions/cache@v4
- uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ matrix.rust }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-${{ matrix.rust }}-

- name: Build
run: cargo build --verbose
key: ${{ matrix.rust }}

- name: Run tests
run: cargo test --verbose
Expand All @@ -51,11 +41,12 @@ jobs:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-action@stable
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
components: rustfmt, clippy

- uses: Swatinem/rust-cache@v2

- name: Check formatting
run: cargo fmt --all -- --check

Expand All @@ -69,11 +60,11 @@ jobs:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-action@stable
with:
toolchain: stable
uses: dtolnay/rust-toolchain@stable

- uses: Swatinem/rust-cache@v2

- name: Check documentation
run: cargo doc --no-deps --document-private-items
env:
RUSTDOCFLAGS: -D warnings
RUSTDOCFLAGS: "-Dwarnings"
46 changes: 46 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,52 @@ readme = "README.md"
keywords = ["absmartly", "ab-testing", "feature-flags", "experimentation", "sdk"]
categories = ["development-tools", "web-programming"]
exclude = [".github/", "target/", ".claude/"]
rust-version = "1.70"

[lints.rust]
unsafe_code = "forbid"
missing_docs = "warn"
missing_debug_implementations = "warn"
unreachable_pub = "warn"
unused_qualifications = "warn"
unused_results = "warn"

[lints.clippy]
correctness = { level = "deny", priority = -1 }
suspicious = { level = "deny", priority = -1 }

style = { level = "warn", priority = -1 }
complexity = { level = "warn", priority = -1 }
perf = { level = "warn", priority = -1 }

pedantic = { level = "warn", priority = -1 }

module_name_repetitions = "allow"
must_use_candidate = "allow"

unwrap_used = "warn"
expect_used = "warn"
panic = "warn"
todo = "warn"
unimplemented = "warn"
dbg_macro = "warn"
print_stdout = "warn"
print_stderr = "warn"
wildcard_imports = "warn"
enum_glob_use = "warn"
rc_buffer = "warn"
str_to_string = "warn"
string_to_string = "warn"
inefficient_to_string = "warn"
implicit_clone = "warn"
needless_pass_by_value = "warn"
redundant_closure_for_method_calls = "warn"
semicolon_if_nothing_returned = "warn"
doc_markdown = "warn"
manual_let_else = "warn"
match_wildcard_for_single_variants = "warn"
missing_errors_doc = "warn"
missing_panics_doc = "warn"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
Expand Down
2 changes: 2 additions & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
msrv = "1.70"
doc-valid-idents = ["ABsmartly", "GitHub", "JavaScript", "TypeScript"]
18 changes: 12 additions & 6 deletions src/assigner.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
//! Deterministic variant assignment based on unit hashing.

use crate::murmur3::murmur3_32;
use crate::utils::choose_variant;

/// Assigns experiment variants deterministically based on a hashed unit identifier.
#[derive(Debug)]
pub struct VariantAssigner {
unit_hash: u32,
}

impl VariantAssigner {
/// Creates a new assigner from a unit identifier string.
pub fn new(unit: &str) -> Self {
let unit_hash = murmur3_32(unit.as_bytes(), 0);
Self { unit_hash }
}

/// Assigns a variant index based on the split weights and experiment seeds.
pub fn assign(&self, split: &[f64], seed_hi: u32, seed_lo: u32) -> usize {
let prob = self.probability(seed_hi, seed_lo);
choose_variant(split, prob)
Expand All @@ -23,7 +29,7 @@ impl VariantAssigner {
buffer[8..12].copy_from_slice(&self.unit_hash.to_le_bytes());

let hash = murmur3_32(&buffer, 0);
(hash as f64) / (0xFFFFFFFFu32 as f64)
f64::from(hash) / f64::from(0xFFFF_FFFFu32)
}
}

Expand All @@ -36,31 +42,31 @@ mod tests {
fn test_assigner_bleh_email() {
let hashed_unit = hash_unit("bleh@absmartly.com");
let assigner = VariantAssigner::new(&hashed_unit);
let variant = assigner.assign(&[0.5, 0.5], 0x00000000, 0x00000000);
let variant = assigner.assign(&[0.5, 0.5], 0x0000_0000, 0x0000_0000);
assert_eq!(variant, 0);
}

#[test]
fn test_assigner_bleh_email_different_seed() {
let hashed_unit = hash_unit("bleh@absmartly.com");
let assigner = VariantAssigner::new(&hashed_unit);
let variant = assigner.assign(&[0.5, 0.5], 0x00000000, 0x00000001);
let variant = assigner.assign(&[0.5, 0.5], 0x0000_0000, 0x0000_0001);
assert_eq!(variant, 1);
}

#[test]
fn test_assigner_123456789() {
let hashed_unit = hash_unit("123456789");
let assigner = VariantAssigner::new(&hashed_unit);
assert_eq!(assigner.assign(&[0.5, 0.5], 0x00000000, 0x00000000), 1);
assert_eq!(assigner.assign(&[0.5, 0.5], 0x00000000, 0x00000001), 0);
assert_eq!(assigner.assign(&[0.5, 0.5], 0x0000_0000, 0x0000_0000), 1);
assert_eq!(assigner.assign(&[0.5, 0.5], 0x0000_0000, 0x0000_0001), 0);
}

#[test]
fn test_assigner_three_way_split() {
let hashed_unit = hash_unit("bleh@absmartly.com");
let assigner = VariantAssigner::new(&hashed_unit);
let variant = assigner.assign(&[0.33, 0.33, 0.34], 0x00000000, 0x00000001);
let variant = assigner.assign(&[0.33, 0.33, 0.34], 0x0000_0000, 0x0000_0001);
assert_eq!(variant, 2);
}
}
Loading
Loading