diff --git a/crates/turborepo-ci/src/lib.rs b/crates/turborepo-ci/src/lib.rs index 01ce4d9a7f3ce..a1cf077bef753 100644 --- a/crates/turborepo-ci/src/lib.rs +++ b/crates/turborepo-ci/src/lib.rs @@ -215,4 +215,38 @@ mod tests { } } } + + #[test] + fn test_gitlab_ci_group_name_sanitization() { + use chrono::DateTime; + + let gitlab_vendor = get_vendor("GitLab CI"); + let behavior = gitlab_vendor.behavior.as_ref().unwrap(); + + // Test with a package name containing @ and / + let group_name = "@organisation/package:build".to_string(); + let start_time = DateTime::from_timestamp(1234567890, 0).unwrap(); + let end_time = DateTime::from_timestamp(1234567900, 0).unwrap(); + + let start_fn = (behavior.group_prefix)(group_name.clone()); + let end_fn = (behavior.group_suffix)(group_name.clone()); + + let start_output = start_fn(start_time); + let end_output = end_fn(end_time); + + // The section identifier should be sanitized (@ -> at, / -> -) + assert!(start_output.contains("section_start:1234567890:at-organisation-package:build")); + assert!(end_output.contains("section_end:1234567900:at-organisation-package:build")); + + // The description should contain the original group name + assert!(start_output.contains("@organisation/package:build")); + + // Test with a simple package name (should work unchanged) + let simple_group_name = "simple-package:build".to_string(); + let simple_start_fn = (behavior.group_prefix)(simple_group_name.clone()); + let simple_start_output = simple_start_fn(start_time); + + assert!(simple_start_output.contains("section_start:1234567890:simple-package:build")); + assert!(simple_start_output.contains("simple-package:build")); + } } diff --git a/crates/turborepo-ci/src/vendors.rs b/crates/turborepo-ci/src/vendors.rs index a1f4d60db5b24..8c1496eb5ec9d 100644 --- a/crates/turborepo-ci/src/vendors.rs +++ b/crates/turborepo-ci/src/vendors.rs @@ -299,8 +299,12 @@ pub(crate) fn get_vendors() -> &'static [Vendor] { |group_name| { Arc::new(move |start_time| { let timestamp = start_time.timestamp(); + // GitLab CI section names only allow /a-zA-Z0-9._-/ characters + // Replace @ with 'at' and / with '-' for section identifier + let sanitized_group_name = + group_name.replace('@', "at-").replace('/', "-"); format!( - "\\e[0Ksection_start:{timestamp}:{group_name}\\r\\ + "\\e[0Ksection_start:{timestamp}:{sanitized_group_name}\\r\\ e[0K{group_name}" ) }) @@ -308,7 +312,12 @@ pub(crate) fn get_vendors() -> &'static [Vendor] { |group_name| { Arc::new(move |end_time| { let timestamp = end_time.timestamp(); - format!("\\e[0Ksection_end:{timestamp}:{group_name}\\r\\e[0K") + // Use the same sanitization for section end + let sanitized_group_name = + group_name.replace('@', "at-").replace('/', "-"); + format!( + "\\e[0Ksection_end:{timestamp}:{sanitized_group_name}\\r\\e[0K" + ) }) }, )),