Skip to content

Commit 2ab57cf

Browse files
runningcodeclaude
andcommitted
refactor(build): Use --metadata parameter for flexible key-value metadata (EME-XXX)
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 <noreply@anthropic.com>
1 parent d43c2a5 commit 2ab57cf

File tree

3 files changed

+64
-75
lines changed

3 files changed

+64
-75
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
- 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.
88
- We can detect most common CI environments based on the environment variables these set.
99
- 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.
10-
- 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.
10+
- 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.
1111
- 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.
1212

1313
## 2.58.2

src/commands/build/upload.rs

Lines changed: 51 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::borrow::Cow;
2+
use std::collections::HashMap;
23
use std::io::Write as _;
34
use std::path::Path;
45

@@ -124,14 +125,11 @@ pub fn make_command(command: Command) -> Command {
124125
.help("Disable collection and sending of git metadata.")
125126
)
126127
.arg(
127-
Arg::new("gradle_plugin_version")
128-
.long("gradle-plugin-version")
129-
.help("The version of the Sentry Gradle plugin used to build the artifact.")
130-
)
131-
.arg(
132-
Arg::new("fastlane_plugin_version")
133-
.long("fastlane-plugin-version")
134-
.help("The version of the Sentry Fastlane plugin used to build the artifact.")
128+
Arg::new("metadata")
129+
.long("metadata")
130+
.value_name("KEY=VALUE")
131+
.help("Custom metadata to include in the build upload (e.g., --metadata gradle-plugin=4.12.0). Can be specified multiple times.")
132+
.action(ArgAction::Append)
135133
)
136134
}
137135

@@ -159,10 +157,24 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
159157

160158
let build_configuration = matches.get_one("build_configuration").map(String::as_str);
161159
let release_notes = matches.get_one("release_notes").map(String::as_str);
162-
let gradle_plugin_version = matches.get_one("gradle_plugin_version").map(String::as_str);
163-
let fastlane_plugin_version = matches
164-
.get_one("fastlane_plugin_version")
165-
.map(String::as_str);
160+
161+
// Parse metadata key-value pairs
162+
let metadata: HashMap<String, String> = matches
163+
.get_many::<String>("metadata")
164+
.map(|values| {
165+
values
166+
.filter_map(|s| {
167+
let parts: Vec<&str> = s.splitn(2, '=').collect();
168+
if parts.len() == 2 {
169+
Some((parts[0].to_string(), parts[1].to_string()))
170+
} else {
171+
warn!("Ignoring invalid metadata format: {s}. Expected format: KEY=VALUE");
172+
None
173+
}
174+
})
175+
.collect()
176+
})
177+
.unwrap_or_default();
166178

167179
let api = Api::current();
168180
let authenticated_api = api.authenticated()?;
@@ -185,22 +197,15 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
185197

