fix(idl-gen): sort types array for deterministic output

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.
This commit is contained in:
r4bbit 2026-06-16 10:22:10 +02:00
parent 7461c9552b
commit b0ac30039b
4 changed files with 47 additions and 19 deletions

1
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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"

View File

@ -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);