From 47358f8f289af2194a0ee7446ca876bd1cca0357 Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Mon, 1 Jun 2026 17:09:29 +0200 Subject: [PATCH 01/18] build(nix): add per-crate crane workspace builds Add crane-based package outputs for the main OpenShell crates and a default symlinkJoin package. The new workspace helper derives each crate's transitive workspace dependency closure, builds from minimal source trees, and declares the assets each crate needs at compile time. Build each crate in three layers: 1. crates.io dependencies with crane buildDepsOnly 2. first-party workspace dependency libraries 3. the final real crate The workspace-libs layer builds the selected package with the same `-p ` selection as final so Cargo feature unification matches, but overlays a crane-generated dummy source for the leaf crate. After that layer builds, remove the dummy leaf artifacts with `cargo clean --release -p ` so the final layer cannot reuse or package stub outputs. This lets leaf edits reuse cached first-party libs while still compiling and linking the real leaf crate. Add explicit `[lib]` target names and `path = "src/lib.rs"` entries to workspace crates. The Nix source minimizer keeps every member Cargo.toml but omits source trees outside the selected crate closure; explicit target paths let Cargo resolve those member manifests without relying on auto-discovery of files that are intentionally absent. They also give crane's dummy source generation a stable target shape. Guard the openshell-core build script's `.git` rerun paths so Cargo does not mark core dirty in Nix source trees where `.git` is absent. Without this, core recompiled in the final layer and cascaded into its dependents. Known limitation: the VM driver package is wired into the flake, but the Nix build does not yet provide the compressed VM runtime artifacts that openshell-driver-vm embeds. For now that crate builds via its stub-resource fallback rather than producing a fully usable VM driver package. Ignore Nix `result*` symlinks created by local builds. --- .gitignore | 3 + crates/openshell-bootstrap/Cargo.toml | 4 + crates/openshell-core/Cargo.toml | 4 + crates/openshell-core/build.rs | 8 +- crates/openshell-driver-docker/Cargo.toml | 4 + crates/openshell-driver-kubernetes/Cargo.toml | 4 + crates/openshell-driver-podman/Cargo.toml | 4 + crates/openshell-ocsf/Cargo.toml | 4 + crates/openshell-policy/Cargo.toml | 4 + crates/openshell-prover/Cargo.toml | 4 + crates/openshell-providers/Cargo.toml | 4 + crates/openshell-router/Cargo.toml | 4 + crates/openshell-server-macros/Cargo.toml | 2 + crates/openshell-tui/Cargo.toml | 4 + crates/openshell-vfio/Cargo.toml | 4 + flake.lock | 16 ++ flake.nix | 81 +++++++++ nix/workspace.nix | 158 ++++++++++++++++++ 18 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 nix/workspace.nix diff --git a/.gitignore b/.gitignore index a3d613775..048511ad8 100644 --- a/.gitignore +++ b/.gitignore @@ -225,3 +225,6 @@ rfc.md # Markdown/mermaid lint tooling deps scripts/lint-mermaid/node_modules/ + +# Nix +result* diff --git a/crates/openshell-bootstrap/Cargo.toml b/crates/openshell-bootstrap/Cargo.toml index c860cb138..f57550209 100644 --- a/crates/openshell-bootstrap/Cargo.toml +++ b/crates/openshell-bootstrap/Cargo.toml @@ -9,6 +9,10 @@ license.workspace = true repository.workspace = true rust-version.workspace = true +[lib] +name = "openshell_bootstrap" +path = "src/lib.rs" + [dependencies] openshell-core = { path = "../openshell-core", default-features = false } bollard = "0.20" diff --git a/crates/openshell-core/Cargo.toml b/crates/openshell-core/Cargo.toml index bf3581164..c4a417ed1 100644 --- a/crates/openshell-core/Cargo.toml +++ b/crates/openshell-core/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_core" +path = "src/lib.rs" + [dependencies] prost = { workspace = true } prost-types = { workspace = true } diff --git a/crates/openshell-core/build.rs b/crates/openshell-core/build.rs index 7955772a6..82ffcc38d 100644 --- a/crates/openshell-core/build.rs +++ b/crates/openshell-core/build.rs @@ -12,8 +12,12 @@ fn main() -> Result<(), Box> { // builds where .git is absent, this silently does nothing and the binary // falls back to CARGO_PKG_VERSION (which is already sed-patched by the // build pipeline). - println!("cargo:rerun-if-changed=../../.git/HEAD"); - println!("cargo:rerun-if-changed=../../.git/refs/tags"); + if Path::new("../../.git/HEAD").exists() { + println!("cargo:rerun-if-changed=../../.git/HEAD"); + } + if Path::new("../../.git/refs/tags").exists() { + println!("cargo:rerun-if-changed=../../.git/refs/tags"); + } if let Some(version) = git_version() { println!("cargo:rustc-env=OPENSHELL_GIT_VERSION={version}"); diff --git a/crates/openshell-driver-docker/Cargo.toml b/crates/openshell-driver-docker/Cargo.toml index 7e1bc069c..60660b521 100644 --- a/crates/openshell-driver-docker/Cargo.toml +++ b/crates/openshell-driver-docker/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_driver_docker" +path = "src/lib.rs" + [dependencies] openshell-core = { path = "../openshell-core", default-features = false } diff --git a/crates/openshell-driver-kubernetes/Cargo.toml b/crates/openshell-driver-kubernetes/Cargo.toml index 07fa91015..2bd378b11 100644 --- a/crates/openshell-driver-kubernetes/Cargo.toml +++ b/crates/openshell-driver-kubernetes/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_driver_kubernetes" +path = "src/lib.rs" + [[bin]] name = "openshell-driver-kubernetes" path = "src/main.rs" diff --git a/crates/openshell-driver-podman/Cargo.toml b/crates/openshell-driver-podman/Cargo.toml index ed798c0ab..d3e2464a9 100644 --- a/crates/openshell-driver-podman/Cargo.toml +++ b/crates/openshell-driver-podman/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_driver_podman" +path = "src/lib.rs" + [[bin]] name = "openshell-driver-podman" path = "src/main.rs" diff --git a/crates/openshell-ocsf/Cargo.toml b/crates/openshell-ocsf/Cargo.toml index 14cc93ba3..fca761bd1 100644 --- a/crates/openshell-ocsf/Cargo.toml +++ b/crates/openshell-ocsf/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_ocsf" +path = "src/lib.rs" + [dependencies] chrono = { version = "0.4", features = ["serde"] } serde = { workspace = true } diff --git a/crates/openshell-policy/Cargo.toml b/crates/openshell-policy/Cargo.toml index 16719de13..216f01459 100644 --- a/crates/openshell-policy/Cargo.toml +++ b/crates/openshell-policy/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_policy" +path = "src/lib.rs" + [dependencies] openshell-core = { path = "../openshell-core", default-features = false } serde = { workspace = true } diff --git a/crates/openshell-prover/Cargo.toml b/crates/openshell-prover/Cargo.toml index ee815f3a3..749c05379 100644 --- a/crates/openshell-prover/Cargo.toml +++ b/crates/openshell-prover/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_prover" +path = "src/lib.rs" + [features] bundled-z3 = ["z3/bundled"] diff --git a/crates/openshell-providers/Cargo.toml b/crates/openshell-providers/Cargo.toml index 9b294d7b7..dc10e2e10 100644 --- a/crates/openshell-providers/Cargo.toml +++ b/crates/openshell-providers/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_providers" +path = "src/lib.rs" + [dependencies] glob = { workspace = true } openshell-core = { path = "../openshell-core", default-features = false } diff --git a/crates/openshell-router/Cargo.toml b/crates/openshell-router/Cargo.toml index 97bbf4dc7..ffdd3378d 100644 --- a/crates/openshell-router/Cargo.toml +++ b/crates/openshell-router/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_router" +path = "src/lib.rs" + [dependencies] openshell-core = { path = "../openshell-core", default-features = false } bytes = { workspace = true } diff --git a/crates/openshell-server-macros/Cargo.toml b/crates/openshell-server-macros/Cargo.toml index f929d43a6..fc04db568 100644 --- a/crates/openshell-server-macros/Cargo.toml +++ b/crates/openshell-server-macros/Cargo.toml @@ -10,6 +10,8 @@ license.workspace = true repository.workspace = true [lib] +name = "openshell_server_macros" +path = "src/lib.rs" proc-macro = true [dependencies] diff --git a/crates/openshell-tui/Cargo.toml b/crates/openshell-tui/Cargo.toml index 238166136..cff3ea9c7 100644 --- a/crates/openshell-tui/Cargo.toml +++ b/crates/openshell-tui/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_tui" +path = "src/lib.rs" + [dependencies] openshell-core = { path = "../openshell-core", default-features = false } openshell-bootstrap = { path = "../openshell-bootstrap" } diff --git a/crates/openshell-vfio/Cargo.toml b/crates/openshell-vfio/Cargo.toml index b6d7cc3cd..7752d4543 100644 --- a/crates/openshell-vfio/Cargo.toml +++ b/crates/openshell-vfio/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_vfio" +path = "src/lib.rs" + [dependencies] serde = { workspace = true } serde_json = { workspace = true } diff --git a/flake.lock b/flake.lock index 7b9881771..8de4f5362 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,20 @@ { "nodes": { + "crane": { + "locked": { + "lastModified": 1780099841, + "narHash": "sha256-EVZd2RsbpreRUDSi9rBwPY+ZxoyMaiEBbZxxhljbaS4=", + "owner": "ipetkov", + "repo": "crane", + "rev": "0532eb17955225173906d671fb36306bdeb1e2dc", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -36,6 +51,7 @@ }, "root": { "inputs": { + "crane": "crane", "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay", diff --git a/flake.nix b/flake.nix index 13c4857bc..37c782e51 100644 --- a/flake.nix +++ b/flake.nix @@ -11,6 +11,7 @@ url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; }; + crane.url = "github:ipetkov/crane"; treefmt-nix = { url = "github:numtide/treefmt-nix"; inputs.nixpkgs.follows = "nixpkgs"; @@ -22,6 +23,7 @@ flake-utils, nixpkgs, rust-overlay, + crane, treefmt-nix, ... }: @@ -32,13 +34,92 @@ inherit system; overlays = [ (import rust-overlay) ]; }; + lib = pkgs.lib; rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + + craneLib = (crane.mkLib pkgs).overrideToolchain (_: rustToolchain); + + # Crate-by-crate crane helpers (workspace graph, minimal per-crate + # source, buildWorkspaceCrate). See nix/workspace.nix. + workspace = import ./nix/workspace.nix { + inherit lib pkgs craneLib; + root = ./.; + }; + inherit (workspace) buildWorkspaceCrate; + + # z3 (found via pkg-config) and libclang (for z3-sys bindgen) are only + # needed by crates whose closure contains openshell-prover. + withZ3 = { + nativeBuildInputs = [ pkgs.pkg-config ]; + buildInputs = [ pkgs.z3 ]; + env.LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + }; + + # Each crate declares the compile-time assets its build needs — its own + # plus those of its workspace deps (proto/ arrives via openshell-core, + # providers/ via openshell-providers, registry/ via openshell-prover). + crates = { + openshell-cli = buildWorkspaceCrate ( + { + dir = "openshell-cli"; + assets = [ + ./proto + ./providers + ./crates/openshell-prover/registry + ]; + } + // withZ3 + ); + openshell-server = buildWorkspaceCrate ( + { + dir = "openshell-server"; + assets = [ + ./proto + ./providers + ./crates/openshell-prover/registry + ./crates/openshell-server/migrations + ]; + } + // withZ3 + ); + openshell-sandbox = buildWorkspaceCrate { + dir = "openshell-sandbox"; + assets = [ + ./proto + ./crates/openshell-sandbox/data + ./crates/openshell-sandbox/src/skills + ]; + }; + openshell-driver-vm = buildWorkspaceCrate { + dir = "openshell-driver-vm"; + assets = [ + ./proto + ./crates/openshell-driver-vm/scripts + ]; + }; + openshell-driver-kubernetes = buildWorkspaceCrate { + dir = "openshell-driver-kubernetes"; + assets = [ ./proto ]; + }; + openshell-driver-podman = buildWorkspaceCrate { + dir = "openshell-driver-podman"; + assets = [ ./proto ]; + }; + }; + treefmtEval = treefmt-nix.lib.evalModule pkgs { projectRootFile = "flake.nix"; programs.nixfmt.enable = true; }; in { + packages = crates // { + default = pkgs.symlinkJoin { + name = "openshell-0.0.0"; + paths = lib.attrValues crates; + }; + }; + devShells.default = pkgs.mkShell { packages = with pkgs; [ rustToolchain diff --git a/nix/workspace.nix b/nix/workspace.nix new file mode 100644 index 000000000..41f642192 --- /dev/null +++ b/nix/workspace.nix @@ -0,0 +1,158 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Crate-by-crate crane helpers for a Cargo workspace. +# +# Each crate is built from a minimal source, its own code plus that of its +# transitive workspace dependencies, and gets its own dependency cache. Crates +# outside that closure are reduced to their Cargo.toml so cargo can resolve +# the workspace without their source and without any Cargo.toml edits. +# Editing one crate never rebuilds an unrelated crate, and (because crane +# launders the source before building deps) never rebuilds any crate's +# dependency cache. +{ + lib, + pkgs, + craneLib, + # Workspace root: holds the virtual Cargo.toml, Cargo.lock and .cargo/. + root, + # Member directory, relative to root. + crateDir ? "crates", + # Version stamped onto every crate derivation. + version ? "0.0.0", +}: +let + cratesRoot = root + "/${crateDir}"; + + # Workspace dependency graph, derived from the Cargo.tomls + crateDirs = lib.attrNames (lib.filterAttrs (_: t: t == "directory") (builtins.readDir cratesRoot)); + + # Direct intra-workspace path-dependencies of a crate, as dir names. + directDeps = + dir: + let + manifest = builtins.fromTOML (builtins.readFile (cratesRoot + "/${dir}/Cargo.toml")); + in + lib.pipe (manifest.dependencies or { }) [ + lib.attrValues + (lib.filter (v: builtins.isAttrs v && v ? path)) + (map (v: baseNameOf v.path)) + (lib.filter (d: builtins.elem d crateDirs)) + ]; + + # Transitive closure of a crate within the workspace: its own dir plus every workspace dep. + closureOf = + dir: + map (e: e.key) ( + builtins.genericClosure { + startSet = [ { key = dir; } ]; + operator = e: map (key: { inherit key; }) (directDeps e.key); + } + ); + + # Every member's Cargo.toml, cargo must see all of them to resolve the + # workspace even for crates whose source we leave out. + allManifests = map (d: cratesRoot + "/${d}/Cargo.toml") crateDirs; + + # Source tree carrying the real sources of the given crate dirs, plus every + # member's Cargo.toml and the given assets. + mkSrc = + { + dirs, + assets ? [ ], + }: + lib.fileset.toSource { + inherit root; + fileset = lib.fileset.unions ( + [ + (root + "/Cargo.toml") + (root + "/Cargo.lock") + (root + "/.cargo") + ] + ++ allManifests + ++ map (d: craneLib.fileset.commonCargoSources (cratesRoot + "/${d}")) dirs + ++ assets + ); + }; + + # Build one workspace crate (pname == dir) in three cached layers. Every layer + # uses the SAME `-p ` selection, so cargo's feature unification is + # identical across them and the compiled artifacts are reusable: + # 1. crates.io deps — buildDepsOnly; immune to first-party code. + # 2. workspace-dep libs — build `-p ` with the crate's OWN source + # stubbed (real path-deps), so its libs compile with + # the crate's real feature set and get cached. + # 3. the crate itself — reuses 1 + 2; only the crate's own code recompiles. + buildWorkspaceCrate = + { + dir, + assets ? [ ], + nativeBuildInputs ? [ ], + buildInputs ? [ ], + env ? { }, + }: + let + closure = closureOf dir; + workspaceDeps = lib.filter (d: d != dir) closure; + common = { + pname = dir; + inherit + version + nativeBuildInputs + buildInputs + env + ; + strictDeps = true; + # Build only, skip the cargo test/check phase for now. + doCheck = false; + cargoExtraArgs = "--locked -p ${dir}"; + }; + + cratesDeps = craneLib.buildDepsOnly (common // { src = mkSrc { dirs = [ ]; }; }); + + mkWorkspaceLibsSrc = + let + base = mkSrc { + dirs = workspaceDeps; + inherit assets; + }; + dummyCrate = craneLib.mkDummySrc { src = cratesRoot + "/${dir}"; }; + in + pkgs.runCommandLocal "source" { } '' + cp -r ${base} $out + chmod -R u+w $out + rm -rf "$out/${crateDir}/${dir}" + cp -r ${dummyCrate} "$out/${crateDir}/${dir}" + ''; + + workspaceLibs = + if workspaceDeps == [ ] then + cratesDeps + else + craneLib.buildPackage ( + common + // { + pname = "${dir}-workspace-libs"; + src = mkWorkspaceLibsSrc; + cargoArtifacts = cratesDeps; + doInstallCargoArtifacts = true; + postInstall = '' + cargo clean --release -p ${dir} + ''; + } + ); + in + craneLib.buildPackage ( + common + // { + src = mkSrc { + dirs = closure; + inherit assets; + }; + cargoArtifacts = workspaceLibs; + } + ); +in +{ + inherit buildWorkspaceCrate; +} From 1c785fd82ecb63efd4b1b17de12654d75b7d4758 Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 2 Jun 2026 14:55:32 +0200 Subject: [PATCH 02/18] refactor(nix): centralize crate build specs --- flake.nix | 62 ++++----------------------------------------------- nix/crate.nix | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 58 deletions(-) create mode 100644 nix/crate.nix diff --git a/flake.nix b/flake.nix index 37c782e51..074aab395 100644 --- a/flake.nix +++ b/flake.nix @@ -47,65 +47,11 @@ }; inherit (workspace) buildWorkspaceCrate; - # z3 (found via pkg-config) and libclang (for z3-sys bindgen) are only - # needed by crates whose closure contains openshell-prover. - withZ3 = { - nativeBuildInputs = [ pkgs.pkg-config ]; - buildInputs = [ pkgs.z3 ]; - env.LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; - }; - - # Each crate declares the compile-time assets its build needs — its own - # plus those of its workspace deps (proto/ arrives via openshell-core, - # providers/ via openshell-providers, registry/ via openshell-prover). - crates = { - openshell-cli = buildWorkspaceCrate ( - { - dir = "openshell-cli"; - assets = [ - ./proto - ./providers - ./crates/openshell-prover/registry - ]; - } - // withZ3 - ); - openshell-server = buildWorkspaceCrate ( - { - dir = "openshell-server"; - assets = [ - ./proto - ./providers - ./crates/openshell-prover/registry - ./crates/openshell-server/migrations - ]; - } - // withZ3 - ); - openshell-sandbox = buildWorkspaceCrate { - dir = "openshell-sandbox"; - assets = [ - ./proto - ./crates/openshell-sandbox/data - ./crates/openshell-sandbox/src/skills - ]; - }; - openshell-driver-vm = buildWorkspaceCrate { - dir = "openshell-driver-vm"; - assets = [ - ./proto - ./crates/openshell-driver-vm/scripts - ]; - }; - openshell-driver-kubernetes = buildWorkspaceCrate { - dir = "openshell-driver-kubernetes"; - assets = [ ./proto ]; - }; - openshell-driver-podman = buildWorkspaceCrate { - dir = "openshell-driver-podman"; - assets = [ ./proto ]; - }; + crateSpecs = import ./nix/crate.nix { + inherit pkgs; + root = ./.; }; + crates = lib.mapAttrs (_: buildWorkspaceCrate) crateSpecs; treefmtEval = treefmt-nix.lib.evalModule pkgs { projectRootFile = "flake.nix"; diff --git a/nix/crate.nix b/nix/crate.nix new file mode 100644 index 000000000..3164d8fbb --- /dev/null +++ b/nix/crate.nix @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +{ + pkgs, + root, +}: +let + # z3 (found via pkg-config) and libclang (for z3-sys bindgen) are only needed + # by crates whose closure contains openshell-prover. + withZ3 = { + nativeBuildInputs = [ pkgs.pkg-config ]; + buildInputs = [ pkgs.z3 ]; + env.LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + }; +in +{ + # Each crate declares the compile-time assets its build needs: its own plus + # those of its workspace deps (proto/ arrives via openshell-core, providers/ + # via openshell-providers, registry/ via openshell-prover). + openshell-cli = withZ3 // { + dir = "openshell-cli"; + assets = [ + (root + "/proto") + (root + "/providers") + (root + "/crates/openshell-prover/registry") + ]; + }; + openshell-server = withZ3 // { + dir = "openshell-server"; + assets = [ + (root + "/proto") + (root + "/providers") + (root + "/crates/openshell-prover/registry") + (root + "/crates/openshell-server/migrations") + ]; + }; + openshell-sandbox = { + dir = "openshell-sandbox"; + assets = [ + (root + "/proto") + (root + "/crates/openshell-sandbox/data") + (root + "/crates/openshell-sandbox/src/skills") + ]; + }; + openshell-driver-vm = { + dir = "openshell-driver-vm"; + assets = [ + (root + "/proto") + (root + "/crates/openshell-driver-vm/scripts") + ]; + }; + openshell-driver-kubernetes = { + dir = "openshell-driver-kubernetes"; + assets = [ (root + "/proto") ]; + }; + openshell-driver-podman = { + dir = "openshell-driver-podman"; + assets = [ (root + "/proto") ]; + }; +} From 1c31a356e4c22d07987001c243387d35e36cfd4d Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 2 Jun 2026 16:04:30 +0200 Subject: [PATCH 03/18] fix(nix): provide protoc for builds --- crates/openshell-core/build.rs | 29 ++++++++++++++++++++--------- flake.nix | 2 ++ nix/crate.nix | 9 ++++++++- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/crates/openshell-core/build.rs b/crates/openshell-core/build.rs index 82ffcc38d..6740fc064 100644 --- a/crates/openshell-core/build.rs +++ b/crates/openshell-core/build.rs @@ -26,15 +26,16 @@ fn main() -> Result<(), Box> { // --- Protobuf compilation --- // Re-run when anything under proto/ changes (including newly added .proto files). println!("cargo:rerun-if-changed={PROTO_REL}"); - // Use bundled protoc from protobuf-src. The system protoc (from apt-get) - // does not bundle the well-known type includes (google/protobuf/struct.proto - // etc.), so we must use protobuf-src which ships both the binary and the - // include tree. - // SAFETY: This is run at build time in a single-threaded build script context. - // No other threads are reading environment variables concurrently. - #[allow(unsafe_code)] - unsafe { - env::set_var("PROTOC", protobuf_src::protoc()); + if env::var_os("PROTOC").is_none() && !path_has_protoc() { + // Keep non-Nix builds working without requiring users to install protoc. + // Nix builds provide protoc explicitly, so they do not rely on this + // vendored fallback. + // SAFETY: This is run at build time in a single-threaded build script context. + // No other threads are reading environment variables concurrently. + #[allow(unsafe_code)] + unsafe { + env::set_var("PROTOC", protobuf_src::protoc()); + } } let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); @@ -76,6 +77,16 @@ fn collect_proto_files(dir: &Path, out: &mut Vec) -> std::io::Result<() Ok(()) } +fn path_has_protoc() -> bool { + let Some(path) = env::var_os("PATH") else { + return false; + }; + + env::split_paths(&path) + .map(|dir| dir.join(format!("protoc{}", env::consts::EXE_SUFFIX))) + .any(|candidate| candidate.is_file()) +} + /// Derive a version string from `git describe --tags`. /// /// Implements the "guess-next-dev" convention used by the release pipeline diff --git a/flake.nix b/flake.nix index 074aab395..cf8d9ce51 100644 --- a/flake.nix +++ b/flake.nix @@ -71,6 +71,8 @@ rustToolchain # Required to find packages pkg-config + # Required for protobuf code generation. + protobuf # Required for bindgen generation. llvmPackages.libclang # system dependency for openshell-prover diff --git a/nix/crate.nix b/nix/crate.nix index 3164d8fbb..018bf6e9b 100644 --- a/nix/crate.nix +++ b/nix/crate.nix @@ -9,7 +9,10 @@ let # z3 (found via pkg-config) and libclang (for z3-sys bindgen) are only needed # by crates whose closure contains openshell-prover. withZ3 = { - nativeBuildInputs = [ pkgs.pkg-config ]; + nativeBuildInputs = [ + pkgs.pkg-config + pkgs.protobuf + ]; buildInputs = [ pkgs.z3 ]; env.LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; }; @@ -37,6 +40,7 @@ in }; openshell-sandbox = { dir = "openshell-sandbox"; + nativeBuildInputs = [ pkgs.protobuf ]; assets = [ (root + "/proto") (root + "/crates/openshell-sandbox/data") @@ -45,6 +49,7 @@ in }; openshell-driver-vm = { dir = "openshell-driver-vm"; + nativeBuildInputs = [ pkgs.protobuf ]; assets = [ (root + "/proto") (root + "/crates/openshell-driver-vm/scripts") @@ -52,10 +57,12 @@ in }; openshell-driver-kubernetes = { dir = "openshell-driver-kubernetes"; + nativeBuildInputs = [ pkgs.protobuf ]; assets = [ (root + "/proto") ]; }; openshell-driver-podman = { dir = "openshell-driver-podman"; + nativeBuildInputs = [ pkgs.protobuf ]; assets = [ (root + "/proto") ]; }; } From f77866c8e1ff3bb8977680ba6b0a6c780f8b8a3c Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 2 Jun 2026 16:36:36 +0200 Subject: [PATCH 04/18] refactor(nix): propagate workspace crate inputs --- flake.nix | 18 ++++++++--- nix/crate.nix | 81 +++++++++++++++++++++++++++++++++++------------ nix/workspace.nix | 45 ++++++++++++++++---------- 3 files changed, 103 insertions(+), 41 deletions(-) diff --git a/flake.nix b/flake.nix index cf8d9ce51..50c56474b 100644 --- a/flake.nix +++ b/flake.nix @@ -39,19 +39,29 @@ craneLib = (crane.mkLib pkgs).overrideToolchain (_: rustToolchain); + crateSpecs = import ./nix/crate.nix { + inherit pkgs; + root = ./.; + }; + # Crate-by-crate crane helpers (workspace graph, minimal per-crate # source, buildWorkspaceCrate). See nix/workspace.nix. workspace = import ./nix/workspace.nix { inherit lib pkgs craneLib; root = ./.; + inherit crateSpecs; }; inherit (workspace) buildWorkspaceCrate; - crateSpecs = import ./nix/crate.nix { - inherit pkgs; - root = ./.; + workspaceCrates = lib.mapAttrs (_: buildWorkspaceCrate) crateSpecs; + crates = { + openshell = workspaceCrates.openshell-cli.package; + openshell-gateway = workspaceCrates.openshell-server.package; + openshell-sandbox = workspaceCrates.openshell-sandbox.package; + openshell-driver-vm = workspaceCrates.openshell-driver-vm.package; + openshell-driver-kubernetes = workspaceCrates.openshell-driver-kubernetes.package; + openshell-driver-podman = workspaceCrates.openshell-driver-podman.package; }; - crates = lib.mapAttrs (_: buildWorkspaceCrate) crateSpecs; treefmtEval = treefmt-nix.lib.evalModule pkgs { projectRootFile = "flake.nix"; diff --git a/nix/crate.nix b/nix/crate.nix index 018bf6e9b..1458e7ec3 100644 --- a/nix/crate.nix +++ b/nix/crate.nix @@ -5,23 +5,15 @@ pkgs, root, }: -let - # z3 (found via pkg-config) and libclang (for z3-sys bindgen) are only needed - # by crates whose closure contains openshell-prover. - withZ3 = { - nativeBuildInputs = [ - pkgs.pkg-config - pkgs.protobuf - ]; - buildInputs = [ pkgs.z3 ]; - env.LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; - }; -in { - # Each crate declares the compile-time assets its build needs: its own plus - # those of its workspace deps (proto/ arrives via openshell-core, providers/ - # via openshell-providers, registry/ via openshell-prover). - openshell-cli = withZ3 // { + # Each crate declares the compile-time assets and build tools it needs. The + # workspace builder collects nativeBuildInputs/buildInputs/env from the + # transitive Cargo closure. + openshell-bootstrap = { + dir = "openshell-bootstrap"; + assets = [ (root + "/proto") ]; + }; + openshell-cli = { dir = "openshell-cli"; assets = [ (root + "/proto") @@ -29,7 +21,7 @@ in (root + "/crates/openshell-prover/registry") ]; }; - openshell-server = withZ3 // { + openshell-server = { dir = "openshell-server"; assets = [ (root + "/proto") @@ -38,9 +30,17 @@ in (root + "/crates/openshell-server/migrations") ]; }; + openshell-core = { + dir = "openshell-core"; + nativeBuildInputs = [ pkgs.protobuf ]; + assets = [ (root + "/proto") ]; + }; + openshell-driver-docker = { + dir = "openshell-driver-docker"; + assets = [ (root + "/proto") ]; + }; openshell-sandbox = { dir = "openshell-sandbox"; - nativeBuildInputs = [ pkgs.protobuf ]; assets = [ (root + "/proto") (root + "/crates/openshell-sandbox/data") @@ -49,7 +49,6 @@ in }; openshell-driver-vm = { dir = "openshell-driver-vm"; - nativeBuildInputs = [ pkgs.protobuf ]; assets = [ (root + "/proto") (root + "/crates/openshell-driver-vm/scripts") @@ -57,12 +56,52 @@ in }; openshell-driver-kubernetes = { dir = "openshell-driver-kubernetes"; - nativeBuildInputs = [ pkgs.protobuf ]; assets = [ (root + "/proto") ]; }; openshell-driver-podman = { dir = "openshell-driver-podman"; - nativeBuildInputs = [ pkgs.protobuf ]; assets = [ (root + "/proto") ]; }; + openshell-ocsf = { + dir = "openshell-ocsf"; + assets = [ (root + "/crates/openshell-ocsf/schemas") ]; + }; + openshell-policy = { + dir = "openshell-policy"; + assets = [ (root + "/proto") ]; + }; + openshell-prover = { + dir = "openshell-prover"; + nativeBuildInputs = [ pkgs.pkg-config ]; + buildInputs = [ pkgs.z3 ]; + env.LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + assets = [ + (root + "/crates/openshell-prover/registry") + (root + "/crates/openshell-prover/testdata") + ]; + }; + openshell-providers = { + dir = "openshell-providers"; + assets = [ + (root + "/proto") + (root + "/providers") + ]; + }; + openshell-router = { + dir = "openshell-router"; + assets = [ (root + "/proto") ]; + }; + openshell-server-macros = { + dir = "openshell-server-macros"; + }; + openshell-tui = { + dir = "openshell-tui"; + assets = [ + (root + "/proto") + (root + "/providers") + ]; + }; + openshell-vfio = { + dir = "openshell-vfio"; + }; } diff --git a/nix/workspace.nix b/nix/workspace.nix index 41f642192..4189d5a85 100644 --- a/nix/workspace.nix +++ b/nix/workspace.nix @@ -18,6 +18,8 @@ root, # Member directory, relative to root. crateDir ? "crates", + # Crate metadata keyed by workspace crate directory. + crateSpecs ? { }, # Version stamped onto every crate derivation. version ? "0.0.0", }: @@ -50,6 +52,12 @@ let } ); + specFor = dir: lib.attrByPath [ dir ] { } crateSpecs; + + closureList = closure: field: lib.concatLists (map (d: (specFor d).${field} or [ ]) closure); + + closureEnv = closure: lib.foldl' lib.recursiveUpdate { } (map (d: (specFor d).env or { }) closure); + # Every member's Cargo.toml, cargo must see all of them to resolve the # workspace even for crates whose source we leave out. allManifests = map (d: cratesRoot + "/${d}/Cargo.toml") crateDirs; @@ -94,14 +102,17 @@ let let closure = closureOf dir; workspaceDeps = lib.filter (d: d != dir) closure; + effectiveNativeBuildInputs = lib.unique ( + closureList closure "nativeBuildInputs" ++ nativeBuildInputs + ); + effectiveBuildInputs = lib.unique (closureList closure "buildInputs" ++ buildInputs); + effectiveEnv = lib.recursiveUpdate (closureEnv closure) env; common = { pname = dir; - inherit - version - nativeBuildInputs - buildInputs - env - ; + inherit version; + nativeBuildInputs = effectiveNativeBuildInputs; + buildInputs = effectiveBuildInputs; + env = effectiveEnv; strictDeps = true; # Build only, skip the cargo test/check phase for now. doCheck = false; @@ -142,16 +153,18 @@ let } ); in - craneLib.buildPackage ( - common - // { - src = mkSrc { - dirs = closure; - inherit assets; - }; - cargoArtifacts = workspaceLibs; - } - ); + { + package = craneLib.buildPackage ( + common + // { + src = mkSrc { + dirs = closure; + inherit assets; + }; + cargoArtifacts = workspaceLibs; + } + ); + }; in { inherit buildWorkspaceCrate; From 28b3b8231c391ed63ee8dc0fcf7f84c047eaedf5 Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 2 Jun 2026 17:33:21 +0200 Subject: [PATCH 05/18] test(sandbox): make unit tests hermetic --- .../src/procfs.rs | 28 ++++++++++++++----- .../openshell-supervisor-network/src/proxy.rs | 12 ++++---- .../src/child_env.rs | 4 +-- .../src/process.rs | 2 +- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/crates/openshell-supervisor-network/src/procfs.rs b/crates/openshell-supervisor-network/src/procfs.rs index 3ac8dbe14..3f2de8d34 100644 --- a/crates/openshell-supervisor-network/src/procfs.rs +++ b/crates/openshell-supervisor-network/src/procfs.rs @@ -508,6 +508,17 @@ mod tests { use super::*; use std::io::Write; + #[cfg(target_os = "linux")] + fn find_on_path(name: &str) -> PathBuf { + std::env::var_os("PATH") + .and_then(|paths| { + std::env::split_paths(&paths) + .map(|dir| dir.join(name)) + .find(|path| path.is_file()) + }) + .unwrap_or_else(|| panic!("{name} not found on PATH")) + } + /// Block until `/proc//exe` points at `target`. `Command::spawn` returns /// once the child is scheduled, not once it has completed `exec()`; on /// contended runners the readlink can still show the parent (test harness) @@ -602,10 +613,12 @@ mod tests { fn binary_path_strips_deleted_suffix() { use std::os::unix::fs::PermissionsExt; - // Copy /bin/sleep to a temp path we control so we can unlink it. + // Copy a shell to a temp path we control so we can unlink it. Nix + // coreutils dispatches by argv[0], so a copied `sleep` exits when + // renamed to these test filenames. let tmp = tempfile::TempDir::new().unwrap(); let exe_path = tmp.path().join("deleted-sleep"); - std::fs::copy("/bin/sleep", &exe_path).unwrap(); + std::fs::copy(find_on_path("sh"), &exe_path).unwrap(); std::fs::set_permissions(&exe_path, std::fs::Permissions::from_mode(0o755)).unwrap(); // Spawn a child from the temp binary, then unlink it while the @@ -613,7 +626,7 @@ mod tests { // `/proc//exe`, but readlink will now return the tainted // " (deleted)" string. let mut cmd = std::process::Command::new(&exe_path); - cmd.arg("5"); + cmd.args(["-c", "sleep 5; :"]); let mut child = spawn_retrying_on_etxtbsy(&mut cmd); let pid: i32 = child.id().cast_signed(); wait_for_child_exec(pid, &exe_path); @@ -649,6 +662,7 @@ mod tests { /// must be returned unchanged — we only strip when `stat()` reports /// the raw readlink target missing. This guards against the trusted /// identity source misattributing a running binary to a truncated + /// /// sibling path. #[cfg(target_os = "linux")] #[test] @@ -659,11 +673,11 @@ mod tests { // Basename literally ends with " (deleted)" while the file is still // on disk — a pathological but legal filename. let exe_path = tmp.path().join("sleepy (deleted)"); - std::fs::copy("/bin/sleep", &exe_path).unwrap(); + std::fs::copy(find_on_path("sh"), &exe_path).unwrap(); std::fs::set_permissions(&exe_path, std::fs::Permissions::from_mode(0o755)).unwrap(); let mut cmd = std::process::Command::new(&exe_path); - cmd.arg("5"); + cmd.args(["-c", "sleep 5; :"]); let mut child = spawn_retrying_on_etxtbsy(&mut cmd); let pid: i32 = child.id().cast_signed(); wait_for_child_exec(pid, &exe_path); @@ -702,11 +716,11 @@ mod tests { raw_name.extend_from_slice(b".bin"); let exe_path = tmp.path().join(OsString::from_vec(raw_name)); - std::fs::copy("/bin/sleep", &exe_path).unwrap(); + std::fs::copy(find_on_path("sh"), &exe_path).unwrap(); std::fs::set_permissions(&exe_path, std::fs::Permissions::from_mode(0o755)).unwrap(); let mut cmd = std::process::Command::new(&exe_path); - cmd.arg("5"); + cmd.args(["-c", "sleep 5; :"]); let mut child = spawn_retrying_on_etxtbsy(&mut cmd); let pid: i32 = child.id().cast_signed(); wait_for_child_exec(pid, &exe_path); diff --git a/crates/openshell-supervisor-network/src/proxy.rs b/crates/openshell-supervisor-network/src/proxy.rs index d467b022e..2eb3dde88 100644 --- a/crates/openshell-supervisor-network/src/proxy.rs +++ b/crates/openshell-supervisor-network/src/proxy.rs @@ -6376,9 +6376,10 @@ network_policies: #[tokio::test] async fn test_resolve_check_allowed_ips_rejects_outside_allowlist() { - // 8.8.8.8 resolves to a public IP which is NOT in 10.0.0.0/8 + // A resolved public IP outside 10.0.0.0/8 must be rejected. let nets = parse_allowed_ips(&["10.0.0.0/8".to_string()]).unwrap(); - let result = resolve_and_check_allowed_ips("dns.google", 443, &nets, 0).await; + let addrs = ["8.8.8.8:443".parse().unwrap()]; + let result = validate_allowed_ips_for_resolved_addrs("dns.google", 443, &addrs, &nets); assert!(result.is_err()); let err = result.unwrap_err(); assert!( @@ -7042,14 +7043,13 @@ network_policies: #[tokio::test] async fn test_forward_public_ip_allowed_without_allowed_ips() { - // Public IPs (e.g. dns.google -> 8.8.8.8) should pass through - // resolve_and_reject_internal without needing allowed_ips. - let result = resolve_and_reject_internal("dns.google", 80, 0).await; + // Public resolved IPs should pass through without needing allowed_ips. + let addrs = ["8.8.8.8:80".parse().unwrap()]; + let result = reject_internal_resolved_addrs("dns.google", &addrs); assert!( result.is_ok(), "Public IP should be allowed without allowed_ips: {result:?}" ); - let addrs = result.unwrap(); assert!(!addrs.is_empty(), "Should resolve to at least one address"); // All resolved addresses should be public. for addr in &addrs { diff --git a/crates/openshell-supervisor-process/src/child_env.rs b/crates/openshell-supervisor-process/src/child_env.rs index 32eecbee3..8ba836203 100644 --- a/crates/openshell-supervisor-process/src/child_env.rs +++ b/crates/openshell-supervisor-process/src/child_env.rs @@ -47,7 +47,7 @@ mod tests { #[test] fn apply_proxy_env_includes_node_proxy_opt_in_and_local_bypass() { - let mut cmd = Command::new("/usr/bin/env"); + let mut cmd = Command::new("env"); cmd.stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::null()); @@ -67,7 +67,7 @@ mod tests { #[test] fn apply_tls_env_sets_node_and_bundle_paths() { - let mut cmd = Command::new("/usr/bin/env"); + let mut cmd = Command::new("env"); cmd.stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::null()); diff --git a/crates/openshell-supervisor-process/src/process.rs b/crates/openshell-supervisor-process/src/process.rs index 9f9fe1822..d19303fba 100644 --- a/crates/openshell-supervisor-process/src/process.rs +++ b/crates/openshell-supervisor-process/src/process.rs @@ -1310,7 +1310,7 @@ mod tests { #[tokio::test] async fn inject_provider_env_sets_placeholder_values() { - let mut cmd = Command::new("/usr/bin/env"); + let mut cmd = Command::new("env"); cmd.stdin(StdStdio::null()) .stdout(StdStdio::piped()) .stderr(StdStdio::null()); From 848b98aeff9fe24bbe313dfa7f0779858e2527bd Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 2 Jun 2026 17:33:41 +0200 Subject: [PATCH 06/18] test(nix): add per-crate cargo test checks --- flake.nix | 2 ++ nix/crate.nix | 11 +++++++++++ nix/workspace.nix | 37 ++++++++++++++++++++++++++++++++----- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/flake.nix b/flake.nix index 50c56474b..91763a184 100644 --- a/flake.nix +++ b/flake.nix @@ -76,6 +76,8 @@ }; }; + checks = lib.mapAttrs' (name: crate: lib.nameValuePair "${name}-test" crate.test) workspaceCrates; + devShells.default = pkgs.mkShell { packages = with pkgs; [ rustToolchain diff --git a/nix/crate.nix b/nix/crate.nix index 1458e7ec3..3993eb599 100644 --- a/nix/crate.nix +++ b/nix/crate.nix @@ -15,6 +15,10 @@ }; openshell-cli = { dir = "openshell-cli"; + nativeCheckInputs = [ + pkgs.cacert + pkgs.git + ]; assets = [ (root + "/proto") (root + "/providers") @@ -28,7 +32,9 @@ (root + "/providers") (root + "/crates/openshell-prover/registry") (root + "/crates/openshell-server/migrations") + (root + "/deploy/rpm/gateway.toml.default") ]; + cargoTestExtraArgs = "--features test-support"; }; openshell-core = { dir = "openshell-core"; @@ -41,10 +47,15 @@ }; openshell-sandbox = { dir = "openshell-sandbox"; + nativeCheckInputs = [ + pkgs.bash + pkgs.coreutils + ]; assets = [ (root + "/proto") (root + "/crates/openshell-sandbox/data") (root + "/crates/openshell-sandbox/src/skills") + (root + "/crates/openshell-sandbox/testdata") ]; }; openshell-driver-vm = { diff --git a/nix/workspace.nix b/nix/workspace.nix index 4189d5a85..9d8e847af 100644 --- a/nix/workspace.nix +++ b/nix/workspace.nix @@ -96,8 +96,10 @@ let dir, assets ? [ ], nativeBuildInputs ? [ ], + nativeCheckInputs ? [ ], buildInputs ? [ ], env ? { }, + cargoTestExtraArgs ? "", }: let closure = closureOf dir; @@ -107,6 +109,10 @@ let ); effectiveBuildInputs = lib.unique (closureList closure "buildInputs" ++ buildInputs); effectiveEnv = lib.recursiveUpdate (closureEnv closure) env; + src = mkSrc { + dirs = closure; + inherit assets; + }; common = { pname = dir; inherit version; @@ -152,18 +158,39 @@ let ''; } ); + + cratesTestDeps = craneLib.buildPackage ( + common + // { + pname = "${dir}-test-deps"; + src = mkWorkspaceLibsSrc; + inherit nativeCheckInputs; + cargoArtifacts = workspaceLibs; + cargoExtraArgs = "${common.cargoExtraArgs} --tests ${cargoTestExtraArgs}"; + doInstallCargoArtifacts = true; + } + ); in - { + let package = craneLib.buildPackage ( common // { - src = mkSrc { - dirs = closure; - inherit assets; - }; + inherit src; cargoArtifacts = workspaceLibs; } ); + + test = craneLib.cargoTest ( + common + // { + doCheck = true; + inherit src nativeCheckInputs cargoTestExtraArgs; + cargoArtifacts = cratesTestDeps; + } + ); + in + { + inherit package test; }; in { From c86b02b750943a6d6b1f54dd65c5df8a125abcbb Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 9 Jun 2026 12:04:00 +0200 Subject: [PATCH 07/18] ci: replace branch checks with nix workflow --- .github/workflows/branch-e2e.yml | 206 --------------------- .github/workflows/e2e-label-help.yml | 82 --------- .github/workflows/helm-lint.yml | 99 ---------- .github/workflows/nix-ci.yml | 96 ++++++++++ .github/workflows/required-ci-gates.yml | 233 ------------------------ .github/workflows/rust-cache-seed.yml | 72 -------- 6 files changed, 96 insertions(+), 692 deletions(-) delete mode 100644 .github/workflows/branch-e2e.yml delete mode 100644 .github/workflows/e2e-label-help.yml delete mode 100644 .github/workflows/helm-lint.yml create mode 100644 .github/workflows/nix-ci.yml delete mode 100644 .github/workflows/required-ci-gates.yml delete mode 100644 .github/workflows/rust-cache-seed.yml diff --git a/.github/workflows/branch-e2e.yml b/.github/workflows/branch-e2e.yml deleted file mode 100644 index 8a9e7fe29..000000000 --- a/.github/workflows/branch-e2e.yml +++ /dev/null @@ -1,206 +0,0 @@ -name: Branch E2E Checks - -on: - push: - branches: - - "pull-request/[0-9]+" - workflow_dispatch: {} - -permissions: {} - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - pr_metadata: - name: Resolve PR metadata - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - outputs: - should_run: ${{ steps.gate.outputs.should_run }} - run_core_e2e: ${{ steps.labels.outputs.run_core_e2e }} - run_gpu_e2e: ${{ steps.labels.outputs.run_gpu_e2e }} - run_kubernetes_ha_e2e: ${{ steps.labels.outputs.run_kubernetes_ha_e2e }} - run_any_e2e: ${{ steps.labels.outputs.run_any_e2e }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - - id: gate - uses: ./.github/actions/pr-gate - - id: labels - if: steps.gate.outputs.should_run == 'true' - env: - EVENT_NAME: ${{ github.event_name }} - LABELS_JSON: ${{ steps.gate.outputs.labels_json }} - shell: bash - run: | - set -euo pipefail - if [ "$EVENT_NAME" != "push" ]; then - run_core_e2e=true - run_gpu_e2e=true - run_kubernetes_ha_e2e=true - else - run_core_e2e="$(jq -r 'index("test:e2e") != null' <<< "$LABELS_JSON")" - run_gpu_e2e="$(jq -r 'index("test:e2e-gpu") != null' <<< "$LABELS_JSON")" - run_kubernetes_ha_e2e="$(jq -r 'index("test:e2e-kubernetes") != null' <<< "$LABELS_JSON")" - fi - if [ "$run_core_e2e" = "true" ] || [ "$run_gpu_e2e" = "true" ] || [ "$run_kubernetes_ha_e2e" = "true" ]; then - run_any_e2e=true - else - run_any_e2e=false - fi - { - echo "run_core_e2e=$run_core_e2e" - echo "run_gpu_e2e=$run_gpu_e2e" - echo "run_kubernetes_ha_e2e=$run_kubernetes_ha_e2e" - echo "run_any_e2e=$run_any_e2e" - } >> "$GITHUB_OUTPUT" - - build-gateway: - needs: [pr_metadata] - if: needs.pr_metadata.outputs.should_run == 'true' && (needs.pr_metadata.outputs.run_core_e2e == 'true' || needs.pr_metadata.outputs.run_kubernetes_ha_e2e == 'true') - permissions: - contents: read - packages: write - uses: ./.github/workflows/docker-build.yml - with: - component: gateway - image-tag: ${{ github.sha }} - - build-supervisor: - needs: [pr_metadata] - if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_any_e2e == 'true' - permissions: - contents: read - packages: write - uses: ./.github/workflows/docker-build.yml - with: - component: supervisor - image-tag: ${{ github.sha }} - - e2e: - needs: [pr_metadata, build-gateway, build-supervisor] - if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' - permissions: - contents: read - packages: read - uses: ./.github/workflows/e2e-test.yml - with: - image-tag: ${{ github.sha }} - runner: linux-arm64-cpu8 - - gpu-e2e: - needs: [pr_metadata, build-supervisor] - if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_gpu_e2e == 'true' - permissions: - contents: read - packages: read - uses: ./.github/workflows/e2e-gpu-test.yaml - with: - image-tag: ${{ github.sha }} - - kubernetes-e2e: - needs: [pr_metadata, build-gateway, build-supervisor] - if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' - permissions: - contents: read - packages: read - uses: ./.github/workflows/e2e-kubernetes-test.yml - with: - image-tag: ${{ github.sha }} - - kubernetes-ha-e2e: - needs: [pr_metadata, build-gateway, build-supervisor] - if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_kubernetes_ha_e2e == 'true' - permissions: - contents: read - packages: read - uses: ./.github/workflows/e2e-kubernetes-test.yml - with: - image-tag: ${{ github.sha }} - job-name: Kubernetes HA E2E (Rust smoke) - extra-helm-values: deploy/helm/openshell/ci/values-high-availability.yaml - external-postgres-secret: openshell-ha-pg - - core-e2e-result: - name: Core E2E result - needs: [pr_metadata, build-gateway, build-supervisor, e2e, kubernetes-e2e] - if: always() && needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' - runs-on: ubuntu-latest - steps: - - name: Verify core E2E jobs - env: - BUILD_GATEWAY_RESULT: ${{ needs.build-gateway.result }} - BUILD_SUPERVISOR_RESULT: ${{ needs.build-supervisor.result }} - E2E_RESULT: ${{ needs.e2e.result }} - KUBERNETES_E2E_RESULT: ${{ needs.kubernetes-e2e.result }} - run: | - set -euo pipefail - failed=0 - for item in \ - "build-gateway:$BUILD_GATEWAY_RESULT" \ - "build-supervisor:$BUILD_SUPERVISOR_RESULT" \ - "e2e:$E2E_RESULT" \ - "kubernetes-e2e:$KUBERNETES_E2E_RESULT"; do - name="${item%%:*}" - result="${item#*:}" - if [ "$result" != "success" ]; then - echo "::error::$name concluded $result" - failed=1 - fi - done - exit "$failed" - - gpu-e2e-result: - name: GPU E2E result - needs: [pr_metadata, build-supervisor, gpu-e2e] - if: always() && needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_gpu_e2e == 'true' - runs-on: ubuntu-latest - steps: - - name: Verify GPU E2E jobs - env: - BUILD_SUPERVISOR_RESULT: ${{ needs.build-supervisor.result }} - GPU_E2E_RESULT: ${{ needs.gpu-e2e.result }} - run: | - set -euo pipefail - failed=0 - for item in \ - "build-supervisor:$BUILD_SUPERVISOR_RESULT" \ - "gpu-e2e:$GPU_E2E_RESULT"; do - name="${item%%:*}" - result="${item#*:}" - if [ "$result" != "success" ]; then - echo "::error::$name concluded $result" - failed=1 - fi - done - exit "$failed" - - kubernetes-ha-e2e-result: - name: Kubernetes HA E2E result - needs: [pr_metadata, build-gateway, build-supervisor, kubernetes-ha-e2e] - if: always() && needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_kubernetes_ha_e2e == 'true' - runs-on: ubuntu-latest - steps: - - name: Verify Kubernetes HA E2E jobs - env: - BUILD_GATEWAY_RESULT: ${{ needs.build-gateway.result }} - BUILD_SUPERVISOR_RESULT: ${{ needs.build-supervisor.result }} - KUBERNETES_HA_E2E_RESULT: ${{ needs.kubernetes-ha-e2e.result }} - run: | - set -euo pipefail - failed=0 - for item in \ - "build-gateway:$BUILD_GATEWAY_RESULT" \ - "build-supervisor:$BUILD_SUPERVISOR_RESULT" \ - "kubernetes-ha-e2e:$KUBERNETES_HA_E2E_RESULT"; do - name="${item%%:*}" - result="${item#*:}" - if [ "$result" != "success" ]; then - echo "::error::$name concluded $result" - failed=1 - fi - done - exit "$failed" diff --git a/.github/workflows/e2e-label-help.yml b/.github/workflows/e2e-label-help.yml deleted file mode 100644 index 1190bcd3d..000000000 --- a/.github/workflows/e2e-label-help.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: E2E Label Help - -# When an E2E label is applied, post a PR comment -# telling the maintainer the next manual step. We don't dispatch the workflow -# ourselves: a workflow_dispatch-triggered run does not surface in the PR's -# Checks tab, so we'd lose in-progress visibility. Instead we point the -# maintainer at either the existing run (re-run from the UI) or the -# `/ok to test ` command needed to refresh the mirror. -# -# Uses `pull_request_target` so forked PRs get a token capable of posting -# comments. The job never checks out PR code; it only calls the GitHub API. - -on: - pull_request_target: - types: [labeled] - -permissions: {} - -jobs: - hint: - name: Post next-step hint for E2E label - if: github.event.label.name == 'test:e2e' || github.event.label.name == 'test:e2e-gpu' || github.event.label.name == 'test:e2e-kubernetes' - runs-on: ubuntu-latest - permissions: - pull-requests: write - actions: read - contents: read - steps: - - name: Post comment - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - LABEL_NAME: ${{ github.event.label.name }} - shell: bash - run: | - set -euo pipefail - - workflow_file=branch-e2e.yml - workflow_name="Branch E2E Checks" - case "$LABEL_NAME" in - test:e2e) - suite_summary="the standard E2E suite" - build_summary="gateway and supervisor images" - status_summary="The matching required CI gate status on this PR will flip green automatically once the run finishes." - ;; - test:e2e-gpu) - suite_summary="GPU E2E" - build_summary="supervisor image" - status_summary="The matching required CI gate status on this PR will flip green automatically once the run finishes." - ;; - test:e2e-kubernetes) - suite_summary="Kubernetes HA E2E" - build_summary="gateway and supervisor images" - status_summary="This is an optional proof-of-life suite; failures are visible in the workflow run but do not publish a required CI gate status." - ;; - *) echo "Unrecognized label $LABEL_NAME"; exit 1 ;; - esac - - mirror_ref="pull-request/$PR_NUMBER" - mirror_sha=$(gh api "repos/$GH_REPO/branches/$mirror_ref" --jq '.commit.sha' 2>/dev/null || echo "") - short_pr=${PR_HEAD_SHA:0:7} - - if [ -z "$mirror_sha" ]; then - body="Label \`$LABEL_NAME\` applied, but \`$mirror_ref\` does not exist yet. A maintainer needs to comment \`/ok to test $PR_HEAD_SHA\` to mirror this PR. Once the mirror exists, re-apply the label or re-run [$workflow_name](https://github.com/$GH_REPO/actions/workflows/$workflow_file) from the Actions tab." - elif [ "$mirror_sha" != "$PR_HEAD_SHA" ]; then - short_mirror=${mirror_sha:0:7} - body="Label \`$LABEL_NAME\` applied, but \`$mirror_ref\` is at \`$short_mirror\` while the PR head is \`$short_pr\`. A maintainer needs to comment \`/ok to test $PR_HEAD_SHA\` to refresh the mirror. Once the mirror catches up, re-run [$workflow_name](https://github.com/$GH_REPO/actions/workflows/$workflow_file) from the Actions tab." - else - run_id=$(gh api "repos/$GH_REPO/actions/workflows/$workflow_file/runs?head_sha=$PR_HEAD_SHA&event=push" \ - --jq '.workflow_runs | sort_by(.created_at) | reverse | .[0].id // empty') - if [ -n "$run_id" ]; then - instructions="Open [the existing run](https://github.com/$GH_REPO/actions/runs/$run_id) and click **Re-run all jobs** to execute with the label set." - else - workflow_link="[$workflow_name](https://github.com/$GH_REPO/actions/workflows/$workflow_file)" - instructions="Open $workflow_link, find the run for commit \`$short_pr\`, and click **Re-run all jobs** to execute with the label set." - fi - body="Label \`$LABEL_NAME\` applied for \`$short_pr\`. $instructions The run will execute $suite_summary after building the required $build_summary once. $status_summary" - fi - - gh pr comment "$PR_NUMBER" --body "$body" diff --git a/.github/workflows/helm-lint.yml b/.github/workflows/helm-lint.yml deleted file mode 100644 index 4c60b2181..000000000 --- a/.github/workflows/helm-lint.yml +++ /dev/null @@ -1,99 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -name: Helm Lint - -on: - push: - branches: - - "pull-request/[0-9]+" - workflow_dispatch: - -env: - MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -permissions: - contents: read - packages: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - pr_metadata: - name: Resolve PR metadata - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - outputs: - should_run: ${{ steps.gate.outputs.should_run }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - - - id: gate - uses: ./.github/actions/pr-gate - - helm_changes: - name: Detect Helm changes - needs: pr_metadata - if: needs.pr_metadata.outputs.should_run == 'true' - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - outputs: - should_run: ${{ steps.default.outputs.should_run || steps.changes.outputs.any_changed }} - steps: - - id: default - if: github.event_name != 'push' - shell: bash - run: echo "should_run=true" >> "$GITHUB_OUTPUT" - - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - if: github.event_name == 'push' - - - id: merge-base - if: github.event_name == 'push' - uses: ./.github/actions/pr-merge-base - with: - gh_token: ${{ secrets.GITHUB_TOKEN }} - - - id: changes - if: github.event_name == 'push' - uses: tj-actions/changed-files@aa08304bd477b800d468db44fe10f6c61f7f7b11 # v42.1.0 - with: - base_sha: ${{ steps.merge-base.outputs.base_sha }} - skip_initial_fetch: ${{ steps.merge-base.outputs.base_sha != '' }} - files: | - deploy/helm/** - mise.toml - mise.lock - tasks/helm.toml - .github/workflows/helm-lint.yml - - helm-lint: - name: Helm Lint - needs: [pr_metadata, helm_changes] - if: needs.pr_metadata.outputs.should_run == 'true' && needs.helm_changes.outputs.should_run == 'true' - runs-on: linux-amd64-cpu8 - container: - image: ghcr.io/nvidia/openshell/ci:latest - credentials: - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - - - name: Install tools - run: mise install --locked - - - name: Lint Helm chart - run: mise run helm:lint - - - name: Check Helm chart README - run: mise run helm:docs:check - - - name: Run Helm chart unit tests - run: mise run helm:test diff --git a/.github/workflows/nix-ci.yml b/.github/workflows/nix-ci.yml new file mode 100644 index 000000000..1110e0e4b --- /dev/null +++ b/.github/workflows/nix-ci.yml @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Nix CI + +on: + push: + branches: + - main + - "pull-request/[0-9]+" + workflow_dispatch: + +permissions: + contents: read + id-token: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + build: + name: Build ${{ matrix.package }} (${{ matrix.target.system }}) + runs-on: ${{ matrix.target.runner }} + strategy: + fail-fast: false + matrix: + target: + - system: x86_64-linux + runner: linux-amd64-cpu8 + - system: aarch64-linux + runner: linux-arm64-cpu8 + package: + - openshell + - openshell-gateway + - openshell-sandbox + - openshell-driver-kubernetes + - openshell-driver-podman + - openshell-driver-vm + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Enable Magic Nix Cache + uses: DeterminateSystems/magic-nix-cache-action@main + + - name: Build package + run: nix build ".#packages.${{ matrix.target.system }}.${{ matrix.package }}" --no-link --print-build-logs --no-update-lock-file + + checks: + name: Check ${{ matrix.check }} (${{ matrix.target.system }}) + needs: build + runs-on: ${{ matrix.target.runner }} + strategy: + fail-fast: false + matrix: + target: + - system: x86_64-linux + runner: linux-amd64-cpu8 + - system: aarch64-linux + runner: linux-arm64-cpu8 + check: + - openshell-bootstrap-test + - openshell-cli-test + - openshell-core-test + - openshell-driver-docker-test + - openshell-driver-kubernetes-test + - openshell-driver-podman-test + - openshell-driver-vm-test + - openshell-ocsf-test + - openshell-policy-test + - openshell-prover-test + - openshell-providers-test + - openshell-router-test + - openshell-sandbox-test + - openshell-server-macros-test + - openshell-server-test + - openshell-tui-test + - openshell-vfio-test + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Enable Magic Nix Cache + uses: DeterminateSystems/magic-nix-cache-action@main + + - name: Build check + run: nix build ".#checks.${{ matrix.target.system }}.${{ matrix.check }}" --no-link --print-build-logs --no-update-lock-file diff --git a/.github/workflows/required-ci-gates.yml b/.github/workflows/required-ci-gates.yml deleted file mode 100644 index ca068cf5c..000000000 --- a/.github/workflows/required-ci-gates.yml +++ /dev/null @@ -1,233 +0,0 @@ -name: Required CI Gates - -on: - pull_request_target: - types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] - workflow_run: - workflows: - - Branch Checks - - Branch E2E Checks - - Helm Lint - types: [completed] - -permissions: - actions: read - contents: read - pull-requests: read - statuses: write - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.workflow_run.head_sha || github.run_id }} - cancel-in-progress: true - -jobs: - publish: - name: Publish required CI gate statuses - runs-on: ubuntu-latest - steps: - - name: Evaluate required CI gates - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - EVENT_NAME: ${{ github.event_name }} - PR_NUMBER_FROM_EVENT: ${{ github.event.pull_request.number }} - PR_HEAD_SHA_FROM_EVENT: ${{ github.event.pull_request.head.sha }} - PR_LABELS_FROM_EVENT: ${{ toJSON(github.event.pull_request.labels.*.name) }} - WORKFLOW_RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} - WORKFLOW_RUN_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} - WORKFLOW_RUN_EVENT: ${{ github.event.workflow_run.event }} - shell: bash - run: | - set -euo pipefail - - post_status() { - local context="$1" - local state="$2" - local description="$3" - local target_url="${4:-}" - - args=( - --method POST - "repos/$GH_REPO/statuses/$HEAD_SHA" - -f "state=$state" - -f "context=$context" - -f "description=$description" - ) - if [ -n "$target_url" ]; then - args+=(-f "target_url=$target_url") - fi - - echo "$context: $state - $description" - gh api "${args[@]}" >/dev/null - } - - has_label() { - local label="$1" - jq -e --arg label "$label" 'index($label) != null' <<< "$LABELS_JSON" >/dev/null - } - - resolve_pull_request_event() { - PR_NUMBER="$PR_NUMBER_FROM_EVENT" - HEAD_SHA="$PR_HEAD_SHA_FROM_EVENT" - LABELS_JSON=$(jq -c . <<< "$PR_LABELS_FROM_EVENT") - } - - load_pr_context() { - PR_NUMBER="$1" - - local pr state - pr=$(gh api "repos/$GH_REPO/pulls/$PR_NUMBER") - state=$(jq -r '.state' <<< "$pr") - if [ "$state" != "open" ]; then - echo "PR #$PR_NUMBER is $state; nothing to publish." - exit 0 - fi - - HEAD_SHA=$(jq -r '.head.sha' <<< "$pr") - LABELS_JSON=$(gh api "repos/$GH_REPO/issues/$PR_NUMBER" --jq '[.labels[].name]') - } - - resolve_workflow_run_event() { - if [ "$WORKFLOW_RUN_EVENT" != "push" ]; then - echo "Ignoring workflow_run from event '$WORKFLOW_RUN_EVENT'." - exit 0 - fi - - if [[ "$WORKFLOW_RUN_HEAD_BRANCH" =~ ^pull-request/([0-9]+)$ ]]; then - load_pr_context "${BASH_REMATCH[1]}" - return - fi - - local associated_prs pr - associated_prs=$(gh api "repos/$GH_REPO/commits/$WORKFLOW_RUN_HEAD_SHA/pulls") - pr=$(jq -c 'map(select(.state == "open"))[0] // empty' <<< "$associated_prs") - if [ -z "$pr" ]; then - echo "No open PR associated with $WORKFLOW_RUN_HEAD_SHA; nothing to publish." - exit 0 - fi - - load_pr_context "$(jq -r '.number' <<< "$pr")" - } - - resolve_context() { - if [ "$EVENT_NAME" = "pull_request_target" ]; then - resolve_pull_request_event - elif [ "$EVENT_NAME" = "workflow_run" ]; then - resolve_workflow_run_event - else - echo "Unsupported event '$EVENT_NAME'." - exit 1 - fi - - PR_URL="https://github.com/$GH_REPO/pull/$PR_NUMBER" - MIRROR_REF="pull-request/$PR_NUMBER" - } - - verify_mirror() { - local context="$1" - local mirror_sha - - mirror_sha=$(gh api "repos/$GH_REPO/branches/$MIRROR_REF" --jq '.commit.sha' 2>/dev/null || true) - if [ -z "$mirror_sha" ]; then - post_status "$context" pending "Waiting for /ok to test mirror" "$PR_URL" - return 1 - fi - - if [ "$mirror_sha" != "$HEAD_SHA" ]; then - post_status "$context" pending "Waiting for /ok to test mirror" "$PR_URL" - return 1 - fi - - return 0 - } - - evaluate_workflow() { - local context="$1" - local workflow_file="$2" - local workflow_name="$3" - local required_label="${4:-}" - local required_job_name="${5:-}" - local workflow_url="https://github.com/$GH_REPO/actions/workflows/$workflow_file" - - if [ -n "$required_label" ] && ! has_label "$required_label"; then - post_status "$context" success "$required_label not applied" "$PR_URL" - return 0 - fi - - if ! verify_mirror "$context"; then - return 0 - fi - - local runs latest run_id status conclusion run_url real_success - runs=$(gh api "repos/$GH_REPO/actions/workflows/$workflow_file/runs?head_sha=$HEAD_SHA&event=push" --jq '.workflow_runs') - latest=$(jq -c --arg branch "$MIRROR_REF" '[.[] | select(.head_branch == $branch)] | sort_by(.created_at) | reverse | .[0] // empty' <<< "$runs") - - if [ -z "$latest" ]; then - post_status "$context" pending "Waiting for $workflow_name" "$workflow_url" - return 0 - fi - - run_id=$(jq -r '.id' <<< "$latest") - status=$(jq -r '.status' <<< "$latest") - conclusion=$(jq -r '.conclusion' <<< "$latest") - run_url=$(jq -r '.html_url' <<< "$latest") - - if [ "$status" != "completed" ]; then - post_status "$context" pending "$workflow_name is $status" "$run_url" - return 0 - fi - - if [ -n "$required_job_name" ]; then - local jobs required_job job_status job_conclusion - jobs=$(gh api "repos/$GH_REPO/actions/runs/$run_id/jobs?per_page=100" --jq '.jobs') - required_job=$(jq -c --arg name "$required_job_name" '[.[] | select(.name == $name)] | .[0] // empty' <<< "$jobs") - - if [ -z "$required_job" ]; then - if [ "$conclusion" = "success" ]; then - post_status "$context" pending "Waiting for $required_job_name" "$run_url" - else - post_status "$context" failure "$required_job_name did not run" "$run_url" - fi - return 0 - fi - - job_status=$(jq -r '.status' <<< "$required_job") - job_conclusion=$(jq -r '.conclusion' <<< "$required_job") - - if [ "$job_status" != "completed" ]; then - post_status "$context" pending "$required_job_name is $job_status" "$run_url" - return 0 - fi - - if [ "$job_conclusion" = "success" ]; then - post_status "$context" success "$required_job_name passed" "$run_url" - elif [ "$job_conclusion" = "skipped" ] && [ "$conclusion" = "success" ]; then - post_status "$context" pending "Waiting for $required_job_name" "$run_url" - else - post_status "$context" failure "$required_job_name concluded $job_conclusion" "$run_url" - fi - return 0 - fi - - if [ "$conclusion" != "success" ]; then - post_status "$context" failure "$workflow_name concluded $conclusion" "$run_url" - return 0 - fi - - real_success=$(gh api "repos/$GH_REPO/actions/runs/$run_id/jobs?per_page=100" \ - --jq '[.jobs[] | select(.conclusion == "success" and .name != "Resolve PR metadata")] | length') - - if [ "$real_success" -lt 1 ]; then - post_status "$context" failure "No real CI jobs ran" "$run_url" - return 0 - fi - - post_status "$context" success "$workflow_name passed" "$run_url" - } - - resolve_context - - evaluate_workflow "OpenShell / Branch Checks" "branch-checks.yml" "Branch Checks" - evaluate_workflow "OpenShell / E2E" "branch-e2e.yml" "Branch E2E Checks" "test:e2e" "Core E2E result" - evaluate_workflow "OpenShell / GPU E2E" "branch-e2e.yml" "Branch E2E Checks" "test:e2e-gpu" "GPU E2E result" - evaluate_workflow "OpenShell / Helm Lint" "helm-lint.yml" "Helm Lint" diff --git a/.github/workflows/rust-cache-seed.yml b/.github/workflows/rust-cache-seed.yml deleted file mode 100644 index 741e4c434..000000000 --- a/.github/workflows/rust-cache-seed.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: Rust Cache Seed - -on: - push: - branches: - - main - workflow_dispatch: - -env: - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: "0" - MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SCCACHE_GHA_ENABLED: "true" - -permissions: - contents: read - packages: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - rust: - name: Rust (${{ matrix.runner }}) - strategy: - fail-fast: false - matrix: - runner: [linux-amd64-cpu8, linux-arm64-cpu8] - runs-on: ${{ matrix.runner }} - env: - SCCACHE_GHA_VERSION: branch-checks-rust-${{ matrix.runner }} - container: - image: ghcr.io/nvidia/openshell/ci:latest - credentials: - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - - - name: Install tools - run: mise install --locked - - - name: Configure GHA sccache backend - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - - - name: Cache Rust target and registry - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2 - with: - shared-key: rust-checks-${{ matrix.runner }} - cache-on-failure: true - - - name: Format - run: mise run rust:format:check - - - name: Lint - run: mise run rust:lint - - - name: Test - run: mise run test:rust - - - name: sccache stats - if: always() - run: | - set +e - stats_bin="${SCCACHE_PATH:-sccache}" - "$stats_bin" --show-stats - status=$? - if [ "$status" -ne 0 ]; then - echo "::warning::sccache stats unavailable (exit $status)" - fi - exit 0 From 19b1b67ca3703fd358c5a96dc765a649fe433e54 Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 9 Jun 2026 12:15:50 +0200 Subject: [PATCH 08/18] ci: add nix rustfmt lint check --- .github/workflows/nix-ci.yml | 32 +++++++++++++++++++++++++++++--- flake.nix | 10 +++++++++- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/.github/workflows/nix-ci.yml b/.github/workflows/nix-ci.yml index 1110e0e4b..fe653b5e8 100644 --- a/.github/workflows/nix-ci.yml +++ b/.github/workflows/nix-ci.yml @@ -53,8 +53,8 @@ jobs: - name: Build package run: nix build ".#packages.${{ matrix.target.system }}.${{ matrix.package }}" --no-link --print-build-logs --no-update-lock-file - checks: - name: Check ${{ matrix.check }} (${{ matrix.target.system }}) + test: + name: Test ${{ matrix.check }} (${{ matrix.target.system }}) needs: build runs-on: ${{ matrix.target.runner }} strategy: @@ -92,5 +92,31 @@ jobs: - name: Enable Magic Nix Cache uses: DeterminateSystems/magic-nix-cache-action@main - - name: Build check + - name: Run test run: nix build ".#checks.${{ matrix.target.system }}.${{ matrix.check }}" --no-link --print-build-logs --no-update-lock-file + + lint: + name: Lint ${{ matrix.lint }} (${{ matrix.target.system }}) + needs: build + runs-on: ${{ matrix.target.runner }} + strategy: + fail-fast: false + matrix: + target: + - system: x86_64-linux + runner: linux-amd64-cpu8 + - system: aarch64-linux + runner: linux-arm64-cpu8 + lint: + - rustfmt + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Enable Magic Nix Cache + uses: DeterminateSystems/magic-nix-cache-action@main + + - name: Run lint + run: nix build ".#checks.${{ matrix.target.system }}.${{ matrix.lint }}" --no-link --print-build-logs --no-update-lock-file diff --git a/flake.nix b/flake.nix index 91763a184..f30308ebf 100644 --- a/flake.nix +++ b/flake.nix @@ -76,7 +76,15 @@ }; }; - checks = lib.mapAttrs' (name: crate: lib.nameValuePair "${name}-test" crate.test) workspaceCrates; + checks = + lib.mapAttrs' (name: crate: lib.nameValuePair "${name}-test" crate.test) workspaceCrates + // { + rustfmt = craneLib.cargoFmt { + pname = "openshell-workspace"; + src = craneLib.cleanCargoSource ./.; + cargoExtraArgs = "--all"; + }; + }; devShells.default = pkgs.mkShell { packages = with pkgs; [ From c03df86596815f4049fcc00010a2d3b0012d8cef Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 9 Jun 2026 12:30:15 +0200 Subject: [PATCH 09/18] ci: add nix clippy lint checks --- .github/workflows/nix-ci.yml | 23 ++++++++++++++++++++--- flake.nix | 10 +++++++++- nix/workspace.nix | 12 +++++++++++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/.github/workflows/nix-ci.yml b/.github/workflows/nix-ci.yml index fe653b5e8..fcb8d0323 100644 --- a/.github/workflows/nix-ci.yml +++ b/.github/workflows/nix-ci.yml @@ -51,7 +51,7 @@ jobs: uses: DeterminateSystems/magic-nix-cache-action@main - name: Build package - run: nix build ".#packages.${{ matrix.target.system }}.${{ matrix.package }}" --no-link --print-build-logs --no-update-lock-file + run: nix build ".#packages.${{ matrix.target.system }}.${{ matrix.package }}" --no-link --no-update-lock-file test: name: Test ${{ matrix.check }} (${{ matrix.target.system }}) @@ -93,7 +93,7 @@ jobs: uses: DeterminateSystems/magic-nix-cache-action@main - name: Run test - run: nix build ".#checks.${{ matrix.target.system }}.${{ matrix.check }}" --no-link --print-build-logs --no-update-lock-file + run: nix build ".#checks.${{ matrix.target.system }}.${{ matrix.check }}" --no-link --no-update-lock-file lint: name: Lint ${{ matrix.lint }} (${{ matrix.target.system }}) @@ -109,6 +109,23 @@ jobs: runner: linux-arm64-cpu8 lint: - rustfmt + - openshell-bootstrap-clippy + - openshell-cli-clippy + - openshell-core-clippy + - openshell-driver-docker-clippy + - openshell-driver-kubernetes-clippy + - openshell-driver-podman-clippy + - openshell-driver-vm-clippy + - openshell-ocsf-clippy + - openshell-policy-clippy + - openshell-prover-clippy + - openshell-providers-clippy + - openshell-router-clippy + - openshell-sandbox-clippy + - openshell-server-macros-clippy + - openshell-server-clippy + - openshell-tui-clippy + - openshell-vfio-clippy steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 @@ -119,4 +136,4 @@ jobs: uses: DeterminateSystems/magic-nix-cache-action@main - name: Run lint - run: nix build ".#checks.${{ matrix.target.system }}.${{ matrix.lint }}" --no-link --print-build-logs --no-update-lock-file + run: nix build ".#checks.${{ matrix.target.system }}.${{ matrix.lint }}" --no-link --no-update-lock-file diff --git a/flake.nix b/flake.nix index f30308ebf..aa7b1cb20 100644 --- a/flake.nix +++ b/flake.nix @@ -63,6 +63,13 @@ openshell-driver-podman = workspaceCrates.openshell-driver-podman.package; }; + crateTests = lib.mapAttrs' ( + name: crate: lib.nameValuePair "${name}-test" crate.test + ) workspaceCrates; + crateClippy = lib.mapAttrs' ( + name: crate: lib.nameValuePair "${name}-clippy" crate.clippy + ) workspaceCrates; + treefmtEval = treefmt-nix.lib.evalModule pkgs { projectRootFile = "flake.nix"; programs.nixfmt.enable = true; @@ -77,7 +84,8 @@ }; checks = - lib.mapAttrs' (name: crate: lib.nameValuePair "${name}-test" crate.test) workspaceCrates + crateTests + // crateClippy // { rustfmt = craneLib.cargoFmt { pname = "openshell-workspace"; diff --git a/nix/workspace.nix b/nix/workspace.nix index 9d8e847af..dce48ceea 100644 --- a/nix/workspace.nix +++ b/nix/workspace.nix @@ -188,9 +188,19 @@ let cargoArtifacts = cratesTestDeps; } ); + + clippy = craneLib.cargoClippy ( + common + // { + inherit src; + nativeBuildInputs = lib.unique (effectiveNativeBuildInputs ++ nativeCheckInputs); + cargoArtifacts = cratesTestDeps; + cargoClippyExtraArgs = "--all-targets -- -D warnings"; + } + ); in { - inherit package test; + inherit package test clippy; }; in { From a98c4d2fcaca3805c398501e47b5aa7d6006d29b Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 9 Jun 2026 14:39:07 +0200 Subject: [PATCH 10/18] ci: upload nix outputs to cachix --- .github/workflows/nix-ci.yml | 52 +++++++++++++++++++++++------------- flake.nix | 9 +++++++ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/.github/workflows/nix-ci.yml b/.github/workflows/nix-ci.yml index fcb8d0323..b687141a1 100644 --- a/.github/workflows/nix-ci.yml +++ b/.github/workflows/nix-ci.yml @@ -12,7 +12,6 @@ on: permissions: contents: read - id-token: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -46,12 +45,17 @@ jobs: - name: Install Nix uses: DeterminateSystems/nix-installer-action@main - - - name: Enable Magic Nix Cache - uses: DeterminateSystems/magic-nix-cache-action@main - - - name: Build package - run: nix build ".#packages.${{ matrix.target.system }}.${{ matrix.package }}" --no-link --no-update-lock-file + with: + extra-conf: | + accept-flake-config = true + + - name: Build and upload package + env: + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + run: | + : "${CACHIX_AUTH_TOKEN:?CACHIX_AUTH_TOKEN is required to upload to Cachix}" + nix run --inputs-from . nixpkgs#cachix -- watch-exec openshell -- \ + nix build ".#packages.${{ matrix.target.system }}.${{ matrix.package }}" --no-link --no-update-lock-file test: name: Test ${{ matrix.check }} (${{ matrix.target.system }}) @@ -88,12 +92,17 @@ jobs: - name: Install Nix uses: DeterminateSystems/nix-installer-action@main - - - name: Enable Magic Nix Cache - uses: DeterminateSystems/magic-nix-cache-action@main - - - name: Run test - run: nix build ".#checks.${{ matrix.target.system }}.${{ matrix.check }}" --no-link --no-update-lock-file + with: + extra-conf: | + accept-flake-config = true + + - name: Run and upload test + env: + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + run: | + : "${CACHIX_AUTH_TOKEN:?CACHIX_AUTH_TOKEN is required to upload to Cachix}" + nix run --inputs-from . nixpkgs#cachix -- watch-exec openshell -- \ + nix build ".#checks.${{ matrix.target.system }}.${{ matrix.check }}" --no-link --no-update-lock-file lint: name: Lint ${{ matrix.lint }} (${{ matrix.target.system }}) @@ -131,9 +140,14 @@ jobs: - name: Install Nix uses: DeterminateSystems/nix-installer-action@main - - - name: Enable Magic Nix Cache - uses: DeterminateSystems/magic-nix-cache-action@main - - - name: Run lint - run: nix build ".#checks.${{ matrix.target.system }}.${{ matrix.lint }}" --no-link --no-update-lock-file + with: + extra-conf: | + accept-flake-config = true + + - name: Run and upload lint + env: + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + run: | + : "${CACHIX_AUTH_TOKEN:?CACHIX_AUTH_TOKEN is required to upload to Cachix}" + nix run --inputs-from . nixpkgs#cachix -- watch-exec openshell -- \ + nix build ".#checks.${{ matrix.target.system }}.${{ matrix.lint }}" --no-link --no-update-lock-file diff --git a/flake.nix b/flake.nix index aa7b1cb20..73478f3fe 100644 --- a/flake.nix +++ b/flake.nix @@ -4,6 +4,13 @@ { description = "OpenShell development environment"; + nixConfig = { + extra-substituters = [ "https://openshell.cachix.org" ]; + extra-trusted-public-keys = [ + "openshell.cachix.org-1:OAr5MunsfH5PZvUsfD08OtGx5RtcwdNZGJdU5FqLm5w=" + ]; + }; + inputs = { flake-utils.url = "github:numtide/flake-utils"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; @@ -105,6 +112,8 @@ llvmPackages.libclang # system dependency for openshell-prover z3 + # caching utility + cachix ]; env = { From 543b4b0d10024083acc288c1836eb87815374691 Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 9 Jun 2026 15:08:52 +0200 Subject: [PATCH 11/18] ci: use cachix action for nix cache --- .github/workflows/nix-ci.yml | 49 ++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/.github/workflows/nix-ci.yml b/.github/workflows/nix-ci.yml index b687141a1..e3ddd9bf0 100644 --- a/.github/workflows/nix-ci.yml +++ b/.github/workflows/nix-ci.yml @@ -49,13 +49,15 @@ jobs: extra-conf: | accept-flake-config = true - - name: Build and upload package - env: - CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} - run: | - : "${CACHIX_AUTH_TOKEN:?CACHIX_AUTH_TOKEN is required to upload to Cachix}" - nix run --inputs-from . nixpkgs#cachix -- watch-exec openshell -- \ - nix build ".#packages.${{ matrix.target.system }}.${{ matrix.package }}" --no-link --no-update-lock-file + - name: Set up Cachix + uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17 + with: + name: openshell + authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} + skipAddingSubstituter: true + + - name: Build package + run: nix build ".#packages.${{ matrix.target.system }}.${{ matrix.package }}" --no-link --no-update-lock-file test: name: Test ${{ matrix.check }} (${{ matrix.target.system }}) @@ -96,17 +98,18 @@ jobs: extra-conf: | accept-flake-config = true - - name: Run and upload test - env: - CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} - run: | - : "${CACHIX_AUTH_TOKEN:?CACHIX_AUTH_TOKEN is required to upload to Cachix}" - nix run --inputs-from . nixpkgs#cachix -- watch-exec openshell -- \ - nix build ".#checks.${{ matrix.target.system }}.${{ matrix.check }}" --no-link --no-update-lock-file + - name: Set up Cachix + uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17 + with: + name: openshell + authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} + skipAddingSubstituter: true + + - name: Run test + run: nix build ".#checks.${{ matrix.target.system }}.${{ matrix.check }}" --no-link --no-update-lock-file lint: name: Lint ${{ matrix.lint }} (${{ matrix.target.system }}) - needs: build runs-on: ${{ matrix.target.runner }} strategy: fail-fast: false @@ -144,10 +147,12 @@ jobs: extra-conf: | accept-flake-config = true - - name: Run and upload lint - env: - CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} - run: | - : "${CACHIX_AUTH_TOKEN:?CACHIX_AUTH_TOKEN is required to upload to Cachix}" - nix run --inputs-from . nixpkgs#cachix -- watch-exec openshell -- \ - nix build ".#checks.${{ matrix.target.system }}.${{ matrix.lint }}" --no-link --no-update-lock-file + - name: Set up Cachix + uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17 + with: + name: openshell + authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} + skipAddingSubstituter: true + + - name: Run lint + run: nix build ".#checks.${{ matrix.target.system }}.${{ matrix.lint }}" --no-link --no-update-lock-file From 66cc377624a793627dbd844a5f90baa1d75c006b Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 9 Jun 2026 15:37:04 +0200 Subject: [PATCH 12/18] ci: deduplicate nix workflow setup --- .github/workflows/nix-ci.yml | 57 ++++++++++-------------------------- 1 file changed, 15 insertions(+), 42 deletions(-) diff --git a/.github/workflows/nix-ci.yml b/.github/workflows/nix-ci.yml index e3ddd9bf0..cb753a678 100644 --- a/.github/workflows/nix-ci.yml +++ b/.github/workflows/nix-ci.yml @@ -28,7 +28,7 @@ jobs: strategy: fail-fast: false matrix: - target: + target: &targets - system: x86_64-linux runner: linux-amd64-cpu8 - system: aarch64-linux @@ -41,15 +41,18 @@ jobs: - openshell-driver-podman - openshell-driver-vm steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - &checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Install Nix + - &install-nix + name: Install Nix uses: DeterminateSystems/nix-installer-action@main with: extra-conf: | accept-flake-config = true - - name: Set up Cachix + - &setup-cachix + name: Set up Cachix uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17 with: name: openshell @@ -66,11 +69,7 @@ jobs: strategy: fail-fast: false matrix: - target: - - system: x86_64-linux - runner: linux-amd64-cpu8 - - system: aarch64-linux - runner: linux-arm64-cpu8 + target: *targets check: - openshell-bootstrap-test - openshell-cli-test @@ -90,20 +89,9 @@ jobs: - openshell-tui-test - openshell-vfio-test steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@main - with: - extra-conf: | - accept-flake-config = true - - - name: Set up Cachix - uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17 - with: - name: openshell - authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - skipAddingSubstituter: true + - *checkout + - *install-nix + - *setup-cachix - name: Run test run: nix build ".#checks.${{ matrix.target.system }}.${{ matrix.check }}" --no-link --no-update-lock-file @@ -114,11 +102,7 @@ jobs: strategy: fail-fast: false matrix: - target: - - system: x86_64-linux - runner: linux-amd64-cpu8 - - system: aarch64-linux - runner: linux-arm64-cpu8 + target: *targets lint: - rustfmt - openshell-bootstrap-clippy @@ -139,20 +123,9 @@ jobs: - openshell-tui-clippy - openshell-vfio-clippy steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@main - with: - extra-conf: | - accept-flake-config = true - - - name: Set up Cachix - uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17 - with: - name: openshell - authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - skipAddingSubstituter: true + - *checkout + - *install-nix + - *setup-cachix - name: Run lint run: nix build ".#checks.${{ matrix.target.system }}.${{ matrix.lint }}" --no-link --no-update-lock-file From 86ae2f377b4632f7aa355826c59b62c4017720db Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 9 Jun 2026 15:58:04 +0200 Subject: [PATCH 13/18] ci: factor nix build workflow action --- .github/actions/setup-nix/action.yml | 36 ++++++++++++++++ .github/workflows/nix-ci.yml | 61 +++++++++++++++------------- 2 files changed, 68 insertions(+), 29 deletions(-) create mode 100644 .github/actions/setup-nix/action.yml diff --git a/.github/actions/setup-nix/action.yml b/.github/actions/setup-nix/action.yml new file mode 100644 index 000000000..db8c77fed --- /dev/null +++ b/.github/actions/setup-nix/action.yml @@ -0,0 +1,36 @@ +name: Setup Nix +description: Install Nix, configure Cachix, and build a flake target. + +inputs: + build: + description: Flake output namespace, such as packages or checks. + required: true + system: + description: Nix system to build for. + required: true + target: + description: Package or check target to build. + required: true + cachix_auth_token: + description: Cachix write token. + required: true + +runs: + using: composite + steps: + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + with: + extra-conf: | + accept-flake-config = true + + - name: Set up Cachix + uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17 + with: + name: openshell + authToken: ${{ inputs.cachix_auth_token }} + skipAddingSubstituter: true + + - name: Build target + shell: bash + run: nix build ".#${{ inputs.build }}.${{ inputs.system }}.${{ inputs.target }}" --no-link --no-update-lock-file diff --git a/.github/workflows/nix-ci.yml b/.github/workflows/nix-ci.yml index cb753a678..eb6d89349 100644 --- a/.github/workflows/nix-ci.yml +++ b/.github/workflows/nix-ci.yml @@ -28,7 +28,7 @@ jobs: strategy: fail-fast: false matrix: - target: &targets + target: - system: x86_64-linux runner: linux-amd64-cpu8 - system: aarch64-linux @@ -41,26 +41,15 @@ jobs: - openshell-driver-podman - openshell-driver-vm steps: - - &checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - &install-nix - name: Install Nix - uses: DeterminateSystems/nix-installer-action@main - with: - extra-conf: | - accept-flake-config = true - - - &setup-cachix - name: Set up Cachix - uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17 - with: - name: openshell - authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - skipAddingSubstituter: true + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Build package - run: nix build ".#packages.${{ matrix.target.system }}.${{ matrix.package }}" --no-link --no-update-lock-file + uses: ./.github/actions/setup-nix + with: + build: packages + system: ${{ matrix.target.system }} + target: ${{ matrix.package }} + cachix_auth_token: ${{ secrets.CACHIX_AUTH_TOKEN }} test: name: Test ${{ matrix.check }} (${{ matrix.target.system }}) @@ -69,7 +58,11 @@ jobs: strategy: fail-fast: false matrix: - target: *targets + target: + - system: x86_64-linux + runner: linux-amd64-cpu8 + - system: aarch64-linux + runner: linux-arm64-cpu8 check: - openshell-bootstrap-test - openshell-cli-test @@ -89,12 +82,15 @@ jobs: - openshell-tui-test - openshell-vfio-test steps: - - *checkout - - *install-nix - - *setup-cachix + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Run test - run: nix build ".#checks.${{ matrix.target.system }}.${{ matrix.check }}" --no-link --no-update-lock-file + uses: ./.github/actions/setup-nix + with: + build: checks + system: ${{ matrix.target.system }} + target: ${{ matrix.check }} + cachix_auth_token: ${{ secrets.CACHIX_AUTH_TOKEN }} lint: name: Lint ${{ matrix.lint }} (${{ matrix.target.system }}) @@ -102,7 +98,11 @@ jobs: strategy: fail-fast: false matrix: - target: *targets + target: + - system: x86_64-linux + runner: linux-amd64-cpu8 + - system: aarch64-linux + runner: linux-arm64-cpu8 lint: - rustfmt - openshell-bootstrap-clippy @@ -123,9 +123,12 @@ jobs: - openshell-tui-clippy - openshell-vfio-clippy steps: - - *checkout - - *install-nix - - *setup-cachix + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Run lint - run: nix build ".#checks.${{ matrix.target.system }}.${{ matrix.lint }}" --no-link --no-update-lock-file + uses: ./.github/actions/setup-nix + with: + build: checks + system: ${{ matrix.target.system }} + target: ${{ matrix.lint }} + cachix_auth_token: ${{ secrets.CACHIX_AUTH_TOKEN }} From cc014b8b80f6a3340d0a6e94a5daa8c6d590882a Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 9 Jun 2026 15:59:07 +0200 Subject: [PATCH 14/18] ci: build nix container images --- .github/workflows/nix-ci.yml | 26 ++++++++++++++++ flake.nix | 23 +++++++++++---- nix/images.nix | 57 ++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 nix/images.nix diff --git a/.github/workflows/nix-ci.yml b/.github/workflows/nix-ci.yml index eb6d89349..5b9bf83ab 100644 --- a/.github/workflows/nix-ci.yml +++ b/.github/workflows/nix-ci.yml @@ -51,6 +51,32 @@ jobs: target: ${{ matrix.package }} cachix_auth_token: ${{ secrets.CACHIX_AUTH_TOKEN }} + images: + name: Build Image ${{ matrix.image }} (${{ matrix.target.system }}) + needs: build + runs-on: ${{ matrix.target.runner }} + strategy: + fail-fast: false + matrix: + target: + - system: x86_64-linux + runner: linux-amd64-cpu8 + - system: aarch64-linux + runner: linux-arm64-cpu8 + image: + - openshell-gateway-image + - openshell-supervisor-image + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Build image + uses: ./.github/actions/setup-nix + with: + build: packages + system: ${{ matrix.target.system }} + target: ${{ matrix.image }} + cachix_auth_token: ${{ secrets.CACHIX_AUTH_TOKEN }} + test: name: Test ${{ matrix.check }} (${{ matrix.target.system }}) needs: build diff --git a/flake.nix b/flake.nix index 73478f3fe..a689d25de 100644 --- a/flake.nix +++ b/flake.nix @@ -70,6 +70,16 @@ openshell-driver-podman = workspaceCrates.openshell-driver-podman.package; }; + images = + if pkgs.stdenv.hostPlatform.isLinux then + import ./nix/images.nix { + inherit pkgs; + gateway = crates.openshell-gateway; + supervisor = crates.openshell-sandbox; + } + else + { }; + crateTests = lib.mapAttrs' ( name: crate: lib.nameValuePair "${name}-test" crate.test ) workspaceCrates; @@ -83,12 +93,15 @@ }; in { - packages = crates // { - default = pkgs.symlinkJoin { - name = "openshell-0.0.0"; - paths = lib.attrValues crates; + packages = + crates + // images + // { + default = pkgs.symlinkJoin { + name = "openshell-0.0.0"; + paths = lib.attrValues crates; + }; }; - }; checks = crateTests diff --git a/nix/images.nix b/nix/images.nix new file mode 100644 index 000000000..c8dcbb732 --- /dev/null +++ b/nix/images.nix @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +{ + pkgs, + gateway, + supervisor, +}: +{ + openshell-gateway-image = pkgs.dockerTools.buildLayeredImage { + name = "openshell/gateway"; + tag = "nix"; + + contents = [ + gateway + pkgs.cacert + ]; + + extraCommands = '' + mkdir -p app usr/local/bin + cp --dereference ${gateway}/bin/openshell-gateway usr/local/bin/openshell-gateway + chmod 0555 usr/local/bin/openshell-gateway + ''; + + config = { + Entrypoint = [ "/usr/local/bin/openshell-gateway" ]; + Cmd = [ + "--bind-address" + "0.0.0.0" + "--port" + "8080" + ]; + Env = [ "SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt" ]; + ExposedPorts = { + "8080/tcp" = { }; + }; + User = "1000:1000"; + WorkingDir = "/app"; + }; + }; + + openshell-supervisor-image = pkgs.dockerTools.buildLayeredImage { + name = "openshell/supervisor"; + tag = "nix"; + + contents = [ supervisor ]; + + extraCommands = '' + cp --dereference ${supervisor}/bin/openshell-sandbox openshell-sandbox + chmod 0550 openshell-sandbox + ''; + + config = { + Entrypoint = [ "/openshell-sandbox" ]; + }; + }; +} From 89717430afba1675c49e052b1571de8334522fd4 Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 9 Jun 2026 16:07:37 +0200 Subject: [PATCH 15/18] ci: add nix SPDX header check Signed-off-by: Simon Scatton --- .github/workflows/nix-ci.yml | 12 ++++-------- flake.nix | 7 +++++++ scripts/update_license_headers.py | 1 + 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/nix-ci.yml b/.github/workflows/nix-ci.yml index 5b9bf83ab..f37f97c8b 100644 --- a/.github/workflows/nix-ci.yml +++ b/.github/workflows/nix-ci.yml @@ -119,18 +119,14 @@ jobs: cachix_auth_token: ${{ secrets.CACHIX_AUTH_TOKEN }} lint: - name: Lint ${{ matrix.lint }} (${{ matrix.target.system }}) - runs-on: ${{ matrix.target.runner }} + name: Lint ${{ matrix.lint }} + runs-on: linux-amd64-cpu8 strategy: fail-fast: false matrix: - target: - - system: x86_64-linux - runner: linux-amd64-cpu8 - - system: aarch64-linux - runner: linux-arm64-cpu8 lint: - rustfmt + - spdx-headers - openshell-bootstrap-clippy - openshell-cli-clippy - openshell-core-clippy @@ -155,6 +151,6 @@ jobs: uses: ./.github/actions/setup-nix with: build: checks - system: ${{ matrix.target.system }} + system: x86_64-linux target: ${{ matrix.lint }} cachix_auth_token: ${{ secrets.CACHIX_AUTH_TOKEN }} diff --git a/flake.nix b/flake.nix index a689d25de..e920da975 100644 --- a/flake.nix +++ b/flake.nix @@ -91,6 +91,12 @@ projectRootFile = "flake.nix"; programs.nixfmt.enable = true; }; + + spdxHeaders = pkgs.runCommand "spdx-headers" { src = lib.cleanSource ./.; } '' + cd "$src" + ${pkgs.python3}/bin/python scripts/update_license_headers.py --check + touch "$out" + ''; in { packages = @@ -112,6 +118,7 @@ src = craneLib.cleanCargoSource ./.; cargoExtraArgs = "--all"; }; + spdx-headers = spdxHeaders; }; devShells.default = pkgs.mkShell { diff --git a/scripts/update_license_headers.py b/scripts/update_license_headers.py index f56dbc293..4780b9049 100755 --- a/scripts/update_license_headers.py +++ b/scripts/update_license_headers.py @@ -43,6 +43,7 @@ ".yaml": "#", ".yml": "#", ".rego": "#", + ".nix": "#", } # Directories to skip entirely (relative to repo root). From 2fbf7e3c85c87e4d9bebc942754bca0786fd24a5 Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 9 Jun 2026 16:18:39 +0200 Subject: [PATCH 16/18] ci: batch nix workflow targets --- .github/actions/setup-nix/action.yml | 23 ++++-- .github/workflows/nix-ci.yml | 119 +++++++++++++-------------- 2 files changed, 73 insertions(+), 69 deletions(-) diff --git a/.github/actions/setup-nix/action.yml b/.github/actions/setup-nix/action.yml index db8c77fed..a99a548cd 100644 --- a/.github/actions/setup-nix/action.yml +++ b/.github/actions/setup-nix/action.yml @@ -1,5 +1,5 @@ -name: Setup Nix -description: Install Nix, configure Cachix, and build a flake target. +name: Nix Build +description: Install Nix, configure Cachix, and build flake targets. inputs: build: @@ -8,8 +8,8 @@ inputs: system: description: Nix system to build for. required: true - target: - description: Package or check target to build. + targets: + description: Newline-separated package or check targets to build. required: true cachix_auth_token: description: Cachix write token. @@ -31,6 +31,17 @@ runs: authToken: ${{ inputs.cachix_auth_token }} skipAddingSubstituter: true - - name: Build target + - name: Build targets shell: bash - run: nix build ".#${{ inputs.build }}.${{ inputs.system }}.${{ inputs.target }}" --no-link --no-update-lock-file + env: + BUILD: ${{ inputs.build }} + SYSTEM: ${{ inputs.system }} + TARGETS: ${{ inputs.targets }} + run: | + attrs=() + while IFS= read -r target; do + [ -n "$target" ] || continue + attrs+=(".#${BUILD}.${SYSTEM}.${target}") + done <<< "$TARGETS" + + nix build "${attrs[@]}" --no-link --no-update-lock-file diff --git a/.github/workflows/nix-ci.yml b/.github/workflows/nix-ci.yml index f37f97c8b..d45a99313 100644 --- a/.github/workflows/nix-ci.yml +++ b/.github/workflows/nix-ci.yml @@ -23,7 +23,7 @@ defaults: jobs: build: - name: Build ${{ matrix.package }} (${{ matrix.target.system }}) + name: Build (${{ matrix.target.system }}) runs-on: ${{ matrix.target.runner }} strategy: fail-fast: false @@ -33,26 +33,25 @@ jobs: runner: linux-amd64-cpu8 - system: aarch64-linux runner: linux-arm64-cpu8 - package: - - openshell - - openshell-gateway - - openshell-sandbox - - openshell-driver-kubernetes - - openshell-driver-podman - - openshell-driver-vm steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Build package + - name: Build packages uses: ./.github/actions/setup-nix with: build: packages system: ${{ matrix.target.system }} - target: ${{ matrix.package }} + targets: | + openshell + openshell-gateway + openshell-sandbox + openshell-driver-kubernetes + openshell-driver-podman + openshell-driver-vm cachix_auth_token: ${{ secrets.CACHIX_AUTH_TOKEN }} images: - name: Build Image ${{ matrix.image }} (${{ matrix.target.system }}) + name: Build Images (${{ matrix.target.system }}) needs: build runs-on: ${{ matrix.target.runner }} strategy: @@ -63,22 +62,21 @@ jobs: runner: linux-amd64-cpu8 - system: aarch64-linux runner: linux-arm64-cpu8 - image: - - openshell-gateway-image - - openshell-supervisor-image steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Build image + - name: Build images uses: ./.github/actions/setup-nix with: build: packages system: ${{ matrix.target.system }} - target: ${{ matrix.image }} + targets: | + openshell-gateway-image + openshell-supervisor-image cachix_auth_token: ${{ secrets.CACHIX_AUTH_TOKEN }} test: - name: Test ${{ matrix.check }} (${{ matrix.target.system }}) + name: Test (${{ matrix.target.system }}) needs: build runs-on: ${{ matrix.target.runner }} strategy: @@ -89,68 +87,63 @@ jobs: runner: linux-amd64-cpu8 - system: aarch64-linux runner: linux-arm64-cpu8 - check: - - openshell-bootstrap-test - - openshell-cli-test - - openshell-core-test - - openshell-driver-docker-test - - openshell-driver-kubernetes-test - - openshell-driver-podman-test - - openshell-driver-vm-test - - openshell-ocsf-test - - openshell-policy-test - - openshell-prover-test - - openshell-providers-test - - openshell-router-test - - openshell-sandbox-test - - openshell-server-macros-test - - openshell-server-test - - openshell-tui-test - - openshell-vfio-test steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Run test + - name: Run tests uses: ./.github/actions/setup-nix with: build: checks system: ${{ matrix.target.system }} - target: ${{ matrix.check }} + targets: | + openshell-bootstrap-test + openshell-cli-test + openshell-core-test + openshell-driver-docker-test + openshell-driver-kubernetes-test + openshell-driver-podman-test + openshell-driver-vm-test + openshell-ocsf-test + openshell-policy-test + openshell-prover-test + openshell-providers-test + openshell-router-test + openshell-sandbox-test + openshell-server-macros-test + openshell-server-test + openshell-tui-test + openshell-vfio-test cachix_auth_token: ${{ secrets.CACHIX_AUTH_TOKEN }} lint: - name: Lint ${{ matrix.lint }} + name: Lint runs-on: linux-amd64-cpu8 - strategy: - fail-fast: false - matrix: - lint: - - rustfmt - - spdx-headers - - openshell-bootstrap-clippy - - openshell-cli-clippy - - openshell-core-clippy - - openshell-driver-docker-clippy - - openshell-driver-kubernetes-clippy - - openshell-driver-podman-clippy - - openshell-driver-vm-clippy - - openshell-ocsf-clippy - - openshell-policy-clippy - - openshell-prover-clippy - - openshell-providers-clippy - - openshell-router-clippy - - openshell-sandbox-clippy - - openshell-server-macros-clippy - - openshell-server-clippy - - openshell-tui-clippy - - openshell-vfio-clippy steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Run lint + - name: Run lints uses: ./.github/actions/setup-nix with: build: checks system: x86_64-linux - target: ${{ matrix.lint }} + targets: | + rustfmt + spdx-headers + openshell-bootstrap-clippy + openshell-cli-clippy + openshell-core-clippy + openshell-driver-docker-clippy + openshell-driver-kubernetes-clippy + openshell-driver-podman-clippy + openshell-driver-vm-clippy + openshell-ocsf-clippy + openshell-policy-clippy + openshell-prover-clippy + openshell-providers-clippy + openshell-router-clippy + openshell-sandbox-clippy + openshell-server-macros-clippy + openshell-server-clippy + openshell-tui-clippy + openshell-vfio-clippy cachix_auth_token: ${{ secrets.CACHIX_AUTH_TOKEN }} From adf92e7c02987279917374fb83bfafca19fd9994 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Wed, 17 Jun 2026 16:20:24 +0200 Subject: [PATCH 17/18] fix(nix): repair supervisor crate source packaging (#1953) Signed-off-by: Evan Lezar --- crates/openshell-supervisor-network/Cargo.toml | 3 +++ crates/openshell-supervisor-process/Cargo.toml | 3 +++ nix/crate.nix | 6 +++--- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/openshell-supervisor-network/Cargo.toml b/crates/openshell-supervisor-network/Cargo.toml index 71febf0af..33610cfc6 100644 --- a/crates/openshell-supervisor-network/Cargo.toml +++ b/crates/openshell-supervisor-network/Cargo.toml @@ -10,6 +10,9 @@ license.workspace = true repository.workspace = true rust-version.workspace = true +[lib] +path = "src/lib.rs" + [dependencies] openshell-core = { path = "../openshell-core" } openshell-ocsf = { path = "../openshell-ocsf" } diff --git a/crates/openshell-supervisor-process/Cargo.toml b/crates/openshell-supervisor-process/Cargo.toml index b2dad859e..08b1d315e 100644 --- a/crates/openshell-supervisor-process/Cargo.toml +++ b/crates/openshell-supervisor-process/Cargo.toml @@ -10,6 +10,9 @@ license.workspace = true repository.workspace = true rust-version.workspace = true +[lib] +path = "src/lib.rs" + [dependencies] openshell-core = { path = "../openshell-core" } openshell-ocsf = { path = "../openshell-ocsf" } diff --git a/nix/crate.nix b/nix/crate.nix index 3993eb599..8b4b22128 100644 --- a/nix/crate.nix +++ b/nix/crate.nix @@ -53,9 +53,9 @@ ]; assets = [ (root + "/proto") - (root + "/crates/openshell-sandbox/data") - (root + "/crates/openshell-sandbox/src/skills") - (root + "/crates/openshell-sandbox/testdata") + (root + "/crates/openshell-supervisor-network/data") + (root + "/crates/openshell-supervisor-network/testdata") + (root + "/crates/openshell-supervisor-process/src/skills") ]; }; openshell-driver-vm = { From 0fffb47a244cf3f79b3e7e77d2ea974193702f78 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Thu, 18 Jun 2026 10:23:20 +0200 Subject: [PATCH 18/18] fix(nix): make flake checks hermetic Signed-off-by: Evan Lezar --- crates/openshell-driver-vm/src/runtime.rs | 33 +++++++++++++++-------- nix/crate.nix | 1 + 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/crates/openshell-driver-vm/src/runtime.rs b/crates/openshell-driver-vm/src/runtime.rs index a6ddfe011..784552c0d 100644 --- a/crates/openshell-driver-vm/src/runtime.rs +++ b/crates/openshell-driver-vm/src/runtime.rs @@ -1247,17 +1247,7 @@ fn hash_path_id(path: &Path) -> String { } fn secure_socket_base(subdir: &str) -> Result { - let base = std::env::var_os("XDG_RUNTIME_DIR").map_or_else( - || { - let fallback = PathBuf::from("/tmp"); - if fallback.is_dir() { - fallback - } else { - std::env::temp_dir() - } - }, - PathBuf::from, - ); + let base = std::env::var_os("XDG_RUNTIME_DIR").map_or_else(socket_fallback_base, PathBuf::from); let dir = base.join(subdir); if dir.exists() { @@ -1296,6 +1286,27 @@ fn secure_socket_base(subdir: &str) -> Result { Ok(dir) } +fn socket_fallback_base() -> PathBuf { + let temp_dir = { + let fallback = PathBuf::from("/tmp"); + if fallback.is_dir() { + fallback + } else { + std::env::temp_dir() + } + }; + + #[cfg(unix)] + { + temp_dir.join(format!("openshell-{}", unsafe { libc::getuid() })) + } + + #[cfg(not(unix))] + { + temp_dir.join("openshell") + } +} + fn gvproxy_socket_base(overlay_disk: &Path) -> Result { Ok(secure_socket_base("osd-gv")?.join(hash_path_id(overlay_disk))) } diff --git a/nix/crate.nix b/nix/crate.nix index 8b4b22128..392b17770 100644 --- a/nix/crate.nix +++ b/nix/crate.nix @@ -39,6 +39,7 @@ openshell-core = { dir = "openshell-core"; nativeBuildInputs = [ pkgs.protobuf ]; + nativeCheckInputs = [ pkgs.lsof ]; assets = [ (root + "/proto") ]; }; openshell-driver-docker = {