From 45b422090dbb76311ddb6010c51dd3379a58f8d1 Mon Sep 17 00:00:00 2001 From: Alvaro Bartolome <36760800+alvarobartt@users.noreply.github.com> Date: Thu, 24 Sep 2026 12:25:23 +0000 Subject: [PATCH 1/9] Add missing `model` in `curl` request --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 58fe118..cd425d2 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ And, just query your Jev-compatible API at `/v1/systemone` (or `/v1/decide`). curl http://localhost:3000/v1/systemone \ -H "Content-Type: application/json" \ -d '{ + "model": "convaiinnovations/laya", "state": { "message": "I was charged twice for invoice 4411. Please refund me today." }, From d1a047059022d1a3b42bfafe1d7c4e2bf0ed5c52 Mon Sep 17 00:00:00 2001 From: Alvaro Bartolome <36760800+alvarobartt@users.noreply.github.com> Date: Thu, 24 Sep 2026 12:25:49 +0000 Subject: [PATCH 2/9] Add `insta` dependency for snapshot testing --- Cargo.lock | 19 +++++++++++++++++++ Cargo.toml | 1 + 2 files changed, 20 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index bf94773..3957bfa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2175,6 +2175,18 @@ dependencies = [ "web-time", ] +[[package]] +name = "insta" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" +dependencies = [ + "console", + "once_cell", + "similar", + "tempfile", +] + [[package]] name = "ipnet" version = "2.12.2" @@ -3813,6 +3825,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "slab" version = "0.4.12" @@ -3950,6 +3968,7 @@ dependencies = [ "candle-nn", "clap", "hf-hub", + "insta", "serde", "serde_json", "tokenizers 1.0.0-rc.2", diff --git a/Cargo.toml b/Cargo.toml index 12c3116..3335c94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ candle-nn = { version = "0.11.0", features = ["accelerate"] } candle-kernels = { git = "https://github.com/huggingface/candle", rev = "e68b659fd096ed0947855d43b2eaa55634759c56" } [dev-dependencies] +insta = "1.48.0" tower = { version = "0.5", features = ["util"] } [features] From ffe980f8a271192c8fd3b1a0aae2619ebb405057 Mon Sep 17 00:00:00 2001 From: Alvaro Bartolome <36760800+alvarobartt@users.noreply.github.com> Date: Thu, 24 Sep 2026 13:11:23 +0000 Subject: [PATCH 3/9] Add `redactions` and `yaml` features for `insta` --- Cargo.lock | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 +- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 3957bfa..2ef1fdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2183,6 +2183,9 @@ checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" dependencies = [ "console", "once_cell", + "pest", + "pest_derive", + "serde", "similar", "tempfile", ] @@ -2905,6 +2908,48 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284b60557f2c4a2e72ad3f2d34d42685a2fa4a6a61d0d2a10c0ae2a5e916c2cf" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9d1f08a115309ee99268cf85e5228e0e56aa9caf8841ec12866b6be07c3109" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pest_meta" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed93ba1a9ffcca32130a5188701c81c0c49cf00d4b7c5007d5148951d743adcb" +dependencies = [ + "pest", +] + [[package]] name = "pin-project" version = "1.1.13" @@ -4519,6 +4564,12 @@ version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "ug" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index 3335c94..e1974db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ candle-nn = { version = "0.11.0", features = ["accelerate"] } candle-kernels = { git = "https://github.com/huggingface/candle", rev = "e68b659fd096ed0947855d43b2eaa55634759c56" } [dev-dependencies] -insta = "1.48.0" +insta = { version = "1.48.0", features = ["redactions", "yaml"] } tower = { version = "0.5", features = ["util"] } [features] From 890557893588b705cb67ede28faceaed4282ef2d Mon Sep 17 00:00:00 2001 From: Alvaro Bartolome <36760800+alvarobartt@users.noreply.github.com> Date: Thu, 24 Sep 2026 13:11:36 +0000 Subject: [PATCH 4/9] Add tests for `modernbert.rs` and `laya.rs` --- src/models/laya.rs | 70 ++++++++++ src/models/modernbert.rs | 88 ++++++++++++ ...models__laya__tests__laya_fp32_logits.snap | 13 ++ ..._laya__tests__laya_fp32_probabilities.snap | 13 ++ ..._tests__modernbert_fp32_hidden_states.snap | 132 ++++++++++++++++++ 5 files changed, 316 insertions(+) create mode 100644 src/models/snapshots/sys1__models__laya__tests__laya_fp32_logits.snap create mode 100644 src/models/snapshots/sys1__models__laya__tests__laya_fp32_probabilities.snap create mode 100644 src/models/snapshots/sys1__models__modernbert__tests__modernbert_fp32_hidden_states.snap diff --git a/src/models/laya.rs b/src/models/laya.rs index ddb20a2..57049e9 100644 --- a/src/models/laya.rs +++ b/src/models/laya.rs @@ -798,6 +798,76 @@ fn round4(value: f32) -> f64 { mod tests { use super::*; + #[tokio::test] + async fn fp32_logits_and_probabilities() -> anyhow::Result<()> { + let path = crate::hub::download( + "convaiinnovations/laya", + "aa8c91ca088ec597df95a0d1c76b3063cb2ae5e8", + ) + .await?; + + let model = Laya::load(&path, DType::F32)?; + let request: DecisionRequest = serde_json::from_value(json!({ + "state": { + "message": "I was charged twice for invoice 4411. Please refund me today.", + "account_tier": "enterprise" + }, + "questions": { + "route": { + "type": "choice", + "instructions": "Where should this ticket go?", + "criteria": { + "billing": "payments, refunds, invoices", + "bug": "the product is broken", + "account": "login or access" + } + }, + "urgency": { + "type": "score", + "instructions": "How urgent is this message?", + "criteria": [ + "routine, no rush", + "today", + "urgent", + "critical, about to churn" + ] + }, + "escalate": { + "type": "noul", + "instructions": "Escalate to a human immediately?" + } + } + }))?; + + let prepared = model.prepare(request).expect("prepare the test request"); + let logits = model.forward(std::slice::from_ref(&prepared))?; + let probabilities: Vec<_> = prepared + .items + .iter() + .zip(&logits) + .map(|(item, logits)| { + let bucket = bucket(item.question.kind, logits.len()); + let temperature = model + .config + .temperature_by_options + .get(&bucket) + .copied() + .unwrap_or(model.config.temperature[item.question.kind]) + .clamp(0.5, 5.0); + probability(logits, temperature) + }) + .collect(); + + insta::assert_yaml_snapshot!("laya_fp32_logits", logits, { + "[][]" => insta::rounded_redaction(3), + }); + insta::assert_yaml_snapshot!("laya_fp32_probabilities", probabilities, { + "[][]" => insta::rounded_redaction(4), + }); + + Ok(()) + } + fn item(id: &str, kind: usize, ids: &[u32]) -> Item { Item { question: Question { diff --git a/src/models/modernbert.rs b/src/models/modernbert.rs index d768ca7..9b240a1 100644 --- a/src/models/modernbert.rs +++ b/src/models/modernbert.rs @@ -407,3 +407,91 @@ fn local_attention_mask( .collect(); Tensor::from_vec(mask, (length, length), device)?.to_dtype(dtype) } + +#[cfg(test)] +mod tests { + use super::*; + use candle_core::IndexOp; + + #[test] + fn local_attention_mask_only_exposes_nearby_tokens() -> Result<()> { + let mask = local_attention_mask(4, 1, DType::F32, &Device::Cpu)?.to_vec2::()?; + + assert_eq!( + mask, + vec![ + vec![0.0, 0.0, f32::NEG_INFINITY, f32::NEG_INFINITY], + vec![0.0, 0.0, 0.0, f32::NEG_INFINITY], + vec![f32::NEG_INFINITY, 0.0, 0.0, 0.0], + vec![f32::NEG_INFINITY, f32::NEG_INFINITY, 0.0, 0.0], + ] + ); + Ok(()) + } + + #[test] + fn global_attention_mask_hides_padding_tokens() -> Result<()> { + let mask = Tensor::new(&[[1u32, 1, 0]], &Device::Cpu)?; + let mask = global_attention_mask(&mask, 2, DType::F32)? + .flatten_all()? + .to_vec1::()?; + + assert_eq!(mask, vec![0.0, 0.0, f32::MIN, 0.0, 0.0, f32::MIN]); + Ok(()) + } + + #[tokio::test] + async fn fp32_hidden_states() -> anyhow::Result<()> { + let path = crate::hub::download( + "convaiinnovations/laya", + "aa8c91ca088ec597df95a0d1c76b3063cb2ae5e8", + ) + .await?; + + let device = crate::device::load()?; + let config = Config::load(&path.join("encoder/config.json"))?; + let weights = path.join("model.safetensors"); + let vb = unsafe { VarBuilder::from_mmaped_safetensors(&[weights], DType::F32, &device)? }; + let vb = vb.rename_f(|name| { + name.strip_prefix("model.") + .map(|name| format!("encoder.{name}")) + .unwrap_or_else(|| name.to_owned()) + }); + let encoder = Encoder::load(vb, &config, DType::F32)?; + + let tokenizer_path = path.join("tokenizer/tokenizer.json"); + let tokenizer = crate::tokenizer::from_json(&fs::read(tokenizer_path)?)?; + let cls_id = tokenizer + .token_to_id("[CLS]") + .ok_or_else(|| anyhow::anyhow!("tokenizer is missing [CLS]"))?; + let sep_id = tokenizer + .token_to_id("[SEP]") + .ok_or_else(|| anyhow::anyhow!("tokenizer is missing [SEP]"))?; + let pad_id = tokenizer + .token_to_id("[PAD]") + .ok_or_else(|| anyhow::anyhow!("tokenizer is missing [PAD]"))?; + + let mut ids = vec![cls_id]; + ids.extend(tokenizer.encode("What is Deep Learning?")?); + ids.push(sep_id); + + let mut mask = vec![1f32; ids.len()]; + ids.resize(32, pad_id); + mask.resize(32, 0.0); + + let length = ids.len(); + let ids = Tensor::from_vec(ids, (1, length), &device)?; + let mask = Tensor::from_vec(mask, (1, length), &device)?; + let hidden = encoder.forward(&ids, &mask, true)?; + + insta::assert_yaml_snapshot!( + "modernbert_fp32_hidden_states", + hidden.i((0, 0, 0..128))?.to_vec1::()?, + { + "[]" => insta::rounded_redaction(3), + } + ); + + Ok(()) + } +} diff --git a/src/models/snapshots/sys1__models__laya__tests__laya_fp32_logits.snap b/src/models/snapshots/sys1__models__laya__tests__laya_fp32_logits.snap new file mode 100644 index 0000000..0220f9a --- /dev/null +++ b/src/models/snapshots/sys1__models__laya__tests__laya_fp32_logits.snap @@ -0,0 +1,13 @@ +--- +source: src/models/laya.rs +expression: logits +--- +- - 3.048 + - -2.88 + - -3.174 +- - -1.368 + - 4.087 + - 0.282 + - -2.044 +- - 0.87 + - 0.414 diff --git a/src/models/snapshots/sys1__models__laya__tests__laya_fp32_probabilities.snap b/src/models/snapshots/sys1__models__laya__tests__laya_fp32_probabilities.snap new file mode 100644 index 0000000..c71a14c --- /dev/null +++ b/src/models/snapshots/sys1__models__laya__tests__laya_fp32_probabilities.snap @@ -0,0 +1,13 @@ +--- +source: src/models/laya.rs +expression: probabilities +--- +- - 0.9402 + - 0.0324 + - 0.0274 +- - 0.012 + - 0.9363 + - 0.0448 + - 0.007 +- - 0.5572 + - 0.4428 diff --git a/src/models/snapshots/sys1__models__modernbert__tests__modernbert_fp32_hidden_states.snap b/src/models/snapshots/sys1__models__modernbert__tests__modernbert_fp32_hidden_states.snap new file mode 100644 index 0000000..6722bcc --- /dev/null +++ b/src/models/snapshots/sys1__models__modernbert__tests__modernbert_fp32_hidden_states.snap @@ -0,0 +1,132 @@ +--- +source: src/models/modernbert.rs +expression: "hidden.i((0, 0, 0..128))?.to_vec1::()?" +--- +- -0.648 +- -0.029 +- -0.685 +- -0.476 +- 0.853 +- -0.637 +- -0.019 +- 0.5 +- 0.065 +- -0.294 +- 0.315 +- 0.305 +- 0.39 +- -0.645 +- -0.379 +- 0.116 +- -0.542 +- 0.094 +- 0.054 +- -0.275 +- 0.155 +- -0.766 +- -0.16 +- 0.967 +- 0.423 +- 0.102 +- -0.644 +- -0.655 +- 0.666 +- 0.045 +- 0.093 +- -0.285 +- -0.281 +- 0.501 +- 0.06 +- 0.236 +- -0.364 +- -0.344 +- -0.236 +- 0.572 +- -0.101 +- -0.874 +- 0.175 +- 0.305 +- 0.519 +- 0.298 +- 0.528 +- -0.867 +- 0.675 +- 0.056 +- -0.294 +- 0.139 +- 0.292 +- 0.145 +- -0.182 +- -0.736 +- 0.059 +- -0.186 +- 0.002 +- 0.258 +- 0.714 +- -0.346 +- -0.14 +- -0.162 +- -0.963 +- 0.379 +- 0.322 +- 0.185 +- -0.117 +- -0.075 +- -0.753 +- 0.073 +- 0.55 +- -0.004 +- -0.591 +- -0.076 +- -0.274 +- 0.249 +- 0.277 +- -0.05 +- 0.003 +- -0.418 +- -0.65 +- -0.654 +- 0.469 +- -0.516 +- -0.586 +- 0.349 +- -0.956 +- 0.35 +- -0.326 +- 0.506 +- 0.147 +- -0.492 +- -0.524 +- 0.377 +- 0.341 +- -1.196 +- -0.696 +- -0.089 +- 2.355 +- -0.271 +- -0.653 +- -0.341 +- -0.922 +- 0.045 +- -0.251 +- 0.14 +- -0.759 +- 0.234 +- -0.774 +- -0.411 +- -0.155 +- -0.073 +- 0.062 +- -1.014 +- -0.102 +- 0.027 +- 0.122 +- -0.047 +- 0.195 +- -0.13 +- 0.149 +- 0.58 +- -0.343 +- -0.767 +- 0.254 +- -0.863 From e45707bf41a8bf0cddf7885d90b29928bbddbabb Mon Sep 17 00:00:00 2001 From: Alvaro Bartolome <36760800+alvarobartt@users.noreply.github.com> Date: Thu, 24 Sep 2026 13:12:27 +0000 Subject: [PATCH 5/9] Rename `clippy.yml` to `linting.yml` (and don't test) --- .github/workflows/{clippy.yml => linting.yml} | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) rename .github/workflows/{clippy.yml => linting.yml} (61%) diff --git a/.github/workflows/clippy.yml b/.github/workflows/linting.yml similarity index 61% rename from .github/workflows/clippy.yml rename to .github/workflows/linting.yml index 0cacae3..2024622 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/linting.yml @@ -1,16 +1,13 @@ -name: Clippy +name: Lint on: - push: - branches: [main] - pull_request: - branches: [main] + workflow_call: permissions: contents: read jobs: - clippy: + lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -20,7 +17,7 @@ jobs: ~/.cargo/registry ~/.cargo/git target - key: clippy-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} - restore-keys: clippy-${{ runner.os }}- + key: lint-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} + restore-keys: lint-${{ runner.os }}- + - run: cargo fmt --check - run: cargo clippy --locked --no-default-features --features cpu --all-targets -- -D warnings - - run: cargo test --locked --no-default-features --features cpu From 600da88a5b71731196c3508d9f45f83b26e790be Mon Sep 17 00:00:00 2001 From: Alvaro Bartolome <36760800+alvarobartt@users.noreply.github.com> Date: Thu, 24 Sep 2026 13:12:48 +0000 Subject: [PATCH 6/9] Add `.github/workflows/test.yml` --- .github/workflows/tests.yml | 49 +++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..fdbd4b5 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,49 @@ +name: Tests + +on: + workflow_call: + secrets: + HF_TOKEN: + required: false + +permissions: + contents: read + +jobs: + cpu: + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.cache/huggingface/hub + ~/.cargo/registry + ~/.cargo/git + target + key: test-cpu-${{ hashFiles('Cargo.lock') }}-aa8c91ca088ec597df95a0d1c76b3063cb2ae5e8 + restore-keys: test-cpu- + - name: Test + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + run: cargo test --release --locked --no-default-features --features cpu + + metal: + runs-on: macos-14 + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.cache/huggingface/hub + ~/.cargo/registry + ~/.cargo/git + target + key: test-metal-${{ hashFiles('Cargo.lock') }}-aa8c91ca088ec597df95a0d1c76b3063cb2ae5e8 + restore-keys: test-metal- + - name: Test + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + run: cargo test --release --locked --no-default-features --features metal From 4700d44723b92e74eed6786c082e915b0e2526f4 Mon Sep 17 00:00:00 2001 From: Alvaro Bartolome <36760800+alvarobartt@users.noreply.github.com> Date: Thu, 24 Sep 2026 13:13:26 +0000 Subject: [PATCH 7/9] Update `.github/workflows/{build,publish}.yml` --- .github/workflows/build.yml | 33 +++++++++++++++++++++++++++-- .github/workflows/publish.yml | 40 ++++++++++++++++++++++++----------- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 10155f9..fa95b61 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build containers +name: Build and publish images on: push: @@ -8,15 +8,25 @@ on: branches: [main] permissions: + actions: write contents: read packages: write concurrency: - group: build-containers-${{ github.event.pull_request.number || github.ref }} + group: ci-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: + lint: + uses: ./.github/workflows/linting.yml + + tests: + needs: lint + uses: ./.github/workflows/tests.yml + secrets: inherit + build: + needs: tests if: github.ref_type != 'tag' runs-on: ${{ matrix.runner }} strategy: @@ -251,6 +261,7 @@ jobs: fi promote: + needs: tests if: github.ref_type == 'tag' runs-on: ubuntu-latest timeout-minutes: 30 @@ -335,3 +346,21 @@ jobs: exit 1 fi done <<< "${DESTINATION_TAGS}" + + publish-crate: + needs: tests + if: github.ref_type == 'tag' + runs-on: ubuntu-latest + permissions: + actions: write + contents: read + steps: + - name: Dispatch crates.io publication + env: + GH_TOKEN: ${{ github.token }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + gh workflow run publish.yml \ + --ref "${DEFAULT_BRANCH}" \ + -f tag="${GITHUB_REF_NAME}" \ + -f sha="${GITHUB_SHA}" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 961b75a..20ae782 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,31 +1,47 @@ name: Publish crate on: - push: - tags: ["v*"] + workflow_dispatch: + inputs: + tag: + description: Release tag that passed linting and tests + required: true + type: string + sha: + description: Tested commit to publish + required: true + type: string permissions: contents: read +concurrency: + group: crates-${{ inputs.tag }} + cancel-in-progress: false + jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: publish-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} - restore-keys: publish-${{ runner.os }}- - - name: Verify release version + ref: ${{ inputs.sha }} + fetch-depth: 0 + - name: Verify tested release + env: + RELEASE_SHA: ${{ inputs.sha }} + RELEASE_TAG: ${{ inputs.tag }} shell: bash run: | + set -euo pipefail + case "${RELEASE_TAG}" in + v*) ;; + *) exit 1 ;; + esac + test "$(git rev-list -n 1 "refs/tags/${RELEASE_TAG}")" = "${RELEASE_SHA}" crate_version="$(cargo metadata --locked --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "sys1") | .version')" - test "${GITHUB_REF_NAME}" = "v${crate_version}" + test "${RELEASE_TAG}" = "v${crate_version}" - name: Publish to crates.io - run: cargo publish --locked env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish --locked From ef405015cbb47647af12b6917f60c19340e72bcf Mon Sep 17 00:00:00 2001 From: Alvaro Bartolome <36760800+alvarobartt@users.noreply.github.com> Date: Thu, 24 Sep 2026 14:46:57 +0000 Subject: [PATCH 8/9] Detach stages from each other --- .github/workflows/build.yml | 88 ++++++++++++++++++++++------------- .github/workflows/linting.yml | 10 +++- .github/workflows/tests.yml | 41 +++++++++++----- 3 files changed, 95 insertions(+), 44 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fa95b61..dca0068 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,11 +1,9 @@ -name: Build and publish images +name: Build images on: - push: - branches: [main] - tags: ["v*"] - pull_request: - branches: [main] + workflow_run: + workflows: [Tests] + types: [completed] permissions: actions: write @@ -13,21 +11,42 @@ permissions: packages: write concurrency: - group: ci-${{ github.event.pull_request.number || github.ref }} + group: build-${{ github.event.workflow_run.head_sha }} cancel-in-progress: true jobs: - lint: - uses: ./.github/workflows/linting.yml + context: + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + outputs: + event: ${{ steps.source.outputs.event }} + sha: ${{ steps.source.outputs.sha }} + short_sha: ${{ steps.source.outputs.short_sha }} + tag: ${{ steps.source.outputs.tag }} + steps: + - name: Resolve tested source + id: source + env: + SOURCE_EVENT: ${{ github.event.workflow_run.event }} + SOURCE_REF: ${{ github.event.workflow_run.head_branch }} + SOURCE_SHA: ${{ github.event.workflow_run.head_sha }} + shell: bash + run: | + set -euo pipefail + + tag="" + if [[ "${SOURCE_EVENT}" == "push" && "${SOURCE_REF}" == v* ]]; then + tag="${SOURCE_REF}" + fi - tests: - needs: lint - uses: ./.github/workflows/tests.yml - secrets: inherit + echo "event=${SOURCE_EVENT}" >> "${GITHUB_OUTPUT}" + echo "sha=${SOURCE_SHA}" >> "${GITHUB_OUTPUT}" + echo "short_sha=${SOURCE_SHA:0:7}" >> "${GITHUB_OUTPUT}" + echo "tag=${tag}" >> "${GITHUB_OUTPUT}" build: - needs: tests - if: github.ref_type != 'tag' + needs: context + if: needs.context.outputs.tag == '' runs-on: ${{ matrix.runner }} strategy: fail-fast: false @@ -99,10 +118,13 @@ jobs: runner: ubuntu-24.04-arm steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ needs.context.outputs.sha }} - name: Determine cache permissions id: cache env: - TRUSTED_CACHE_WRITER: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + TRUSTED_CACHE_WRITER: ${{ needs.context.outputs.event == 'push' }} shell: bash run: | if [[ "${TRUSTED_CACHE_WRITER}" == "true" ]]; then @@ -140,7 +162,7 @@ jobs: images: ghcr.io/${{ github.repository }} flavor: latest=false tags: | - type=sha,format=short,prefix=,suffix=${{ matrix.staging_suffix }} + type=raw,value=${{ needs.context.outputs.short_sha }}${{ matrix.staging_suffix }} - name: Build ${{ matrix.name }} image for ${{ matrix.platform }} uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: @@ -154,15 +176,15 @@ jobs: ACTIONS_RESULTS_URL=${{ env.ACTIONS_RESULTS_URL }} ACTIONS_RUNTIME_TOKEN=${{ env.ACTIONS_RUNTIME_TOKEN }} platforms: ${{ matrix.platform }} - push: ${{ github.event_name != 'pull_request' }} + push: ${{ needs.context.outputs.event == 'push' }} tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} cache-from: ${{ matrix.name == 'cpu' && format('type=gha,scope={0}', matrix.cache_scope) || format('type=registry,ref=ghcr.io/{0}:buildcache-{1}', github.repository, matrix.cache_scope) }} cache-to: ${{ matrix.name == 'cpu' && format('type=gha,mode=max,scope={0}', matrix.cache_scope) || (steps.cache.outputs.write == 'true' && format('type=registry,ref=ghcr.io/{0}:buildcache-{1},mode=max', github.repository, matrix.cache_scope) || '') }} assemble: - if: github.event_name == 'push' && github.ref_type != 'tag' - needs: build + if: needs.context.outputs.event == 'push' && needs.context.outputs.tag == '' + needs: [context, build] runs-on: ubuntu-latest strategy: fail-fast: false @@ -215,8 +237,8 @@ jobs: images: ghcr.io/${{ github.repository }} flavor: latest=false tags: | - type=sha,format=short,prefix=,suffix=${{ matrix.suffix }},priority=300 - type=sha,format=short,prefix=,priority=300,enable=${{ matrix.name == 'cpu' }} + type=raw,value=${{ needs.context.outputs.short_sha }}${{ matrix.suffix }},priority=300 + type=raw,value=${{ needs.context.outputs.short_sha }},priority=300,enable=${{ matrix.name == 'cpu' }} type=raw,value=latest${{ matrix.suffix }} type=raw,value=latest,enable=${{ matrix.name == 'cpu' }} - name: Assemble ${{ matrix.name }} image @@ -224,6 +246,7 @@ jobs: DESTINATION_TAGS: ${{ steps.metadata.outputs.tags }} EXPECTED_PLATFORMS: ${{ matrix.expected_platforms }} SOURCE_SUFFIXES: ${{ matrix.source_suffixes }} + SOURCE_SHA: ${{ needs.context.outputs.sha }} shell: bash run: | set -euo pipefail @@ -231,7 +254,7 @@ jobs: sources=() while IFS= read -r source_suffix; do if [[ -n "${source_suffix}" ]]; then - sources+=("ghcr.io/${GITHUB_REPOSITORY}:${GITHUB_SHA:0:7}${source_suffix}") + sources+=("ghcr.io/${GITHUB_REPOSITORY}:${SOURCE_SHA:0:7}${source_suffix}") fi done <<< "${SOURCE_SUFFIXES}" @@ -261,8 +284,8 @@ jobs: fi promote: - needs: tests - if: github.ref_type == 'tag' + needs: context + if: needs.context.outputs.tag != '' runs-on: ubuntu-latest timeout-minutes: 30 strategy: @@ -296,18 +319,19 @@ jobs: images: ghcr.io/${{ github.repository }} flavor: latest=false tags: | - type=semver,pattern={{version}},suffix=${{ matrix.suffix }} - type=semver,pattern={{version}},enable=${{ matrix.name == 'cpu' }} + type=semver,pattern={{version}},value=${{ needs.context.outputs.tag }},suffix=${{ matrix.suffix }} + type=semver,pattern={{version}},value=${{ needs.context.outputs.tag }},enable=${{ matrix.name == 'cpu' }} type=raw,value=latest${{ matrix.suffix }} type=raw,value=latest,enable=${{ matrix.name == 'cpu' }} - name: Promote ${{ matrix.name }} image env: DESTINATION_TAGS: ${{ steps.metadata.outputs.tags }} + SOURCE_SHA: ${{ needs.context.outputs.sha }} shell: bash run: | set -euo pipefail - source_tag="ghcr.io/${GITHUB_REPOSITORY}:${GITHUB_SHA:0:7}${{ matrix.suffix }}" + source_tag="ghcr.io/${GITHUB_REPOSITORY}:${SOURCE_SHA:0:7}${{ matrix.suffix }}" source_digest="" for attempt in {1..60}; do @@ -348,8 +372,8 @@ jobs: done <<< "${DESTINATION_TAGS}" publish-crate: - needs: tests - if: github.ref_type == 'tag' + needs: context + if: needs.context.outputs.tag != '' runs-on: ubuntu-latest permissions: actions: write @@ -362,5 +386,5 @@ jobs: run: | gh workflow run publish.yml \ --ref "${DEFAULT_BRANCH}" \ - -f tag="${GITHUB_REF_NAME}" \ - -f sha="${GITHUB_SHA}" + -f tag="${{ needs.context.outputs.tag }}" \ + -f sha="${{ needs.context.outputs.sha }}" diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 2024622..6e74ae3 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -1,11 +1,19 @@ name: Lint on: - workflow_call: + push: + branches: [main] + tags: ["v*"] + pull_request: + branches: [main] permissions: contents: read +concurrency: + group: lint-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: lint: runs-on: ubuntu-latest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fdbd4b5..98652a8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,20 +1,35 @@ name: Tests on: - workflow_call: - secrets: - HF_TOKEN: - required: false + workflow_run: + workflows: [Lint] + types: [completed] permissions: contents: read +concurrency: + group: tests-${{ github.event.workflow_run.head_sha }} + cancel-in-progress: true + jobs: + prerequisite: + runs-on: ubuntu-latest + steps: + - name: Require successful linting + env: + LINT_RESULT: ${{ github.event.workflow_run.conclusion }} + run: test "${LINT_RESULT}" = success + cpu: + needs: prerequisite runs-on: ubuntu-latest timeout-minutes: 45 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.event.workflow_run.head_sha }} - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | @@ -22,18 +37,22 @@ jobs: ~/.cargo/registry ~/.cargo/git target - key: test-cpu-${{ hashFiles('Cargo.lock') }}-aa8c91ca088ec597df95a0d1c76b3063cb2ae5e8 - restore-keys: test-cpu- + key: test-${{ github.event.workflow_run.event }}-cpu-${{ hashFiles('Cargo.lock') }}-aa8c91ca088ec597df95a0d1c76b3063cb2ae5e8 + restore-keys: test-${{ github.event.workflow_run.event }}-cpu- - name: Test env: - HF_TOKEN: ${{ secrets.HF_TOKEN }} + HF_TOKEN: ${{ github.event.workflow_run.event != 'pull_request' && secrets.HF_TOKEN || '' }} run: cargo test --release --locked --no-default-features --features cpu metal: - runs-on: macos-14 + needs: prerequisite + runs-on: macos-15 timeout-minutes: 45 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.event.workflow_run.head_sha }} - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | @@ -41,9 +60,9 @@ jobs: ~/.cargo/registry ~/.cargo/git target - key: test-metal-${{ hashFiles('Cargo.lock') }}-aa8c91ca088ec597df95a0d1c76b3063cb2ae5e8 - restore-keys: test-metal- + key: test-${{ github.event.workflow_run.event }}-metal-${{ hashFiles('Cargo.lock') }}-aa8c91ca088ec597df95a0d1c76b3063cb2ae5e8 + restore-keys: test-${{ github.event.workflow_run.event }}-metal- - name: Test env: - HF_TOKEN: ${{ secrets.HF_TOKEN }} + HF_TOKEN: ${{ github.event.workflow_run.event != 'pull_request' && secrets.HF_TOKEN || '' }} run: cargo test --release --locked --no-default-features --features metal From 7f30b2a547d8f007ed835c7e50de9641eaa76948 Mon Sep 17 00:00:00 2001 From: Alvaro Bartolome <36760800+alvarobartt@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:26:21 +0000 Subject: [PATCH 9/9] Update GitHub Actions to be orchestrated with `ci.yml` --- .github/workflows/build.yml | 300 ++----------------------------- .github/workflows/ci.yml | 79 ++++++++ .github/workflows/linting.yml | 22 ++- .github/workflows/publish.yml | 14 +- .github/workflows/push.yml | 202 +++++++++++++++++++++ .github/workflows/tests.yml | 46 ++--- .github/workflows/trufflehog.yml | 11 +- 7 files changed, 349 insertions(+), 325 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/push.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dca0068..36e0100 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,52 +1,18 @@ -name: Build images +name: Build on: - workflow_run: - workflows: [Tests] - types: [completed] - -permissions: - actions: write - contents: read - packages: write - -concurrency: - group: build-${{ github.event.workflow_run.head_sha }} - cancel-in-progress: true + workflow_call: + inputs: + push: + required: true + type: boolean + source_sha: + required: true + type: string jobs: - context: - if: github.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-latest - outputs: - event: ${{ steps.source.outputs.event }} - sha: ${{ steps.source.outputs.sha }} - short_sha: ${{ steps.source.outputs.short_sha }} - tag: ${{ steps.source.outputs.tag }} - steps: - - name: Resolve tested source - id: source - env: - SOURCE_EVENT: ${{ github.event.workflow_run.event }} - SOURCE_REF: ${{ github.event.workflow_run.head_branch }} - SOURCE_SHA: ${{ github.event.workflow_run.head_sha }} - shell: bash - run: | - set -euo pipefail - - tag="" - if [[ "${SOURCE_EVENT}" == "push" && "${SOURCE_REF}" == v* ]]; then - tag="${SOURCE_REF}" - fi - - echo "event=${SOURCE_EVENT}" >> "${GITHUB_OUTPUT}" - echo "sha=${SOURCE_SHA}" >> "${GITHUB_OUTPUT}" - echo "short_sha=${SOURCE_SHA:0:7}" >> "${GITHUB_OUTPUT}" - echo "tag=${tag}" >> "${GITHUB_OUTPUT}" - build: - needs: context - if: needs.context.outputs.tag == '' + name: ${{ matrix.name }} (${{ matrix.platform }}) runs-on: ${{ matrix.runner }} strategy: fail-fast: false @@ -54,7 +20,6 @@ jobs: include: - name: cpu file: Dockerfile - suffix: -cpu staging_suffix: -cpu-amd64 cache_scope: cpu-amd64 build_args: "" @@ -62,7 +27,6 @@ jobs: runner: ubuntu-latest - name: cpu file: Dockerfile - suffix: -cpu staging_suffix: -cpu-arm64 cache_scope: cpu-arm64 build_args: "" @@ -70,7 +34,6 @@ jobs: runner: ubuntu-24.04-arm - name: turing file: Dockerfile.cuda - suffix: -turing staging_suffix: -turing-amd64 cache_scope: cuda-turing build_args: CUDA_COMPUTE_CAPS=75 @@ -78,7 +41,6 @@ jobs: runner: ubuntu-latest - name: ampere file: Dockerfile.cuda - suffix: -ampere staging_suffix: -ampere-amd64 cache_scope: cuda-ampere build_args: CUDA_COMPUTE_CAPS=80;86 @@ -86,7 +48,6 @@ jobs: runner: ubuntu-latest - name: ada-lovelace file: Dockerfile.cuda - suffix: -ada-lovelace staging_suffix: -ada-lovelace-amd64 cache_scope: cuda-ada-lovelace build_args: CUDA_COMPUTE_CAPS=89 @@ -94,7 +55,6 @@ jobs: runner: ubuntu-latest - name: hopper file: Dockerfile.cuda - suffix: -hopper staging_suffix: -hopper-amd64 cache_scope: cuda-hopper build_args: CUDA_COMPUTE_CAPS=90 @@ -102,7 +62,6 @@ jobs: runner: ubuntu-latest - name: blackwell file: Dockerfile.cuda - suffix: -blackwell staging_suffix: -blackwell-amd64 cache_scope: cuda-blackwell-amd64 build_args: CUDA_COMPUTE_CAPS=100;120 @@ -110,7 +69,6 @@ jobs: runner: ubuntu-latest - name: blackwell file: Dockerfile.cuda - suffix: -blackwell staging_suffix: -blackwell-arm64 cache_scope: cuda-blackwell-arm64 build_args: CUDA_COMPUTE_CAPS=121 @@ -120,20 +78,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - ref: ${{ needs.context.outputs.sha }} - - name: Determine cache permissions - id: cache - env: - TRUSTED_CACHE_WRITER: ${{ needs.context.outputs.event == 'push' }} - shell: bash - run: | - if [[ "${TRUSTED_CACHE_WRITER}" == "true" ]]; then - echo "write=true" >> "${GITHUB_OUTPUT}" - echo "sccache=on" >> "${GITHUB_OUTPUT}" - else - echo "write=false" >> "${GITHUB_OUTPUT}" - echo "sccache=off" >> "${GITHUB_OUTPUT}" - fi + ref: ${{ inputs.source_sha }} - name: Free disk space for CUDA if: matrix.name != 'cpu' uses: jlumbroso/free-disk-space@ceedf095f4ec1a097402bc6bd80831f2e1a6fde6 # v2.0.0 @@ -142,19 +87,19 @@ jobs: swap-storage: false - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - name: Configure sccache credentials - if: steps.cache.outputs.write == 'true' + if: inputs.push uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || ''); core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - name: Log in to GHCR - if: steps.cache.outputs.write == 'true' + if: inputs.push uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + password: ${{ github.token }} - name: Docker metadata id: metadata uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 @@ -162,8 +107,8 @@ jobs: images: ghcr.io/${{ github.repository }} flavor: latest=false tags: | - type=raw,value=${{ needs.context.outputs.short_sha }}${{ matrix.staging_suffix }} - - name: Build ${{ matrix.name }} image for ${{ matrix.platform }} + type=raw,value=${{ inputs.source_sha }}${{ matrix.staging_suffix }} + - name: Build uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . @@ -171,220 +116,13 @@ jobs: target: runtime build-args: | ${{ matrix.build_args }} - SCCACHE_GHA_ENABLED=${{ steps.cache.outputs.sccache }} + SCCACHE_GHA_ENABLED=${{ inputs.push && 'on' || 'off' }} secrets: | ACTIONS_RESULTS_URL=${{ env.ACTIONS_RESULTS_URL }} ACTIONS_RUNTIME_TOKEN=${{ env.ACTIONS_RUNTIME_TOKEN }} platforms: ${{ matrix.platform }} - push: ${{ needs.context.outputs.event == 'push' }} + push: ${{ inputs.push }} tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} cache-from: ${{ matrix.name == 'cpu' && format('type=gha,scope={0}', matrix.cache_scope) || format('type=registry,ref=ghcr.io/{0}:buildcache-{1}', github.repository, matrix.cache_scope) }} - cache-to: ${{ matrix.name == 'cpu' && format('type=gha,mode=max,scope={0}', matrix.cache_scope) || (steps.cache.outputs.write == 'true' && format('type=registry,ref=ghcr.io/{0}:buildcache-{1},mode=max', github.repository, matrix.cache_scope) || '') }} - - assemble: - if: needs.context.outputs.event == 'push' && needs.context.outputs.tag == '' - needs: [context, build] - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - name: cpu - suffix: -cpu - source_suffixes: |- - -cpu-amd64 - -cpu-arm64 - expected_platforms: |- - linux/amd64 - linux/arm64 - - name: turing - suffix: -turing - source_suffixes: -turing-amd64 - expected_platforms: linux/amd64 - - name: ampere - suffix: -ampere - source_suffixes: -ampere-amd64 - expected_platforms: linux/amd64 - - name: ada-lovelace - suffix: -ada-lovelace - source_suffixes: -ada-lovelace-amd64 - expected_platforms: linux/amd64 - - name: hopper - suffix: -hopper - source_suffixes: -hopper-amd64 - expected_platforms: linux/amd64 - - name: blackwell - suffix: -blackwell - source_suffixes: |- - -blackwell-amd64 - -blackwell-arm64 - expected_platforms: |- - linux/amd64 - linux/arm64 - steps: - - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - - name: Log in to GHCR - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Docker metadata - id: metadata - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 - with: - images: ghcr.io/${{ github.repository }} - flavor: latest=false - tags: | - type=raw,value=${{ needs.context.outputs.short_sha }}${{ matrix.suffix }},priority=300 - type=raw,value=${{ needs.context.outputs.short_sha }},priority=300,enable=${{ matrix.name == 'cpu' }} - type=raw,value=latest${{ matrix.suffix }} - type=raw,value=latest,enable=${{ matrix.name == 'cpu' }} - - name: Assemble ${{ matrix.name }} image - env: - DESTINATION_TAGS: ${{ steps.metadata.outputs.tags }} - EXPECTED_PLATFORMS: ${{ matrix.expected_platforms }} - SOURCE_SUFFIXES: ${{ matrix.source_suffixes }} - SOURCE_SHA: ${{ needs.context.outputs.sha }} - shell: bash - run: | - set -euo pipefail - - sources=() - while IFS= read -r source_suffix; do - if [[ -n "${source_suffix}" ]]; then - sources+=("ghcr.io/${GITHUB_REPOSITORY}:${SOURCE_SHA:0:7}${source_suffix}") - fi - done <<< "${SOURCE_SUFFIXES}" - - create_args=() - while IFS= read -r destination_tag; do - if [[ -n "${destination_tag}" ]]; then - create_args+=(--tag "${destination_tag}") - fi - done <<< "${DESTINATION_TAGS}" - - docker buildx imagetools create "${create_args[@]}" "${sources[@]}" - - first_destination="${DESTINATION_TAGS%%$'\n'*}" - expected_platforms="$(printf '%s\n' "${EXPECTED_PLATFORMS}" | sort)" - actual_platforms="$( - docker buildx imagetools inspect "${first_destination}" --raw | - jq -r '.manifests[].platform | select(.architecture != "unknown") | "\(.os)/\(.architecture)"' | - sort -u - )" - if [[ "${actual_platforms}" != "${expected_platforms}" ]]; then - echo "Platform mismatch for ${first_destination}" >&2 - echo "Expected:" >&2 - printf '%s\n' "${expected_platforms}" >&2 - echo "Actual:" >&2 - printf '%s\n' "${actual_platforms}" >&2 - exit 1 - fi - - promote: - needs: context - if: needs.context.outputs.tag != '' - runs-on: ubuntu-latest - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - include: - - name: cpu - suffix: -cpu - - name: turing - suffix: -turing - - name: ampere - suffix: -ampere - - name: ada-lovelace - suffix: -ada-lovelace - - name: hopper - suffix: -hopper - - name: blackwell - suffix: -blackwell - steps: - - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - - name: Log in to GHCR - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Docker metadata - id: metadata - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 - with: - images: ghcr.io/${{ github.repository }} - flavor: latest=false - tags: | - type=semver,pattern={{version}},value=${{ needs.context.outputs.tag }},suffix=${{ matrix.suffix }} - type=semver,pattern={{version}},value=${{ needs.context.outputs.tag }},enable=${{ matrix.name == 'cpu' }} - type=raw,value=latest${{ matrix.suffix }} - type=raw,value=latest,enable=${{ matrix.name == 'cpu' }} - - name: Promote ${{ matrix.name }} image - env: - DESTINATION_TAGS: ${{ steps.metadata.outputs.tags }} - SOURCE_SHA: ${{ needs.context.outputs.sha }} - shell: bash - run: | - set -euo pipefail - - source_tag="ghcr.io/${GITHUB_REPOSITORY}:${SOURCE_SHA:0:7}${{ matrix.suffix }}" - source_digest="" - - for attempt in {1..60}; do - if source_digest="$(docker buildx imagetools inspect "${source_tag}" --format '{{json .Manifest}}' 2>/dev/null | jq -r '.digest')" \ - && [[ "${source_digest}" == sha256:* ]]; then - break - fi - - if [[ "${attempt}" -eq 60 ]]; then - echo "Source image ${source_tag} was not published within 20 minutes" >&2 - exit 1 - fi - - echo "Waiting for ${source_tag} to be published (attempt ${attempt}/60)" - sleep 20 - done - - immutable_source="ghcr.io/${GITHUB_REPOSITORY}@${source_digest}" - create_args=() - while IFS= read -r destination_tag; do - if [[ -n "${destination_tag}" ]]; then - create_args+=(--tag "${destination_tag}") - fi - done <<< "${DESTINATION_TAGS}" - - docker buildx imagetools create "${create_args[@]}" "${immutable_source}" - - while IFS= read -r destination_tag; do - if [[ -z "${destination_tag}" ]]; then - continue - fi - - destination_digest="$(docker buildx imagetools inspect "${destination_tag}" --format '{{json .Manifest}}' | jq -r '.digest')" - if [[ "${destination_digest}" != "${source_digest}" ]]; then - echo "Digest mismatch for ${destination_tag}: expected ${source_digest}, got ${destination_digest}" >&2 - exit 1 - fi - done <<< "${DESTINATION_TAGS}" - - publish-crate: - needs: context - if: needs.context.outputs.tag != '' - runs-on: ubuntu-latest - permissions: - actions: write - contents: read - steps: - - name: Dispatch crates.io publication - env: - GH_TOKEN: ${{ github.token }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - run: | - gh workflow run publish.yml \ - --ref "${DEFAULT_BRANCH}" \ - -f tag="${{ needs.context.outputs.tag }}" \ - -f sha="${{ needs.context.outputs.sha }}" + cache-to: ${{ matrix.name == 'cpu' && format('type=gha,mode=max,scope={0}', matrix.cache_scope) || (inputs.push && format('type=registry,ref=ghcr.io/{0}:buildcache-{1},mode=max', github.repository, matrix.cache_scope) || '') }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..28aa407 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,79 @@ +name: CI + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + trufflehog: + name: TruffleHog + uses: ./.github/workflows/trufflehog.yml + with: + source_sha: ${{ github.sha }} + + lint: + name: Lint + uses: ./.github/workflows/linting.yml + with: + source_sha: ${{ github.sha }} + + tests: + name: Tests + needs: [trufflehog, lint] + uses: ./.github/workflows/tests.yml + with: + cache_scope: ${{ github.event_name }} + source_sha: ${{ github.sha }} + secrets: + HF_TOKEN: ${{ github.event_name == 'push' && secrets.HF_TOKEN || '' }} + + build: + name: Build + needs: tests + if: github.ref_type != 'tag' + permissions: + actions: write + contents: read + packages: write + uses: ./.github/workflows/build.yml + with: + push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + source_sha: ${{ github.sha }} + + push: + name: Push + needs: [tests, build] + if: >- + always() && + needs.tests.result == 'success' && + github.event_name == 'push' && + (github.ref == 'refs/heads/main' || github.ref_type == 'tag') && + (github.ref_type == 'tag' || needs.build.result == 'success') + permissions: + contents: read + packages: write + uses: ./.github/workflows/push.yml + with: + release_tag: ${{ github.ref_type == 'tag' && github.ref_name || '' }} + source_sha: ${{ github.sha }} + + publish: + name: Publish + needs: tests + if: github.ref_type == 'tag' + uses: ./.github/workflows/publish.yml + with: + sha: ${{ github.sha }} + tag: ${{ github.ref_name }} + secrets: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 6e74ae3..98d1d5c 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -1,31 +1,29 @@ name: Lint on: - push: - branches: [main] - tags: ["v*"] - pull_request: - branches: [main] + workflow_call: + inputs: + source_sha: + required: true + type: string permissions: contents: read -concurrency: - group: lint-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ inputs.source_sha }} - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cargo/registry ~/.cargo/git - target - key: lint-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} - restore-keys: lint-${{ runner.os }}- + key: lint-v2-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} + restore-keys: lint-v2-${{ runner.os }}- - run: cargo fmt --check - run: cargo clippy --locked --no-default-features --features cpu --all-targets -- -D warnings diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 20ae782..3d36f6d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,6 +1,17 @@ -name: Publish crate +name: Publish on: + workflow_call: + inputs: + tag: + required: true + type: string + sha: + required: true + type: string + secrets: + CARGO_REGISTRY_TOKEN: + required: true workflow_dispatch: inputs: tag: @@ -21,6 +32,7 @@ concurrency: jobs: publish: + name: Publish runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 0000000..0a3e0e5 --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,202 @@ +name: Push + +on: + workflow_call: + inputs: + release_tag: + required: false + type: string + default: "" + source_sha: + required: true + type: string + +jobs: + commit: + name: ${{ matrix.name }} + if: inputs.release_tag == '' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: cpu + suffix: -cpu + source_suffixes: |- + -cpu-amd64 + -cpu-arm64 + expected_platforms: |- + linux/amd64 + linux/arm64 + - name: turing + suffix: -turing + source_suffixes: -turing-amd64 + expected_platforms: linux/amd64 + - name: ampere + suffix: -ampere + source_suffixes: -ampere-amd64 + expected_platforms: linux/amd64 + - name: ada-lovelace + suffix: -ada-lovelace + source_suffixes: -ada-lovelace-amd64 + expected_platforms: linux/amd64 + - name: hopper + suffix: -hopper + source_suffixes: -hopper-amd64 + expected_platforms: linux/amd64 + - name: blackwell + suffix: -blackwell + source_suffixes: |- + -blackwell-amd64 + -blackwell-arm64 + expected_platforms: |- + linux/amd64 + linux/arm64 + steps: + - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + - name: Log in to GHCR + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + - name: Docker metadata + id: metadata + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ghcr.io/${{ github.repository }} + flavor: latest=false + tags: | + type=raw,value=${{ inputs.source_sha }}${{ matrix.suffix }} + type=raw,value=${{ inputs.source_sha }},enable=${{ matrix.name == 'cpu' }} + type=raw,value=main${{ matrix.suffix }} + type=raw,value=main,enable=${{ matrix.name == 'cpu' }} + - name: Push + env: + DESTINATION_TAGS: ${{ steps.metadata.outputs.tags }} + EXPECTED_PLATFORMS: ${{ matrix.expected_platforms }} + SOURCE_SHA: ${{ inputs.source_sha }} + SOURCE_SUFFIXES: ${{ matrix.source_suffixes }} + shell: bash + run: | + set -euo pipefail + + sources=() + while IFS= read -r source_suffix; do + if [[ -n "${source_suffix}" ]]; then + sources+=("ghcr.io/${GITHUB_REPOSITORY}:${SOURCE_SHA}${source_suffix}") + fi + done <<< "${SOURCE_SUFFIXES}" + + create_args=() + while IFS= read -r destination_tag; do + if [[ -n "${destination_tag}" ]]; then + create_args+=(--tag "${destination_tag}") + fi + done <<< "${DESTINATION_TAGS}" + + docker buildx imagetools create "${create_args[@]}" "${sources[@]}" + + first_destination="${DESTINATION_TAGS%%$'\n'*}" + expected_platforms="$(printf '%s\n' "${EXPECTED_PLATFORMS}" | sort)" + actual_platforms="$( + docker buildx imagetools inspect "${first_destination}" --raw | + jq -r '.manifests[].platform | select(.architecture != "unknown") | "\(.os)/\(.architecture)"' | + sort -u + )" + if [[ "${actual_platforms}" != "${expected_platforms}" ]]; then + echo "Platform mismatch for ${first_destination}" >&2 + echo "Expected:" >&2 + printf '%s\n' "${expected_platforms}" >&2 + echo "Actual:" >&2 + printf '%s\n' "${actual_platforms}" >&2 + exit 1 + fi + + release: + name: ${{ matrix.name }} + if: inputs.release_tag != '' + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - name: cpu + suffix: -cpu + - name: turing + suffix: -turing + - name: ampere + suffix: -ampere + - name: ada-lovelace + suffix: -ada-lovelace + - name: hopper + suffix: -hopper + - name: blackwell + suffix: -blackwell + steps: + - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + - name: Log in to GHCR + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + - name: Docker metadata + id: metadata + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ghcr.io/${{ github.repository }} + flavor: latest=false + tags: | + type=semver,pattern={{version}},value=${{ inputs.release_tag }},suffix=${{ matrix.suffix }} + type=semver,pattern={{version}},value=${{ inputs.release_tag }},enable=${{ matrix.name == 'cpu' }} + type=raw,value=latest${{ matrix.suffix }} + type=raw,value=latest,enable=${{ matrix.name == 'cpu' }} + - name: Push + env: + DESTINATION_TAGS: ${{ steps.metadata.outputs.tags }} + SOURCE_SHA: ${{ inputs.source_sha }} + shell: bash + run: | + set -euo pipefail + + source_tag="ghcr.io/${GITHUB_REPOSITORY}:${SOURCE_SHA}${{ matrix.suffix }}" + source_digest="" + + for attempt in {1..60}; do + if source_digest="$(docker buildx imagetools inspect "${source_tag}" --format '{{json .Manifest}}' 2>/dev/null | jq -r '.digest')" \ + && [[ "${source_digest}" == sha256:* ]]; then + break + fi + + if [[ "${attempt}" -eq 60 ]]; then + echo "Source image ${source_tag} was not published within 20 minutes" >&2 + exit 1 + fi + + echo "Waiting for ${source_tag} to be published (attempt ${attempt}/60)" + sleep 20 + done + + immutable_source="ghcr.io/${GITHUB_REPOSITORY}@${source_digest}" + create_args=() + while IFS= read -r destination_tag; do + if [[ -n "${destination_tag}" ]]; then + create_args+=(--tag "${destination_tag}") + fi + done <<< "${DESTINATION_TAGS}" + + docker buildx imagetools create "${create_args[@]}" "${immutable_source}" + + while IFS= read -r destination_tag; do + if [[ -z "${destination_tag}" ]]; then + continue + fi + + destination_digest="$(docker buildx imagetools inspect "${destination_tag}" --format '{{json .Manifest}}' | jq -r '.digest')" + if [[ "${destination_digest}" != "${source_digest}" ]]; then + echo "Digest mismatch for ${destination_tag}: expected ${source_digest}, got ${destination_digest}" >&2 + exit 1 + fi + done <<< "${DESTINATION_TAGS}" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 98652a8..b9541b4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,68 +1,60 @@ name: Tests on: - workflow_run: - workflows: [Lint] - types: [completed] + workflow_call: + inputs: + cache_scope: + required: true + type: string + source_sha: + required: true + type: string + secrets: + HF_TOKEN: + required: false permissions: contents: read -concurrency: - group: tests-${{ github.event.workflow_run.head_sha }} - cancel-in-progress: true - jobs: - prerequisite: - runs-on: ubuntu-latest - steps: - - name: Require successful linting - env: - LINT_RESULT: ${{ github.event.workflow_run.conclusion }} - run: test "${LINT_RESULT}" = success - cpu: - needs: prerequisite runs-on: ubuntu-latest timeout-minutes: 45 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - ref: ${{ github.event.workflow_run.head_sha }} + ref: ${{ inputs.source_sha }} - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cache/huggingface/hub ~/.cargo/registry ~/.cargo/git - target - key: test-${{ github.event.workflow_run.event }}-cpu-${{ hashFiles('Cargo.lock') }}-aa8c91ca088ec597df95a0d1c76b3063cb2ae5e8 - restore-keys: test-${{ github.event.workflow_run.event }}-cpu- + key: test-v2-${{ inputs.cache_scope }}-cpu-${{ hashFiles('Cargo.lock') }}-aa8c91ca088ec597df95a0d1c76b3063cb2ae5e8 + restore-keys: test-v2-${{ inputs.cache_scope }}-cpu- - name: Test env: - HF_TOKEN: ${{ github.event.workflow_run.event != 'pull_request' && secrets.HF_TOKEN || '' }} + HF_TOKEN: ${{ secrets.HF_TOKEN }} run: cargo test --release --locked --no-default-features --features cpu metal: - needs: prerequisite runs-on: macos-15 timeout-minutes: 45 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - ref: ${{ github.event.workflow_run.head_sha }} + ref: ${{ inputs.source_sha }} - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cache/huggingface/hub ~/.cargo/registry ~/.cargo/git - target - key: test-${{ github.event.workflow_run.event }}-metal-${{ hashFiles('Cargo.lock') }}-aa8c91ca088ec597df95a0d1c76b3063cb2ae5e8 - restore-keys: test-${{ github.event.workflow_run.event }}-metal- + key: test-v2-${{ inputs.cache_scope }}-metal-${{ hashFiles('Cargo.lock') }}-aa8c91ca088ec597df95a0d1c76b3063cb2ae5e8 + restore-keys: test-v2-${{ inputs.cache_scope }}-metal- - name: Test env: - HF_TOKEN: ${{ github.event.workflow_run.event != 'pull_request' && secrets.HF_TOKEN || '' }} + HF_TOKEN: ${{ secrets.HF_TOKEN }} run: cargo test --release --locked --no-default-features --features metal diff --git a/.github/workflows/trufflehog.yml b/.github/workflows/trufflehog.yml index dc729d3..d5b7514 100644 --- a/.github/workflows/trufflehog.yml +++ b/.github/workflows/trufflehog.yml @@ -1,10 +1,11 @@ name: Secret Leaks on: - push: - branches: [main] - pull_request: - branches: [main] + workflow_call: + inputs: + source_sha: + required: true + type: string permissions: contents: read @@ -17,6 +18,8 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 + persist-credentials: false + ref: ${{ inputs.source_sha }} - name: Secret scanning uses: trufflesecurity/trufflehog@f714bf454f350590f4a24c3ddb1aef02c35bf5b6 # v3.97.5 with: