Skip to content

Commit c981217

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 2ba8eda commit c981217

File tree

3 files changed

+64
-73
lines changed

3 files changed

+64
-73
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
### Improvements
66

7-
- 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.
7+
- 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.
88
- 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.
99

1010
## 2.58.2

src/commands/build/upload.rs

Lines changed: 51 additions & 62 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

@@ -107,14 +108,11 @@ pub fn make_command(command: Command) -> Command {
107108
.help("The release notes to use for the upload.")
108109
)
109110
.arg(
110-
Arg::new("gradle_plugin_version")
111-
.long("gradle-plugin-version")
112-
.help("The version of the Sentry Gradle plugin used to build the artifact.")
113-
)
114-
.arg(
115-
Arg::new("fastlane_plugin_version")
116-
.long("fastlane-plugin-version")
117-
.help("The version of the Sentry Fastlane plugin used to build the artifact.")
111+
Arg::new("metadata")
112+
.long("metadata")
113+
.value_name("KEY=VALUE")
114+
.help("Custom metadata to include in the build upload (e.g., --metadata gradle-plugin=4.12.0). Can be specified multiple times.")
115+
.action(ArgAction::Append)
118116
)
119117
}
120118

@@ -290,10 +288,24 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
290288

291289
let build_configuration = matches.get_one("build_configuration").map(String::as_str);
292290
let release_notes = matches.get_one("release_notes").map(String::as_str);
293-
let gradle_plugin_version = matches.get_one("gradle_plugin_version").map(String::as_str);
294-
let fastlane_plugin_version = matches
295-
.get_one("fastlane_plugin_version")
296-
.map(String::as_str);
291+
292+
// Parse metadata key-value pairs
293+
let metadata: HashMap<String, String> = matches
294+
.get_many::<String>("metadata")
295+
.map(|values| {
296+
values
297+
.filter_map(|s| {
298+
let parts: Vec<&str> = s.splitn(2, '=').collect();
299+
if parts.len() == 2 {
300+
Some((parts[0].to_string(), parts[1].to_string()))
301+
} else {
302+
warn!("Ignoring invalid metadata format: {s}. Expected format: KEY=VALUE");
303+
None
304+
}
305+
})
306+
.collect()
307+
})
308+
.unwrap_or_default();
297309

298310
let api = Api::current();
299311
let authenticated_api = api.authenticated()?;
@@ -316,22 +328,15 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
316328

