From d43c2a51c20a8b9b6a8d35fcf30fc880d18928d0 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 27 Nov 2025 16:48:30 +0100 Subject: [PATCH 1/8] feat(build): Add CLI parameters to track plugin versions (EME-XXX) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for tracking Gradle and Fastlane plugin versions in build uploads by accepting them as CLI parameters and writing them to the metadata file in the uploaded zip archive. This enables the backend to store plugin version information in the PreprodArtifact database table for size analysis and build distribution tracking. Changes: - Add --gradle-plugin-version and --fastlane-plugin-version CLI parameters - Write plugin versions to .sentry-cli-metadata.txt in uploaded zips - Update write_version_metadata() to accept optional plugin versions - Add test coverage for metadata file generation with plugin versions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CHANGELOG.md | 2 + src/commands/build/upload.rs | 117 +++++++++++++++--- src/utils/build/normalize.rs | 17 ++- .../build/build-upload-help-macos.trycmd | 4 + .../build/build-upload-help-not-macos.trycmd | 4 + 5 files changed, 124 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f35a617911..9d5ab4456f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,11 @@ ## Unreleased ### Improvements + - For the `sentry-cli build upload` command, we now only auto-detect Git metadata when we detect we are running in a CI environment, unless the user manually overrides this behavior ([#2974](https://github.com/getsentry/sentry-cli/pull/2974)). This change prevents local development builds from triggiering GitHub status checks for size analysis. - We can detect most common CI environments based on the environment variables these set. - We introduced two new arguments, `--force-git-metadata` and `--no-git-metadata`, which force-enable and force-disable automatic Git data collection, respectively, overriding the default behavior. +- The `sentry-cli build upload` command now accepts `--gradle-plugin-version` and `--fastlane-plugin-version` parameters to track plugin versions ([#2994](https://github.com/getsentry/sentry-cli/pull/2994)). These versions are written to the `.sentry-cli-metadata.txt` file in uploaded build archives, enabling the backend to store plugin version information for size analysis and build distribution tracking. - The `sentry-cli build upload` command now automatically detects the correct branch or tag reference in non-PR GitHub Actions workflows ([#2976](https://github.com/getsentry/sentry-cli/pull/2976)). Previously, `--head-ref` was only auto-detected for pull request workflows. Now it works for push, release, and other workflow types by using the `GITHUB_REF_NAME` environment variable. ## 2.58.2 diff --git a/src/commands/build/upload.rs b/src/commands/build/upload.rs index ac2c770cb9..d92b59b93a 100644 --- a/src/commands/build/upload.rs +++ b/src/commands/build/upload.rs @@ -123,6 +123,16 @@ pub fn make_command(command: Command) -> Command { .conflicts_with("force_git_metadata") .help("Disable collection and sending of git metadata.") ) + .arg( + Arg::new("gradle_plugin_version") + .long("gradle-plugin-version") + .help("The version of the Sentry Gradle plugin used to build the artifact.") + ) + .arg( + Arg::new("fastlane_plugin_version") + .long("fastlane-plugin-version") + .help("The version of the Sentry Fastlane plugin used to build the artifact.") + ) } pub fn execute(matches: &ArgMatches) -> Result<()> { @@ -149,6 +159,10 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { let build_configuration = matches.get_one("build_configuration").map(String::as_str); let release_notes = matches.get_one("release_notes").map(String::as_str); + let gradle_plugin_version = matches.get_one("gradle_plugin_version").map(String::as_str); + let fastlane_plugin_version = matches + .get_one("fastlane_plugin_version") + .map(String::as_str); let api = Api::current(); let authenticated_api = api.authenticated()?; @@ -171,15 +185,22 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { let normalized_zip = if path.is_file() { debug!("Normalizing file: {}", path.display()); - handle_file(path, &byteview)? + handle_file( + path, + &byteview, + gradle_plugin_version, + fastlane_plugin_version, + )? } else if path.is_dir() { debug!("Normalizing directory: {}", path.display()); - handle_directory(path).with_context(|| { - format!( - "Failed to generate uploadable bundle for directory {}", - path.display() - ) - })? + handle_directory(path, gradle_plugin_version, fastlane_plugin_version).with_context( + || { + format!( + "Failed to generate uploadable bundle for directory {}", + path.display() + ) + }, + )? } else { Err(anyhow!( "Path {} is neither a file nor a directory, cannot upload", @@ -259,6 +280,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { Ok(()) } +<<<<<<< HEAD /// Collects git metadata from arguments and VCS introspection. /// /// When `auto_collect` is false, only explicitly provided values are collected; @@ -434,18 +456,31 @@ fn collect_git_metadata( } } -fn handle_file(path: &Path, byteview: &ByteView) -> Result { +fn handle_file( + path: &Path, + byteview: &ByteView, + gradle_plugin_version: Option<&str>, + fastlane_plugin_version: Option<&str>, +) -> Result { // Handle IPA files by converting them to XCArchive #[cfg(all(target_os = "macos", target_arch = "aarch64"))] if is_zip_file(byteview) && is_ipa_file(byteview)? { debug!("Converting IPA file to XCArchive structure"); let archive_temp_dir = TempDir::create()?; return ipa_to_xcarchive(path, byteview, &archive_temp_dir) - .and_then(|path| handle_directory(&path)) + .and_then(|path| { + handle_directory(&path, gradle_plugin_version, fastlane_plugin_version) + }) .with_context(|| format!("Failed to process IPA file {}", path.display())); } - normalize_file(path, byteview).with_context(|| { + normalize_file( + path, + byteview, + gradle_plugin_version, + fastlane_plugin_version, + ) + .with_context(|| { format!( "Failed to generate uploadable bundle for file {}", path.display() @@ -499,7 +534,12 @@ fn validate_is_supported_build(path: &Path, bytes: &[u8]) -> Result<()> { } // For APK and AAB files, we'll copy them directly into the zip -fn normalize_file(path: &Path, bytes: &[u8]) -> Result { +fn normalize_file( + path: &Path, + bytes: &[u8], + gradle_plugin_version: Option<&str>, + fastlane_plugin_version: Option<&str>, +) -> Result { debug!("Creating normalized zip for file: {}", path.display()); let temp_file = TempFile::create()?; @@ -523,20 +563,29 @@ fn normalize_file(path: &Path, bytes: &[u8]) -> Result { zip.start_file(file_name, options)?; zip.write_all(bytes)?; - write_version_metadata(&mut zip)?; + write_version_metadata(&mut zip, gradle_plugin_version, fastlane_plugin_version)?; zip.finish()?; debug!("Successfully created normalized zip for file"); Ok(temp_file) } -fn handle_directory(path: &Path) -> Result { +fn handle_directory( + path: &Path, + gradle_plugin_version: Option<&str>, + fastlane_plugin_version: Option<&str>, +) -> Result { let temp_dir = TempDir::create()?; #[cfg(all(target_os = "macos", target_arch = "aarch64"))] if is_apple_app(path)? { handle_asset_catalogs(path, temp_dir.path()); } - normalize_directory(path, temp_dir.path()) + normalize_directory( + path, + temp_dir.path(), + gradle_plugin_version, + fastlane_plugin_version, + ) } /// Returns artifact url if upload was successful. @@ -674,7 +723,7 @@ mod tests { fs::create_dir_all(test_dir.join("Products"))?; fs::write(test_dir.join("Products").join("app.txt"), "test content")?; - let result_zip = normalize_directory(&test_dir, temp_dir.path())?; + let result_zip = normalize_directory(&test_dir, temp_dir.path(), None, None)?; let zip_file = fs::File::open(result_zip.path())?; let mut archive = ZipArchive::new(zip_file)?; let file = archive.by_index(0)?; @@ -690,7 +739,7 @@ mod tests { let xcarchive_path = Path::new("tests/integration/_fixtures/build/archive.xcarchive"); // Process the XCArchive directory - let result = handle_directory(xcarchive_path)?; + let result = handle_directory(xcarchive_path, None, None)?; // Verify the resulting zip contains parsed assets let zip_file = fs::File::open(result.path())?; @@ -723,7 +772,7 @@ mod tests { let byteview = ByteView::open(ipa_path)?; // Process the IPA file - this should work even without asset catalogs - let result = handle_file(ipa_path, &byteview)?; + let result = handle_file(ipa_path, &byteview, None, None)?; let zip_file = fs::File::open(result.path())?; let mut archive = ZipArchive::new(zip_file)?; @@ -760,7 +809,7 @@ mod tests { let symlink_path = test_dir.join("Products").join("app_link.txt"); symlink("app.txt", &symlink_path)?; - let result_zip = normalize_directory(&test_dir, temp_dir.path())?; + let result_zip = normalize_directory(&test_dir, temp_dir.path(), None, None)?; let zip_file = fs::File::open(result_zip.path())?; let mut archive = ZipArchive::new(zip_file)?; @@ -795,6 +844,7 @@ mod tests { } #[test] +<<<<<<< HEAD fn test_collect_git_metadata_respects_explicit_values_when_auto_collect_disabled() { // Create a mock ArgMatches with explicit --head-sha and --vcs-provider values let app = make_command(Command::new("test")); @@ -877,4 +927,35 @@ mod tests { "head_ref should be empty with auto_collect=false and no explicit value" ); } + + #[test] + fn test_metadata_includes_plugin_versions() -> Result<()> { + let temp_dir = crate::utils::fs::TempDir::create()?; + let test_dir = temp_dir.path().join("TestApp.xcarchive"); + fs::create_dir_all(test_dir.join("Products"))?; + fs::write(test_dir.join("Products").join("app.txt"), "test content")?; + + let result_zip = + normalize_directory(&test_dir, temp_dir.path(), Some("4.12.0"), Some("1.2.3"))?; + let zip_file = fs::File::open(result_zip.path())?; + let mut archive = ZipArchive::new(zip_file)?; + + // Find and read the metadata file + let metadata_file = archive.by_name(".sentry-cli-metadata.txt")?; + let metadata_content = std::io::read_to_string(metadata_file)?; + + assert!( + metadata_content.contains("sentry-cli-version:"), + "Metadata should contain sentry-cli-version" + ); + assert!( + metadata_content.contains("gradle-plugin-version: 4.12.0"), + "Metadata should contain gradle-plugin-version" + ); + assert!( + metadata_content.contains("fastlane-plugin-version: 1.2.3"), + "Metadata should contain fastlane-plugin-version" + ); + Ok(()) + } } diff --git a/src/utils/build/normalize.rs b/src/utils/build/normalize.rs index 31e8b8531d..ca7588bb5b 100644 --- a/src/utils/build/normalize.rs +++ b/src/utils/build/normalize.rs @@ -94,17 +94,30 @@ fn metadata_file_options() -> SimpleFileOptions { pub fn write_version_metadata( zip: &mut ZipWriter, + gradle_plugin_version: Option<&str>, + fastlane_plugin_version: Option<&str>, ) -> Result<()> { let version = get_version(); zip.start_file(".sentry-cli-metadata.txt", metadata_file_options())?; writeln!(zip, "sentry-cli-version: {version}")?; + if let Some(gradle_version) = gradle_plugin_version { + writeln!(zip, "gradle-plugin-version: {gradle_version}")?; + } + if let Some(fastlane_version) = fastlane_plugin_version { + writeln!(zip, "fastlane-plugin-version: {fastlane_version}")?; + } Ok(()) } // For XCArchive directories, we'll zip the entire directory // It's important to not change the contents of the directory or the size // analysis will be wrong and the code signature will break. -pub fn normalize_directory(path: &Path, parsed_assets_path: &Path) -> Result { +pub fn normalize_directory( + path: &Path, + parsed_assets_path: &Path, + gradle_plugin_version: Option<&str>, + fastlane_plugin_version: Option<&str>, +) -> Result { debug!("Creating normalized zip for directory: {}", path.display()); let temp_file = TempFile::create()?; @@ -133,7 +146,7 @@ pub fn normalize_directory(path: &Path, parsed_assets_path: &Path) -> Result + The version of the Sentry Gradle plugin used to build the artifact. + --fastlane-plugin-version + The version of the Sentry Fastlane plugin used to build the artifact. -h, --help Print help diff --git a/tests/integration/_cases/build/build-upload-help-not-macos.trycmd b/tests/integration/_cases/build/build-upload-help-not-macos.trycmd index 3156bd8bfa..626f4612e5 100644 --- a/tests/integration/_cases/build/build-upload-help-not-macos.trycmd +++ b/tests/integration/_cases/build/build-upload-help-not-macos.trycmd @@ -58,6 +58,10 @@ Options: most CI environments. --no-git-metadata Disable collection and sending of git metadata. + --gradle-plugin-version + The version of the Sentry Gradle plugin used to build the artifact. + --fastlane-plugin-version + The version of the Sentry Fastlane plugin used to build the artifact. -h, --help Print help From 2ab57cf90cff6027b685d3bee18ef4395c45ee3b Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 9 Dec 2025 10:00:52 +0100 Subject: [PATCH 2/8] refactor(build): Use --metadata parameter for flexible key-value metadata (EME-XXX) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace specific --gradle-plugin-version and --fastlane-plugin-version parameters with a more general --metadata parameter that accepts key-value pairs. This allows for flexible metadata inclusion in build uploads without requiring new CLI parameters for each metadata type. Usage: --metadata gradle-plugin=4.12.0 --metadata fastlane-plugin=1.2.3 Metadata is written to .sentry-cli-metadata.txt in uploaded archives in a sorted, deterministic format for consistent checksums. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CHANGELOG.md | 2 +- src/commands/build/upload.rs | 115 ++++++++++++++++------------------- src/utils/build/normalize.rs | 22 ++++--- 3 files changed, 64 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d5ab4456f..468c1f6bb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - For the `sentry-cli build upload` command, we now only auto-detect Git metadata when we detect we are running in a CI environment, unless the user manually overrides this behavior ([#2974](https://github.com/getsentry/sentry-cli/pull/2974)). This change prevents local development builds from triggiering GitHub status checks for size analysis. - We can detect most common CI environments based on the environment variables these set. - We introduced two new arguments, `--force-git-metadata` and `--no-git-metadata`, which force-enable and force-disable automatic Git data collection, respectively, overriding the default behavior. -- The `sentry-cli build upload` command now accepts `--gradle-plugin-version` and `--fastlane-plugin-version` parameters to track plugin versions ([#2994](https://github.com/getsentry/sentry-cli/pull/2994)). These versions are written to the `.sentry-cli-metadata.txt` file in uploaded build archives, enabling the backend to store plugin version information for size analysis and build distribution tracking. +- The `sentry-cli build upload` command now accepts a `--metadata` parameter to include custom metadata in build uploads ([#2994](https://github.com/getsentry/sentry-cli/pull/2994)). Metadata can be specified as key-value pairs (e.g., `--metadata gradle-plugin=4.12.0 --metadata fastlane-plugin=1.2.3`) and is written to the `.sentry-cli-metadata.txt` file in uploaded build archives, enabling the backend to store metadata for size analysis and build distribution tracking. - The `sentry-cli build upload` command now automatically detects the correct branch or tag reference in non-PR GitHub Actions workflows ([#2976](https://github.com/getsentry/sentry-cli/pull/2976)). Previously, `--head-ref` was only auto-detected for pull request workflows. Now it works for push, release, and other workflow types by using the `GITHUB_REF_NAME` environment variable. ## 2.58.2 diff --git a/src/commands/build/upload.rs b/src/commands/build/upload.rs index d92b59b93a..65a849e7d4 100644 --- a/src/commands/build/upload.rs +++ b/src/commands/build/upload.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::collections::HashMap; use std::io::Write as _; use std::path::Path; @@ -124,14 +125,11 @@ pub fn make_command(command: Command) -> Command { .help("Disable collection and sending of git metadata.") ) .arg( - Arg::new("gradle_plugin_version") - .long("gradle-plugin-version") - .help("The version of the Sentry Gradle plugin used to build the artifact.") - ) - .arg( - Arg::new("fastlane_plugin_version") - .long("fastlane-plugin-version") - .help("The version of the Sentry Fastlane plugin used to build the artifact.") + Arg::new("metadata") + .long("metadata") + .value_name("KEY=VALUE") + .help("Custom metadata to include in the build upload (e.g., --metadata gradle-plugin=4.12.0). Can be specified multiple times.") + .action(ArgAction::Append) ) } @@ -159,10 +157,24 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { let build_configuration = matches.get_one("build_configuration").map(String::as_str); let release_notes = matches.get_one("release_notes").map(String::as_str); - let gradle_plugin_version = matches.get_one("gradle_plugin_version").map(String::as_str); - let fastlane_plugin_version = matches - .get_one("fastlane_plugin_version") - .map(String::as_str); + + // Parse metadata key-value pairs + let metadata: HashMap = matches + .get_many::("metadata") + .map(|values| { + values + .filter_map(|s| { + let parts: Vec<&str> = s.splitn(2, '=').collect(); + if parts.len() == 2 { + Some((parts[0].to_string(), parts[1].to_string())) + } else { + warn!("Ignoring invalid metadata format: {s}. Expected format: KEY=VALUE"); + None + } + }) + .collect() + }) + .unwrap_or_default(); let api = Api::current(); let authenticated_api = api.authenticated()?; @@ -185,22 +197,15 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { let normalized_zip = if path.is_file() { debug!("Normalizing file: {}", path.display()); - handle_file( - path, - &byteview, - gradle_plugin_version, - fastlane_plugin_version, - )? + handle_file(path, &byteview, &metadata)? } else if path.is_dir() { debug!("Normalizing directory: {}", path.display()); - handle_directory(path, gradle_plugin_version, fastlane_plugin_version).with_context( - || { - format!( - "Failed to generate uploadable bundle for directory {}", - path.display() - ) - }, - )? + handle_directory(path, &metadata).with_context(|| { + format!( + "Failed to generate uploadable bundle for directory {}", + path.display() + ) + })? } else { Err(anyhow!( "Path {} is neither a file nor a directory, cannot upload", @@ -280,7 +285,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { Ok(()) } -<<<<<<< HEAD /// Collects git metadata from arguments and VCS introspection. /// /// When `auto_collect` is false, only explicitly provided values are collected; @@ -459,8 +463,7 @@ fn collect_git_metadata( fn handle_file( path: &Path, byteview: &ByteView, - gradle_plugin_version: Option<&str>, - fastlane_plugin_version: Option<&str>, + metadata: &HashMap, ) -> Result { // Handle IPA files by converting them to XCArchive #[cfg(all(target_os = "macos", target_arch = "aarch64"))] @@ -468,19 +471,11 @@ fn handle_file( debug!("Converting IPA file to XCArchive structure"); let archive_temp_dir = TempDir::create()?; return ipa_to_xcarchive(path, byteview, &archive_temp_dir) - .and_then(|path| { - handle_directory(&path, gradle_plugin_version, fastlane_plugin_version) - }) + .and_then(|path| handle_directory(&path, metadata)) .with_context(|| format!("Failed to process IPA file {}", path.display())); } - normalize_file( - path, - byteview, - gradle_plugin_version, - fastlane_plugin_version, - ) - .with_context(|| { + normalize_file(path, byteview, metadata).with_context(|| { format!( "Failed to generate uploadable bundle for file {}", path.display() @@ -537,8 +532,7 @@ fn validate_is_supported_build(path: &Path, bytes: &[u8]) -> Result<()> { fn normalize_file( path: &Path, bytes: &[u8], - gradle_plugin_version: Option<&str>, - fastlane_plugin_version: Option<&str>, + metadata: &HashMap, ) -> Result { debug!("Creating normalized zip for file: {}", path.display()); @@ -563,29 +557,20 @@ fn normalize_file( zip.start_file(file_name, options)?; zip.write_all(bytes)?; - write_version_metadata(&mut zip, gradle_plugin_version, fastlane_plugin_version)?; + write_version_metadata(&mut zip, metadata)?; zip.finish()?; debug!("Successfully created normalized zip for file"); Ok(temp_file) } -fn handle_directory( - path: &Path, - gradle_plugin_version: Option<&str>, - fastlane_plugin_version: Option<&str>, -) -> Result { +fn handle_directory(path: &Path, metadata: &HashMap) -> Result { let temp_dir = TempDir::create()?; #[cfg(all(target_os = "macos", target_arch = "aarch64"))] if is_apple_app(path)? { handle_asset_catalogs(path, temp_dir.path()); } - normalize_directory( - path, - temp_dir.path(), - gradle_plugin_version, - fastlane_plugin_version, - ) + normalize_directory(path, temp_dir.path(), metadata) } /// Returns artifact url if upload was successful. @@ -723,7 +708,7 @@ mod tests { fs::create_dir_all(test_dir.join("Products"))?; fs::write(test_dir.join("Products").join("app.txt"), "test content")?; - let result_zip = normalize_directory(&test_dir, temp_dir.path(), None, None)?; + let result_zip = normalize_directory(&test_dir, temp_dir.path(), &HashMap::new())?; let zip_file = fs::File::open(result_zip.path())?; let mut archive = ZipArchive::new(zip_file)?; let file = archive.by_index(0)?; @@ -739,7 +724,7 @@ mod tests { let xcarchive_path = Path::new("tests/integration/_fixtures/build/archive.xcarchive"); // Process the XCArchive directory - let result = handle_directory(xcarchive_path, None, None)?; + let result = handle_directory(xcarchive_path, &HashMap::new())?; // Verify the resulting zip contains parsed assets let zip_file = fs::File::open(result.path())?; @@ -772,7 +757,7 @@ mod tests { let byteview = ByteView::open(ipa_path)?; // Process the IPA file - this should work even without asset catalogs - let result = handle_file(ipa_path, &byteview, None, None)?; + let result = handle_file(ipa_path, &byteview, &HashMap::new())?; let zip_file = fs::File::open(result.path())?; let mut archive = ZipArchive::new(zip_file)?; @@ -809,7 +794,7 @@ mod tests { let symlink_path = test_dir.join("Products").join("app_link.txt"); symlink("app.txt", &symlink_path)?; - let result_zip = normalize_directory(&test_dir, temp_dir.path(), None, None)?; + let result_zip = normalize_directory(&test_dir, temp_dir.path(), &HashMap::new())?; let zip_file = fs::File::open(result_zip.path())?; let mut archive = ZipArchive::new(zip_file)?; @@ -844,7 +829,6 @@ mod tests { } #[test] -<<<<<<< HEAD fn test_collect_git_metadata_respects_explicit_values_when_auto_collect_disabled() { // Create a mock ArgMatches with explicit --head-sha and --vcs-provider values let app = make_command(Command::new("test")); @@ -935,8 +919,11 @@ mod tests { fs::create_dir_all(test_dir.join("Products"))?; fs::write(test_dir.join("Products").join("app.txt"), "test content")?; - let result_zip = - normalize_directory(&test_dir, temp_dir.path(), Some("4.12.0"), Some("1.2.3"))?; + let mut metadata = HashMap::new(); + metadata.insert("gradle-plugin".to_string(), "4.12.0".to_string()); + metadata.insert("fastlane-plugin".to_string(), "1.2.3".to_string()); + + let result_zip = normalize_directory(&test_dir, temp_dir.path(), &metadata)?; let zip_file = fs::File::open(result_zip.path())?; let mut archive = ZipArchive::new(zip_file)?; @@ -949,12 +936,12 @@ mod tests { "Metadata should contain sentry-cli-version" ); assert!( - metadata_content.contains("gradle-plugin-version: 4.12.0"), - "Metadata should contain gradle-plugin-version" + metadata_content.contains("gradle-plugin: 4.12.0"), + "Metadata should contain gradle-plugin" ); assert!( - metadata_content.contains("fastlane-plugin-version: 1.2.3"), - "Metadata should contain fastlane-plugin-version" + metadata_content.contains("fastlane-plugin: 1.2.3"), + "Metadata should contain fastlane-plugin" ); Ok(()) } diff --git a/src/utils/build/normalize.rs b/src/utils/build/normalize.rs index ca7588bb5b..82f6dbf70a 100644 --- a/src/utils/build/normalize.rs +++ b/src/utils/build/normalize.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::collections::HashMap; #[cfg(not(windows))] use std::fs; use std::fs::File; @@ -94,17 +95,19 @@ fn metadata_file_options() -> SimpleFileOptions { pub fn write_version_metadata( zip: &mut ZipWriter, - gradle_plugin_version: Option<&str>, - fastlane_plugin_version: Option<&str>, + metadata: &HashMap, ) -> Result<()> { let version = get_version(); zip.start_file(".sentry-cli-metadata.txt", metadata_file_options())?; writeln!(zip, "sentry-cli-version: {version}")?; - if let Some(gradle_version) = gradle_plugin_version { - writeln!(zip, "gradle-plugin-version: {gradle_version}")?; - } - if let Some(fastlane_version) = fastlane_plugin_version { - writeln!(zip, "fastlane-plugin-version: {fastlane_version}")?; + + // Write metadata in sorted order for deterministic output + let mut keys: Vec<_> = metadata.keys().collect(); + keys.sort(); + for key in keys { + if let Some(value) = metadata.get(key) { + writeln!(zip, "{key}: {value}")?; + } } Ok(()) } @@ -115,8 +118,7 @@ pub fn write_version_metadata( pub fn normalize_directory( path: &Path, parsed_assets_path: &Path, - gradle_plugin_version: Option<&str>, - fastlane_plugin_version: Option<&str>, + metadata: &HashMap, ) -> Result { debug!("Creating normalized zip for directory: {}", path.display()); @@ -146,7 +148,7 @@ pub fn normalize_directory( )?; } - write_version_metadata(&mut zip, gradle_plugin_version, fastlane_plugin_version)?; + write_version_metadata(&mut zip, metadata)?; zip.finish()?; debug!("Successfully created normalized zip for directory with {file_count} files"); From 4d232d8e1803d255c91e4d6a25b50ceca530914a Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 9 Dec 2025 10:55:07 +0100 Subject: [PATCH 3/8] test: Update help output tests to use --metadata parameter (EME-XXX) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove references to --gradle-plugin-version and --fastlane-plugin-version from test fixtures, replacing them with the new --metadata parameter that was introduced in the previous commits. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../_cases/build/build-upload-help-macos.trycmd | 7 +++---- .../_cases/build/build-upload-help-not-macos.trycmd | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/integration/_cases/build/build-upload-help-macos.trycmd b/tests/integration/_cases/build/build-upload-help-macos.trycmd index ad716f792b..35479054fd 100644 --- a/tests/integration/_cases/build/build-upload-help-macos.trycmd +++ b/tests/integration/_cases/build/build-upload-help-macos.trycmd @@ -59,10 +59,9 @@ Options: most CI environments. --no-git-metadata Disable collection and sending of git metadata. - --gradle-plugin-version - The version of the Sentry Gradle plugin used to build the artifact. - --fastlane-plugin-version - The version of the Sentry Fastlane plugin used to build the artifact. + --metadata + Custom metadata to include in the build upload (e.g., --metadata gradle-plugin=4.12.0). + Can be specified multiple times. -h, --help Print help diff --git a/tests/integration/_cases/build/build-upload-help-not-macos.trycmd b/tests/integration/_cases/build/build-upload-help-not-macos.trycmd index 626f4612e5..382ab249c2 100644 --- a/tests/integration/_cases/build/build-upload-help-not-macos.trycmd +++ b/tests/integration/_cases/build/build-upload-help-not-macos.trycmd @@ -58,10 +58,9 @@ Options: most CI environments. --no-git-metadata Disable collection and sending of git metadata. - --gradle-plugin-version - The version of the Sentry Gradle plugin used to build the artifact. - --fastlane-plugin-version - The version of the Sentry Fastlane plugin used to build the artifact. + --metadata + Custom metadata to include in the build upload (e.g., --metadata gradle-plugin=4.12.0). + Can be specified multiple times. -h, --help Print help From 7809fa6c4ef6a81438be38f88adec4dd1a40cac6 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 9 Dec 2025 10:56:49 +0100 Subject: [PATCH 4/8] fix: Use to_owned() instead of to_string() for clippy (EME-XXX) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace to_string() calls on string literals with to_owned() as recommended by clippy lint str_to_string. This is more idiomatic for converting &str to String when the source is already a string literal. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/commands/build/upload.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/build/upload.rs b/src/commands/build/upload.rs index 65a849e7d4..662b8c1a11 100644 --- a/src/commands/build/upload.rs +++ b/src/commands/build/upload.rs @@ -166,7 +166,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { .filter_map(|s| { let parts: Vec<&str> = s.splitn(2, '=').collect(); if parts.len() == 2 { - Some((parts[0].to_string(), parts[1].to_string())) + Some((parts[0].to_owned(), parts[1].to_owned())) } else { warn!("Ignoring invalid metadata format: {s}. Expected format: KEY=VALUE"); None @@ -920,8 +920,8 @@ mod tests { fs::write(test_dir.join("Products").join("app.txt"), "test content")?; let mut metadata = HashMap::new(); - metadata.insert("gradle-plugin".to_string(), "4.12.0".to_string()); - metadata.insert("fastlane-plugin".to_string(), "1.2.3".to_string()); + metadata.insert("gradle-plugin".to_owned(), "4.12.0".to_owned()); + metadata.insert("fastlane-plugin".to_owned(), "1.2.3".to_owned()); let result_zip = normalize_directory(&test_dir, temp_dir.path(), &metadata)?; let zip_file = fs::File::open(result_zip.path())?; From 6e57330f2d53e6f0368043ef9b74a1c55def78d9 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 10 Dec 2025 14:17:30 +0100 Subject: [PATCH 5/8] fix: Add validation for metadata keys (EME-XXX) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address security and data integrity issues identified in PR review: 1. Reject empty metadata keys to prevent malformed output lines - Check that parts[0] is not empty before accepting key-value pairs - Update warning message to clarify requirement for non-empty keys 2. Reject reserved 'sentry-cli-version' key to prevent spoofing - Filter out any user-provided 'sentry-cli-version' metadata - Log warning when reserved key is detected - Prevents duplicate entries and version spoofing in tracking This ensures the .sentry-cli-metadata.txt file maintains integrity and prevents backend parser confusion. Fixes issues reported by Cursor bot in PR #2994. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/commands/build/upload.rs | 41 +++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/src/commands/build/upload.rs b/src/commands/build/upload.rs index 662b8c1a11..aed2ec0bf6 100644 --- a/src/commands/build/upload.rs +++ b/src/commands/build/upload.rs @@ -165,10 +165,16 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { values .filter_map(|s| { let parts: Vec<&str> = s.splitn(2, '=').collect(); - if parts.len() == 2 { - Some((parts[0].to_owned(), parts[1].to_owned())) + if parts.len() == 2 && !parts[0].is_empty() { + let key = parts[0]; + // Reject reserved key that's automatically set + if key == "sentry-cli-version" { + warn!("Ignoring reserved metadata key 'sentry-cli-version'. This is set automatically."); + return None; + } + Some((key.to_owned(), parts[1].to_owned())) } else { - warn!("Ignoring invalid metadata format: {s}. Expected format: KEY=VALUE"); + warn!("Ignoring invalid metadata format: {s}. Expected format: KEY=VALUE with non-empty key"); None } }) @@ -945,4 +951,33 @@ mod tests { ); Ok(()) } + + #[test] + fn test_metadata_with_empty_map() -> Result<()> { + let temp_dir = crate::utils::fs::TempDir::create()?; + let test_dir = temp_dir.path().join("TestApp.xcarchive"); + fs::create_dir_all(test_dir.join("Products"))?; + fs::write(test_dir.join("Products").join("app.txt"), "test content")?; + + // Empty metadata should work fine + let metadata = HashMap::new(); + + let result_zip = normalize_directory(&test_dir, temp_dir.path(), &metadata)?; + let zip_file = fs::File::open(result_zip.path())?; + let mut archive = ZipArchive::new(zip_file)?; + + let metadata_file = archive.by_name(".sentry-cli-metadata.txt")?; + let metadata_content = std::io::read_to_string(metadata_file)?; + + // Should only contain sentry-cli-version + assert!( + metadata_content.contains("sentry-cli-version:"), + "Metadata should contain sentry-cli-version" + ); + + // Should not have any other lines besides the version + let line_count = metadata_content.lines().count(); + assert_eq!(line_count, 1, "Should only have one line for empty metadata"); + Ok(()) + } } From ded4c51221d683d94fc177da11ae36c6fee5c12b Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 10 Dec 2025 15:32:23 +0100 Subject: [PATCH 6/8] refactor(build): Use SENTRY_PIPELINE env var for plugin versions (EME-XXX) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace --metadata CLI argument with automatic detection of plugin versions from the SENTRY_PIPELINE environment variable. This simplifies the interface and leverages existing infrastructure used for User-Agent headers. Changes: - Remove --metadata argument from CLI - Parse SENTRY_PIPELINE to extract plugin name and version - Format: "sentry-gradle-plugin/4.12.0" or "sentry-fastlane-plugin/1.2.3" - Only recognize known Sentry plugins (gradle and fastlane) - Update function signatures from HashMap to (plugin_name, plugin_version) - Write plugin info to .sentry-cli-metadata.txt in uploaded archives - Update all tests to use new parameter format The SENTRY_PIPELINE variable is already set by the Gradle and Fastlane plugins, so this requires no changes to those plugins while enabling version tracking for size analysis and build distribution. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/commands/build/upload.rs | 143 ++++++++++-------- src/utils/build/normalize.rs | 19 +-- .../build/build-upload-help-macos.trycmd | 3 - .../build/build-upload-help-not-macos.trycmd | 3 - 4 files changed, 92 insertions(+), 76 deletions(-) diff --git a/src/commands/build/upload.rs b/src/commands/build/upload.rs index aed2ec0bf6..847a9c09f3 100644 --- a/src/commands/build/upload.rs +++ b/src/commands/build/upload.rs @@ -1,5 +1,4 @@ use std::borrow::Cow; -use std::collections::HashMap; use std::io::Write as _; use std::path::Path; @@ -124,13 +123,6 @@ pub fn make_command(command: Command) -> Command { .conflicts_with("force_git_metadata") .help("Disable collection and sending of git metadata.") ) - .arg( - Arg::new("metadata") - .long("metadata") - .value_name("KEY=VALUE") - .help("Custom metadata to include in the build upload (e.g., --metadata gradle-plugin=4.12.0). Can be specified multiple times.") - .action(ArgAction::Append) - ) } pub fn execute(matches: &ArgMatches) -> Result<()> { @@ -158,29 +150,30 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { let build_configuration = matches.get_one("build_configuration").map(String::as_str); let release_notes = matches.get_one("release_notes").map(String::as_str); - // Parse metadata key-value pairs - let metadata: HashMap = matches - .get_many::("metadata") - .map(|values| { - values - .filter_map(|s| { - let parts: Vec<&str> = s.splitn(2, '=').collect(); - if parts.len() == 2 && !parts[0].is_empty() { - let key = parts[0]; - // Reject reserved key that's automatically set - if key == "sentry-cli-version" { - warn!("Ignoring reserved metadata key 'sentry-cli-version'. This is set automatically."); - return None; - } - Some((key.to_owned(), parts[1].to_owned())) - } else { - warn!("Ignoring invalid metadata format: {s}. Expected format: KEY=VALUE with non-empty key"); - None - } - }) - .collect() + // Parse plugin info from SENTRY_PIPELINE environment variable + // Format: "sentry-gradle-plugin/4.12.0" or "sentry-fastlane-plugin/1.2.3" + let (plugin_name, plugin_version) = config + .get_pipeline_env() + .and_then(|pipeline| { + let parts: Vec<&str> = pipeline.splitn(2, '/').collect(); + if parts.len() == 2 { + let name = parts[0]; + let version = parts[1]; + + // Only extract known Sentry plugins + if name == "sentry-gradle-plugin" || name == "sentry-fastlane-plugin" { + debug!("Detected {name} version {version} from SENTRY_PIPELINE"); + Some((name.to_owned(), version.to_owned())) + } else { + debug!("SENTRY_PIPELINE contains unrecognized plugin: {name}"); + None + } + } else { + debug!("SENTRY_PIPELINE format not recognized: {pipeline}"); + None + } }) - .unwrap_or_default(); + .unzip(); let api = Api::current(); let authenticated_api = api.authenticated()?; @@ -203,10 +196,10 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { let normalized_zip = if path.is_file() { debug!("Normalizing file: {}", path.display()); - handle_file(path, &byteview, &metadata)? + handle_file(path, &byteview, plugin_name.as_deref(), plugin_version.as_deref())? } else if path.is_dir() { debug!("Normalizing directory: {}", path.display()); - handle_directory(path, &metadata).with_context(|| { + handle_directory(path, plugin_name.as_deref(), plugin_version.as_deref()).with_context(|| { format!( "Failed to generate uploadable bundle for directory {}", path.display() @@ -469,7 +462,8 @@ fn collect_git_metadata( fn handle_file( path: &Path, byteview: &ByteView, - metadata: &HashMap, + plugin_name: Option<&str>, + plugin_version: Option<&str>, ) -> Result { // Handle IPA files by converting them to XCArchive #[cfg(all(target_os = "macos", target_arch = "aarch64"))] @@ -477,11 +471,11 @@ fn handle_file( debug!("Converting IPA file to XCArchive structure"); let archive_temp_dir = TempDir::create()?; return ipa_to_xcarchive(path, byteview, &archive_temp_dir) - .and_then(|path| handle_directory(&path, metadata)) + .and_then(|path| handle_directory(&path, plugin_name, plugin_version)) .with_context(|| format!("Failed to process IPA file {}", path.display())); } - normalize_file(path, byteview, metadata).with_context(|| { + normalize_file(path, byteview, plugin_name, plugin_version).with_context(|| { format!( "Failed to generate uploadable bundle for file {}", path.display() @@ -538,7 +532,8 @@ fn validate_is_supported_build(path: &Path, bytes: &[u8]) -> Result<()> { fn normalize_file( path: &Path, bytes: &[u8], - metadata: &HashMap, + plugin_name: Option<&str>, + plugin_version: Option<&str>, ) -> Result { debug!("Creating normalized zip for file: {}", path.display()); @@ -563,20 +558,24 @@ fn normalize_file( zip.start_file(file_name, options)?; zip.write_all(bytes)?; - write_version_metadata(&mut zip, metadata)?; + write_version_metadata(&mut zip, plugin_name, plugin_version)?; zip.finish()?; debug!("Successfully created normalized zip for file"); Ok(temp_file) } -fn handle_directory(path: &Path, metadata: &HashMap) -> Result { +fn handle_directory( + path: &Path, + plugin_name: Option<&str>, + plugin_version: Option<&str>, +) -> Result { let temp_dir = TempDir::create()?; #[cfg(all(target_os = "macos", target_arch = "aarch64"))] if is_apple_app(path)? { handle_asset_catalogs(path, temp_dir.path()); } - normalize_directory(path, temp_dir.path(), metadata) + normalize_directory(path, temp_dir.path(), plugin_name, plugin_version) } /// Returns artifact url if upload was successful. @@ -714,7 +713,7 @@ mod tests { fs::create_dir_all(test_dir.join("Products"))?; fs::write(test_dir.join("Products").join("app.txt"), "test content")?; - let result_zip = normalize_directory(&test_dir, temp_dir.path(), &HashMap::new())?; + let result_zip = normalize_directory(&test_dir, temp_dir.path(), None, None)?; let zip_file = fs::File::open(result_zip.path())?; let mut archive = ZipArchive::new(zip_file)?; let file = archive.by_index(0)?; @@ -730,7 +729,7 @@ mod tests { let xcarchive_path = Path::new("tests/integration/_fixtures/build/archive.xcarchive"); // Process the XCArchive directory - let result = handle_directory(xcarchive_path, &HashMap::new())?; + let result = handle_directory(xcarchive_path, None, None)?; // Verify the resulting zip contains parsed assets let zip_file = fs::File::open(result.path())?; @@ -763,7 +762,7 @@ mod tests { let byteview = ByteView::open(ipa_path)?; // Process the IPA file - this should work even without asset catalogs - let result = handle_file(ipa_path, &byteview, &HashMap::new())?; + let result = handle_file(ipa_path, &byteview, None, None)?; let zip_file = fs::File::open(result.path())?; let mut archive = ZipArchive::new(zip_file)?; @@ -800,7 +799,7 @@ mod tests { let symlink_path = test_dir.join("Products").join("app_link.txt"); symlink("app.txt", &symlink_path)?; - let result_zip = normalize_directory(&test_dir, temp_dir.path(), &HashMap::new())?; + let result_zip = normalize_directory(&test_dir, temp_dir.path(), None, None)?; let zip_file = fs::File::open(result_zip.path())?; let mut archive = ZipArchive::new(zip_file)?; @@ -919,17 +918,18 @@ mod tests { } #[test] - fn test_metadata_includes_plugin_versions() -> Result<()> { + fn test_metadata_includes_gradle_plugin_version() -> Result<()> { let temp_dir = crate::utils::fs::TempDir::create()?; let test_dir = temp_dir.path().join("TestApp.xcarchive"); fs::create_dir_all(test_dir.join("Products"))?; fs::write(test_dir.join("Products").join("app.txt"), "test content")?; - let mut metadata = HashMap::new(); - metadata.insert("gradle-plugin".to_owned(), "4.12.0".to_owned()); - metadata.insert("fastlane-plugin".to_owned(), "1.2.3".to_owned()); - - let result_zip = normalize_directory(&test_dir, temp_dir.path(), &metadata)?; + let result_zip = normalize_directory( + &test_dir, + temp_dir.path(), + Some("sentry-gradle-plugin"), + Some("4.12.0"), + )?; let zip_file = fs::File::open(result_zip.path())?; let mut archive = ZipArchive::new(zip_file)?; @@ -942,27 +942,52 @@ mod tests { "Metadata should contain sentry-cli-version" ); assert!( - metadata_content.contains("gradle-plugin: 4.12.0"), - "Metadata should contain gradle-plugin" + metadata_content.contains("sentry-gradle-plugin: 4.12.0"), + "Metadata should contain sentry-gradle-plugin" + ); + Ok(()) + } + + #[test] + fn test_metadata_includes_fastlane_plugin_version() -> Result<()> { + let temp_dir = crate::utils::fs::TempDir::create()?; + let test_dir = temp_dir.path().join("TestApp.xcarchive"); + fs::create_dir_all(test_dir.join("Products"))?; + fs::write(test_dir.join("Products").join("app.txt"), "test content")?; + + let result_zip = normalize_directory( + &test_dir, + temp_dir.path(), + Some("sentry-fastlane-plugin"), + Some("1.2.3"), + )?; + let zip_file = fs::File::open(result_zip.path())?; + let mut archive = ZipArchive::new(zip_file)?; + + // Find and read the metadata file + let metadata_file = archive.by_name(".sentry-cli-metadata.txt")?; + let metadata_content = std::io::read_to_string(metadata_file)?; + + assert!( + metadata_content.contains("sentry-cli-version:"), + "Metadata should contain sentry-cli-version" ); assert!( - metadata_content.contains("fastlane-plugin: 1.2.3"), - "Metadata should contain fastlane-plugin" + metadata_content.contains("sentry-fastlane-plugin: 1.2.3"), + "Metadata should contain sentry-fastlane-plugin" ); Ok(()) } #[test] - fn test_metadata_with_empty_map() -> Result<()> { + fn test_metadata_without_plugin() -> Result<()> { let temp_dir = crate::utils::fs::TempDir::create()?; let test_dir = temp_dir.path().join("TestApp.xcarchive"); fs::create_dir_all(test_dir.join("Products"))?; fs::write(test_dir.join("Products").join("app.txt"), "test content")?; - // Empty metadata should work fine - let metadata = HashMap::new(); - - let result_zip = normalize_directory(&test_dir, temp_dir.path(), &metadata)?; + // No plugin info provided + let result_zip = normalize_directory(&test_dir, temp_dir.path(), None, None)?; let zip_file = fs::File::open(result_zip.path())?; let mut archive = ZipArchive::new(zip_file)?; @@ -977,7 +1002,7 @@ mod tests { // Should not have any other lines besides the version let line_count = metadata_content.lines().count(); - assert_eq!(line_count, 1, "Should only have one line for empty metadata"); + assert_eq!(line_count, 1, "Should only have one line when no plugin info"); Ok(()) } } diff --git a/src/utils/build/normalize.rs b/src/utils/build/normalize.rs index 82f6dbf70a..110fcdea16 100644 --- a/src/utils/build/normalize.rs +++ b/src/utils/build/normalize.rs @@ -1,5 +1,4 @@ use std::borrow::Cow; -use std::collections::HashMap; #[cfg(not(windows))] use std::fs; use std::fs::File; @@ -95,19 +94,16 @@ fn metadata_file_options() -> SimpleFileOptions { pub fn write_version_metadata( zip: &mut ZipWriter, - metadata: &HashMap, + plugin_name: Option<&str>, + plugin_version: Option<&str>, ) -> Result<()> { let version = get_version(); zip.start_file(".sentry-cli-metadata.txt", metadata_file_options())?; writeln!(zip, "sentry-cli-version: {version}")?; - // Write metadata in sorted order for deterministic output - let mut keys: Vec<_> = metadata.keys().collect(); - keys.sort(); - for key in keys { - if let Some(value) = metadata.get(key) { - writeln!(zip, "{key}: {value}")?; - } + // Write plugin info if available + if let (Some(name), Some(version)) = (plugin_name, plugin_version) { + writeln!(zip, "{name}: {version}")?; } Ok(()) } @@ -118,7 +114,8 @@ pub fn write_version_metadata( pub fn normalize_directory( path: &Path, parsed_assets_path: &Path, - metadata: &HashMap, + plugin_name: Option<&str>, + plugin_version: Option<&str>, ) -> Result { debug!("Creating normalized zip for directory: {}", path.display()); @@ -148,7 +145,7 @@ pub fn normalize_directory( )?; } - write_version_metadata(&mut zip, metadata)?; + write_version_metadata(&mut zip, plugin_name, plugin_version)?; zip.finish()?; debug!("Successfully created normalized zip for directory with {file_count} files"); diff --git a/tests/integration/_cases/build/build-upload-help-macos.trycmd b/tests/integration/_cases/build/build-upload-help-macos.trycmd index 35479054fd..acde89c3f4 100644 --- a/tests/integration/_cases/build/build-upload-help-macos.trycmd +++ b/tests/integration/_cases/build/build-upload-help-macos.trycmd @@ -59,9 +59,6 @@ Options: most CI environments. --no-git-metadata Disable collection and sending of git metadata. - --metadata - Custom metadata to include in the build upload (e.g., --metadata gradle-plugin=4.12.0). - Can be specified multiple times. -h, --help Print help diff --git a/tests/integration/_cases/build/build-upload-help-not-macos.trycmd b/tests/integration/_cases/build/build-upload-help-not-macos.trycmd index 382ab249c2..3156bd8bfa 100644 --- a/tests/integration/_cases/build/build-upload-help-not-macos.trycmd +++ b/tests/integration/_cases/build/build-upload-help-not-macos.trycmd @@ -58,9 +58,6 @@ Options: most CI environments. --no-git-metadata Disable collection and sending of git metadata. - --metadata - Custom metadata to include in the build upload (e.g., --metadata gradle-plugin=4.12.0). - Can be specified multiple times. -h, --help Print help From 53daba1881bd0cd6d7c9be8dc3c9f3bc1b83bfb5 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 10 Dec 2025 17:01:24 +0100 Subject: [PATCH 7/8] style: Apply cargo fmt formatting (EME-XXX) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Format code to match Rust style guidelines. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/commands/build/upload.rs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/commands/build/upload.rs b/src/commands/build/upload.rs index 847a9c09f3..e4ee2589af 100644 --- a/src/commands/build/upload.rs +++ b/src/commands/build/upload.rs @@ -196,15 +196,22 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { let normalized_zip = if path.is_file() { debug!("Normalizing file: {}", path.display()); - handle_file(path, &byteview, plugin_name.as_deref(), plugin_version.as_deref())? + handle_file( + path, + &byteview, + plugin_name.as_deref(), + plugin_version.as_deref(), + )? } else if path.is_dir() { debug!("Normalizing directory: {}", path.display()); - handle_directory(path, plugin_name.as_deref(), plugin_version.as_deref()).with_context(|| { - format!( - "Failed to generate uploadable bundle for directory {}", - path.display() - ) - })? + handle_directory(path, plugin_name.as_deref(), plugin_version.as_deref()).with_context( + || { + format!( + "Failed to generate uploadable bundle for directory {}", + path.display() + ) + }, + )? } else { Err(anyhow!( "Path {} is neither a file nor a directory, cannot upload", @@ -1002,7 +1009,10 @@ mod tests { // Should not have any other lines besides the version let line_count = metadata_content.lines().count(); - assert_eq!(line_count, 1, "Should only have one line when no plugin info"); + assert_eq!( + line_count, 1, + "Should only have one line when no plugin info" + ); Ok(()) } } From c837eb62f1d164d503814ac9bf1d81856074ede1 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 10 Dec 2025 17:16:20 +0100 Subject: [PATCH 8/8] docs(changelog): Update build upload plugin tracking description (EME-606) Update changelog to reflect that plugin versions are automatically tracked from SENTRY_PIPELINE environment variable rather than via CLI parameters. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 468c1f6bb2..0d210a894f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - For the `sentry-cli build upload` command, we now only auto-detect Git metadata when we detect we are running in a CI environment, unless the user manually overrides this behavior ([#2974](https://github.com/getsentry/sentry-cli/pull/2974)). This change prevents local development builds from triggiering GitHub status checks for size analysis. - We can detect most common CI environments based on the environment variables these set. - We introduced two new arguments, `--force-git-metadata` and `--no-git-metadata`, which force-enable and force-disable automatic Git data collection, respectively, overriding the default behavior. -- The `sentry-cli build upload` command now accepts a `--metadata` parameter to include custom metadata in build uploads ([#2994](https://github.com/getsentry/sentry-cli/pull/2994)). Metadata can be specified as key-value pairs (e.g., `--metadata gradle-plugin=4.12.0 --metadata fastlane-plugin=1.2.3`) and is written to the `.sentry-cli-metadata.txt` file in uploaded build archives, enabling the backend to store metadata for size analysis and build distribution tracking. +- The `sentry-cli build upload` command now automatically tracks Sentry plugin versions from the `SENTRY_PIPELINE` environment variable ([#2994](https://github.com/getsentry/sentry-cli/pull/2994)). When `SENTRY_PIPELINE` contains a recognized Sentry plugin (e.g., `sentry-gradle-plugin/4.12.0` or `sentry-fastlane-plugin/1.2.3`), the plugin version is written to the `.sentry-cli-metadata.txt` file in uploaded build archives, enabling the backend to store metadata for size analysis and build distribution tracking. - The `sentry-cli build upload` command now automatically detects the correct branch or tag reference in non-PR GitHub Actions workflows ([#2976](https://github.com/getsentry/sentry-cli/pull/2976)). Previously, `--head-ref` was only auto-detected for pull request workflows. Now it works for push, release, and other workflow types by using the `GITHUB_REF_NAME` environment variable. ## 2.58.2