From af3c967fe8a168d75ecee00782b26f4654311c75 Mon Sep 17 00:00:00 2001 From: Michael Williams Date: Thu, 29 Oct 2026 17:41:54 -0700 Subject: [PATCH 1/7] build: align native metadata and refresh audited dependencies --- native/ortex/Cargo.lock | 299 +------------------------------------- native/ortex/Cargo.toml | 9 +- native/ortex/src/model.rs | 3 - scripts/audit.py | 15 ++ 4 files changed, 20 insertions(+), 306 deletions(-) diff --git a/native/ortex/Cargo.lock b/native/ortex/Cargo.lock index 705e174..caaa7f5 100644 --- a/native/ortex/Cargo.lock +++ b/native/ortex/Cargo.lock @@ -17,15 +17,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -38,17 +29,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" -[[package]] -name = "getrandom" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - [[package]] name = "half" version = "2.3.1" @@ -71,12 +51,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "libc" -version = "0.2.153" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" - [[package]] name = "libloading" version = "0.8.9" @@ -87,24 +61,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata", -] - [[package]] name = "matrixmultiply" version = "0.3.2" @@ -135,16 +91,6 @@ dependencies = [ "rawpointer", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - [[package]] name = "num-complex" version = "0.4.3" @@ -203,23 +149,14 @@ dependencies = [ [[package]] name = "ortex" -version = "0.1.0" +version = "0.1.10" dependencies = [ "half", "ndarray", - "num-traits", "ort", "rustler", - "rustls", - "tracing-subscriber", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "pin-project-lite" version = "0.2.9" @@ -282,36 +219,12 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax", -] - [[package]] name = "regex-syntax" version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "spin", - "untrusted", - "windows-sys", -] - [[package]] name = "rustler" version = "0.29.1" @@ -345,64 +258,6 @@ dependencies = [ "unreachable", ] -[[package]] -name = "rustls" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" -dependencies = [ - "log", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "048a63e5b3ac996d78d402940b5fa47973d2d080c6c6fffa1d0f19c4445310b7" - -[[package]] -name = "rustls-webpki" -version = "0.102.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "smallvec" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "subtle" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" - [[package]] name = "syn" version = "2.0.16" @@ -414,16 +269,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", -] - [[package]] name = "tracing" version = "0.1.37" @@ -442,36 +287,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", ] [[package]] @@ -489,126 +304,14 @@ dependencies = [ "void", ] -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - [[package]] name = "void" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" - -[[package]] -name = "zeroize" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/native/ortex/Cargo.toml b/native/ortex/Cargo.toml index 10f6db3..383541b 100644 --- a/native/ortex/Cargo.toml +++ b/native/ortex/Cargo.toml @@ -1,6 +1,8 @@ [package] name = "ortex" -version = "0.1.0" +version = "0.1.10" +license = "MIT" +repository = "https://github.com/RiverFinancial/ortex" authors = [] edition = "2018" rust-version = "1.92" @@ -12,12 +14,9 @@ crate-type = ["cdylib"] [dependencies] rustler = "0.29.0" -ort = { version = "2.0.0-rc.8", default-features = false, features = ["ndarray", "half"] } +ort = { version = "=2.0.0-rc.8", default-features = false, features = ["ndarray", "half"] } ndarray = "0.16.1" half = "2.2.1" -tracing-subscriber = { version = "0.3", features = [ "env-filter", "fmt" ] } -num-traits = "0.2.15" -rustls = "0.22.4" [features] default = ["ort/load-dynamic"] diff --git a/native/ortex/src/model.rs b/native/ortex/src/model.rs index ccc8108..e204e3a 100644 --- a/native/ortex/src/model.rs +++ b/native/ortex/src/model.rs @@ -40,9 +40,6 @@ pub fn init( eps: Vec, opt: i32, ) -> Result { - // TODO: send tracing logs to erlang/elixir _somehow_ - // tracing_subscriber::fmt::init(); - let session = Session::builder()? .with_optimization_level(map_opt_level(opt))? .with_execution_providers(eps)? diff --git a/scripts/audit.py b/scripts/audit.py index 36e9b82..4bac8fb 100644 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -1,5 +1,6 @@ """Report Rust advisories and retired Hex dependencies without hiding failures.""" +import json import os import subprocess import sys @@ -41,6 +42,20 @@ def main(): except (OSError, subprocess.TimeoutExpired) as error: status, output = 1, str(error) (destination / f"{name}.txt").write_text(output) + if name == "cargo-audit": + try: + data = json.JSONDecoder().raw_decode(output)[0] + (destination / "cargo-audit.json").write_text(json.dumps(data, indent=2) + "\n") + issues = data.get("vulnerabilities", {}).get("list", []) + warnings = sum(len(items) for items in data.get("warnings", {}).values()) + output = f"{len(issues)} vulnerabilities; {warnings} informational warnings.\n" + if issues: + output += "\n".join( + f"{item['advisory']['id']}: {item['package']['name']} {item['package']['version']} — {item['advisory']['title']}" + for item in issues + ) + except (ValueError, KeyError): + pass sections.append( f"## {name}: {'failed' if status else 'passed'}\n\n```text\n{output.rstrip()}\n```\n" ) From 04c83789c19f4c5e96725a7fbe8f91aca3158370 Mon Sep 17 00:00:00 2001 From: Michael Williams Date: Thu, 29 Oct 2026 17:41:54 -0700 Subject: [PATCH 2/7] build: validate release artifacts and generate conventional release notes --- docs/testing.md | 4 +- scripts/package.py | 83 ++++++++++++- scripts/release.py | 203 +++++++++++++++++++++++++++++++ test/package/missing_runtime.exs | 11 ++ test/tooling/release_test.py | 107 ++++++++++++++++ 5 files changed, 406 insertions(+), 2 deletions(-) create mode 100644 scripts/release.py create mode 100644 test/package/missing_runtime.exs create mode 100644 test/tooling/release_test.py diff --git a/docs/testing.md b/docs/testing.md index 455ed27..2e97fb4 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -17,7 +17,9 @@ that configuration; the foundation tests main's existing behavior. temporary directory, and runs the separate test/package project against it. It checks source compilation and an explicitly configured prebuilt library with Cargo/rustc replaced by failing stubs. The runtime path comes from the caller's -ORT_DYLIB_PATH. This is a package installation check, not the feature stack's +ORT_DYLIB_PATH. The package check also rejects a missing runtime in a separate VM; ort rc.8's +legacy behavior is a caught NIF panic, not the migrated descriptive loader error. +This is a package installation check, not the feature stack's automatic RustlerPrecompiled download path. That feature can reuse this harness. `make test.models` checks XGBoost, Iris, and MNIST. Prepared artifacts and reference diff --git a/scripts/package.py b/scripts/package.py index a9ef151..1c806d6 100644 --- a/scripts/package.py +++ b/scripts/package.py @@ -2,10 +2,13 @@ import argparse import gzip +import hashlib import io +import json import os import re import shutil +import struct import subprocess import sys import tarfile @@ -40,7 +43,7 @@ def unpack(archive, destination): def nif_archive(target): - policy = __import__("json").loads((ROOT / "ci/compatibility.json").read_text()) + policy = json.loads((ROOT / "ci/compatibility.json").read_text()) targets = {row["target"] for key in ["pr", "platforms", "alpine"] for row in policy[key]} if target not in targets: raise ValueError("target is not in the artifact contract") @@ -59,6 +62,72 @@ def nif_archive(target): return destination +def inspect_binary(data, target): + if "linux" in target: + if data[:4] != b"\x7fELF" or data[4:6] != b"\x02\x01": + raise ValueError("expected a little-endian ELF64 library") + machine = struct.unpack_from(" + unless error.original == :nif_panicked, do: reraise(error, __STACKTRACE__) +end + +IO.puts("Missing runtime was rejected in a separate VM") diff --git a/test/tooling/release_test.py b/test/tooling/release_test.py new file mode 100644 index 0000000..26e3b52 --- /dev/null +++ b/test/tooling/release_test.py @@ -0,0 +1,107 @@ +"""Test release classification and fail-closed artifact checks.""" + +import hashlib +import io +import json +import struct +import tarfile +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from harness_test import load + + +class ReleaseTest(unittest.TestCase): + def test_conventional_commits_keep_breaking_and_unknown_subjects(self): + release = load("release") + self.assertEqual( + release.group_commit("feat(io): add names", ""), ("Features", "io: add names") + ) + self.assertEqual( + release.group_commit("fix!: change shape", ""), ("Breaking changes", "change shape") + ) + self.assertEqual( + release.group_commit("chore: update runtime", "BREAKING CHANGE: new floor"), + ("Breaking changes", "update runtime"), + ) + self.assertEqual( + release.group_commit("An inherited subject", ""), ("Other", "An inherited subject") + ) + + def test_binary_architecture_must_match_target(self): + package = load("package") + header = bytearray(64) + header[:6] = b"\x7fELF\x02\x01" + struct.pack_into(" Date: Thu, 29 Oct 2026 17:41:54 -0700 Subject: [PATCH 3/7] build: assemble documentation and a source dependency inventory --- Makefile | 7 ++++ README.md | 96 ++++++++++++++++++++++--------------------- docs/development.md | 3 +- docs/releasing.md | 46 +++++++++++++++++++++ mix.exs | 13 ++++-- scripts/inventory.exs | 15 +++++++ scripts/inventory.py | 66 +++++++++++++++++++++++++++++ 7 files changed, 195 insertions(+), 51 deletions(-) create mode 100644 docs/releasing.md create mode 100644 scripts/inventory.exs create mode 100644 scripts/inventory.py diff --git a/Makefile b/Makefile index d8dc4c9..5aba60e 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ export ORT_DYLIB_PATH ?= $(shell $(PYTHON) scripts/install_runtime.py path 2>/de .PHONY: test.system test.package test.models test.tooling coverage bench fixtures package .PHONY: test.alpine .PHONY: setup.audit audit setup.rust +.PHONY: inventory release.notes help: ## Show development commands @awk 'BEGIN {FS = ":.*## "} /^[a-zA-Z0-9_.-]+:.*## / {printf " %-22s %s\n", $$1, $$2}' $(MAKEFILE_LIST) @@ -88,3 +89,9 @@ setup.rust: ## Install the pinned Rust compiler, formatter, and Clippy audit: ## Report Rust advisories and retired Hex dependencies $(PYTHON) scripts/audit.py + +inventory: ## Write the source dependency inventory (consumer-supplied ORT excluded) + MIX_ENV=prod $(PYTHON) scripts/inventory.py + +release.notes: ## Render conventional-commit notes and assembled artifact hashes + $(PYTHON) scripts/release.py notes $(ARGS) diff --git a/README.md b/README.md index 53c54fc..75e1ed0 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,66 @@ # Ortex -`Ortex` is a wrapper around [ONNX Runtime](https://onnxruntime.ai/) (implemented as -bindings to [`ort`](https://github.com/pykeio/ort)). Ortex leverages -[`Nx.Serving`](https://hexdocs.pm/nx/Nx.Serving.html) to easily deploy ONNX models -that run concurrently and distributed in a cluster. Ortex also provides a storage-only -tensor implementation for ease of use. +Ortex wraps [ONNX Runtime](https://onnxruntime.ai/) for Elixir through Rust and +supports numeric tensor transfer and inference with Nx.Serving. -ONNX models are a standard machine learning model format that can be exported from most ML -libraries like PyTorch and TensorFlow. Ortex allows for easy loading and fast inference of -ONNX models using different backends available to ONNX Runtime such as CUDA, TensorRT, Core -ML, and ARM Compute Library. +This is the RiverFinancial fork. Pin a reviewed commit from +https://github.com/RiverFinancial/ortex when consuming unreleased work; the +foundation stack does not publish a new Hex release. -## Examples +## Development -TL;DR: +Review `mise.toml`, run `mise trust` and `mise install`, then: -```elixir -iex> model = Ortex.load("./models/resnet50.onnx") -#Ortex.Model< - inputs: [{"input", "Float32", [nil, 3, 224, 224]}] - outputs: [{"output", "Float32", [nil, 1000]}]> -iex> {output} = Ortex.run(model, Nx.broadcast(0.0, {1, 3, 224, 224})) -iex> output |> Nx.backend_transfer() |> Nx.argmax -#Nx.Tensor< - s64 - 499 -> +```sh +mise exec -- make setup +mise exec -- make doctor +mise exec -- make help ``` -Inspecting a model shows the expected inputs, outputs, data types, and shapes. Axes with -`nil` represent a dynamic size. - -To see more real world examples see the `examples` folder. +Supported BEAM pairs are Elixir 1.19 / OTP 28 and Elixir 1.20 / OTP 29. Rust 1.92 +is the minimum. The source NIF currently retains ort rc.8 / Rustler 0.29; their +migration is separate feature work. Nx versions from 0.6.2 up to 1.0 are allowed +(excluding 1.0), with 0.13.1 locked for normal development. -### Serving - -`Ortex` also implements `Nx.Serving` behaviour. To use it in your application's -supervision tree consult the `Nx.Serving` docs. +CPU dynamic loading is the development default. ONNX Runtime is installed +separately by `make setup`. Make reads its recorded path; set ORT_DYLIB_PATH to +use another library. When consuming this source package, configure the mode +explicitly in your application's config: ```elixir -iex> serving = Nx.Serving.new(Ortex.Serving, model) -iex> batch = Nx.Batch.stack([{Nx.broadcast(0.0, {3, 224, 224})}]) -iex> {result} = Nx.Serving.run(serving, batch) -iex> result |> Nx.backend_transfer() |> Nx.argmax(axis: 1) -#Nx.Tensor< - s64[1] - [499] -> +import Config +config :ortex, Ortex.Native, features: ["ort/load-dynamic"] ``` -## Installation +The consumer supplies ORT_DYLIB_PATH. Execution providers require a matching +runtime and explicit Cargo features; they are not inferred from the host OS. -`Ortex` can be installed by adding `ortex` to your list of dependencies in `mix.exs`: +## Checks and examples -```elixir -def deps do - [ - {:ortex, "~> 0.1.10"} - ] -end +```sh +make check +make test.system +make test.package +make test.models +make coverage +make docs +make test.alpine +make bench ARGS=5000 ``` -You will need [Rust](https://www.rust-lang.org/tools/install) for compilation to succeed. +Library tests and doctests use normal ExUnit discovery under test/. System, +package, and model suites are documented in [testing](docs/testing.md). XGBoost, +Iris, and MNIST have prepared reference inputs and outputs; for example, with +ORT_DYLIB_PATH set: + +```sh +MIX_ENV=test mix run examples/run.exs xgboost +``` + +Numeric outputs use Ortex.Backend for storage. Transfer to a computation backend +before arithmetic. See [examples](examples/README.md) and +[Nx.Serving](https://hexdocs.pm/nx/Nx.Serving.html) for usage. + +[Development](docs/development.md), [CI policy](docs/ci.md), and +[release procedures](docs/releasing.md) describe the supported environment, +validation matrices, artifacts, and publishing gates. diff --git a/docs/development.md b/docs/development.md index db101f6..5d2781d 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,6 +1,7 @@ # Development -Use `mise install`, then `mise exec -- make setup` and `mise exec -- make doctor`. +Review mise.toml and run `mise trust`, then `mise install`, +`mise exec -- make setup`, and `mise exec -- make doctor`. Rustup reads the pinned rust-toolchain.toml. GNU Make 3.81 or newer is sufficient; Windows developers can use Git Bash with Make available on PATH. diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..c2c45c0 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,46 @@ +# Releasing the fork + +Use atomic conventional commits: `feat`, `fix`, `perf`, `docs`, `build`, `ci`, +`test`, `refactor`, `style`, and `chore`. An optional scope describes the affected +area. `!` or a `BREAKING CHANGE:` footer marks a breaking change. Release notes +bucket these commits, retain other subjects under Other, and include artifact +SHA-256 values. These conventions do not require rewriting existing history. + +Before the first fork release, decide its Hex package identity and ownership, +choose a new version, and update Mix, Cargo.toml, Cargo.lock, and the changelog. +The current version is inherited from main; this stack does not claim that a new +0.1.10 release can be published over the existing public package. + +`make package` builds the source Hex archive. `scripts/package.py nif --target +TARGET` archives the already tested library in priv/native. `make inventory` +writes a CycloneDX source dependency inventory, including build-time Rust crates. +It is not an inventory of the precise code retained by the linker in each NIF. +ONNX Runtime is consumer supplied and is explicitly outside the package inventory. + +`scripts/package.py assemble --all` requires all seven target archives, checks +their member names and binary architectures, builds the Hex package, and writes +artifacts.json and SHA256SUMS. Partial local assembly omits --all and is explicitly +marked incomplete. A release must never use partial assembly. Native archive +metadata is normalized; no claim of reproducible native compilation is made. + +Push a matching vVERSION tag whose commit is already on the fork's main branch. +The staging workflow runs full compatibility and dependency audits, builds docs +and the inventory, assembles the complete bundle, generates notes, and creates a +draft GitHub release after protected-environment approval. Provenance covers the +bundle. Review it before publishing the GitHub release. No publication occurs as +part of developing this stack. Dependency advisories must be addressed before +the release audit can pass. + +Hex publication is disabled unless ORTEX_HEX_PUBLISH is explicitly set to true +in repository variables. Before enabling it, configure ownership, a Hex workload +identity restricted to this repository and publish.yml, and required reviewers +and tag restrictions on the GitHub release environment. Public package workload +identities cannot bootstrap a package's first release. The current publisher's +package scope is hexpm/ortex; change that scope and Mix package metadata together +if River chooses a distinct package identity. Do not configure an upstream +package's credentials or ownership as part of foundation work. + +Publishing the reviewed GitHub release triggers the protected Hex job. It checks +tag ancestry and matching versions, verifies every downloaded artifact's +attestation and checksums, obtains a short-lived package-scoped token through +OIDC, and publishes the package and docs. No long-lived Hex key is stored. diff --git a/mix.exs b/mix.exs index b86b807..3c0797b 100644 --- a/mix.exs +++ b/mix.exs @@ -17,7 +17,13 @@ defmodule Ortex.MixProject do homepage_url: "https://github.com/RiverFinancial/ortex", docs: [ main: "readme", - extras: ["README.md"] + extras: [ + "README.md", + "docs/development.md", + "docs/testing.md", + "docs/ci.md", + "docs/releasing.md" + ] ], package: package() ] @@ -38,7 +44,7 @@ defmodule Ortex.MixProject do defp deps do [ {:rustler, "~> 0.29.1"}, - {:nx, "~> 0.6"}, + {:nx, ">= 0.6.2 and < 1.0.0"}, {:credo, "~> 1.7.19", only: :test, runtime: false}, {:ex_doc, "~> 0.40.4", only: :docs, runtime: false} ] @@ -46,7 +52,8 @@ defmodule Ortex.MixProject do defp package do [ - files: ~w(lib .formatter.exs mix.exs README* LICENSE* native/ortex/src/ config/config.exs + files: + ~w(lib .formatter.exs mix.exs README* LICENSE* docs native/ortex/src/ config/config.exs native/ortex/Cargo.lock native/ortex/Cargo.toml native/ortex/.cargo/config.toml), licenses: ["MIT"], links: %{"GitHub" => "https://github.com/RiverFinancial/ortex"}, diff --git a/scripts/inventory.exs b/scripts/inventory.exs new file mode 100644 index 0000000..d72e511 --- /dev/null +++ b/scripts/inventory.exs @@ -0,0 +1,15 @@ +components = + Mix.Dep.cached() + |> Enum.map(fn dep -> + {:hex, name, version, _, _, _, _, _} = dep.opts[:lock] + + %{ + type: "library", + name: to_string(name), + version: version, + purl: "pkg:hex/#{name}@#{version}" + } + end) + +File.mkdir_p!("dist") +File.write!("dist/hex-components.json", :json.encode(components)) diff --git a/scripts/inventory.py b/scripts/inventory.py new file mode 100644 index 0000000..1795066 --- /dev/null +++ b/scripts/inventory.py @@ -0,0 +1,66 @@ +"""Write a source dependency inventory in CycloneDX format; ORT is external.""" + +import json +import subprocess +from pathlib import Path + +from package import version + +ROOT = Path(__file__).resolve().parents[1] + + +def main(): + subprocess.run(["mix", "run", "--no-start", "scripts/inventory.exs"], cwd=ROOT, check=True) + result = subprocess.check_output( + [ + "cargo", + "metadata", + "--locked", + "--format-version", + "1", + "--features", + "ort/load-dynamic", + ], + cwd=ROOT / "native/ortex", + text=True, + ) + metadata = json.loads(result) + components = json.loads((ROOT / "dist/hex-components.json").read_text()) + for package in metadata["packages"]: + if package["name"] == "ortex": + continue + component = { + "type": "library", + "name": package["name"], + "version": package["version"], + "purl": f"pkg:cargo/{package['name']}@{package['version']}", + } + if package.get("license"): + component["licenses"] = [{"expression": package["license"].replace("/", " OR ")}] + components.append(component) + for component in components: + component["bom-ref"] = component["purl"] + bom = { + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 1, + "metadata": { + "component": {"type": "library", "name": "ortex", "version": version()}, + "properties": [ + { + "name": "ortex:scope", + "value": "Source dependency inventory, including build-time Rust crates. Not a per-binary linkage inventory.", + }, + { + "name": "ortex:external-runtime", + "value": "ONNX Runtime is supplied by the consumer and is not bundled.", + }, + ], + }, + "components": sorted(components, key=lambda item: item["purl"]), + } + (ROOT / "dist/source.cdx.json").write_text(json.dumps(bom, indent=2) + "\n") + + +if __name__ == "__main__": + main() From a789218d5a86bba0356a291378c063c79e919049 Mon Sep 17 00:00:00 2001 From: Michael Williams Date: Thu, 29 Oct 2026 17:41:54 -0700 Subject: [PATCH 4/7] ci: stage attested fork releases and gate Hex publishing --- .github/workflows/publish.yml | 51 ++++++++++++++++++++ .github/workflows/release.yml | 91 +++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..bf1dea5 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,51 @@ +name: Publish approved Hex release +on: + release: + types: [published] +permissions: + contents: read +concurrency: + group: publish-${{ github.event.release.tag_name }} + cancel-in-progress: false +jobs: + publish: + if: github.repository == 'RiverFinancial/ortex' && vars.ORTEX_HEX_PUBLISH == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 20 + environment: release + permissions: + contents: read + attestations: read # Verify the previously attested artifacts before publishing. + id-token: write # Exchange the protected job identity for a package-scoped Hex token. + env: + MIX_ENV: docs + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.release.tag_name }} + fetch-depth: 0 + persist-credentials: false + - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 + with: + elixir-version: '1.20.4' + otp-version: '29.1.1' + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: '3.12.12' + - name: Verify the approved release bundle + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + python scripts/release.py validate --tag "$RELEASE_TAG" + gh release download "$RELEASE_TAG" --repo RiverFinancial/ortex --dir dist + for artifact in dist/*; do + gh attestation verify "$artifact" --repo RiverFinancial/ortex + done + python scripts/release.py verify + make deps + - name: Publish package and documentation with Hex workload identity + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + ORTEX_HEX_PUBLISH: ${{ vars.ORTEX_HEX_PUBLISH }} + run: python scripts/release.py publish --tag "$RELEASE_TAG" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b33ddfd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,91 @@ +name: Stage release +on: + push: + tags: ['v*'] +permissions: + contents: read +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false +jobs: + validate: + # actionlint 1.7.12 does not yet parse GitHub's $/ reusable-workflow syntax. + uses: ./.github/workflows/checks.yml # zizmor: ignore[self-repository] + with: + scope: full + audit: + uses: ./.github/workflows/audit.yml # zizmor: ignore[self-repository] + assemble: + needs: [validate, audit] + runs-on: ubuntu-24.04 + timeout-minutes: 25 + env: + MIX_ENV: docs + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 + with: + elixir-version: '1.20.4' + otp-version: '29.1.1' + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: '3.12.12' + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + pattern: nif-* + merge-multiple: true + path: dist + - name: Assemble reviewed artifacts + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + python scripts/release.py validate --tag "$RELEASE_TAG" + make deps docs inventory + tar -czf "dist/docs-${RELEASE_TAG#v}.tar.gz" -C doc . + python scripts/package.py assemble --all + previous=$(git describe --tags --abbrev=0 --match 'v[0-9]*' HEAD^ 2>/dev/null || true) + if test -n "$previous"; then + python scripts/release.py notes --from "$previous" --output dist/RELEASE_NOTES.md + else + python scripts/release.py notes --output dist/RELEASE_NOTES.md + fi + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: release-bundle + path: | + dist/ortex-* + dist/docs-* + dist/source.cdx.json + dist/artifacts.json + dist/SHA256SUMS + dist/RELEASE_NOTES.md + retention-days: 30 + if-no-files-found: error + stage: + if: github.repository == 'RiverFinancial/ortex' + needs: assemble + runs-on: ubuntu-24.04 + timeout-minutes: 10 + environment: release + permissions: + contents: write # Create only the fork's draft release and upload its bundle. + id-token: write # Authenticate build provenance attestation. + attestations: write # Record provenance for the completed release bundle. + steps: + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: release-bundle + path: dist + - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: dist/* + - name: Stage the draft release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.ref_name }} + run: | + gh release create "$RELEASE_TAG" --repo RiverFinancial/ortex --draft --verify-tag --notes-file dist/RELEASE_NOTES.md + gh release upload "$RELEASE_TAG" dist/* --repo RiverFinancial/ortex From 05b4605ec4cc380469cb8aaf6367f2e3393448af Mon Sep 17 00:00:00 2001 From: Michael Williams Date: Thu, 29 Oct 2026 17:51:28 -0700 Subject: [PATCH 5/7] fix: use portable Python pins throughout release automation --- .github/workflows/publish.yml | 2 +- .github/workflows/release.yml | 2 +- docs/testing.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bf1dea5..08f7641 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -31,7 +31,7 @@ jobs: otp-version: '29.1.1' - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: - python-version: '3.12.12' + python-version: '3.14.10' - name: Verify the approved release bundle env: GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b33ddfd..1d97ce2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,7 +32,7 @@ jobs: otp-version: '29.1.1' - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: - python-version: '3.12.12' + python-version: '3.14.10' - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: pattern: nif-* diff --git a/docs/testing.md b/docs/testing.md index 2e97fb4..ed9e840 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -28,7 +28,7 @@ from the original estimators; MNIST uses Python ORT as a binding reference, with synthetic pixels. None of these checks claim to validate ORT's operators or model accuracy. Iris uses tensor probabilities on main; ZipMap needs the separate container-output feature. Install fixtures/requirements.txt in a separate Python -3.12 environment before `make fixtures`. Generation validates exports with ONNX +3.12 environment before `make fixtures PYTHON=/path/to/exporter/bin/python`. Generation validates exports with ONNX and Python ORT; ordinary tests need neither Python exporters nor network access. `make test.tooling` checks the harnesses themselves, including partial staging, From e790fd8550c1eb8f4f6e879478557883e15dc47d Mon Sep 17 00:00:00 2001 From: Michael Williams Date: Thu, 29 Oct 2026 17:58:51 -0700 Subject: [PATCH 6/7] ci: verify complete release bundles and Rust-free Alpine consumers --- .github/workflows/checks.yml | 54 +++++++++++++++++++++++++++++++-- .github/workflows/release.yml | 41 +++++-------------------- ci/containers/alpine.Dockerfile | 18 +++++++++-- docs/ci.md | 7 ++++- docs/releasing.md | 4 ++- 5 files changed, 84 insertions(+), 40 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 865bd14..13ee7b6 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -170,9 +170,59 @@ jobs: path: dist/*-${{ matrix.target }}.tar.gz retention-days: 14 if-no-files-found: error + bundle-check: + needs: [policy, hygiene, source, msrv, alpine] + if: needs.policy.outputs.scope == 'full' + runs-on: ubuntu-24.04 + timeout-minutes: 25 + env: + MIX_ENV: docs + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 + with: + elixir-version: '1.20.4' + otp-version: '29.1.1' + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: '3.14.10' + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + pattern: nif-* + merge-multiple: true + path: dist + - name: Exercise complete release assembly without publication + run: | + python scripts/release.py validate + make deps docs inventory + version=$(python scripts/release.py validate) + tar -czf "dist/docs-$version.tar.gz" -C doc . + python scripts/package.py assemble --all + python scripts/release.py verify + previous=$(git describe --tags --abbrev=0 --match 'v[0-9]*' HEAD^ 2>/dev/null || true) + if test -n "$previous"; then + python scripts/release.py notes --from "$previous" --output dist/RELEASE_NOTES.md + else + python scripts/release.py notes --output dist/RELEASE_NOTES.md + fi + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: release-bundle + path: | + dist/ortex-* + dist/docs-* + dist/source.cdx.json + dist/artifacts.json + dist/SHA256SUMS + dist/RELEASE_NOTES.md + retention-days: 14 + if-no-files-found: error validated: if: always() - needs: [policy, hygiene, source, msrv, alpine] + needs: [policy, hygiene, source, msrv, alpine, bundle-check] runs-on: ubuntu-24.04 timeout-minutes: 5 steps: @@ -185,7 +235,7 @@ jobs: jobs = json.loads(os.environ['RESULTS']) scope = jobs['policy']['outputs'].get('scope') for name, job in jobs.items(): - allowed = {'success', 'skipped'} if name == 'alpine' and scope == 'pr' else {'success'} + allowed = {'success', 'skipped'} if name in {'alpine', 'bundle-check'} and scope == 'pr' else {'success'} if job['result'] not in allowed: raise SystemExit(f"{name}: {job['result']}") PY diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1d97ce2..45b7078 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,58 +15,31 @@ jobs: scope: full audit: uses: ./.github/workflows/audit.yml # zizmor: ignore[self-repository] - assemble: + verify-release: needs: [validate, audit] runs-on: ubuntu-24.04 - timeout-minutes: 25 - env: - MIX_ENV: docs + timeout-minutes: 10 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 - with: - elixir-version: '1.20.4' - otp-version: '29.1.1' - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: '3.14.10' - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: - pattern: nif-* - merge-multiple: true + name: release-bundle path: dist - - name: Assemble reviewed artifacts + - name: Verify the tag and complete tested bundle env: RELEASE_TAG: ${{ github.ref_name }} run: | python scripts/release.py validate --tag "$RELEASE_TAG" - make deps docs inventory - tar -czf "dist/docs-${RELEASE_TAG#v}.tar.gz" -C doc . - python scripts/package.py assemble --all - previous=$(git describe --tags --abbrev=0 --match 'v[0-9]*' HEAD^ 2>/dev/null || true) - if test -n "$previous"; then - python scripts/release.py notes --from "$previous" --output dist/RELEASE_NOTES.md - else - python scripts/release.py notes --output dist/RELEASE_NOTES.md - fi - - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: release-bundle - path: | - dist/ortex-* - dist/docs-* - dist/source.cdx.json - dist/artifacts.json - dist/SHA256SUMS - dist/RELEASE_NOTES.md - retention-days: 30 - if-no-files-found: error + python scripts/release.py verify stage: - if: github.repository == 'RiverFinancial/ortex' - needs: assemble + if: github.repository == 'RiverFinancial/ortex' && vars.ORTEX_STAGE_RELEASES == 'true' + needs: verify-release runs-on: ubuntu-24.04 timeout-minutes: 10 environment: release diff --git a/ci/containers/alpine.Dockerfile b/ci/containers/alpine.Dockerfile index fe0ef07..0748479 100644 --- a/ci/containers/alpine.Dockerfile +++ b/ci/containers/alpine.Dockerfile @@ -1,4 +1,4 @@ -FROM hexpm/elixir:1.20.4-erlang-29.1.1-alpine-3.24.2@sha256:ad851f40ce103dcb4ad56f23877d99473ef5c013e09b9e57921ffe877ac6d6a9 +FROM hexpm/elixir:1.20.4-erlang-29.1.1-alpine-3.24.2@sha256:ad851f40ce103dcb4ad56f23877d99473ef5c013e09b9e57921ffe877ac6d6a9 AS build RUN apk add --no-cache build-base git bash python3 rust=1.96.1-r0 cargo=1.96.1-r0 ca-certificates && \ apk add --no-cache \ --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main \ @@ -11,4 +11,18 @@ COPY mix.exs mix.lock ./ RUN mix local.hex --force && mix local.rebar --force && mix deps.get --check-locked COPY . . RUN mix compile --warnings-as-errors -CMD ["sh", "-ec", "mix test && python3 scripts/fixtures.py && mix test test/models --include model && python3 scripts/scenario.py && python3 scripts/package.py test --mode source && python3 scripts/package.py test --mode prebuilt && if test -n \"${TARGET:-}\"; then python3 scripts/package.py nif --target \"$TARGET\"; fi"] +RUN mix test && python3 scripts/fixtures.py && mix test test/models --include model && \ + python3 scripts/scenario.py && python3 scripts/package.py test --mode source + +FROM hexpm/elixir:1.20.4-erlang-29.1.1-alpine-3.24.2@sha256:ad851f40ce103dcb4ad56f23877d99473ef5c013e09b9e57921ffe877ac6d6a9 +RUN apk add --no-cache git bash python3 ca-certificates && \ + apk add --no-cache \ + --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main \ + --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \ + --repository=https://dl-cdn.alpinelinux.org/alpine/edge/testing \ + onnxruntime=1.24.4-r1 +WORKDIR /app +ENV MIX_ENV=test ORT_DYLIB_PATH=/usr/lib/libonnxruntime.so.1.24.4 +COPY --from=build /app /app +RUN ! command -v cargo && ! command -v rustc +CMD ["sh", "-ec", "python3 scripts/package.py test --mode prebuilt && if test -n \"${TARGET:-}\"; then python3 scripts/package.py nif --target \"$TARGET\"; fi"] diff --git a/docs/ci.md b/docs/ci.md index b197929..c48c825 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -1,7 +1,7 @@ # CI policy Checks runs all hygiene checks and the library, model, system, and package suites -on three PR combinations. Main and the nightly use seven native combinations +on three PR combinations. Main and the nightly use eight native combinations (pairwise BEAM/Nx/runtime boundaries and five host platforms), plus both Alpine architectures. This is an explicit selection, not a Cartesian product. The `ci:full` PR label opts into the complete matrix before merge. @@ -46,3 +46,8 @@ Alpine edge repositories, whose transitive package selection is mutable. These images establish compatibility, not bit-for-bit reproducibility. glibc libraries are tested separately from musl. Published native artifacts include only the NIF; the consumer supplies ONNX Runtime. + +Full sweeps also assemble and verify the complete seven-target bundle, docs, +source inventory, and release notes. The staging workflow consumes that tested +bundle. Alpine prebuilt installation runs in a separate final image with neither +Cargo nor rustc installed. diff --git a/docs/releasing.md b/docs/releasing.md index c2c45c0..1c8910f 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -23,7 +23,9 @@ artifacts.json and SHA256SUMS. Partial local assembly omits --all and is explici marked incomplete. A release must never use partial assembly. Native archive metadata is normalized; no claim of reproducible native compilation is made. -Push a matching vVERSION tag whose commit is already on the fork's main branch. +Configure required reviewers and tag restrictions on the release environment, +then explicitly enable ORTEX_STAGE_RELEASES in repository variables. Push a +matching vVERSION tag whose commit is already on the fork's main branch. The staging workflow runs full compatibility and dependency audits, builds docs and the inventory, assembles the complete bundle, generates notes, and creates a draft GitHub release after protected-environment approval. Provenance covers the From 427d9c3c152961e67ac64acec340dd403a68c6ae Mon Sep 17 00:00:00 2001 From: Michael Williams Date: Thu, 29 Oct 2026 18:15:23 -0700 Subject: [PATCH 7/7] fix: retain Hex and Rebar in the Rust-free Alpine image --- ci/containers/alpine.Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/containers/alpine.Dockerfile b/ci/containers/alpine.Dockerfile index 0748479..16cc30e 100644 --- a/ci/containers/alpine.Dockerfile +++ b/ci/containers/alpine.Dockerfile @@ -24,5 +24,6 @@ RUN apk add --no-cache git bash python3 ca-certificates && \ WORKDIR /app ENV MIX_ENV=test ORT_DYLIB_PATH=/usr/lib/libonnxruntime.so.1.24.4 COPY --from=build /app /app +COPY --from=build /root/.mix /root/.mix RUN ! command -v cargo && ! command -v rustc CMD ["sh", "-ec", "python3 scripts/package.py test --mode prebuilt && if test -n \"${TARGET:-}\"; then python3 scripts/package.py nif --target \"$TARGET\"; fi"]