186198
let normalized_zip = if path.is_file() {
187199
debug!("Normalizing file: {}", path.display());
188-
handle_file(
189-
path,
190-
&byteview,
191-
gradle_plugin_version,
192-
fastlane_plugin_version,
193-
)?
200+
handle_file(path, &byteview, &metadata)?
194201
} else if path.is_dir() {
195202
debug!("Normalizing directory: {}", path.display());
196-
handle_directory(path, gradle_plugin_version, fastlane_plugin_version).with_context(
197-
|| {
198-
format!(
199-
"Failed to generate uploadable bundle for directory {}",
200-
path.display()
201-
)
202-
},
203-
)?
203+
handle_directory(path, &metadata).with_context(|| {
204+
format!(
205+
"Failed to generate uploadable bundle for directory {}",
206+
path.display()
207+
)
208+
})?
204209
} else {
205210
Err(anyhow!(
206211
"Path {} is neither a file nor a directory, cannot upload",
@@ -280,7 +285,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
280285
Ok(())
281286
}
282287

283-
<<<<<<< HEAD
284288
/// Collects git metadata from arguments and VCS introspection.
285289
///
286290
/// When `auto_collect` is false, only explicitly provided values are collected;
@@ -459,28 +463,19 @@ fn collect_git_metadata(
459463
fn handle_file(
460464
path: &Path,
461465
byteview: &ByteView,
462-
gradle_plugin_version: Option<&str>,
463-
fastlane_plugin_version: Option<&str>,
466+
metadata: &HashMap<String, String>,
464467
) -> Result<TempFile> {
465468
// Handle IPA files by converting them to XCArchive
466469
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
467470
if is_zip_file(byteview) && is_ipa_file(byteview)? {
468471
debug!("Converting IPA file to XCArchive structure");
469472
let archive_temp_dir = TempDir::create()?;
470473
return ipa_to_xcarchive(path, byteview, &archive_temp_dir)
471-
.and_then(|path| {
472-
handle_directory(&path, gradle_plugin_version, fastlane_plugin_version)
473-
})
474+
.and_then(|path| handle_directory(&path, metadata))
474475
.with_context(|| format!("Failed to process IPA file {}", path.display()));
475476
}
476477

477-
normalize_file(
478-
path,
479-
byteview,
480-
gradle_plugin_version,
481-
fastlane_plugin_version,
482-
)
483-
.with_context(|| {
478+
normalize_file(path, byteview, metadata).with_context(|| {
484479
format!(
485480
"Failed to generate uploadable bundle for file {}",
486481
path.display()
@@ -537,8 +532,7 @@ fn validate_is_supported_build(path: &Path, bytes: &[u8]) -> Result<()> {
537532
fn normalize_file(
538533
path: &Path,
539534
bytes: &[u8],
540-
gradle_plugin_version: Option<&str>,
541-
fastlane_plugin_version: Option<&str>,
535+
metadata: &HashMap<String, String>,
542536
) -> Result<TempFile> {
543537
debug!("Creating normalized zip for file: {}", path.display());
544538

@@ -563,29 +557,20 @@ fn normalize_file(
563557
zip.start_file(file_name, options)?;
564558
zip.write_all(bytes)?;
565559

566-
write_version_metadata(&mut zip, gradle_plugin_version, fastlane_plugin_version)?;
560+
write_version_metadata(&mut zip, metadata)?;
567561

568562
zip.finish()?;
569563
debug!("Successfully created normalized zip for file");
570564
Ok(temp_file)
571565
}
572566

573-
fn handle_directory(
574-
path: &Path,
575-
gradle_plugin_version: Option<&str>,
576-
fastlane_plugin_version: Option<&str>,
577-
) -> Result<TempFile> {
567+
fn handle_directory(path: &Path, metadata: &HashMap<String, String>) -> Result<TempFile> {
578568
let temp_dir = TempDir::create()?;
579569
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
580570
if is_apple_app(path)? {
581571
handle_asset_catalogs(path, temp_dir.path());
582572
}
583-
normalize_directory(
584-
path,
585-
temp_dir.path(),
586-
gradle_plugin_version,
587-
fastlane_plugin_version,
588-
)
573+
normalize_directory(path, temp_dir.path(), metadata)
589574
}
590575

591576
/// Returns artifact url if upload was successful.
@@ -723,7 +708,7 @@ mod tests {
723708
fs::create_dir_all(test_dir.join("Products"))?;
724709
fs::write(test_dir.join("Products").join("app.txt"), "test content")?;
725710

726-
let result_zip = normalize_directory(&test_dir, temp_dir.path(), None, None)?;
711+
let result_zip = normalize_directory(&test_dir, temp_dir.path(), &HashMap::new())?;
727712
let zip_file = fs::File::open(result_zip.path())?;
728713
let mut archive = ZipArchive::new(zip_file)?;
729714
let file = archive.by_index(0)?;
@@ -739,7 +724,7 @@ mod tests {
739724
let xcarchive_path = Path::new("tests/integration/_fixtures/build/archive.xcarchive");
740725

741726
// Process the XCArchive directory
742-
let result = handle_directory(xcarchive_path, None, None)?;
727+
let result = handle_directory(xcarchive_path, &HashMap::new())?;
743728

744729
// Verify the resulting zip contains parsed assets
745730
let zip_file = fs::File::open(result.path())?;
@@ -772,7 +757,7 @@ mod tests {
772757
let byteview = ByteView::open(ipa_path)?;
773758

774759
// Process the IPA file - this should work even without asset catalogs
775-
let result = handle_file(ipa_path, &byteview, None, None)?;
760+
let result = handle_file(ipa_path, &byteview, &HashMap::new())?;
776761

777762
let zip_file = fs::File::open(result.path())?;
778763
let mut archive = ZipArchive::new(zip_file)?;
@@ -809,7 +794,7 @@ mod tests {
809794
let symlink_path = test_dir.join("Products").join("app_link.txt");
810795
symlink("app.txt", &symlink_path)?;
811796

812-
let result_zip = normalize_directory(&test_dir, temp_dir.path(), None, None)?;
797+
let result_zip = normalize_directory(&test_dir, temp_dir.path(), &HashMap::new())?;
813798
let zip_file = fs::File::open(result_zip.path())?;
814799
let mut archive = ZipArchive::new(zip_file)?;
815800

@@ -844,7 +829,6 @@ mod tests {
844829
}
845830

846831
#[test]
847-
<<<<<<< HEAD
848832
fn test_collect_git_metadata_respects_explicit_values_when_auto_collect_disabled() {
849833
// Create a mock ArgMatches with explicit --head-sha and --vcs-provider values
850834
let app = make_command(Command::new("test"));
@@ -935,8 +919,11 @@ mod tests {
935919
fs::create_dir_all(test_dir.join("Products"))?;
936920
fs::write(test_dir.join("Products").join("app.txt"), "test content")?;
937921

938-
let result_zip =
939-
normalize_directory(&test_dir, temp_dir.path(), Some("4.12.0"), Some("1.2.3"))?;
922+
let mut metadata = HashMap::new();
923+
metadata.insert("gradle-plugin".to_string(), "4.12.0".to_string());
924+
metadata.insert("fastlane-plugin".to_string(), "1.2.3".to_string());
925+
926+
let result_zip = normalize_directory(&test_dir, temp_dir.path(), &metadata)?;
940927
let zip_file = fs::File::open(result_zip.path())?;
941928
let mut archive = ZipArchive::new(zip_file)?;
942929

@@ -949,12 +936,12 @@ mod tests {
949936
"Metadata should contain sentry-cli-version"
950937
);
951938
assert!(
952-
metadata_content.contains("gradle-plugin-version: 4.12.0"),
953-
"Metadata should contain gradle-plugin-version"
939+
metadata_content.contains("gradle-plugin: 4.12.0"),
940+
"Metadata should contain gradle-plugin"
954941
);
955942
assert!(
956-
metadata_content.contains("fastlane-plugin-version: 1.2.3"),
957-
"Metadata should contain fastlane-plugin-version"
943+
metadata_content.contains("fastlane-plugin: 1.2.3"),
944+
"Metadata should contain fastlane-plugin"
958945
);
959946
Ok(())
960947
}

src/utils/build/normalize.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::borrow::Cow;
2+
use std::collections::HashMap;
23
#[cfg(not(windows))]
34
use std::fs;
45
use std::fs::File;
@@ -94,17 +95,19 @@ fn metadata_file_options() -> SimpleFileOptions {
9495

9596
pub fn write_version_metadata<W: std::io::Write + std::io::Seek>(
9697
zip: &mut ZipWriter<W>,
97-
gradle_plugin_version: Option<&str>,
98-
fastlane_plugin_version: Option<&str>,
98+
metadata: &HashMap<String, String>,
9999
) -> Result<()> {
100100
let version = get_version();
101101
zip.start_file(".sentry-cli-metadata.txt", metadata_file_options())?;
102102
writeln!(zip, "sentry-cli-version: {version}")?;
103-
if let Some(gradle_version) = gradle_plugin_version {
104-
writeln!(zip, "gradle-plugin-version: {gradle_version}")?;
105-
}
106-
if let Some(fastlane_version) = fastlane_plugin_version {
107-
writeln!(zip, "fastlane-plugin-version: {fastlane_version}")?;
103+
104+
// Write metadata in sorted order for deterministic output
105+
let mut keys: Vec<_> = metadata.keys().collect();
106+
keys.sort();
107+
for key in keys {
108+
if let Some(value) = metadata.get(key) {
109+
writeln!(zip, "{key}: {value}")?;
110+
}
108111
}
109112
Ok(())
110113
}
@@ -115,8 +118,7 @@ pub fn write_version_metadata<W: std::io::Write + std::io::Seek>(
115118
pub fn normalize_directory(
116119
path: &Path,
117120
parsed_assets_path: &Path,
118-
gradle_plugin_version: Option<&str>,
119-
fastlane_plugin_version: Option<&str>,
121+
metadata: &HashMap<String, String>,
120122
) -> Result<TempFile> {
121123
debug!("Creating normalized zip for directory: {}", path.display());
122124

@@ -146,7 +148,7 @@ pub fn normalize_directory(
146148
)?;
147149
}
148150

149-
write_version_metadata(&mut zip, gradle_plugin_version, fastlane_plugin_version)?;
151+
write_version_metadata(&mut zip, metadata)?;
150152

151153
zip.finish()?;
152154
debug!("Successfully created normalized zip for directory with {file_count} files");

0 commit comments

Comments
 (0)