317329
let normalized_zip = if path.is_file() {
318330
debug!("Normalizing file: {}", path.display());
319-
handle_file(
320-
path,
321-
&byteview,
322-
gradle_plugin_version,
323-
fastlane_plugin_version,
324-
)?
331+
handle_file(path, &byteview, &metadata)?
325332
} else if path.is_dir() {
326333
debug!("Normalizing directory: {}", path.display());
327-
handle_directory(path, gradle_plugin_version, fastlane_plugin_version).with_context(
328-
|| {
329-
format!(
330-
"Failed to generate uploadable bundle for directory {}",
331-
path.display()
332-
)
333-
},
334-
)?
334+
handle_directory(path, &metadata).with_context(|| {
335+
format!(
336+
"Failed to generate uploadable bundle for directory {}",
337+
path.display()
338+
)
339+
})?
335340
} else {
336341
Err(anyhow!(
337342
"Path {} is neither a file nor a directory, cannot upload",
@@ -424,28 +429,19 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
424429
fn handle_file(
425430
path: &Path,
426431
byteview: &ByteView,
427-
gradle_plugin_version: Option<&str>,
428-
fastlane_plugin_version: Option<&str>,
432+
metadata: &HashMap<String, String>,
429433
) -> Result<TempFile> {
430434
// Handle IPA files by converting them to XCArchive
431435
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
432436
if is_zip_file(byteview) && is_ipa_file(byteview)? {
433437
debug!("Converting IPA file to XCArchive structure");
434438
let archive_temp_dir = TempDir::create()?;
435439
return ipa_to_xcarchive(path, byteview, &archive_temp_dir)
436-
.and_then(|path| {
437-
handle_directory(&path, gradle_plugin_version, fastlane_plugin_version)
438-
})
440+
.and_then(|path| handle_directory(&path, metadata))
439441
.with_context(|| format!("Failed to process IPA file {}", path.display()));
440442
}
441443

442-
normalize_file(
443-
path,
444-
byteview,
445-
gradle_plugin_version,
446-
fastlane_plugin_version,
447-
)
448-
.with_context(|| {
444+
normalize_file(path, byteview, metadata).with_context(|| {
449445
format!(
450446
"Failed to generate uploadable bundle for file {}",
451447
path.display()
@@ -502,8 +498,7 @@ fn validate_is_supported_build(path: &Path, bytes: &[u8]) -> Result<()> {
502498
fn normalize_file(
503499
path: &Path,
504500
bytes: &[u8],
505-
gradle_plugin_version: Option<&str>,
506-
fastlane_plugin_version: Option<&str>,
501+
metadata: &HashMap<String, String>,
507502
) -> Result<TempFile> {
508503
debug!("Creating normalized zip for file: {}", path.display());
509504

@@ -528,29 +523,20 @@ fn normalize_file(
528523
zip.start_file(file_name, options)?;
529524
zip.write_all(bytes)?;
530525

531-
write_version_metadata(&mut zip, gradle_plugin_version, fastlane_plugin_version)?;
526+
write_version_metadata(&mut zip, metadata)?;
532527

533528
zip.finish()?;
534529
debug!("Successfully created normalized zip for file");
535530
Ok(temp_file)
536531
}
537532

538-
fn handle_directory(
539-
path: &Path,
540-
gradle_plugin_version: Option<&str>,
541-
fastlane_plugin_version: Option<&str>,
542-
) -> Result<TempFile> {
533+
fn handle_directory(path: &Path, metadata: &HashMap<String, String>) -> Result<TempFile> {
543534
let temp_dir = TempDir::create()?;
544535
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
545536
if is_apple_app(path)? {
546537
handle_asset_catalogs(path, temp_dir.path());
547538
}
548-
normalize_directory(
549-
path,
550-
temp_dir.path(),
551-
gradle_plugin_version,
552-
fastlane_plugin_version,
553-
)
539+
normalize_directory(path, temp_dir.path(), metadata)
554540
}
555541

556542
/// Returns artifact url if upload was successful.
@@ -686,7 +672,7 @@ mod tests {
686672
fs::create_dir_all(test_dir.join("Products"))?;
687673
fs::write(test_dir.join("Products").join("app.txt"), "test content")?;
688674

689-
let result_zip = normalize_directory(&test_dir, temp_dir.path(), None, None)?;
675+
let result_zip = normalize_directory(&test_dir, temp_dir.path(), &HashMap::new())?;
690676
let zip_file = fs::File::open(result_zip.path())?;
691677
let mut archive = ZipArchive::new(zip_file)?;
692678
let file = archive.by_index(0)?;
@@ -702,7 +688,7 @@ mod tests {
702688
let xcarchive_path = Path::new("tests/integration/_fixtures/build/archive.xcarchive");
703689

704690
// Process the XCArchive directory
705-
let result = handle_directory(xcarchive_path, None, None)?;
691+
let result = handle_directory(xcarchive_path, &HashMap::new())?;
706692

707693
// Verify the resulting zip contains parsed assets
708694
let zip_file = fs::File::open(result.path())?;
@@ -735,7 +721,7 @@ mod tests {
735721
let byteview = ByteView::open(ipa_path)?;
736722

737723
// Process the IPA file - this should work even without asset catalogs
738-
let result = handle_file(ipa_path, &byteview, None, None)?;
724+
let result = handle_file(ipa_path, &byteview, &HashMap::new())?;
739725

740726
let zip_file = fs::File::open(result.path())?;
741727
let mut archive = ZipArchive::new(zip_file)?;
@@ -772,7 +758,7 @@ mod tests {
772758
let symlink_path = test_dir.join("Products").join("app_link.txt");
773759
symlink("app.txt", &symlink_path)?;
774760

775-
let result_zip = normalize_directory(&test_dir, temp_dir.path(), None, None)?;
761+
let result_zip = normalize_directory(&test_dir, temp_dir.path(), &HashMap::new())?;
776762
let zip_file = fs::File::open(result_zip.path())?;
777763
let mut archive = ZipArchive::new(zip_file)?;
778764

@@ -813,8 +799,11 @@ mod tests {
813799
fs::create_dir_all(test_dir.join("Products"))?;
814800
fs::write(test_dir.join("Products").join("app.txt"), "test content")?;
815801

816-
let result_zip =
817-
normalize_directory(&test_dir, temp_dir.path(), Some("4.12.0"), Some("1.2.3"))?;
802+
let mut metadata = HashMap::new();
803+
metadata.insert("gradle-plugin".to_string(), "4.12.0".to_string());
804+
metadata.insert("fastlane-plugin".to_string(), "1.2.3".to_string());
805+
806+
let result_zip = normalize_directory(&test_dir, temp_dir.path(), &metadata)?;
818807
let zip_file = fs::File::open(result_zip.path())?;
819808
let mut archive = ZipArchive::new(zip_file)?;
820809

@@ -827,12 +816,12 @@ mod tests {
827816
"Metadata should contain sentry-cli-version"
828817
);
829818
assert!(
830-
metadata_content.contains("gradle-plugin-version: 4.12.0"),
831-
"Metadata should contain gradle-plugin-version"
819+
metadata_content.contains("gradle-plugin: 4.12.0"),
820+
"Metadata should contain gradle-plugin"
832821
);
833822
assert!(
834-
metadata_content.contains("fastlane-plugin-version: 1.2.3"),
835-
"Metadata should contain fastlane-plugin-version"
823+
metadata_content.contains("fastlane-plugin: 1.2.3"),
824+
"Metadata should contain fastlane-plugin"
836825
);
837826
Ok(())
838827
}

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)