rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwoodd2f776ca790392f809e11f446559849d41c415ca
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Updated",
"patch": {
"id": "630ccdea1d389a2a15e0ba328f451233a9d02d6c",
"author": {
"id": "did:key:z6MkwGoyYxt6A2VE3fvZyH2rgiWdsXHBeV7jm7GSByS2aagA",
"alias": "ade"
},
"title": "simulation: Introduce cargo test runner",
"state": {
"status": "open",
"conflicts": []
},
"before": "caee776c388ffac2ea55cc9d1e3d7fa108ca6df5",
"after": "d2f776ca790392f809e11f446559849d41c415ca",
"commits": [
"d2f776ca790392f809e11f446559849d41c415ca",
"38a09c4313de1e3cb2a0be9fc076ed03549c4ea6",
"8395d6daf77246a71d4a575e7227e02d64609b1b",
"e5134a66d0a05bbeffe91a527cd31eebf046920c",
"efdaf62cf33a21e955c365f6ef752e19dcc01ece",
"1f70bf533b31b511fd023c41a1d614436181a4d6",
"898adeec651066918c49130d44607c9d12737922",
"f8150f54accaa1ef5ce130c011daba5852dfc5a2",
"ae47b99216ff8f47527e9d65fa22e8b871a68c4c",
"b0969e62039b22c7790d235b378428caa41c4b4e",
"55dfb9c16e9dd977e34ee4994f1e3f9ef5c356eb"
],
"target": "6b460c44298afad6599b223e48bdaa3c7f3ad5cc",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "630ccdea1d389a2a15e0ba328f451233a9d02d6c",
"author": {
"id": "did:key:z6MkwGoyYxt6A2VE3fvZyH2rgiWdsXHBeV7jm7GSByS2aagA",
"alias": "ade"
},
"description": "Enable writing rust based tests that run over a network topology CUE file.",
"base": "48551cde934370c0cf8a4127385d4a4a41c5ba4d",
"oid": "44fd3630cf6101c43219f4f0b97a0626bb55661f",
"timestamp": 1776091497
},
{
"id": "efe29c9265995961344c972968882ce5671dd421",
"author": {
"id": "did:key:z6MkwGoyYxt6A2VE3fvZyH2rgiWdsXHBeV7jm7GSByS2aagA",
"alias": "ade"
},
"description": "",
"base": "48551cde934370c0cf8a4127385d4a4a41c5ba4d",
"oid": "31afed82f4b0264d6c03bbf9e8e4955d64fcc8f2",
"timestamp": 1776422085
},
{
"id": "aaaf907c15c627daeb981edd363242613dd16884",
"author": {
"id": "did:key:z6MkwGoyYxt6A2VE3fvZyH2rgiWdsXHBeV7jm7GSByS2aagA",
"alias": "ade"
},
"description": "Rebased and fixed minor issues",
"base": "caee776c388ffac2ea55cc9d1e3d7fa108ca6df5",
"oid": "d2f776ca790392f809e11f446559849d41c415ca",
"timestamp": 1778520084
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "a5088b03-d3b5-4199-ba2d-8fffbc6c48ba"
},
"info_url": "https://cci.rad.levitte.org//a5088b03-d3b5-4199-ba2d-8fffbc6c48ba.html"
}
Started at: 2026-05-19 17:56:37.191783+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW added
✓ Remote-tracking branch cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW/master created for z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz added
✓ Remote-tracking branch erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz/master created for z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 164 issues · 46 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 630ccdea1d389a2a15e0ba328f451233a9d02d6c
✓ Switched to branch patch/630ccde at revision aaaf907
✓ Branch patch/630ccde setup to track rad/patches/630ccdea1d389a2a15e0ba328f451233a9d02d6c
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout d2f776ca790392f809e11f446559849d41c415ca
HEAD is now at d2f776ca just: Exclude radicle-simulation from pre-push hook
Exit code: 0
$ rad patch show 630ccdea1d389a2a15e0ba328f451233a9d02d6c -p
╭────────────────────────────────────────────────────────────────────────────╮
│ Title simulation: Introduce cargo test runner │
│ Patch 630ccdea1d389a2a15e0ba328f451233a9d02d6c │
│ Author ade z6MkwGo…yS2aagA │
│ Head d2f776ca790392f809e11f446559849d41c415ca │
│ Base caee776c388ffac2ea55cc9d1e3d7fa108ca6df5 │
│ Branches patch/630ccde │
│ Commits ahead 27, behind 0 │
│ Status open │
│ │
│ Enable writing rust based tests that run over a network topology CUE file. │
├────────────────────────────────────────────────────────────────────────────┤
│ d2f776c just: Exclude radicle-simulation from pre-push hook │
│ 38a09c4 simulation: Clippy lint fix │
│ 8395d6d simulation/just: Ensure to vendor Timoni dependencies for test │
│ e5134a6 simulation: Drop 'default-node' from values │
│ efdaf62 simulation/just: Add update-image-tags recipe for radicle version │
│ 1f70bf5 simulation: Update README with test writing │
│ 898adee simulation/just: Add test recipe to justfile │
│ f8150f5 simulation: Cargo test runner │
│ ae47b99 simulation: Introduce CUE Schema and build script │
│ b0969e6 simulation/just: Rewrite inline bash scripts with calls to files │
│ 55dfb9c simulation/just: Extract inline bash scripts to files │
├────────────────────────────────────────────────────────────────────────────┤
│ ● Revision 630ccde @ 48551cd..44fd363 by ade z6MkwGo…yS2aagA 1 month ago │
│ ↑ Revision efe29c9 @ 48551cd..31afed8 by ade z6MkwGo…yS2aagA 1 month ago │
│ ↑ Revision aaaf907 @ caee776..d2f776c by ade z6MkwGo…yS2aagA 1 week ago │
╰────────────────────────────────────────────────────────────────────────────╯
commit d2f776ca790392f809e11f446559849d41c415ca
Author: Adrian Duke <adrian.duke@gmail.com>
Date: Mon May 11 18:17:56 2026 +0100
just: Exclude radicle-simulation from pre-push hook
diff --git a/justfile b/justfile
index a272c0875..276e00977 100644
--- a/justfile
+++ b/justfile
@@ -149,4 +149,4 @@ check-hooks:
[group('pre-push')]
test-rust:
@echo "{{CHECK}}Cargo test...{{NORMAL}}"
- @{{cargo_cmd}} nextest run --workspace --all-features --no-fail-fast
+ @{{cargo_cmd}} nextest run --workspace --all-features --no-fail-fast --exclude radicle-simulation
commit 38a09c4313de1e3cb2a0be9fc076ed03549c4ea6
Author: Adrian Duke <adrian.duke@gmail.com>
Date: Mon May 11 18:10:54 2026 +0100
simulation: Clippy lint fix
diff --git a/simulation/radicle-simulation/src/network.rs b/simulation/radicle-simulation/src/network.rs
index 86bbee5f7..205d9d14a 100644
--- a/simulation/radicle-simulation/src/network.rs
+++ b/simulation/radicle-simulation/src/network.rs
@@ -14,14 +14,14 @@ pub struct NetworkGuard {
impl Drop for NetworkGuard {
fn drop(&mut self) {
- if let Ok(val) = std::env::var("PRESERVE_NETWORK") {
- if val == "1" || val.eq_ignore_ascii_case("true") {
- println!(
- "⏭️ PRESERVE_NETWORK is set. Skipping cleanup for {}.",
- self.cue_path
- );
- return;
- }
+ if let Ok(val) = std::env::var("PRESERVE_NETWORK")
+ && (val == "1" || val.eq_ignore_ascii_case("true"))
+ {
+ println!(
+ "⏭️ PRESERVE_NETWORK is set. Skipping cleanup for {}.",
+ self.cue_path
+ );
+ return;
}
println!("🔄 Tearing down network topology from {}...", self.cue_path);
commit 8395d6daf77246a71d4a575e7227e02d64609b1b
Author: Adrian Duke <adrian.duke@gmail.com>
Date: Mon May 11 18:02:21 2026 +0100
simulation/just: Ensure to vendor Timoni dependencies for test
diff --git a/simulation/justfile b/simulation/justfile
index f21f88eeb..ea23ece0d 100644
--- a/simulation/justfile
+++ b/simulation/justfile
@@ -16,7 +16,7 @@ default:
@just --list
[group('test')]
-test: setup
+test: setup vendor-timoni-dependencies
@cargo test -p radicle-simulation
# Setup and start the complete simulation environment
commit e5134a66d0a05bbeffe91a527cd31eebf046920c
Author: Adrian Duke <adrian.duke@gmail.com>
Date: Mon May 11 18:01:42 2026 +0100
simulation: Drop 'default-node' from values
diff --git a/simulation/modules/radicle-node/values.cue b/simulation/modules/radicle-node/values.cue
index 1fb1d7154..d21226481 100644
--- a/simulation/modules/radicle-node/values.cue
+++ b/simulation/modules/radicle-node/values.cue
@@ -16,10 +16,4 @@ values: {
version: "1.2.0" | "1.4.0" | "1.5.0" | "1.5.0-" | "1.5.0-amd64" | "1.5.0-arm64" | "1.6.0" | "1.6.1" | "1.7.0" | "1.7.1" | "1.8.0" | "latest" | "main" | "production" | "sqlite-patch" | string | *"latest"
...
}
-
- // Provide a default node so that a basic install works out-of-the-box
- topology: "default-node": {
- role: "seed"
- replicas: 1
- }
}
commit efdaf62cf33a21e955c365f6ef752e19dcc01ece
Author: Adrian Duke <adrian.duke@gmail.com>
Date: Mon May 11 18:01:28 2026 +0100
simulation/just: Add update-image-tags recipe for radicle version
diff --git a/scripts/just/update-image-tags.sh b/scripts/just/update-image-tags.sh
new file mode 100755
index 000000000..ad35875f5
--- /dev/null
+++ b/scripts/just/update-image-tags.sh
@@ -0,0 +1,39 @@
+#! /usr/bin/env bash
+set -e
+
+VALUES_FILE="$1"
+
+if [ -z "$VALUES_FILE" ]; then
+ echo "${ERROR}Usage: $0 <path-to-values.cue>${NORMAL}" >&2
+ exit 1
+fi
+
+REPO="radicle_garden/radicle-node"
+API_URL="https://quay.io/api/v1/repository/$REPO/tag/?limit=100"
+
+if [ ! -f "$VALUES_FILE" ]; then
+ echo "${ERROR}Error: $VALUES_FILE not found.${NORMAL}" >&2
+ exit 1
+fi
+
+echo "${CHECK}Fetching tags from quay.io for $REPO...${NORMAL}"
+
+# Fetch tags, extract names, ignore empty lines, sort versions, and remove duplicates
+TAGS=$(curl -sL "$API_URL" | jq -r '.tags[].name' | grep -v '^$' | sort -V | uniq)
+
+if [ -z "$TAGS" ]; then
+ echo "${ERROR}Error: No tags found or failed to fetch tags.${NORMAL}" >&2
+ exit 1
+fi
+
+# Format tags into a CUE enum: "tag1" | "tag2" | string | *"latest"
+# We append `| string | *"latest"` to allow custom local builds
+ENUM=$(echo "$TAGS" | awk '{printf "\"%s\" | ", $0} END {print "string | *\"latest\""}')
+
+echo "${CHECK}Injecting tags into $VALUES_FILE...${NORMAL}"
+
+# Use a temporary file for cross-platform sed compatibility (works on both macOS and Linux)
+sed -e "s@version:.*@version: $ENUM@" "$VALUES_FILE" > "${VALUES_FILE}.tmp"
+mv "${VALUES_FILE}.tmp" "$VALUES_FILE"
+
+echo "${SUCCESS}Successfully updated $VALUES_FILE${NORMAL}"
diff --git a/simulation/justfile b/simulation/justfile
index 4cfab3162..f21f88eeb 100644
--- a/simulation/justfile
+++ b/simulation/justfile
@@ -91,6 +91,11 @@ delete-pvc: (verify-tool "kubectl")
destroy: (verify-tool "kubectl") (verify-tool "talosctl") show-cluster
@CHECK="{{CHECK}}" WARN="{{WARN}}" SUCCESS="{{SUCCESS}}" NORMAL="{{NORMAL}}" ../scripts/just/destroy-cluster.sh "{{cluster_name}}" "{{provisioner}}"
+# Update image tags in a CUE values file
+[group('setup')]
+update-image-tags values_file: (verify-tool "jq") (verify-tool "curl")
+ @CHECK="{{CHECK}}" SUCCESS="{{SUCCESS}}" ERROR="{{ERROR}}" NORMAL="{{NORMAL}}" ../scripts/just/update-image-tags.sh "{{values_file}}"
+
# Check if required tools are in PATH.
[private]
verify-tool tool package_name="":
commit 1f70bf533b31b511fd023c41a1d614436181a4d6
Author: Adrian Duke <adrian.duke@gmail.com>
Date: Mon Apr 13 15:41:36 2026 +0100
simulation: Update README with test writing
diff --git a/simulation/README.md b/simulation/README.md
index e0aa02668..2b8d938bd 100644
--- a/simulation/README.md
+++ b/simulation/README.md
@@ -104,6 +104,57 @@ values: {
When you run `just start-network`, Timoni reads this file, merges it with the module definitions in `modules/radicle-node`, and deploys the resulting pods to Kubernetes.
+## Writing Tests
+
+The `radicle-simulation` crate provides an ergonomic Rust framework for writing tests against these CUE topologies. Here is how to introduce a new test suite:
+
+### Create the Topology
+Create a new `.cue` file in the `instances/` directory (e.g., `instances/my_network.cue`). See [## Defining a Topology]
+
+When you run `cargo test` (or `cargo check`), the `build.rs` script automatically detects this file, parses the topology, and generates a Rust bindings file (`my_network_bindings.rs`) in Cargo's `OUT_DIR`. These bindings contain strongly-typed accessor functions for the nodes defined in your CUE file (e.g., `peer_v1_8_0()`, `peer_relative(offset, index)`).
+
+### Create the Test Suite Entrypoint
+In `radicle-simulation/tests/`, create a main entrypoint for your suite (e.g., `my_suite_main.rs`). Use the `setup_network!` macro to link your CUE file to the test suite:
+
+```rust
+use radicle_simulation::setup_network;
+
+// This macro does two things:
+// 1. Includes the generated `my_network_bindings.rs` into a `network` module.
+// 2. Generates a `require_network()` function that provisions the cluster.
+setup_network!("my_network");
+
+// Declare your actual test modules here
+mod my_suite;
+```
+
+### Write the Tests
+In your test modules (e.g., `radicle-simulation/tests/my_suite/mod.rs`), you can now write your tests.
+
+**NOTE:** Every test *must* call `require_network()` and hold onto the returned guard. This ensures the network is provisioned before the test runs and prevents it from being torn down while the test is executing.
+
+```rust
+use crate::network;
+use crate::require_network;
+
+#[test]
+fn test_nodes_can_sync() -> Result<(), String> {
+ // Ensure the network is running and hold the guard
+ let _guard = require_network();
+
+ // Grab node handles using the auto-generated bindings
+ // `peer_relative(0, 0)` gets the 0th replica of the latest versioned peer.
+ let alice_node = network::peer_relative(0, 0).as_alice()?;
+ let bob_node = network::peer_relative(-1, 0).as_bob()?;
+
+ // Interact with the nodes
+ let repo = alice_node.init_test_repo("my-repo", "A test repo", 1)?;
+ bob_node.clone_repo(&repo.rid, "bob-repo")?;
+
+ Ok(())
+}
+```
+
## Helpful Commands
**Execute a command inside a node:**
@@ -192,6 +243,6 @@ See the [Goals] section for more info.
1. [X] `radicle-node` timoni module.
2. [ ] `radicle-node` custom container builder.
3. [X] `instances` topology definition files.
- 4. [ ] `sim-tests` rust crate.
+ 4. [X] `radicle-simulation` rust crate.
5. [X] `justfile` orchestration.
6. [ ] `observability` definition files.
commit 898adeec651066918c49130d44607c9d12737922
Author: Adrian Duke <adrian.duke@gmail.com>
Date: Mon Apr 13 15:41:03 2026 +0100
simulation/just: Add test recipe to justfile
diff --git a/simulation/README.md b/simulation/README.md
index ccb75c592..e0aa02668 100644
--- a/simulation/README.md
+++ b/simulation/README.md
@@ -33,6 +33,9 @@ networking.firewall.trustedInterfaces = ["talos+"];
The environment is managed entirely via `just`. From the `simulation` directory, you can run:
```shell
+# Run all tests
+$ just test
+
# Start the complete simulation (creates cluster, configures K8s, and deploys the network)
$ just start
diff --git a/simulation/justfile b/simulation/justfile
index 61419bb20..4cfab3162 100644
--- a/simulation/justfile
+++ b/simulation/justfile
@@ -15,6 +15,10 @@ HINT := "💡 " + BOLD
default:
@just --list
+[group('test')]
+test: setup
+ @cargo test -p radicle-simulation
+
# Setup and start the complete simulation environment
[group('start')]
[group('setup')]
commit f8150f54accaa1ef5ce130c011daba5852dfc5a2
Author: Adrian Duke <adrian.duke@gmail.com>
Date: Mon Apr 13 15:11:40 2026 +0100
simulation: Cargo test runner
Introduces a set of macros and network provisioning functions to manage
the state of a network topology file. A simple cross version test has
been introduced to initialise a repository on all peer nodes defined.
The `setup_network` macro has been added to both import the `_bindings.rs`
file from the build script and offer a blocking network provisioner
function `require_network`, it returns an `Arc` that is to be held by
every test interested in executing on the named topology.
Introduces a new network topology `basic_cross_version_network.cue` which
defines a set of peers from `v1.5.0` to `v1.8.0` along with 2 bootstrap nodes.
diff --git a/simulation/instances/basic_cross_version_network.cue b/simulation/instances/basic_cross_version_network.cue
new file mode 100644
index 000000000..2d5e85dda
--- /dev/null
+++ b/simulation/instances/basic_cross_version_network.cue
@@ -0,0 +1,90 @@
+@if(!debug)
+
+package main
+
+values: {
+ topology: {
+ "bootstrap-v1-6-1": {
+ role: "bootstrap"
+ version: "1.6.1"
+ replicas: 1
+ nodeIdSeed: "bootstrap-0"
+ radicleConfig: #BaseBootstrapSeedConfig
+ }
+
+ "bootstrap-v1-7-0": {
+ role: "bootstrap"
+ version: "1.7.0"
+ replicas: 1
+ nodeIdSeed: "bootstrap-1"
+ radicleConfig: #BaseBootstrapSeedConfig
+ }
+
+ "peer-v1-5-0": {
+ role: "peer"
+ version: "1.5.0"
+ replicas: 1
+ radicleConfig: #BasePeerConfig & {
+ preferredSeeds: [
+ (#SeedAddress & {nid: #BootstrapNIDs["bootstrap-0"], name: "bootstrap-v1-6-1"}).out,
+ (#SeedAddress & {nid: #BootstrapNIDs["bootstrap-1"], name: "bootstrap-v1-7-0"}).out,
+ ]
+ }
+ }
+ "peer-v1-6-0": {
+ role: "peer"
+ version: "1.6.0"
+ replicas: 1
+ radicleConfig: #BasePeerConfig & {
+ preferredSeeds: [
+ (#SeedAddress & {nid: #BootstrapNIDs["bootstrap-0"], name: "bootstrap-v1-6-1"}).out,
+ (#SeedAddress & {nid: #BootstrapNIDs["bootstrap-1"], name: "bootstrap-v1-7-0"}).out,
+ ]
+ }
+ }
+ "peer-v1-6-1": {
+ role: "peer"
+ version: "1.6.1"
+ replicas: 1
+ radicleConfig: #BasePeerConfig & {
+ preferredSeeds: [
+ (#SeedAddress & {nid: #BootstrapNIDs["bootstrap-0"], name: "bootstrap-v1-6-1"}).out,
+ (#SeedAddress & {nid: #BootstrapNIDs["bootstrap-1"], name: "bootstrap-v1-7-0"}).out,
+ ]
+ }
+ }
+ "peer-v1-7-0": {
+ role: "peer"
+ version: "1.7.0"
+ replicas: 2
+ radicleConfig: #BasePeerConfig & {
+ preferredSeeds: [
+ (#SeedAddress & {nid: #BootstrapNIDs["bootstrap-0"], name: "bootstrap-v1-6-1"}).out,
+ (#SeedAddress & {nid: #BootstrapNIDs["bootstrap-1"], name: "bootstrap-v1-7-0"}).out,
+ ]
+ }
+ }
+ "peer-v1-7-1": {
+ role: "peer"
+ version: "1.7.1"
+ replicas: 2
+ radicleConfig: #BasePeerConfig & {
+ preferredSeeds: [
+ (#SeedAddress & {nid: #BootstrapNIDs["bootstrap-0"], name: "bootstrap-v1-6-1"}).out,
+ (#SeedAddress & {nid: #BootstrapNIDs["bootstrap-1"], name: "bootstrap-v1-7-0"}).out,
+ ]
+ }
+ }
+ "peer-v1-8-0": {
+ role: "peer"
+ version: "1.8.0"
+ replicas: 1
+ radicleConfig: #BasePeerConfig & {
+ preferredSeeds: [
+ (#SeedAddress & {nid: #BootstrapNIDs["bootstrap-0"], name: "bootstrap-v1-6-1"}).out,
+ (#SeedAddress & {nid: #BootstrapNIDs["bootstrap-1"], name: "bootstrap-v1-7-0"}).out,
+ ]
+ }
+ }
+ }
+}
diff --git a/simulation/radicle-simulation/src/lib.rs b/simulation/radicle-simulation/src/lib.rs
new file mode 100644
index 000000000..4c4e53d88
--- /dev/null
+++ b/simulation/radicle-simulation/src/lib.rs
@@ -0,0 +1,36 @@
+//! Radicle Simulation Testing Framework
+//!
+//! This crate provides a high-level, ergonomic framework for writing integration
+//! and simulation tests for Radicle networks. It abstracts away the complexities
+//! of Kubernetes pod execution, identity management, and Radicle CLI interactions.
+
+pub mod constants;
+pub mod network;
+pub mod node;
+
+/// Sets up the network module and `require_network` helper for a given CUE instance.
+///
+/// This macro dynamically includes the auto-generated Rust bindings for the specified
+/// CUE network topology file (generated by `build.rs`). It also generates a `require_network`
+/// function that tests can call to ensure the network is provisioned and ready before executing.
+#[macro_export]
+macro_rules! setup_network {
+ ($name:expr) => {
+ pub mod network {
+ // Include the auto-generated bindings
+ include!(concat!(env!("OUT_DIR"), "/", $name, "_bindings.rs"));
+ }
+
+ /// Ensures the simulated network is provisioned and ready for testing.
+ ///
+ /// This function initializes the test logger and applies the CUE network topology.
+ /// It returns a reference-counted `NetworkGuard`. The network will remain active
+ /// as long as at least one test holds a guard, and will be automatically torn down
+ /// when the final guard is dropped (unless `PRESERVE_NETWORK=1` is set).
+ pub fn require_network() -> std::sync::Arc<$crate::network::NetworkGuard> {
+ // TODO(Ade): Doesn't feel right here, but is ergonomic
+ let _ = env_logger::builder().is_test(true).try_init();
+ $crate::network::apply_network(concat!("../instances/", $name, ".cue"))
+ }
+ };
+}
diff --git a/simulation/radicle-simulation/src/network.rs b/simulation/radicle-simulation/src/network.rs
new file mode 100644
index 000000000..86bbee5f7
--- /dev/null
+++ b/simulation/radicle-simulation/src/network.rs
@@ -0,0 +1,136 @@
+//! Network lifecycle management for the Radicle simulation environment.
+
+use crate::constants::*;
+use crate::node::Node;
+use std::io::Write;
+use std::process::{Command, Stdio};
+use std::str;
+use std::sync::{Arc, Mutex, Weak};
+
+/// A guard that ensures the network is torn down when tests finish.
+pub struct NetworkGuard {
+ cue_path: String,
+}
+
+impl Drop for NetworkGuard {
+ fn drop(&mut self) {
+ if let Ok(val) = std::env::var("PRESERVE_NETWORK") {
+ if val == "1" || val.eq_ignore_ascii_case("true") {
+ println!(
+ "⏭️ PRESERVE_NETWORK is set. Skipping cleanup for {}.",
+ self.cue_path
+ );
+ return;
+ }
+ }
+
+ println!("🔄 Tearing down network topology from {}...", self.cue_path);
+ let status = Command::new("timoni")
+ .args(["delete", TIMONI_INSTANCE_NAME])
+ .status();
+
+ match status {
+ Ok(s) if s.success() => println!("✅ Network torn down successfully."),
+ _ => eprintln!("⚠️ Warning: Failed to cleanly tear down network."),
+ }
+ }
+}
+
+static NETWORK_GUARD: Mutex<Weak<NetworkGuard>> = Mutex::new(Weak::new());
+
+/// Applies the specified CUE network topology to the Kubernetes cluster.
+///
+/// This function evaluates the CUE files, pipes the resulting JSON to Timoni,
+/// and waits for all pods and Radicle nodes to become fully responsive.
+pub fn apply_network(cue_path: &str) -> Arc<NetworkGuard> {
+ let mut weak_guard = NETWORK_GUARD.lock().unwrap();
+
+ // If the network is already running, just return a clone of the Arc
+ if let Some(arc) = weak_guard.upgrade() {
+ return arc;
+ }
+
+ println!("🔄 Applying network topology from {}...", cue_path);
+
+ // Merge and evaluate CUE files into a single JSON stream
+ let cue_export = Command::new("cue")
+ .args(["export", SCHEMA_CUE_PATH, cue_path, "--out", "json"])
+ .output()
+ .expect("Failed to execute cue export");
+
+ if !cue_export.status.success() {
+ panic!(
+ "CUE export failed:\n{}",
+ String::from_utf8_lossy(&cue_export.stderr)
+ );
+ }
+
+ // Pipe the evaluated JSON directly into Timoni
+ let mut timoni = Command::new("timoni")
+ .args([
+ "apply",
+ TIMONI_INSTANCE_NAME,
+ TIMONI_MODULE_PATH,
+ "--values",
+ "-", // The '-' tells Timoni to read values from stdin
+ ])
+ .stdin(Stdio::piped())
+ .spawn()
+ .expect("Failed to spawn timoni");
+
+ // Write the JSON to Timoni's stdin
+ if let Some(mut stdin) = timoni.stdin.take() {
+ stdin
+ .write_all(&cue_export.stdout)
+ .expect("Failed to write to timoni stdin");
+ }
+
+ let status = timoni.wait().expect("Failed to wait on timoni");
+ assert!(status.success(), "Timoni apply failed for {}", cue_path);
+
+ // Wait for pods to be Ready
+ println!("🔄 Waiting for pods to be ready...");
+ let wait_status = Command::new("kubectl")
+ .args([
+ "wait",
+ "--for=condition=Ready",
+ "pod",
+ "-l",
+ "app=radicle-node",
+ "--timeout=300s",
+ ])
+ .status()
+ .expect("Failed to wait for pods");
+ assert!(wait_status.success(), "Pods did not become ready in time");
+
+ // Wait for Radicle nodes to be fully responsive
+ println!("🔄 Waiting for Radicle nodes to be fully responsive...");
+ let output = Command::new("kubectl")
+ .args([
+ "get",
+ "pods",
+ "-l",
+ "app=radicle-node",
+ "-o",
+ "jsonpath={.items[*].metadata.name}",
+ ])
+ .output()
+ .expect("Failed to execute kubectl get pods");
+
+ let stdout = str::from_utf8(&output.stdout).unwrap_or("");
+ let pods: Vec<&str> = stdout.split_whitespace().collect();
+
+ for pod_name in pods {
+ let node = Node::new(pod_name);
+ node.wait_until_responsive()
+ .expect("Node did not become responsive");
+ }
+ println!("✅ Network {} is ready!", cue_path);
+
+ let arc = Arc::new(NetworkGuard {
+ cue_path: cue_path.to_string(),
+ });
+ *weak_guard = Arc::downgrade(&arc);
+
+ arc
+}
diff --git a/simulation/radicle-simulation/src/node.rs b/simulation/radicle-simulation/src/node.rs
new file mode 100644
index 000000000..c846975c3
--- /dev/null
+++ b/simulation/radicle-simulation/src/node.rs
@@ -0,0 +1,275 @@
+//! Node and Repository abstractions for the Radicle simulation environment.
+//!
+//! This module provides the core types used to interact with Radicle nodes
+//! running inside the Kubernetes cluster. It includes executors for running
+//! commands, persona builders for identity management, and high-level wrappers
+//! for Radicle CLI operations (issues, patches, syncing).
+
+use std::process::Command;
+use std::str;
+use std::thread;
+use std::time::Duration;
+use uuid::Uuid;
+
+/// Escapes single quotes for safe interpolation inside a shell single-quoted string.
+fn escape_sh(s: &str) -> String {
+ s.replace('\'', "'\\''")
+}
+
+/// Trait defining how commands are executed within a Radicle node's environment.
+pub trait NodeExecutor {
+ /// Executes a command on the specified node and returns the combined stdout/stderr.
+ fn exec(&self, node_name: &str, cmd: &[&str]) -> Result<String, String>;
+}
+
+/// Default executor implementation that uses the `kubectl exec` CLI command.
+///
+/// This executor shells out to the local `kubectl` binary to run commands inside
+/// the `node` container of the target Kubernetes pod.
+#[derive(Clone, Debug)]
+pub struct KubectlExecutor;
+
+impl NodeExecutor for KubectlExecutor {
+ fn exec(&self, node_name: &str, cmd: &[&str]) -> Result<String, String> {
+ let mut args = vec!["exec", "-i", node_name, "-c", "node", "--"];
+ args.extend_from_slice(cmd);
+
+ let output = Command::new("kubectl")
+ .args(&args)
+ .output()
+ .map_err(|e| format!("Failed to execute kubectl: {}", e))?;
+
+ let stdout = str::from_utf8(&output.stdout)
+ .unwrap_or("")
+ .trim()
+ .to_string();
+ let stderr = str::from_utf8(&output.stderr)
+ .unwrap_or("")
+ .trim()
+ .to_string();
+
+ if output.status.success() {
+ // Git commands often write success messages to stderr, so we combine them
+ let mut combined = stdout;
+ if !stderr.is_empty() {
+ combined.push('\n');
+ combined.push_str(&stderr);
+ }
+ Ok(combined.trim().to_string())
+ } else {
+ let mut combined = stderr;
+ if !stdout.is_empty() {
+ combined.push('\n');
+ combined.push_str(&stdout);
+ }
+ Err(format!(
+ "Command failed: {}\nStderr: {}",
+ args.join(" "),
+ combined.trim()
+ ))
+ }
+ }
+}
+
+/// Represents a Radicle node running in the simulated network.
+///
+/// This struct provides methods to interact with the node's CLI, manage its Git
+/// identity (persona), and initialize or clone repositories.
+#[derive(Clone, Debug)]
+pub struct Node<E = KubectlExecutor> {
+ /// Kubernetes pod name of the node.
+ pub name: String,
+ /// The executor used to run commands on this node.
+ pub executor: E,
+ /// Optional Git identity (Name, Email) assigned to this node.
+ pub persona: Option<(String, String)>,
+}
+
+impl Node<KubectlExecutor> {
+ /// Creates a new `Node` instance using the default `KubectlExecutor`.
+ pub fn new(name: impl Into<String>) -> Self {
+ Self {
+ name: name.into(),
+ executor: KubectlExecutor,
+ persona: None,
+ }
+ }
+}
+
+impl<E: NodeExecutor + Clone> Node<E> {
+ /// Internal helper to set the Git identity on the node and store it in the struct.
+ pub fn with_persona(mut self, name: &str, email: &str) -> Result<Self, String> {
+ self.setup_identity(name, email)?;
+ self.persona = Some((name.to_string(), email.to_string()));
+ Ok(self)
+ }
+
+ /// Configures the node with the standard "Alice" test persona.
+ pub fn as_alice(self) -> Result<Self, String> {
+ self.with_persona("Alice", "alice@radicle.local")
+ }
+
+ /// Executes a command inside the node container via the configured executor.
+ ///
+ /// This method automatically logs the command being executed (at the `INFO` level)
+ /// and its output or failure (at the `DEBUG` or `ERROR` levels).
+ pub fn exec(&self, cmd: &[&str]) -> Result<String, String> {
+ let identity = if let Some((name, _)) = &self.persona {
+ format!("{}@{}", name, self.name)
+ } else {
+ self.name.clone()
+ };
+
+ log::info!("[{}] $ {}", identity, cmd.join(" "));
+
+ let result = self.executor.exec(&self.name, cmd);
+
+ match &result {
+ Ok(output) if !output.trim().is_empty() => {
+ log::debug!("[{}] Output:\n{}", identity, output.trim());
+ }
+ Err(err) => {
+ log::error!("[{}] Failed:\n{}", identity, err);
+ }
+ _ => {}
+ }
+
+ result
+ }
+
+ /// Convenience wrapper for executing raw shell scripts inside the node.
+ pub fn exec_sh(&self, script: &str) -> Result<String, String> {
+ self.exec(&["sh", "-c", script])
+ }
+
+ /// Configures the global Git user name and email on the node.
+ ///
+ /// Uses a retry loop to prevent parallel tests from failing due to `gitconfig` file locks.
+ pub fn setup_identity(&self, name: &str, email: &str) -> Result<(), String> {
+ let name_esc = escape_sh(name);
+ let email_esc = escape_sh(email);
+
+ let mut retries = 10;
+ while retries > 0 {
+ if self
+ .exec_sh(&format!("git config --global user.name '{name_esc}'"))
+ .is_ok()
+ {
+ break;
+ }
+ retries -= 1;
+ thread::sleep(Duration::from_millis(100));
+ }
+ if retries == 0 {
+ return Err("Failed to set git config user.name".to_string());
+ }
+
+ let mut retries = 10;
+ while retries > 0 {
+ if self
+ .exec_sh(&format!("git config --global user.email '{email_esc}'"))
+ .is_ok()
+ {
+ break;
+ }
+ retries -= 1;
+ thread::sleep(Duration::from_millis(100));
+ }
+ if retries == 0 {
+ return Err("Failed to set git config user.email".to_string());
+ }
+
+ Ok(())
+ }
+
+ /// Initializes a new Radicle repository on this node.
+ ///
+ /// This method automatically appends a UUID to the repository name to ensure
+ /// parallel tests do not collide. It initializes a Git repository, creates the
+ /// specified number of commits, and initializes it as a Radicle project.
+ pub fn init_test_repo(
+ &self,
+ base_name: &str,
+ desc: &str,
+ commits: u32,
+ ) -> Result<Repository<E>, String> {
+ let (author, email) = self
+ .persona
+ .as_ref()
+ .ok_or("Persona not set! Call .as_alice() (or similar) before creating a repo.")?;
+
+ let uuid = Uuid::new_v4()
+ .to_string()
+ .chars()
+ .take(6)
+ .collect::<String>();
+ let repo_name = format!("{}-{}-{}", base_name, author, uuid);
+
+ let author_esc = escape_sh(author);
+ let email_esc = escape_sh(email);
+ let desc_esc = escape_sh(desc);
+
+ let script = format!(
+ "mkdir -p {repo_name} && \
+ cd {repo_name} && \
+ git init && \
+ git config user.name '{author_esc}' && \
+ git config user.email '{email_esc}' && \
+ echo 'commit 1' > file_1.txt && \
+ git add file_1.txt && \
+ git commit -m 'Commit 1' && \
+ rad init --name {repo_name} --description '{desc_esc}' --public --no-confirm"
+ );
+ self.exec_sh(&script)?;
+
+ if commits > 1 {
+ for i in 2..=commits {
+ let commit_script = format!(
+ "cd {repo_name} && \
+ echo 'commit {i}' > file_{i}.txt && \
+ git add file_{i}.txt && \
+ git commit -m 'Commit {i}' && \
+ git push rad master"
+ );
+ self.exec_sh(&commit_script)?;
+ }
+ }
+
+ let rid = self.exec_sh(&format!("cd {} && rad . 2>/dev/null", repo_name))?;
+
+ Ok(Repository {
+ node: self.clone(),
+ name: repo_name,
+ rid,
+ })
+ }
+
+ /// Waits until the Radicle node API is fully responsive.
+ pub fn wait_until_responsive(&self) -> Result<(), String> {
+ let mut retries = 30;
+ while retries > 0 {
+ if self.exec(&["rad", "node", "status"]).is_ok() {
+ return Ok(());
+ }
+ retries -= 1;
+ thread::sleep(Duration::from_secs(1));
+ }
+ Err(format!("Node {} did not become responsive", self.name))
+ }
+}
+
+/// Represents a Radicle repository residing on a specific node.
+///
+/// This struct provides ergonomic, high-level methods for interacting with
+/// the repository, such as syncing, managing issues, and handling patches.
+/// All commands executed through this struct are automatically run within
+/// the repository's directory on the node.
+#[derive(Clone, Debug)]
+pub struct Repository<E = KubectlExecutor> {
+ /// The node where this repository resides.
+ pub node: Node<E>,
+ /// The local directory name of the repository.
+ pub name: String,
+ /// The Radicle ID (RID) of the repository.
+ pub rid: String,
+}
diff --git a/simulation/radicle-simulation/tests/cross_version_suite/mod.rs b/simulation/radicle-simulation/tests/cross_version_suite/mod.rs
new file mode 100644
index 000000000..f9a7704cf
--- /dev/null
+++ b/simulation/radicle-simulation/tests/cross_version_suite/mod.rs
@@ -0,0 +1 @@
+pub mod repository_creation;
diff --git a/simulation/radicle-simulation/tests/cross_version_suite/repository_creation.rs b/simulation/radicle-simulation/tests/cross_version_suite/repository_creation.rs
new file mode 100644
index 000000000..efa84615d
--- /dev/null
+++ b/simulation/radicle-simulation/tests/cross_version_suite/repository_creation.rs
@@ -0,0 +1,35 @@
+use crate::network;
+use crate::require_network;
+use radicle_simulation::node::Node;
+use rstest::rstest;
+
+// TODO(Ade): The `negX` numbering is horrible... need a better naming convention.
+#[rstest]
+#[case::v_neg4_single_commit(network::peer_relative(-4, 0), 1)]
+#[case::v_neg4_multiple_commits(network::peer_relative(-4, 0), 3)]
+#[case::v_neg3_single_commit(network::peer_relative(-3, 0), 1)]
+#[case::v_neg3_multiple_commits(network::peer_relative(-3, 0), 3)]
+#[case::v_neg2_single_commit(network::peer_relative(-2, 0), 1)]
+#[case::v_neg2_multiple_commits(network::peer_relative(-2, 0), 3)]
+#[case::v_neg1_single_commit(network::peer_relative(-1, 0), 1)]
+#[case::v_neg1_multiple_commits(network::peer_relative(-1, 0), 3)]
+#[case::v_current_single_commit(network::peer_relative(0, 0), 1)]
+#[case::v_current_multiple_commits(network::peer_relative(0, 0), 3)]
+fn initializes_valid_repositories_with_single_and_multiple_commits(
+ #[case] mut node: Node,
+ #[case] commits: u32,
+) -> Result<(), String> {
+ let _guard = require_network();
+
+ node = node.as_alice()?;
+
+ let repo = node.init_test_repo("Repo", "Test Repo", commits)?;
+
+ assert!(
+ repo.rid.starts_with("rad:"),
+ "Expected valid RID, got: {}",
+ repo.rid
+ );
+
+ Ok(())
+}
diff --git a/simulation/radicle-simulation/tests/cross_version_suite_main.rs b/simulation/radicle-simulation/tests/cross_version_suite_main.rs
new file mode 100644
index 000000000..1cee9d5a1
--- /dev/null
+++ b/simulation/radicle-simulation/tests/cross_version_suite_main.rs
@@ -0,0 +1,7 @@
+use radicle_simulation::setup_network;
+
+// Sets up the network once for the whole suite.
+// It generates `crate::network` and `crate::require_network`.
+setup_network!("basic_cross_version_network");
+
+mod cross_version_suite;
commit ae47b99216ff8f47527e9d65fa22e8b871a68c4c
Author: Adrian Duke <adrian.duke@gmail.com>
Date: Mon Apr 13 12:31:30 2026 +0100
simulation: Introduce CUE Schema and build script
Refactors out common schema elements from `network.cue` as well as introducing
a naming convention for the topology members.
Adds a `build.rs` build script that parses the `instances/*.cue` files
and constructs a small rust file containing both functions for each
named node in the topology and a relative node selector that sorts all
the peer version numbers and maps onto [0..-inf], 0 being latest version,
-1 being version immediately before that... and so on.
Relative peer versioning should enable a continuous sliding window of
tests over the prior X versions, where as version named nodes allow
testing features at specific versions.
diff --git a/Cargo.lock b/Cargo.lock
index 0b4e6a7a6..35075849d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -973,6 +973,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f"
dependencies = [
"log",
+ "regex",
]
[[package]]
@@ -984,6 +985,7 @@ dependencies = [
"anstream 0.6.21",
"anstyle",
"env_filter",
+ "jiff",
"log",
]
@@ -1143,6 +1145,100 @@ dependencies = [
"num",
]
+[[package]]
+name = "futures"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-timer"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -1867,6 +1963,12 @@ dependencies = [
"bstr",
]
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
[[package]]
name = "group"
version = "0.13.0"
@@ -2081,6 +2183,12 @@ dependencies = [
"icu_properties",
]
+[[package]]
+name = "indenter"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
+
[[package]]
name = "indexmap"
version = "2.13.0"
@@ -3327,6 +3435,18 @@ dependencies = [
"windows 0.62.2",
]
+[[package]]
+name = "radicle-simulation"
+version = "0.1.0"
+dependencies = [
+ "env_logger",
+ "log",
+ "rstest",
+ "ruast",
+ "serde_json",
+ "uuid",
+]
+
[[package]]
name = "radicle-std-ext"
version = "0.2.0"
@@ -3539,6 +3659,12 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+[[package]]
+name = "relative-path"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
+
[[package]]
name = "rfc6979"
version = "0.4.0"
@@ -3570,6 +3696,44 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "rstest"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330"
+dependencies = [
+ "futures",
+ "futures-timer",
+ "rstest_macros",
+ "rustc_version",
+]
+
+[[package]]
+name = "rstest_macros"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25"
+dependencies = [
+ "cfg-if",
+ "glob",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "relative-path",
+ "rustc_version",
+ "syn 2.0.117",
+ "unicode-ident",
+]
+
+[[package]]
+name = "ruast"
+version = "0.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1eba4ae58b527c97246d526b90973ef18b61e98b6529fe6a33593e69009788"
+dependencies = [
+ "indenter",
+]
+
[[package]]
name = "rustc-demangle"
version = "0.1.27"
@@ -3954,6 +4118,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
[[package]]
name = "smallvec"
version = "1.15.1"
diff --git a/Cargo.toml b/Cargo.toml
index 868c7371b..5139dd04d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,6 @@
[workspace]
-members = ["crates/*"]
+members = ["crates/*", "simulation/radicle-simulation"]
+default-members = ["crates/*"]
resolver = "2"
[workspace.package]
diff --git a/simulation/instances/schema.cue b/simulation/instances/schema.cue
new file mode 100644
index 000000000..8af2a9bc2
--- /dev/null
+++ b/simulation/instances/schema.cue
@@ -0,0 +1,85 @@
+@if(!debug)
+
+package main
+
+//
+// Pre-calculated NIDs.
+//
+#BootstrapNIDs: {
+ "bootstrap-0": "z6MkhJ3cwzpAoNjFnJXWETSPHcDyw2HuBVEhgkyTfbjQHY1B"
+ "bootstrap-1": "z6MkjcaeSHhQVJU1UeXpnHHZ6mp67zDfQYNMDotHGxbrk7Nj"
+ "bootstrap-2": "z6MkjNGhuJvdp2noidRMLqco4jFnNNSWzCxSZH5nJV1pGrwQ"
+ "bootstrap-3": "z6MkpEsXUMSnmyfwdEVkAKijTxGy9WKmNoHWpoxxLM6bbz9M"
+}
+
+//
+// Shared configs
+//
+#SeedAddress: {
+ nid: string
+ name: string
+ role: string | *"bootstrap"
+ index: int | *0
+ out: "\(nid)@\(name)-\(index).\(role).default.svc.cluster.local:8776"
+}
+
+#BaseBootstrapSeedConfig: {
+ node: {
+ listen: ["0.0.0.0:8776"]
+ seedingPolicy: {
+ default: "allow"
+ scope: "all"
+ }
+ ...
+ }
+ ...
+}
+
+#BasePeerConfig: {
+ node: {
+ listen: []
+ peers: type: "dynamic"
+ connect: []
+ externalAddresses: [] // Explicitly override the default to be empty
+ log: "INFO"
+ relay: "auto"
+ limits: {
+ routingMaxSize: 1000
+ routingMaxAge: 604800
+ gossipMaxAge: 1209600
+ fetchConcurrency: 1
+ maxOpenFiles: 4096
+ rate: {
+ inbound: {fillRate: 5.0, capacity: 1024}
+ outbound: {fillRate: 10.0, capacity: 2048}
+ }
+ connection: {inbound: 128, outbound: 16}
+ fetchPackReceive: "500.0 MiB"
+ }
+ seedingPolicy: default: "block"
+ ...
+ }
+ ...
+}
+
+//
+// Topology Constraints
+//
+values: {
+ topology: {
+ //
+ // Naming conventions so bindings can be generated automatically
+ // See: [`radicle-simulations/build.rs`]
+ //
+
+ // Prefix enforcement `bootstrap-`, `peer-`, `seed-`
+ [=~"^(bootstrap|peer|seed)-[a-zA-Z0-9-]+$"]: {
+ role: string
+ replicas: int | *1
+ }
+ // Peer version enforcement
+ [=~"^peer-v[0-9]+-[0-9]+-[0-9]+$"]: {
+ role: "peer"
+ }
+ }
+}
diff --git a/simulation/radicle-simulation/Cargo.toml b/simulation/radicle-simulation/Cargo.toml
new file mode 100644
index 000000000..deeb63164
--- /dev/null
+++ b/simulation/radicle-simulation/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "radicle-simulation"
+version = "0.1.0"
+build = "build.rs"
+homepage.workspace = true
+repository.workspace = true
+license.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+uuid = { version = "1.8.0", features = ["v4"] }
+log = "0.4"
+env_logger = "0.11"
+
+[dev-dependencies]
+rstest = "0.19.0"
+serde_json = "1.0"
+ruast = "0.0.23"
+
+[build-dependencies]
+serde_json = "1.0"
+ruast = "0.0.23"
diff --git a/simulation/radicle-simulation/build.rs b/simulation/radicle-simulation/build.rs
new file mode 100644
index 000000000..3f2fa898a
--- /dev/null
+++ b/simulation/radicle-simulation/build.rs
@@ -0,0 +1,384 @@
+use std::env;
+use std::fs;
+use std::path::Path as StdPath;
+use std::process::Command;
+
+// Include the shared constants
+include!("src/constants.rs");
+
+// Helper to parse "peer-v1-5-0" into a tuple (1, 5, 0) for sorting
+fn parse_version(name: &str) -> (u32, u32, u32) {
+ let parts: Vec<&str> = name.split('-').collect();
+ if parts.len() >= 4 && parts[0] == "peer" && parts[1].starts_with('v') {
+ let major = parts[1][1..].parse().unwrap_or(0);
+ let minor = parts[2].parse().unwrap_or(0);
+ let patch = parts[3].parse().unwrap_or(0);
+ (major, minor, patch)
+ } else {
+ (0, 0, 0)
+ }
+}
+
+// Helper to generate the `radicle_simulation::node::Node` path
+fn node_type_path() -> ruast::Path {
+ ruast::Path::new(vec![
+ ruast::PathSegment::simple("radicle_simulation"),
+ ruast::PathSegment::simple("node"),
+ ruast::PathSegment::simple("Node"),
+ ])
+}
+
+// Helper to generate the `radicle_simulation::node::Node::new` path
+fn node_new_path() -> ruast::Path {
+ ruast::Path::new(vec![
+ ruast::PathSegment::simple("radicle_simulation"),
+ ruast::PathSegment::simple("node"),
+ ruast::PathSegment::simple("Node"),
+ ruast::PathSegment::simple("new"),
+ ])
+}
+
+// Helper to build the AST for `format!("{}-{}", node_name, index)`
+fn build_format_macro(node_name: &str) -> ruast::Expr {
+ let mut ts = ruast::TokenStream::new();
+ ts.extend(ruast::TokenStream::from(ruast::Expr::new(ruast::Lit::str(
+ format!("{}-{{}}", node_name),
+ ))));
+ ts.push(ruast::Token::Comma);
+ ts.extend(ruast::TokenStream::from(ruast::Expr::new(
+ ruast::Path::single("index"),
+ )));
+
+ ruast::Expr::new(ruast::MacCall::new(
+ ruast::Path::single("format"),
+ ruast::DelimArgs::parenthesis(ts),
+ ))
+}
+
+// Generates the AST for an individual node function
+// `pub fn <node_name>([index: usize]) -> radicle_simulation::node::Node`
+fn generate_node_function(node_name: &str, replicas: u64) -> ruast::Item {
+ let fn_name = node_name.replace("-", "_");
+ let mut base_fn = ruast::Fn::empty(fn_name);
+
+ base_fn
+ .fn_decl
+ .set_output(ruast::Type::Path(node_type_path()));
+
+ if replicas == 1 {
+ base_fn.body = Some(ruast::Block::from(ruast::Expr::new(node_new_path()).call(
+ vec![ruast::Expr::new(ruast::Lit::str(format!(
+ "{}-0",
+ node_name
+ )))],
+ )));
+ } else {
+ base_fn.fn_decl.add_input(ruast::Param::ident(
+ "index",
+ ruast::Type::Path(ruast::Path::single("usize")),
+ ));
+
+ let format_mac = build_format_macro(node_name);
+
+ base_fn.body = Some(ruast::Block::from(
+ ruast::Expr::new(node_new_path()).call(vec![format_mac]),
+ ));
+ }
+
+ ruast::Item::public(base_fn)
+}
+
+// Generates the AST for the `peer_relative` function
+// `pub fn peer_relative(offset: isize, index: usize) -> radicle_simulation::node::Node`
+fn generate_peer_relative_function(versioned_peers: &[(String, (u32, u32, u32))]) -> ruast::Item {
+ let len = versioned_peers.len() as isize;
+ let mut peer_relative_fn = ruast::Fn::empty("peer_relative");
+
+ peer_relative_fn.fn_decl.add_input(ruast::Param::ident(
+ "offset",
+ ruast::Type::Path(ruast::Path::single("isize")),
+ ));
+ peer_relative_fn.fn_decl.add_input(ruast::Param::ident(
+ "index",
+ ruast::Type::Path(ruast::Path::single("usize")),
+ ));
+ peer_relative_fn
+ .fn_decl
+ .set_output(ruast::Type::Path(node_type_path()));
+
+ if len > 0 {
+ let match_arms = versioned_peers
+ .iter()
+ .enumerate()
+ .map(|(i, (node_name, _))| {
+ let offset = (i as isize) - len + 1;
+ let format_mac = build_format_macro(node_name);
+
+ ruast::Arm::new(
+ ruast::Pat::Lit(ruast::Expr::new(ruast::Lit::int(offset.to_string()))),
+ None,
+ ruast::Expr::new(node_new_path()).call(vec![format_mac]),
+ )
+ })
+ .chain(std::iter::once({
+ // Build `panic!("Invalid relative offset. Available offsets: ...")`
+ let mut ts = ruast::TokenStream::new();
+ ts.extend(ruast::TokenStream::from(ruast::Expr::new(ruast::Lit::str(
+ format!(
+ "Invalid relative offset. Available offsets: {}..=0",
+ 1 - len
+ ),
+ ))));
+
+ let panic_mac = ruast::Expr::new(ruast::MacCall::new(
+ ruast::Path::single("panic"),
+ ruast::DelimArgs::parenthesis(ts),
+ ));
+
+ ruast::Arm::new(ruast::Pat::Wild, None, panic_mac)
+ }))
+ .collect::<Vec<_>>();
+
+ peer_relative_fn.body = Some(ruast::Block::from(ruast::Expr::new(ruast::Match::new(
+ ruast::Expr::new(ruast::Path::single("offset")),
+ match_arms,
+ ))));
+ } else {
+ // Build `panic!("No versioned peers available")`
+ let mut ts = ruast::TokenStream::new();
+ ts.extend(ruast::TokenStream::from(ruast::Expr::new(ruast::Lit::str(
+ "No versioned peers available",
+ ))));
+
+ let panic_mac = ruast::Expr::new(ruast::MacCall::new(
+ ruast::Path::single("panic"),
+ ruast::DelimArgs::parenthesis(ts),
+ ));
+
+ peer_relative_fn.body = Some(ruast::Block::from(panic_mac));
+ };
+
+ ruast::Item::public(peer_relative_fn)
+}
+
+/// Pure function to generate Rust bindings from a JSON string using `ruast`.
+fn generate_bindings(json_str: &str) -> Result<String, String> {
+ let topology: serde_json::Value =
+ serde_json::from_str(json_str).map_err(|e| format!("Invalid JSON: {}", e))?;
+
+ let obj = topology.as_object().cloned().unwrap_or_default();
+
+ let mut keys: Vec<String> = obj.keys().cloned().collect();
+ keys.sort();
+
+ // Generate the individual node functions
+ let node_items = keys.iter().map(|node_name| {
+ let config = &obj[node_name];
+ let replicas = config.get("replicas").and_then(|r| r.as_u64()).unwrap_or(1);
+ generate_node_function(node_name, replicas)
+ });
+
+ // Extract and sort versioned peers
+ let mut versioned_peers: Vec<(String, (u32, u32, u32))> = keys
+ .iter()
+ .filter(|name| name.starts_with("peer-v"))
+ .map(|name| {
+ let version = parse_version(name);
+ (name.clone(), version)
+ })
+ .collect();
+
+ versioned_peers.sort_by_key(|k| k.1);
+
+ // Generate the `peer_relative` function
+ let peer_relative_fn = generate_peer_relative_function(&versioned_peers);
+
+ // Assemble the final crate AST
+ let mut krate = ruast::Crate::new();
+
+ for item in node_items {
+ krate.add_item(item);
+ }
+ krate.add_item(peer_relative_fn);
+
+ Ok(krate.to_string())
+}
+
+fn is_valid_cue_instance(path: &StdPath) -> bool {
+ let is_cue = path.extension().and_then(|s| s.to_str()) == Some("cue");
+ let is_not_schema = path.file_name().and_then(|s| s.to_str()) != Some(SCHEMA_FILE);
+ is_cue && is_not_schema
+}
+
+#[allow(dead_code)]
+fn main() {
+ // Tell Cargo to recompile if anything in the instances directory changes.
+ //
+ // This may not trigger because of the top-level `Cargo.toml` containing
+ // `default-members` that doesn't include `radicle-simulation` (so that we
+ // don't run the simulation tests with a regular `cargo test`).
+ //
+ // You instead can use `$ cargo check -p radicle-simulation`
+ println!("cargo:rerun-if-changed={}", INSTANCES_DIR);
+
+ let out_dir = env::var_os("OUT_DIR").unwrap();
+ let out_path = StdPath::new(&out_dir);
+
+ let entries = fs::read_dir(INSTANCES_DIR).expect("Failed to read instances directory");
+
+ for entry in entries {
+ let entry = entry.expect("Failed to read directory entry");
+ let path = entry.path();
+
+ if path.is_file() && is_valid_cue_instance(&path) {
+ let file_stem = path.file_stem().unwrap().to_str().unwrap();
+ let cue_file = path.to_str().unwrap();
+
+ // Export the topology block from CUE to JSON
+ let output = Command::new("cue")
+ .args([
+ "export",
+ cue_file,
+ SCHEMA_CUE_PATH,
+ "-e",
+ CUE_TOPOLOGY_PATH,
+ "--out",
+ "json",
+ ])
+ .output()
+ .unwrap_or_else(|_| {
+ panic!(
+ "Failed to run `cue export` on {}. Ensure the CUE CLI is installed.",
+ cue_file
+ )
+ });
+
+ if !output.status.success() {
+ panic!(
+ "Failed to export {}:\n{}",
+ cue_file,
+ String::from_utf8_lossy(&output.stderr)
+ );
+ }
+
+ let json_str = String::from_utf8_lossy(&output.stdout);
+
+ match generate_bindings(&json_str) {
+ Ok(generated_code) => {
+ let dest_path = out_path.join(format!("{}_bindings.rs", file_stem));
+ fs::write(&dest_path, generated_code).unwrap();
+ }
+ Err(e) => {
+ println!(
+ "cargo:warning=Failed to generate bindings for {}: {}",
+ cue_file, e
+ );
+ }
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_version() {
+ assert_eq!(parse_version("peer-v1-5-0"), (1, 5, 0));
+ assert_eq!(parse_version("peer-v1-6-1"), (1, 6, 1));
+ assert_eq!(parse_version("peer-v2-0-0"), (2, 0, 0));
+
+ assert_eq!(parse_version("bootstrap-v1-6-1"), (0, 0, 0));
+ assert_eq!(parse_version("peer-latest"), (0, 0, 0));
+ assert_eq!(parse_version("random-string"), (0, 0, 0));
+ }
+
+ #[test]
+ fn test_generate_bindings_empty() {
+ let json = "{}";
+ let code = generate_bindings(json).unwrap();
+ assert!(code.contains("No versioned peers available"));
+ }
+
+ #[test]
+ fn test_generate_bindings_single_replica() {
+ let json = r#"{
+ "peer-v1-5-0": {
+ "replicas": 1
+ }
+ }"#;
+ let code = generate_bindings(json).unwrap();
+ let code_no_space = code.replace(" ", "");
+
+ assert!(code_no_space.contains("pubfnpeer_v1_5_0()->radicle_simulation::node::Node"));
+ assert!(code_no_space.contains("radicle_simulation::node::Node::new(\"peer-v1-5-0-0\")"));
+ assert!(
+ code_no_space.contains(
+ "0=>radicle_simulation::node::Node::new(format!(\"peer-v1-5-0-{}\",index))"
+ )
+ );
+ }
+
+ #[test]
+ fn test_generate_bindings_multi_replica() {
+ let json = r#"{
+ "peer-v1-7-0": {
+ "replicas": 3
+ }
+ }"#;
+ let code = generate_bindings(json).unwrap();
+ let code_no_space = code.replace(" ", "");
+
+ assert!(
+ code_no_space.contains("pubfnpeer_v1_7_0(index:usize)->radicle_simulation::node::Node")
+ );
+ assert!(
+ code_no_space
+ .contains("radicle_simulation::node::Node::new(format!(\"peer-v1-7-0-{}\",index))")
+ );
+ }
+
+ #[test]
+ fn test_generate_bindings_relative_offsets() {
+ let json = r#"{
+ "peer-v1-7-0": { "replicas": 1 },
+ "peer-v1-5-0": { "replicas": 1 },
+ "peer-v1-6-1": { "replicas": 1 }
+ }"#;
+ let code = generate_bindings(json).unwrap();
+ let code_no_space = code.replace(" ", "");
+
+ assert!(code_no_space.contains(
+ "-2=>radicle_simulation::node::Node::new(format!(\"peer-v1-5-0-{}\",index))"
+ ));
+ assert!(code_no_space.contains(
+ "-1=>radicle_simulation::node::Node::new(format!(\"peer-v1-6-1-{}\",index))"
+ ));
+ assert!(
+ code_no_space.contains(
+ "0=>radicle_simulation::node::Node::new(format!(\"peer-v1-7-0-{}\",index))"
+ )
+ );
+
+ assert!(code.contains("Available offsets: -2..=0"));
+ }
+
+ #[test]
+ fn test_is_valid_cue_instance() {
+ assert!(is_valid_cue_instance(StdPath::new("network.cue")));
+ assert!(is_valid_cue_instance(StdPath::new("phase1_network.cue")));
+ assert!(is_valid_cue_instance(StdPath::new(
+ "/some/path/network.cue"
+ )));
+
+ assert!(!is_valid_cue_instance(StdPath::new("schema.cue")));
+ assert!(!is_valid_cue_instance(StdPath::new(
+ "../instances/schema.cue"
+ )));
+
+ assert!(!is_valid_cue_instance(StdPath::new("network.json")));
+ assert!(!is_valid_cue_instance(StdPath::new("schema.json")));
+ assert!(!is_valid_cue_instance(StdPath::new("README.md")));
+ }
+}
diff --git a/simulation/radicle-simulation/src/constants.rs b/simulation/radicle-simulation/src/constants.rs
new file mode 100644
index 000000000..02427a81c
--- /dev/null
+++ b/simulation/radicle-simulation/src/constants.rs
@@ -0,0 +1,37 @@
+// Shared constants for the Radicle simulation testing framework.
+//
+// These constants are used by both the main test framework (`src/`) and the
+// build script (`build.rs`). Because `build.rs` is compiled separately, it
+// includes this file directly via the `include!` macro.
+
+macro_rules! instances_dir {
+ () => {
+ "../instances"
+ };
+}
+
+macro_rules! schema_file {
+ () => {
+ "schema.cue"
+ };
+}
+
+/// The directory containing the CUE network topology instances.
+pub const INSTANCES_DIR: &str = instances_dir!();
+
+/// Schema filename
+pub const SCHEMA_FILE: &str = schema_file!();
+
+/// The path to the shared CUE schema file.
+pub const SCHEMA_CUE_PATH: &str = concat!(instances_dir!(), "/", schema_file!());
+
+/// The CUE path used to extract the topology map during export.
+pub const CUE_TOPOLOGY_PATH: &str = "values.topology";
+
+/// The path to the Timoni module used to deploy the network.
+#[allow(unused)]
+pub const TIMONI_MODULE_PATH: &str = "../modules/radicle-node";
+
+/// The name of the Timoni instance deployed to the Kubernetes cluster.
+#[allow(unused)]
+pub const TIMONI_INSTANCE_NAME: &str = "radicle-network";
diff --git a/simulation/radicle-simulation/tests/build_tests.rs b/simulation/radicle-simulation/tests/build_tests.rs
new file mode 100644
index 000000000..92fa76f9d
--- /dev/null
+++ b/simulation/radicle-simulation/tests/build_tests.rs
@@ -0,0 +1,5 @@
+//
+// Load build.rs as a module so the tests can be ran
+//
+#[path = "../build.rs"]
+mod build;
commit b0969e62039b22c7790d235b378428caa41c4b4e
Author: Adrian Duke <adrian.duke@gmail.com>
Date: Thu Apr 16 15:29:56 2026 +0100
simulation/just: Rewrite inline bash scripts with calls to files
diff --git a/scripts/just/create-cluster.sh b/scripts/just/create-cluster.sh
index d7399407e..b3bb77b6f 100755
--- a/scripts/just/create-cluster.sh
+++ b/scripts/just/create-cluster.sh
@@ -10,7 +10,7 @@ then
echo "${CHECK}Creating Talos cluster '$CLUSTER_NAME' using $PROVISIONER...${NORMAL}"
mkdir -p "$CLUSTERS_DIR"
if [ "$PROVISIONER" = "qemu" ]; then
- sudo --preserve-env=HOME talosctl cluster create --name="$CLUSTER_NAME" "$PROVISIONER" --config-patch-controlplanes '{"cluster": {"allowSchedulingOnControlPlanes": true}}'
+ sudo --preserve-env=HOME,PATH talosctl cluster create --name="$CLUSTER_NAME" "$PROVISIONER" --config-patch-controlplanes '{"cluster": {"allowSchedulingOnControlPlanes": true}}'
else
talosctl cluster create --name="$CLUSTER_NAME" "$PROVISIONER" --config-patch-controlplanes '{"cluster": {"allowSchedulingOnControlPlanes": true}}'
fi
diff --git a/scripts/just/destroy-cluster.sh b/scripts/just/destroy-cluster.sh
index 886b81bdb..31b84a295 100755
--- a/scripts/just/destroy-cluster.sh
+++ b/scripts/just/destroy-cluster.sh
@@ -16,7 +16,7 @@ fi
echo "${CHECK}Destroying talos cluster '$CLUSTER_NAME'...${NORMAL}"
if [ "$PROVISIONER" = "qemu" ]
then
- sudo --preserve-env=HOME talosctl cluster destroy --name "$CLUSTER_NAME" --provisioner "$PROVISIONER"
+ sudo --preserve-env=HOME,PATH talosctl cluster destroy --name "$CLUSTER_NAME" --provisioner "$PROVISIONER"
else
talosctl cluster destroy --name "$CLUSTER_NAME" --provisioner "$PROVISIONER"
fi
diff --git a/scripts/just/ensure-ovmf.sh b/scripts/just/ensure-ovmf.sh
new file mode 100755
index 000000000..5847a37e5
--- /dev/null
+++ b/scripts/just/ensure-ovmf.sh
@@ -0,0 +1,11 @@
+#! /usr/bin/env bash
+set -e
+if [ -z "${OVMF_FD_PATH:-}" ]; then
+ # Not on NixOS / not using the devshell — assume OVMF is installed normally
+ exit 0
+fi
+if [ ! -f "/usr/share/OVMF/OVMF_CODE.fd" ]; then
+ echo "{{CHECK}}Symlinking OVMF firmware from Nix store into /usr/share/OVMF...{{NORMAL}}"
+ sudo mkdir -p /usr/share/OVMF
+ sudo ln -sf "$OVMF_FD_PATH"/* /usr/share/OVMF/
+fi
diff --git a/simulation/justfile b/simulation/justfile
index b1549a562..61419bb20 100644
--- a/simulation/justfile
+++ b/simulation/justfile
@@ -31,19 +31,7 @@ setup: configure-cluster
# Create the Talos cluster if it doesn't exist
[private]
create-cluster: (verify-tool "talosctl") ensure-ovmf
- #!/usr/bin/env bash
- set -e
- if [ ! -d "{{clusters_dir}}/{{cluster_name}}" ]; then
- echo "{{CHECK}}Creating Talos cluster '{{cluster_name}}' using {{provisioner}}...{{NORMAL}}"
- mkdir -p "{{clusters_dir}}"
- if [ "{{provisioner}}" = "qemu" ]; then
- sudo --preserve-env=HOME,PATH talosctl cluster create --name={{cluster_name}} {{provisioner}} --config-patch-controlplanes '{"cluster": {"allowSchedulingOnControlPlanes": true}}'
- else
- talosctl cluster create --name={{cluster_name}} {{provisioner}} --config-patch-controlplanes '{"cluster": {"allowSchedulingOnControlPlanes": true}}'
- fi
- else
- echo "{{SUCCESS}}Cluster '{{cluster_name}}' already exists.{{NORMAL}}"
- fi
+ @CHECK="{{CHECK}}" SUCCESS="{{SUCCESS}}" NORMAL="{{NORMAL}}" ../scripts/just/create-cluster.sh "{{clusters_dir}}" "{{cluster_name}}" "{{provisioner}}"
# Configure the Kubernetes cluster
[private]
@@ -67,17 +55,7 @@ start-network: (verify-tool "timoni") vendor-timoni-dependencies
# Vendor Timoni dependencies
[private]
vendor-timoni-dependencies: (verify-tool "timoni")
- #!/usr/bin/env bash
- set -e
- cd {{radicle_node_module}}
- if [ ! -d "{{module_pkg}}" ]; then
- echo "{{CHECK}}Fetching Timoni pkg files...{{NORMAL}}"
- timoni artifact pull oci://ghcr.io/stefanprodan/timoni/schemas -o cue.mod/pkg
- fi
- if [ ! -d "{{module_gen}}" ]; then
- echo "{{CHECK}}Fetching Timoni k8s gen files...{{NORMAL}}"
- timoni mod vendor k8s
- fi
+ @CHECK="{{CHECK}}" NORMAL="{{NORMAL}}" ../scripts/just/vendor-timoni-dependencies.sh "{{radicle_node_module}}" "{{module_pkg}}" "{{module_gen}}"
# Show cluster status
[group('inspect')]
@@ -107,62 +85,16 @@ delete-pvc: (verify-tool "kubectl")
# Destroy the Talos cluster and clean up kubeconfig
[group('delete')]
destroy: (verify-tool "kubectl") (verify-tool "talosctl") show-cluster
- #!/usr/bin/env bash
- set -e
- echo ""
- echo -n "Are you sure you want to destroy the cluster and remove kubeconfig entries? [y/N] "
- read answer
- if [ "${answer:-N}" != "y" ]; then
- echo "Aborted."
- exit 1
- fi
-
- echo "{{CHECK}}Destroying talos cluster '{{cluster_name}}'...{{NORMAL}}"
- if [ "{{provisioner}}" = "qemu" ]; then
- sudo --preserve-env=HOME,PATH talosctl cluster destroy --name {{cluster_name}} --provisioner {{provisioner}}
- else
- talosctl cluster destroy --name {{cluster_name}} --provisioner {{provisioner}}
- fi
-
- echo "{{CHECK}}Removing kube config entries...{{NORMAL}}"
- CONTEXT=$(kubectl config current-context 2>/dev/null || echo "")
- if [ -n "$CONTEXT" ]; then
- CLUSTER=$(echo "$CONTEXT" | cut -d '@' -f 2)
- kubectl config delete-context "$CONTEXT" || true
- kubectl config delete-cluster "$CLUSTER" || true
- kubectl config unset "users.$CONTEXT" || true
- fi
- echo "{{WARN}}Make sure you remove the '{{cluster_name}}' entry from: ~/.talos/config{{NORMAL}}"
- echo "{{SUCCESS}}Cluster destroyed.{{NORMAL}}"
+ @CHECK="{{CHECK}}" WARN="{{WARN}}" SUCCESS="{{SUCCESS}}" NORMAL="{{NORMAL}}" ../scripts/just/destroy-cluster.sh "{{cluster_name}}" "{{provisioner}}"
# Check if required tools are in PATH.
[private]
verify-tool tool package_name="":
- #!/usr/bin/env bash
- set -e
- if ! command -v {{tool}} >/dev/null 2>&1; then
- PKG="{{package_name}}"
- if [ -z "$PKG" ]; then
- PKG="{{tool}}"
- fi
- echo "{{ERROR}}Missing required tool: {{tool + NORMAL}}"
- echo "{{HINT}}Use your systems package manager to install '$PKG'.{{NORMAL}}"
- exit 1
- fi
+ @ERROR="{{ERROR}}" NORMAL="{{NORMAL}}" HINT="{{HINT}}" ../scripts/just/verify-tool.sh "{{tool}}" "{{package_name}}"
# Ensure OVMF firmware is discoverable by talosctl.
# On NixOS, OVMF lives in the Nix store — not in /usr/share/OVMF where
# talosctl expects it. We symlink the firmware files into place.
[private]
ensure-ovmf:
- #!/usr/bin/env bash
- set -e
- if [ -z "${OVMF_FD_PATH:-}" ]; then
- # Not on NixOS / not using the devshell — assume OVMF is installed normally
- exit 0
- fi
- if [ ! -f /usr/share/OVMF/OVMF_CODE.fd ]; then
- echo "{{CHECK}}Symlinking OVMF firmware from Nix store into /usr/share/OVMF...{{NORMAL}}"
- sudo mkdir -p /usr/share/OVMF
- sudo ln -sf "$OVMF_FD_PATH"/* /usr/share/OVMF/
- fi
+ @CHECK="{{CHECK}}" NORMAL="{{NORMAL}}" ../scripts/just/ensure-ovmf.sh
commit 55dfb9c16e9dd977e34ee4994f1e3f9ef5c356eb
Author: Adrian Duke <adrian.duke@gmail.com>
Date: Thu Apr 23 14:42:51 2026 +0100
simulation/just: Extract inline bash scripts to files
diff --git a/scripts/just/create-cluster.sh b/scripts/just/create-cluster.sh
new file mode 100755
index 000000000..d7399407e
--- /dev/null
+++ b/scripts/just/create-cluster.sh
@@ -0,0 +1,19 @@
+#! /usr/bin/env bash
+set -e
+
+CLUSTERS_DIR=$1
+CLUSTER_NAME=$2
+PROVISIONER=$3
+
+if [ ! -d "$CLUSTERS_DIR/$CLUSTER_NAME" ]
+then
+ echo "${CHECK}Creating Talos cluster '$CLUSTER_NAME' using $PROVISIONER...${NORMAL}"
+ mkdir -p "$CLUSTERS_DIR"
+ if [ "$PROVISIONER" = "qemu" ]; then
+ sudo --preserve-env=HOME talosctl cluster create --name="$CLUSTER_NAME" "$PROVISIONER" --config-patch-controlplanes '{"cluster": {"allowSchedulingOnControlPlanes": true}}'
+ else
+ talosctl cluster create --name="$CLUSTER_NAME" "$PROVISIONER" --config-patch-controlplanes '{"cluster": {"allowSchedulingOnControlPlanes": true}}'
+ fi
+else
+ echo "${SUCCESS}Cluster '$CLUSTER_NAME' already exists.${NORMAL}"
+fi
diff --git a/scripts/just/destroy-cluster.sh b/scripts/just/destroy-cluster.sh
new file mode 100755
index 000000000..886b81bdb
--- /dev/null
+++ b/scripts/just/destroy-cluster.sh
@@ -0,0 +1,34 @@
+#! /usr/bin/env bash
+set -e
+
+CLUSTER_NAME=$1
+PROVISIONER=$2
+
+echo ""
+echo -n "Are you sure you want to destroy the cluster and remove kubeconfig entries? [y/N] "
+read -r answer
+if [ "${answer:-N}" != "y" ]
+then
+ echo "Aborted."
+ exit 1
+fi
+
+echo "${CHECK}Destroying talos cluster '$CLUSTER_NAME'...${NORMAL}"
+if [ "$PROVISIONER" = "qemu" ]
+then
+ sudo --preserve-env=HOME talosctl cluster destroy --name "$CLUSTER_NAME" --provisioner "$PROVISIONER"
+else
+ talosctl cluster destroy --name "$CLUSTER_NAME" --provisioner "$PROVISIONER"
+fi
+
+echo "${CHECK}Removing kube config entries...${NORMAL}"
+CONTEXT=$(kubectl config current-context 2>/dev/null || echo "")
+if [ -n "$CONTEXT" ]
+then
+ CLUSTER=$(echo "$CONTEXT" | cut -d '@' -f 2)
+ kubectl config delete-context "$CONTEXT" || true
+ kubectl config delete-cluster "$CLUSTER" || true
+ kubectl config unset "users.$CONTEXT" || true
+fi
+echo "${WARN}Make sure you remove the '$CLUSTER_NAME' entry from: ~/.talos/config${NORMAL}"
+echo "${SUCCESS}Cluster destroyed.${NORMAL}"
diff --git a/scripts/just/vendor-timoni-dependencies.sh b/scripts/just/vendor-timoni-dependencies.sh
new file mode 100755
index 000000000..5cca870eb
--- /dev/null
+++ b/scripts/just/vendor-timoni-dependencies.sh
@@ -0,0 +1,18 @@
+#! /usr/bin/env bash
+set -e
+
+RADICLE_NODE_MODULE=$1
+MODULE_PKG=$2
+MODULE_GEN=$3
+
+cd "$RADICLE_NODE_MODULE"
+if [ ! -d "$MODULE_PKG" ]
+then
+ echo "${CHECK}Fetching Timoni pkg files...${NORMAL}"
+ timoni artifact pull oci://ghcr.io/stefanprodan/timoni/schemas -o cue.mod/pkg
+fi
+if [ ! -d "$MODULE_GEN" ]
+then
+ echo "${CHECK}Fetching Timoni k8s gen files...${NORMAL}"
+ timoni mod vendor k8s
+fi
Exit code: 0
shell: 'export RUSTDOCFLAGS=''-D warnings'' cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny warnings cargo build --all-targets --workspace cargo doc --workspace --no-deps --all-features cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name a5088b03-d3b5-4199-ba2d-8fffbc6c48ba -v /opt/radcis/ci.rad.levitte.org/cci/state/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/s:/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/w:/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/w -w /a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/s/script.sh
+ export 'RUSTDOCFLAGS=-D warnings'
+ RUSTDOCFLAGS='-D warnings'
+ cargo --version
info: syncing channel updates for '1.95-x86_64-unknown-linux-gnu'
info: latest update on 2026-04-16, rust version 1.95.0 (59807616e 2026-04-14)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-src'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-src'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
cargo 1.95.0 (f2d3ce0bd 2026-03-21)
+ rustc --version
rustc 1.95.0 (59807616e 2026-04-14)
+ cargo fmt --check
+ cargo clippy --all-targets --workspace -- --deny warnings
Updating crates.io index
Downloading crates ...
Downloaded generic-array v0.14.7
Downloaded futures-channel v0.3.32
Downloaded gix-utils v0.3.1
Downloaded adler2 v2.0.1
Downloaded indicatif v0.18.4
Downloaded iana-time-zone v0.1.65
Downloaded ff v0.13.1
Downloaded data-encoding-macro-internal v0.1.17
Downloaded colorchoice v1.0.5
Downloaded errno v0.3.14
Downloaded amplify_derive v4.0.1
Downloaded email_address v0.2.9
Downloaded filetime v0.2.27
Downloaded aes-gcm v0.10.3
Downloaded erased-serde v0.4.10
Downloaded amplify_syn v2.0.1
Downloaded aho-corasick v1.1.4
Downloaded fastrand v2.3.0
Downloaded gix-chunk v0.5.0
Downloaded addr2line v0.25.1
Downloaded gix-credentials v0.36.0
Downloaded faster-hex v0.10.0
Downloaded curve25519-dalek-derive v0.1.1
Downloaded anstyle-parse v0.2.7
Downloaded base16ct v0.2.0
Downloaded ascii v1.1.0
Downloaded normalize-line-endings v0.3.0
Downloaded amplify_num v0.5.3
Downloaded crypto-common v0.1.7
Downloaded displaydoc v0.2.5
Downloaded git2 v0.20.4
Downloaded gix-quote v0.6.2
Downloaded base-x v0.2.11
Downloaded heapless v0.8.0
Downloaded git-ref-format-core v0.6.0
Downloaded icu_locale_core v2.1.1
Downloaded keccak v0.1.6
Downloaded is_terminal_polyfill v1.70.2
Downloaded indenter v0.3.4
Downloaded gix-hash v0.22.1
Downloaded gix-prompt v0.13.1
Downloaded borrow-or-share v0.2.4
Downloaded pastey v0.2.1
Downloaded rand_chacha v0.9.0
Downloaded nonempty v0.9.0
Downloaded dyn-clone v1.0.20
Downloaded gix-traverse v0.52.0
Downloaded once_cell v1.21.4
Downloaded indexmap v2.13.0
Downloaded pkcs8 v0.10.2
Downloaded itertools v0.14.0
Downloaded num-cmp v0.1.0
Downloaded inout v0.1.4
Downloaded gix-actor v0.39.0
Downloaded phf_shared v0.11.3
Downloaded matchers v0.2.0
Downloaded gix-object v0.56.0
Downloaded pin-project-lite v0.2.17
Downloaded same-file v1.0.6
Downloaded qcheck v1.0.0
Downloaded itoa v1.0.17
Downloaded signals_receipts v0.2.5
Downloaded radicle-git-ext v0.12.0
Downloaded rustc-demangle v0.1.27
Downloaded scrypt v0.11.0
Downloaded num-rational v0.4.2
Downloaded ref-cast v1.0.25
Downloaded rand_core v0.9.5
Downloaded radicle-std-ext v0.2.0
Downloaded proc-macro2 v1.0.106
Downloaded relative-path v1.9.3
Downloaded sval_ref v2.17.0
Downloaded inquire v0.9.4
Downloaded shlex v1.3.0
Downloaded gix-pack v0.65.0
Downloaded quote v1.0.45
Downloaded signature v1.6.4
Downloaded walkdir v2.5.0
Downloaded ssh-encoding v0.2.0
Downloaded rand_core v0.6.4
Downloaded sval_dynamic v2.17.0
Downloaded parking_lot v0.12.5
Downloaded subtle v2.6.1
Downloaded sval_json v2.17.0
Downloaded ref-cast-impl v1.0.25
Downloaded siphasher v0.3.11
Downloaded structured-logger v1.0.5
Downloaded derive_more-impl v2.1.1
Downloaded snapbox-macros v0.3.10
Downloaded p256 v0.13.2
Downloaded universal-hash v0.5.1
Downloaded polyval v0.6.2
Downloaded tree-sitter-language v0.1.7
Downloaded rstest_macros v0.19.0
Downloaded tar v0.4.45
Downloaded tinyvec_macros v0.1.1
Downloaded schemars_derive v1.2.1
Downloaded hash32 v0.3.1
Downloaded typeid v1.0.3
Downloaded unit-prefix v0.5.2
Downloaded toml_datetime v0.7.5+spec-1.1.0
Downloaded timeago v0.4.2
Downloaded signal-hook-registry v1.4.8
Downloaded toml_writer v1.0.7+spec-1.1.0
Downloaded unicode-ident v1.0.24
Downloaded value-bag-serde1 v1.12.0
Downloaded tracing-core v0.1.36
Downloaded xattr v1.6.1
Downloaded uuid-simd v0.8.0
Downloaded yoke-derive v0.8.1
Downloaded schemars v1.2.1
Downloaded zmij v1.0.21
Downloaded writeable v0.6.2
Downloaded value-bag v1.12.0
Downloaded shell-words v1.1.1
Downloaded uuid v1.22.0
Downloaded tinyvec v1.11.0
Downloaded ssh-cipher v0.2.0
Downloaded serde_fmt v1.1.0
Downloaded test-log-macros v0.2.19
Downloaded libm v0.2.16
Downloaded ssh-key v0.6.7
Downloaded zerovec v0.11.5
Downloaded yansi v1.0.1
Downloaded tree-sitter-go v0.23.4
Downloaded sval_buffer v2.17.0
Downloaded utf8parse v0.2.2
Downloaded sval_nested v2.17.0
Downloaded unicode-normalization v0.1.25
Downloaded vcpkg v0.2.15
Downloaded vsimd v0.8.0
Downloaded unicode-display-width v0.3.0
Downloaded zeroize v1.8.2
Downloaded zlib-rs v0.6.3
Downloaded winnow v0.7.15
Downloaded tree-sitter-c v0.23.4
Downloaded sysinfo v0.37.2
Downloaded bloomy v1.2.0
Downloaded zerocopy v0.8.42
Downloaded tree-sitter-md v0.3.2
Downloaded regex-syntax v0.8.10
Downloaded rustix v1.1.4
Downloaded tree-sitter-typescript v0.23.2
Downloaded tree-sitter-rust v0.23.3
Downloaded object v0.37.3
Downloaded url v2.5.8
Downloaded tracing v0.1.44
Downloaded tree-sitter-bash v0.23.3
Downloaded zerotrie v0.2.3
Downloaded tree-sitter v0.24.7
Downloaded tracing-subscriber v0.3.23
Downloaded regex-automata v0.4.14
Downloaded unicode-width v0.2.2
Downloaded tree-sitter-python v0.23.6
Downloaded sqlite3-src v0.7.0
Downloaded unicode-segmentation v1.12.0
Downloaded syn v1.0.109
Downloaded version_check v0.9.5
Downloaded syn v2.0.117
Downloaded libc v0.2.183
Downloaded sha1-checked v0.10.0
Downloaded tree-sitter-ruby v0.23.1
Downloaded libz-sys v1.1.25
Downloaded typenum v1.19.0
Downloaded tokio v1.50.0
Downloaded sha3 v0.10.8
Downloaded tree-sitter-toml-ng v0.6.0
Downloaded tracing-log v0.2.0
Downloaded test-log v0.2.19
Downloaded tree-sitter-highlight v0.24.7
Downloaded snapbox v0.4.17
Downloaded chrono v0.4.44
Downloaded unarray v0.1.4
Downloaded sval v2.17.0
Downloaded strsim v0.11.1
Downloaded curve25519-dalek v4.1.3
Downloaded utf8_iter v1.0.4
Downloaded slab v0.4.12
Downloaded serde_json v1.0.149
Downloaded regex v1.12.3
Downloaded serde v1.0.228
Downloaded thiserror v1.0.69
Downloaded systemd-journal-logger v2.2.2
Downloaded simd-adler32 v0.3.8
Downloaded sharded-slab v0.1.7
Downloaded serde_derive v1.0.228
Downloaded thiserror-impl v2.0.18
Downloaded thiserror v2.0.18
Downloaded clap_builder v4.6.0
Downloaded synstructure v0.13.2
Downloaded sval_fmt v2.17.0
Downloaded ruast v0.0.23
Downloaded streaming-iterator v0.1.9
Downloaded spki v0.7.3
Downloaded socks5-client v0.4.2
Downloaded siphasher v1.0.2
Downloaded signature v2.2.0
Downloaded signal-hook-mio v0.2.5
Downloaded signal-hook v0.3.18
Downloaded sec1 v0.7.3
Downloaded libgit2-sys v0.18.3+1.9.2
Downloaded radicle-surf v0.27.1
Downloaded prodash v31.0.0
Downloaded p384 v0.13.1
Downloaded quick-error v1.2.3
Downloaded p521 v0.13.3
Downloaded num-bigint-dig v0.8.6
Downloaded toml v0.9.12+spec-1.1.0
Downloaded ssh-agent-lib v0.5.2
Downloaded socket2 v0.5.10
Downloaded ryu v1.0.23
Downloaded rustc_version v0.4.1
Downloaded proc-macro-error2 v2.0.1
Downloaded num-bigint v0.4.6
Downloaded crossterm v0.29.0
Downloaded zerovec-derive v0.11.2
Downloaded zerofrom-derive v0.1.6
Downloaded zerofrom v0.1.6
Downloaded yoke v0.8.1
Downloaded wait-timeout v0.2.1
Downloaded value-bag-sval2 v1.12.0
Downloaded tree-sitter-css v0.23.2
Downloaded sqlite3-sys v0.18.0
Downloaded sqlite v0.37.0
Downloaded similar v2.7.0
Downloaded serde_core v1.0.228
Downloaded rsa v0.9.10
Downloaded thiserror-impl v1.0.69
Downloaded rand v0.8.5
Downloaded mio v1.1.1
Downloaded memchr v2.8.0
Downloaded primeorder v0.13.6
Downloaded tree-sitter-json v0.24.8
Downloaded crypto-bigint v0.5.5
Downloaded tree-sitter-html v0.23.2
Downloaded rand_chacha v0.3.1
Downloaded cc v1.2.57
Downloaded sha2 v0.10.9
Downloaded rand_xorshift v0.4.0
Downloaded outref v0.5.2
Downloaded tinystr v0.8.2
Downloaded miniz_oxide v0.8.9
Downloaded thread_local v1.1.9
Downloaded sem_safe v0.2.1
Downloaded tempfile v3.27.0
Downloaded rfc6979 v0.4.0
Downloaded serde-untagged v0.1.9
Downloaded semver v1.0.27
Downloaded rustversion v1.0.22
Downloaded rstest v0.19.0
Downloaded sval_serde v2.17.0
Downloaded stable_deref_trait v1.2.1
Downloaded base64 v0.21.7
Downloaded arc-swap v1.8.2
Downloaded referencing v0.30.0
Downloaded ed25519-dalek v2.2.0
Downloaded serde_spanned v1.0.4
Downloaded secrecy v0.10.3
Downloaded rusty-fork v0.3.1
Downloaded num-traits v0.2.19
Downloaded pem-rfc7468 v0.7.0
Downloaded multibase v0.9.2
Downloaded sha1 v0.10.6
Downloaded icu_properties_data v2.1.2
Downloaded lock_api v0.4.14
Downloaded litrs v1.0.0
Downloaded flate2 v1.1.9
Downloaded convert_case v0.10.0
Downloaded chacha20poly1305 v0.10.1
Downloaded spin v0.9.8
Downloaded smallvec v1.15.1
Downloaded qcheck-macros v1.0.0
Downloaded pkcs1 v0.7.5
Downloaded base64 v0.22.1
Downloaded const-str v0.4.3
Downloaded fnv v1.0.7
Downloaded serde_derive_internals v0.29.1
Downloaded scopeguard v1.2.0
Downloaded pkg-config v0.3.32
Downloaded num-complex v0.4.6
Downloaded num v0.4.3
Downloaded gix-url v0.35.2
Downloaded gix-protocol v0.57.0
Downloaded emojis v0.6.4
Downloaded ed25519 v1.5.3
Downloaded crossbeam-utils v0.8.21
Downloaded salsa20 v0.10.2
Downloaded maybe-async v0.2.10
Downloaded log v0.4.29
Downloaded lexopt v0.3.2
Downloaded futures-sink v0.3.32
Downloaded fast-glob v0.3.3
Downloaded bit-vec v0.8.0
Downloaded proptest v1.10.0
Downloaded poly1305 v0.8.0
Downloaded gix-sec v0.13.1
Downloaded getrandom v0.4.2
Downloaded data-encoding v2.10.0
Downloaded gix-error v0.1.0
Downloaded portable-atomic v1.13.1
Downloaded jsonschema v0.30.0
Downloaded jiff-static v0.2.23
Downloaded idna v1.1.0
Downloaded gix-diff v0.58.0
Downloaded rand v0.9.2
Downloaded num-integer v0.1.46
Downloaded icu_normalizer_data v2.1.1
Downloaded gix-refspec v0.37.0
Downloaded ctr v0.9.2
Downloaded blowfish v0.9.1
Downloaded pbkdf2 v0.12.2
Downloaded gix-lock v21.0.1
Downloaded der v0.7.10
Downloaded crossbeam-channel v0.5.15
Downloaded proc-macro-error-attr2 v2.0.0
Downloaded heck v0.5.0
Downloaded gix-glob v0.24.0
Downloaded gix-features v0.46.1
Downloaded base64ct v1.8.3
Downloaded anstyle v1.0.14
Downloaded percent-encoding v2.3.2
Downloaded opaque-debug v0.3.1
Downloaded nu-ansi-term v0.50.3
Downloaded lazy_static v1.5.0
Downloaded anstyle-parse v1.0.0
Downloaded bytesize v2.3.1
Downloaded potential_utf v0.1.4
Downloaded gix-validate v0.11.0
Downloaded gix-trace v0.1.18
Downloaded pretty_assertions v1.4.1
Downloaded ppv-lite86 v0.2.21
Downloaded phf v0.11.3
Downloaded parking_lot_core v0.9.12
Downloaded gix-shallow v0.8.1
Downloaded gix-packetline v0.21.1
Downloaded find-msvc-tools v0.1.9
Downloaded num-iter v0.1.45
Downloaded jiff v0.2.23
Downloaded idna_adapter v1.2.1
Downloaded hashbrown v0.16.1
Downloaded gix-transport v0.54.0
Downloaded gix-revwalk v0.27.0
Downloaded gix-config-value v0.17.1
Downloaded futures v0.3.32
Downloaded gix-error v0.0.0
Downloaded data-encoding-macro v0.1.19
Downloaded gix-negotiate v0.27.0
Downloaded gix-hashtable v0.12.0
Downloaded futures-util v0.3.32
Downloaded human-panic v2.0.6
Downloaded ghash v0.5.1
Downloaded fancy-regex v0.14.0
Downloaded document-features v0.2.12
Downloaded ct-codecs v1.1.6
Downloaded match-lookup v0.1.2
Downloaded gix-object v0.55.0
Downloaded gix-date v0.13.0
Downloaded futures-executor v0.3.32
Downloaded env_logger v0.11.9
Downloaded glob v0.3.3
Downloaded gix-commitgraph v0.33.0
Downloaded gix-command v0.7.1
Downloaded git-ref-format-macro v0.6.0
Downloaded gimli v0.32.3
Downloaded memmap2 v0.9.10
Downloaded litemap v0.8.1
Downloaded humantime v2.3.0
Downloaded hmac v0.12.1
Downloaded gix-commitgraph v0.32.0
Downloaded futures-macro v0.3.32
Downloaded either v1.15.0
Downloaded crc32fast v1.5.0
Downloaded icu_provider v2.1.1
Downloaded git-ref-format v0.6.0
Downloaded escargot v0.5.15
Downloaded clap v4.6.0
Downloaded bitflags v2.11.0
Downloaded icu_properties v2.1.2
Downloaded derive_more v2.1.1
Downloaded colored v2.2.0
Downloaded ed25519 v2.2.3
Downloaded byteorder v1.5.0
Downloaded bit-set v0.8.0
Downloaded base256emoji v1.0.2
Downloaded noise-framework v0.4.0
Downloaded const-oid v0.9.6
Downloaded bytes v1.11.1
Downloaded block-buffer v0.10.4
Downloaded gix-ref v0.59.0
Downloaded gix-chunk v0.6.0
Downloaded getrandom v0.3.4
Downloaded fluent-uri v0.3.2
Downloaded cipher v0.4.4
Downloaded jobserver v0.1.34
Downloaded gix-revwalk v0.26.0
Downloaded futures-core v0.3.32
Downloaded ec25519 v0.1.0
Downloaded digest v0.10.7
Downloaded elliptic-curve v0.13.8
Downloaded icu_normalizer v2.1.1
Downloaded gix-revision v0.41.0
Downloaded fraction v0.15.3
Downloaded cyphergraphy v0.3.0
Downloaded clap_lex v1.1.0
Downloaded block-padding v0.3.3
Downloaded bcrypt-pbkdf v0.10.0
Downloaded icu_collections v2.1.1
Downloaded group v0.13.0
Downloaded gix-tempfile v21.0.1
Downloaded gix-fs v0.19.1
Downloaded gix-date v0.14.0
Downloaded gix-actor v0.38.0
Downloaded cypheraddr v0.4.1
Downloaded gix-path v0.11.1
Downloaded gix-odb v0.75.0
Downloaded getrandom v0.2.17
Downloaded futures-timer v3.0.3
Downloaded equivalent v1.0.2
Downloaded env_filter v1.0.0
Downloaded dunce v1.0.5
Downloaded backtrace v0.3.76
Downloaded futures-task v0.3.32
Downloaded form_urlencoded v1.2.2
Downloaded clap_derive v4.6.0
Downloaded bstr v1.12.1
Downloaded anyhow v1.0.102
Downloaded diff v0.1.13
Downloaded cpufeatures v0.2.17
Downloaded console v0.16.3
Downloaded cfg-if v1.0.4
Downloaded chacha20 v0.9.1
Downloaded autocfg v1.5.0
Downloaded cbc v0.1.2
Downloaded anstyle-query v1.1.5
Downloaded base32 v0.4.0
Downloaded aes v0.8.4
Downloaded clap_complete v4.6.0
Downloaded anstream v0.6.21
Downloaded amplify v4.9.0
Downloaded ahash v0.8.12
Downloaded aead v0.5.2
Downloaded ecdsa v0.16.9
Downloaded bytecount v0.6.9
Downloaded anstream v1.0.0
Downloaded futures-io v0.3.32
Downloaded linux-raw-sys v0.12.1
Downloaded cyphernet v0.5.3
Compiling libc v0.2.183
Compiling proc-macro2 v1.0.106
Compiling quote v1.0.45
Compiling unicode-ident v1.0.24
Checking cfg-if v1.0.4
Checking memchr v2.8.0
Checking zeroize v1.8.2
Compiling version_check v0.9.5
Compiling typenum v1.19.0
Compiling generic-array v0.14.7
Compiling syn v2.0.117
Checking getrandom v0.2.17
Checking rand_core v0.6.4
Checking regex-syntax v0.8.10
Checking aho-corasick v1.1.4
Checking crypto-common v0.1.7
Compiling jobserver v0.1.34
Compiling shlex v1.3.0
Compiling find-msvc-tools v0.1.9
Checking subtle v2.6.1
Checking regex-automata v0.4.14
Compiling cc v1.2.57
Compiling serde_core v1.0.228
Checking smallvec v1.15.1
Checking const-oid v0.9.6
Checking block-buffer v0.10.4
Checking digest v0.10.7
Checking cpufeatures v0.2.17
Compiling thiserror v2.0.18
Checking bstr v1.12.1
Checking stable_deref_trait v1.2.1
Checking fastrand v2.3.0
Compiling parking_lot_core v0.9.12
Checking scopeguard v1.2.0
Checking lock_api v0.4.14
Checking itoa v1.0.17
Compiling typeid v1.0.3
Checking bitflags v2.11.0
Checking parking_lot v0.12.5
Compiling erased-serde v0.4.10
Checking tinyvec_macros v0.1.1
Checking tinyvec v1.11.0
Compiling crc32fast v1.5.0
Checking gix-trace v0.1.18
Compiling thiserror-impl v2.0.18
Checking gix-validate v0.11.0
Checking unicode-normalization v0.1.25
Checking hashbrown v0.16.1
Checking serde_fmt v1.1.0
Checking gix-utils v0.3.1
Checking byteorder v1.5.0
Checking value-bag-serde1 v1.12.0
Checking value-bag v1.12.0
Compiling serde v1.0.228
Compiling serde_derive v1.0.228
Checking log v0.4.29
Checking same-file v1.0.6
Checking walkdir v2.5.0
Checking prodash v31.0.0
Compiling synstructure v0.13.2
Checking gix-path v0.11.1
Checking zlib-rs v0.6.3
Compiling heapless v0.8.0
Compiling zerofrom-derive v0.1.6
Checking hash32 v0.3.1
Compiling zmij v1.0.21
Compiling getrandom v0.4.2
Checking gix-features v0.46.1
Compiling yoke-derive v0.8.1
Checking faster-hex v0.10.0
Checking zerofrom v0.1.6
Compiling rustix v1.1.4
Compiling pkg-config v0.3.32
Compiling zerovec-derive v0.11.2
Checking sha1 v0.10.6
Checking yoke v0.8.1
Compiling autocfg v1.5.0
Checking linux-raw-sys v0.12.1
Compiling libm v0.2.16
Compiling num-traits v0.2.19
Checking sha1-checked v0.10.0
Checking zerovec v0.11.5
Compiling displaydoc v0.2.5
Checking gix-hash v0.22.1
Checking block-padding v0.3.3
Compiling zerocopy v0.8.42
Checking inout v0.1.4
Checking sha2 v0.10.9
Checking equivalent v1.0.2
Compiling serde_json v1.0.149
Checking indexmap v2.13.0
Checking cipher v0.4.4
Checking tinystr v0.8.2
Checking percent-encoding v2.3.2
Checking once_cell v1.21.4
Checking litemap v0.8.1
Checking writeable v0.6.2
Checking icu_locale_core v2.1.1
Checking zerotrie v0.2.3
Checking potential_utf v0.1.4
Compiling icu_properties_data v2.1.2
Compiling icu_normalizer_data v2.1.1
Compiling semver v1.0.27
Checking icu_provider v2.1.1
Checking icu_collections v2.1.1
Compiling rustc_version v0.4.1
Checking der v0.7.10
Compiling thiserror v1.0.69
Compiling ref-cast v1.0.25
Compiling syn v1.0.109
Compiling vcpkg v0.2.15
Checking icu_properties v2.1.2
Checking icu_normalizer v2.1.1
Checking tempfile v3.27.0
Checking ppv-lite86 v0.2.21
Compiling libz-sys v1.1.25
Compiling thiserror-impl v1.0.69
Compiling ref-cast-impl v1.0.25
Checking spin v0.9.8
Checking lazy_static v1.5.0
Checking idna_adapter v1.2.1
Checking num-integer v0.1.46
Checking hmac v0.12.1
Checking universal-hash v0.5.1
Checking opaque-debug v0.3.1
Checking utf8_iter v1.0.4
Compiling tree-sitter-language v0.1.7
Checking dyn-clone v1.0.20
Checking idna v1.1.0
Checking spki v0.7.3
Compiling libgit2-sys v0.18.3+1.9.2
Checking signature v2.2.0
Checking ff v0.13.1
Checking base16ct v0.2.0
Checking group v0.13.0
Checking sec1 v0.7.3
Checking rand_chacha v0.3.1
Checking form_urlencoded v1.2.2
Compiling serde_derive_internals v0.29.1
Checking crypto-bigint v0.5.5
Checking jiff v0.2.23
Checking elliptic-curve v0.13.8
Compiling schemars_derive v1.2.1
Compiling amplify_syn v2.0.1
Checking rand v0.8.5
Checking url v2.5.8
Checking num-iter v0.1.45
Checking aead v0.5.2
Checking signature v1.6.4
Checking ed25519 v1.5.3
Compiling amplify_derive v4.0.1
Checking schemars v1.2.1
Checking poly1305 v0.8.0
Checking rfc6979 v0.4.0
Checking chacha20 v0.9.1
Checking ct-codecs v1.1.6
Checking amplify_num v0.5.3
Checking ascii v1.1.0
Checking ec25519 v0.1.0
Checking ecdsa v0.16.9
Checking git-ref-format-core v0.6.0
Checking primeorder v0.13.6
Checking amplify v4.9.0
Checking polyval v0.6.2
Compiling curve25519-dalek v4.1.3
Checking base64ct v1.8.3
Compiling num-bigint-dig v0.8.6
Checking cyphergraphy v0.3.0
Checking pem-rfc7468 v0.7.0
Checking ghash v0.5.1
Checking pkcs8 v0.10.2
Checking pbkdf2 v0.12.2
Checking aes v0.8.4
Checking ctr v0.9.2
Compiling sqlite3-src v0.7.0
Compiling curve25519-dalek-derive v0.1.1
Checking keccak v0.1.6
Checking anstyle-query v1.1.5
Checking sha3 v0.10.8
Checking aes-gcm v0.10.3
Checking pkcs1 v0.7.5
Checking ssh-encoding v0.2.0
Checking ed25519 v2.2.3
Checking cbc v0.1.2
Checking blowfish v0.9.1
Checking base32 v0.4.0
Compiling data-encoding v2.10.0
Compiling crossbeam-utils v0.8.21
Checking rsa v0.9.10
Compiling data-encoding-macro-internal v0.1.17
Checking cypheraddr v0.4.1
Checking bcrypt-pbkdf v0.10.0
Checking ssh-cipher v0.2.0
Checking ed25519-dalek v2.2.0
Checking p256 v0.13.2
Checking p521 v0.13.3
Checking p384 v0.13.1
Checking chacha20poly1305 v0.10.1
Checking qcheck v1.0.0
Compiling match-lookup v0.1.2
Checking utf8parse v0.2.2
Checking const-str v0.4.3
Checking base256emoji v1.0.2
Checking data-encoding-macro v0.1.19
Checking noise-framework v0.4.0
Checking ssh-key v0.6.7
Checking socks5-client v0.4.2
Checking secrecy v0.10.3
Checking base-x v0.2.11
Checking multibase v0.9.2
Checking ssh-agent-lib v0.5.2
Checking crossbeam-channel v0.5.15
Checking cyphernet v0.5.3
Checking regex v1.12.3
Checking is_terminal_polyfill v1.70.2
Checking anstyle v1.0.14
Checking colorchoice v1.0.5
Checking winnow v0.7.15
Checking errno v0.3.14
Checking gix-hashtable v0.12.0
Checking nonempty v0.9.0
Checking siphasher v1.0.2
Checking anstyle-parse v0.2.7
Checking radicle-localtime v0.1.0 (/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/w/crates/radicle-localtime)
Checking radicle-git-metadata v0.2.0 (/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/w/crates/radicle-git-metadata)
Checking gix-error v0.1.0
Checking radicle-dag v0.10.0 (/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/w/crates/radicle-dag)
Checking memmap2 v0.9.10
Checking anstream v0.6.21
Checking radicle-git-ref-format v0.1.0 (/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/w/crates/radicle-git-ref-format)
Checking base64 v0.21.7
Compiling unicode-segmentation v1.12.0
Compiling signal-hook v0.3.18
Compiling radicle v0.24.0 (/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/w/crates/radicle)
Compiling convert_case v0.10.0
Checking signal-hook-registry v1.4.8
Checking serde-untagged v0.1.9
Checking gix-error v0.0.0
Checking bytesize v2.3.1
Checking fast-glob v0.3.3
Checking dunce v1.0.5
Compiling derive_more-impl v2.1.1
Checking gix-date v0.14.0
Checking mio v1.1.1
Checking sem_safe v0.2.1
Compiling litrs v1.0.0
Checking unicode-width v0.2.2
Compiling portable-atomic v1.13.1
Compiling document-features v0.2.12
Checking derive_more v2.1.1
Checking signals_receipts v0.2.5
Checking signal-hook-mio v0.2.5
Checking crossterm v0.29.0
Checking console v0.16.3
Checking gix-actor v0.39.0
Checking gix-date v0.13.0
Checking gix-fs v0.19.1
Checking unit-prefix v0.5.2
Checking indicatif v0.18.4
Checking gix-tempfile v21.0.1
Checking gix-actor v0.38.0
Checking gix-object v0.56.0
Checking inquire v0.9.4
Checking unicode-display-width v0.3.0
Checking radicle-signals v0.11.0 (/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/w/crates/radicle-signals)
Checking gix-chunk v0.6.0
Checking gix-quote v0.6.2
Checking gix-commitgraph v0.33.0
Checking gix-object v0.55.0
Checking gix-chunk v0.5.0
Checking iana-time-zone v0.1.65
Checking either v1.15.0
Checking shell-words v1.1.1
Checking pin-project-lite v0.2.17
Checking gix-command v0.7.1
Checking chrono v0.4.44
Checking gix-commitgraph v0.32.0
Checking gix-revwalk v0.27.0
Checking colored v2.2.0
Checking uuid v1.22.0
Compiling rustversion v1.0.22
Compiling object v0.37.3
Checking gix-revwalk v0.26.0
Checking gix-lock v21.0.1
Checking gix-url v0.35.2
Checking gix-config-value v0.17.1
Checking gix-sec v0.13.1
Checking gimli v0.32.3
Checking adler2 v2.0.1
Checking miniz_oxide v0.8.9
Checking gix-prompt v0.13.1
Checking gix-traverse v0.52.0
Checking addr2line v0.25.1
Checking gix-revision v0.41.0
Checking gix-diff v0.58.0
Checking gix-packetline v0.21.1
Checking gix-glob v0.24.0
Compiling tree-sitter v0.24.7
Compiling anyhow v1.0.102
Checking rustc-demangle v0.1.27
Checking backtrace v0.3.76
Checking gix-refspec v0.37.0
Checking gix-transport v0.54.0
Checking gix-pack v0.65.0
Checking arc-swap v1.8.2
Checking gix-credentials v0.36.0
Checking gix-ref v0.59.0
Checking gix-shallow v0.8.1
Checking gix-negotiate v0.27.0
Compiling maybe-async v0.2.10
Compiling proc-macro-error-attr2 v2.0.0
Compiling simd-adler32 v0.3.8
Compiling getrandom v0.3.4
Checking gix-protocol v0.57.0
Compiling proc-macro-error2 v2.0.1
Checking gix-odb v0.75.0
Compiling xattr v1.6.1
Compiling filetime v0.2.27
Checking anstyle-parse v1.0.0
Checking bytes v1.11.1
Checking anstream v1.0.0
Compiling tar v0.4.45
Compiling flate2 v1.1.9
Compiling git-ref-format-macro v0.6.0
Checking snapbox-macros v0.3.10
Checking salsa20 v0.10.2
Checking siphasher v0.3.11
Compiling heck v0.5.0
Checking clap_lex v1.1.0
Checking similar v2.7.0
Checking streaming-iterator v0.1.9
Checking normalize-line-endings v0.3.0
Checking strsim v0.11.1
Checking snapbox v0.4.17
Checking clap_builder v4.6.0
Compiling clap_derive v4.6.0
Checking bloomy v1.2.0
Compiling radicle-surf v0.27.1
Checking scrypt v0.11.0
Checking git-ref-format v0.6.0
Checking systemd-journal-logger v2.2.2
Checking serde_spanned v1.0.4
Checking toml_datetime v0.7.5+spec-1.1.0
Compiling tree-sitter-rust v0.23.3
Compiling tree-sitter-toml-ng v0.6.0
Compiling tree-sitter-typescript v0.23.2
Compiling tree-sitter-c v0.23.4
Compiling tree-sitter-go v0.23.4
Compiling tree-sitter-bash v0.23.3
Compiling tree-sitter-python v0.23.6
Compiling tree-sitter-ruby v0.23.1
Compiling tree-sitter-md v0.3.2
Compiling tree-sitter-json v0.24.8
Compiling tree-sitter-html v0.23.2
Compiling tree-sitter-css v0.23.2
Checking radicle-std-ext v0.2.0
Checking toml_writer v1.0.7+spec-1.1.0
Checking toml v0.9.12+spec-1.1.0
Checking sqlite3-sys v0.18.0
Checking sqlite v0.37.0
Checking radicle-crypto v0.17.0 (/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/w/crates/radicle-crypto)
Checking clap v4.6.0
Checking tokio v1.50.0
Checking env_filter v1.0.0
Checking sysinfo v0.37.2
Compiling radicle-node v0.20.0 (/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/w/crates/radicle-node)
Checking futures-sink v0.3.32
Checking yansi v1.0.1
Checking futures-core v0.3.32
Checking diff v0.1.13
Compiling radicle-cli v0.21.0 (/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/w/crates/radicle-cli)
Checking pretty_assertions v1.4.1
Checking futures-channel v0.3.32
Checking structured-logger v1.0.5
Checking human-panic v2.0.6
Checking env_logger v0.11.9
Checking clap_complete v4.6.0
Checking radicle-systemd v0.13.0 (/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/w/crates/radicle-systemd)
Checking tree-sitter-highlight v0.24.7
Checking itertools v0.14.0
Compiling qcheck-macros v1.0.0
Compiling futures-macro v0.3.32
Checking socket2 v0.5.10
Checking timeago v0.4.2
Compiling escargot v0.5.15
Checking futures-io v0.3.32
Checking humantime v2.3.0
Compiling indenter v0.3.4
Checking lexopt v0.3.2
Checking futures-task v0.3.32
Checking slab v0.4.12
Compiling ruast v0.0.23
Checking futures-util v0.3.32
Compiling rstest_macros v0.19.0
Checking bit-vec v0.8.0
Checking bit-set v0.8.0
Compiling radicle-simulation v0.1.0 (/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/w/simulation/radicle-simulation)
Checking futures-executor v0.3.32
Checking rand_core v0.9.5
Checking num-bigint v0.4.6
Compiling ahash v0.8.12
Compiling glob v0.3.3
Compiling relative-path v1.9.3
Checking num-rational v0.4.2
error: failed to run custom build command for `radicle-simulation v0.1.0 (/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/w/simulation/radicle-simulation)`
Caused by:
process didn't exit successfully: `/a5088b03-d3b5-4199-ba2d-8fffbc6c48ba/w/target/debug/build/radicle-simulation-9a9c48226515947f/build-script-build` (exit status: 101)
--- stdout
cargo:rerun-if-changed=../instances
--- stderr
thread 'main' (5156) panicked at simulation/radicle-simulation/build.rs:250:21:
Failed to run `cue export` on ../instances/basic_cross_version_network.cue. Ensure the CUE CLI is installed.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
warning: build failed, waiting for other jobs to finish...
Exit code: 101
{
"response": "finished",
"result": "failure"
}