From b0ac30039b054ef0711433aa8ab6ef2506b6ef26 Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:22:10 +0200 Subject: [PATCH] fix(idl-gen): sort types array for deterministic output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The IDL `types` array was emitted in HashMap iteration order by spel-framework-core, which is non-deterministic across processes. Two independent regenerations of the same source could therefore disagree on type ordering, producing different bytes. This is what makes the check-idl CI job flaky: a PR's committed IDL is generated locally with one ordering, but CI regenerates with a different ordering and the diff fails — including PRs that were green when posted then breaking main after merge. Sort the top-level `types` array by name before serializing so output is byte-stable regardless of where idl-gen runs. Enable serde_json's `preserve_order` feature so the Value round-trip preserves struct-field key order (otherwise all keys would alphabetize and churn every artifact). Only top-level `types` was unstable; variants and fields already follow source order. Committed artifacts are unchanged — they happened to already be in sorted order. --- Cargo.lock | 1 + artifacts/stablecoin-idl.json | 24 +++++++++++------------ tools/idl-gen/Cargo.toml | 5 ++++- tools/idl-gen/src/main.rs | 36 +++++++++++++++++++++++++++++------ 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e58ef1f..b69078c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2850,6 +2850,7 @@ version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ + "indexmap 2.14.0", "itoa", "memchr", "serde", diff --git a/artifacts/stablecoin-idl.json b/artifacts/stablecoin-idl.json index f90a1a5..b08a75f 100644 --- a/artifacts/stablecoin-idl.json +++ b/artifacts/stablecoin-idl.json @@ -326,6 +326,18 @@ } ], "types": [ + { + "name": "MetadataStandard", + "kind": "enum", + "variants": [ + { + "name": "Simple" + }, + { + "name": "Expanded" + } + ] + }, { "name": "ObservationEntry", "kind": "struct", @@ -339,18 +351,6 @@ "type": "i64" } ] - }, - { - "name": "MetadataStandard", - "kind": "enum", - "variants": [ - { - "name": "Simple" - }, - { - "name": "Expanded" - } - ] } ], "instruction_type": "stablecoin_core::Instruction" diff --git a/tools/idl-gen/Cargo.toml b/tools/idl-gen/Cargo.toml index ddcdc13..d8c6c19 100644 --- a/tools/idl-gen/Cargo.toml +++ b/tools/idl-gen/Cargo.toml @@ -14,5 +14,8 @@ path = "src/main.rs" spel-framework-core = { git = "https://github.com/logos-co/spel.git", tag = "v0.3.0", features = [ "idl-gen", ] } -serde_json = "1.0" +# `preserve_order` keeps object keys in struct-declaration order when +# round-tripping through serde_json::Value (see main.rs), so the only +# reordering we apply is sorting the `types` array. +serde_json = { version = "1.0", features = ["preserve_order"] } toml = "0.8" diff --git a/tools/idl-gen/src/main.rs b/tools/idl-gen/src/main.rs index bfedda7..6325190 100644 --- a/tools/idl-gen/src/main.rs +++ b/tools/idl-gen/src/main.rs @@ -16,13 +16,37 @@ fn main() { let dep_dirs = find_path_dep_dirs(&path); match spel_framework_core::idl_gen::generate_idl_from_file_with_deps(&path, &dep_dirs) { - Ok(idl) => match serde_json::to_string_pretty(&idl) { - Ok(json) => println!("{json}"), - Err(e) => { - eprintln!("Error serializing IDL to JSON: {e}"); - process::exit(1); + Ok(idl) => { + // spel-framework emits the top-level `types` array in HashMap + // iteration order, which is non-deterministic across processes. + // Sort it by name so regenerated IDL is byte-stable regardless of + // where it runs (local `make idl` vs CI vs another contributor). + let mut value = match serde_json::to_value(&idl) { + Ok(value) => value, + Err(e) => { + eprintln!("Error converting IDL to JSON value: {e}"); + process::exit(1); + } + }; + if let Some(types) = value.get_mut("types").and_then(|t| t.as_array_mut()) { + types.sort_by(|a, b| { + let name = |v: &serde_json::Value| { + v.get("name") + .and_then(|n| n.as_str()) + .unwrap_or("") + .to_owned() + }; + name(a).cmp(&name(b)) + }); } - }, + match serde_json::to_string_pretty(&value) { + Ok(json) => println!("{json}"), + Err(e) => { + eprintln!("Error serializing IDL to JSON: {e}"); + process::exit(1); + } + } + } Err(e) => { eprintln!("Error: {e}"); process::exit(1);