From e4b9990adc3dc2a04ca325b12bc96a55d9a09aa7 Mon Sep 17 00:00:00 2001 From: Anthony Shew Date: Fri, 19 Sep 2025 22:21:28 -0600 Subject: [PATCH 1/3] fix: output valid turbo.json for prune with Boundaries definition --- crates/turborepo-lib/src/boundaries/config.rs | 2 + crates/turborepo-lib/src/turbo_json/mod.rs | 90 +++++++++++++++++++ ..._lib__turbo_json__tests__package_rule.snap | 3 +- ...bo_json__tests__tags_and_dependencies.snap | 3 +- 4 files changed, 94 insertions(+), 4 deletions(-) diff --git a/crates/turborepo-lib/src/boundaries/config.rs b/crates/turborepo-lib/src/boundaries/config.rs index 1f0e511d3aa86..a2a657c890ccb 100644 --- a/crates/turborepo-lib/src/boundaries/config.rs +++ b/crates/turborepo-lib/src/boundaries/config.rs @@ -31,6 +31,8 @@ pub struct Rule { #[derive(Serialize, Default, Debug, Clone, Iterable, Deserializable, PartialEq)] pub struct Permissions { + #[serde(skip_serializing_if = "Option::is_none")] pub allow: Option>>>, + #[serde(skip_serializing_if = "Option::is_none")] pub deny: Option>>>, } diff --git a/crates/turborepo-lib/src/turbo_json/mod.rs b/crates/turborepo-lib/src/turbo_json/mod.rs index f72f19d71587b..ada8e5bc053eb 100644 --- a/crates/turborepo-lib/src/turbo_json/mod.rs +++ b/crates/turborepo-lib/src/turbo_json/mod.rs @@ -1102,4 +1102,94 @@ mod tests { assert!(raw_config.future_flags.is_none()); } } + + #[test] + fn test_boundaries_permissions_serialization_skip_none() { + // Test that None values in Permissions struct are not serialized as null + let json_with_partial_permissions = r#"{ + "boundaries": { + "dependencies": { + "allow": ["package-a"] + } + } + }"#; + + // Parse the JSON + let parsed: RawTurboJson = + RawRootTurboJson::parse(json_with_partial_permissions, "turbo.json") + .unwrap() + .into(); + + // Serialize it back to JSON + let serialized = serde_json::to_string(&parsed).unwrap(); + + // The serialized JSON should not contain "deny":null + assert!(!serialized.contains("\"deny\":null")); + assert!(!serialized.contains("\"deny\": null")); + + // But it should contain the allow field + assert!(serialized.contains("\"allow\"")); + + // Test that we can parse the serialized JSON again without errors + let reparsed: RawTurboJson = RawRootTurboJson::parse(&serialized, "turbo.json") + .unwrap() + .into(); + + // Verify the structure is preserved + assert!(reparsed.boundaries.is_some()); + let boundaries = reparsed.boundaries.as_ref().unwrap(); + assert!(boundaries.dependencies.is_some()); + let deps = boundaries.dependencies.as_ref().unwrap(); + assert!(deps.allow.is_some()); + assert!(deps.deny.is_none()); // This should be None, not null + } + + #[test] + fn test_prune_tasks_preserves_boundaries_structure() { + // Test the specific scenario from the bug report + let json_with_boundaries = r#"{ + "tasks": { + "build": {}, + "app-a#build": {} + }, + "boundaries": { + "dependencies": { + "allow": [] + } + } + }"#; + + let parsed: RawTurboJson = RawRootTurboJson::parse(json_with_boundaries, "turbo.json") + .unwrap() + .into(); + + // Simulate the prune operation + let pruned = parsed.prune_tasks(&["app-a"]); + + // Serialize the pruned config + let serialized = serde_json::to_string_pretty(&pruned).unwrap(); + + // The serialized JSON should not contain "deny":null + assert!(!serialized.contains("\"deny\":null")); + assert!(!serialized.contains("\"deny\": null")); + + // Parse the serialized config to ensure it's valid + let reparsed_result = RawRootTurboJson::parse(&serialized, "turbo.json"); + assert!( + reparsed_result.is_ok(), + "Failed to parse pruned config: {:?}", + reparsed_result.err() + ); + + let reparsed: RawTurboJson = reparsed_result.unwrap().into(); + + // Verify boundaries structure is preserved + assert!(reparsed.boundaries.is_some()); + let boundaries = reparsed.boundaries.as_ref().unwrap(); + assert!(boundaries.dependencies.is_some()); + let deps = boundaries.dependencies.as_ref().unwrap(); + assert!(deps.allow.is_some()); + assert!(deps.deny.is_none()); // This should be None, not serialized as + // null + } } diff --git a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__package_rule.snap b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__package_rule.snap index 916a08b05388f..95dfba3660f99 100644 --- a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__package_rule.snap +++ b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__package_rule.snap @@ -6,7 +6,6 @@ expression: raw_boundaries_config "dependencies": { "allow": [ "my-package" - ], - "deny": null + ] } } diff --git a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__tags_and_dependencies.snap b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__tags_and_dependencies.snap index 0bc9b31481351..1e5d9c448becb 100644 --- a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__tags_and_dependencies.snap +++ b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__tags_and_dependencies.snap @@ -8,8 +8,7 @@ expression: raw_boundaries_config "dependencies": { "allow": [ "my-package" - ], - "deny": null + ] } } } From e54a915c37e04ec5da76f3593326df04062d657e Mon Sep 17 00:00:00 2001 From: Anthony Shew Date: Fri, 19 Sep 2025 22:29:10 -0600 Subject: [PATCH 2/3] Apply suggestions from code review --- crates/turborepo-lib/src/turbo_json/mod.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/crates/turborepo-lib/src/turbo_json/mod.rs b/crates/turborepo-lib/src/turbo_json/mod.rs index ada8e5bc053eb..591044cd94cdd 100644 --- a/crates/turborepo-lib/src/turbo_json/mod.rs +++ b/crates/turborepo-lib/src/turbo_json/mod.rs @@ -1105,7 +1105,6 @@ mod tests { #[test] fn test_boundaries_permissions_serialization_skip_none() { - // Test that None values in Permissions struct are not serialized as null let json_with_partial_permissions = r#"{ "boundaries": { "dependencies": { @@ -1114,23 +1113,14 @@ mod tests { } }"#; - // Parse the JSON let parsed: RawTurboJson = RawRootTurboJson::parse(json_with_partial_permissions, "turbo.json") .unwrap() .into(); - // Serialize it back to JSON let serialized = serde_json::to_string(&parsed).unwrap(); // The serialized JSON should not contain "deny":null - assert!(!serialized.contains("\"deny\":null")); - assert!(!serialized.contains("\"deny\": null")); - - // But it should contain the allow field - assert!(serialized.contains("\"allow\"")); - - // Test that we can parse the serialized JSON again without errors let reparsed: RawTurboJson = RawRootTurboJson::parse(&serialized, "turbo.json") .unwrap() .into(); @@ -1146,7 +1136,6 @@ mod tests { #[test] fn test_prune_tasks_preserves_boundaries_structure() { - // Test the specific scenario from the bug report let json_with_boundaries = r#"{ "tasks": { "build": {}, @@ -1169,10 +1158,6 @@ mod tests { // Serialize the pruned config let serialized = serde_json::to_string_pretty(&pruned).unwrap(); - // The serialized JSON should not contain "deny":null - assert!(!serialized.contains("\"deny\":null")); - assert!(!serialized.contains("\"deny\": null")); - // Parse the serialized config to ensure it's valid let reparsed_result = RawRootTurboJson::parse(&serialized, "turbo.json"); assert!( From 2df77e1bb8163b46087eec4418c0d088d5140e0c Mon Sep 17 00:00:00 2001 From: Anthony Shew Date: Fri, 19 Sep 2025 22:33:20 -0600 Subject: [PATCH 3/3] WIP --- crates/turborepo-lib/src/turbo_json/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/turborepo-lib/src/turbo_json/mod.rs b/crates/turborepo-lib/src/turbo_json/mod.rs index ada8e5bc053eb..b42f6fda5a25d 100644 --- a/crates/turborepo-lib/src/turbo_json/mod.rs +++ b/crates/turborepo-lib/src/turbo_json/mod.rs @@ -1190,6 +1190,6 @@ mod tests { let deps = boundaries.dependencies.as_ref().unwrap(); assert!(deps.allow.is_some()); assert!(deps.deny.is_none()); // This should be None, not serialized as - // null + // null } }