From c9f3fdfd933d3dc43d0981a08d5089a94336c480 Mon Sep 17 00:00:00 2001 From: Ryan Brooks Date: Fri, 27 Feb 2026 11:43:28 -0800 Subject: [PATCH 1/2] Upload image metadata json data as part of snapshots job --- src/api/data_types/snapshots.rs | 11 ++++- src/commands/build/snapshots.rs | 85 ++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/api/data_types/snapshots.rs b/src/api/data_types/snapshots.rs index 40d73354c6..0cff9a0540 100644 --- a/src/api/data_types/snapshots.rs +++ b/src/api/data_types/snapshots.rs @@ -22,9 +22,18 @@ pub struct SnapshotsManifest { // Keep in sync with https://github.com/getsentry/sentry/blob/master/src/sentry/preprod/snapshots/manifest.py /// Metadata for a single image in a snapshot manifest. -#[derive(Debug, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct ImageMetadata { pub image_file_name: String, pub width: u32, pub height: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, +} + +// Keep in sync with https://github.com/getsentry/sentry/blob/master/src/sentry/preprod/snapshots/manifest.py +/// A JSON manifest file that users can drop alongside images to provide additional metadata. +#[derive(Debug, Deserialize)] +pub struct SnapshotManifestFile { + pub images: HashMap, } diff --git a/src/commands/build/snapshots.rs b/src/commands/build/snapshots.rs index 2ded836073..8ba37cf3ed 100644 --- a/src/commands/build/snapshots.rs +++ b/src/commands/build/snapshots.rs @@ -12,7 +12,9 @@ use secrecy::ExposeSecret as _; use sha2::{Digest as _, Sha256}; use walkdir::WalkDir; -use crate::api::{Api, CreateSnapshotResponse, ImageMetadata, SnapshotsManifest}; +use crate::api::{ + Api, CreateSnapshotResponse, ImageMetadata, SnapshotManifestFile, SnapshotsManifest, +}; use crate::config::{Auth, Config}; use crate::utils::args::ArgExt as _; @@ -95,7 +97,13 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { style(images.len()).yellow(), if images.len() == 1 { "file" } else { "files" } ); - let manifest_entries = upload_images(images, &org, &project)?; + let mut manifest_entries = upload_images(images, &org, &project)?; + + // Parse JSON manifest files and merge metadata into discovered images + let json_manifests = collect_manifests(dir_path); + if !json_manifests.is_empty() { + merge_manifest_metadata(&mut manifest_entries, &json_manifests); + } // Build manifest from discovered images let manifest = SnapshotsManifest { @@ -253,6 +261,7 @@ fn upload_images( image_file_name, width: image.width, height: image.height, + display_name: None, }, ); } @@ -280,3 +289,75 @@ fn upload_images( } } } + +fn collect_manifests(dir: &Path) -> Vec { + WalkDir::new(dir) + .follow_links(true) + .into_iter() + .filter_entry(|e| !is_hidden(dir, e.path())) + .filter_map(|res| match res { + Ok(entry) => Some(entry), + Err(err) => { + warn!("Failed to access file during directory walk: {err}"); + None + } + }) + .filter(|entry| entry.file_type().is_file()) + .filter(|entry| { + entry + .path() + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.eq_ignore_ascii_case("json")) + .unwrap_or(false) + }) + .filter_map(|entry| { + let path = entry.path(); + debug!("Reading manifest file: {}", path.display()); + let contents = match fs::read_to_string(path) { + Ok(c) => c, + Err(err) => { + warn!("Failed to read manifest file {}: {err}", path.display()); + return None; + } + }; + match serde_json::from_str::(&contents) { + Ok(manifest) => Some(manifest), + Err(err) => { + warn!("Failed to parse manifest file {}: {err}", path.display()); + None + } + } + }) + .collect() +} + +fn merge_manifest_metadata( + manifest_entries: &mut HashMap, + json_manifests: &[SnapshotManifestFile], +) { + for json_manifest in json_manifests { + for json_image in json_manifest.images.values() { + let matched = manifest_entries + .values_mut() + .find(|entry| entry.image_file_name == json_image.image_file_name); + match matched { + Some(entry) => { + if let Some(ref display_name) = json_image.display_name { + debug!( + "Setting display_name for {}: {display_name}", + entry.image_file_name + ); + entry.display_name = Some(display_name.clone()); + } + } + None => { + warn!( + "Manifest entry for '{}' does not match any discovered image", + json_image.image_file_name + ); + } + } + } + } +} From ef67db9806c4d832d9f557b9113b71d6c79beff4 Mon Sep 17 00:00:00 2001 From: Ryan Brooks Date: Fri, 27 Feb 2026 12:34:13 -0800 Subject: [PATCH 2/2] Cleanups --- src/api/data_types/snapshots.rs | 9 +-- src/commands/build/snapshots.rs | 128 ++++++++++++++------------------ 2 files changed, 57 insertions(+), 80 deletions(-) diff --git a/src/api/data_types/snapshots.rs b/src/api/data_types/snapshots.rs index 0cff9a0540..b976f78807 100644 --- a/src/api/data_types/snapshots.rs +++ b/src/api/data_types/snapshots.rs @@ -22,7 +22,7 @@ pub struct SnapshotsManifest { // Keep in sync with https://github.com/getsentry/sentry/blob/master/src/sentry/preprod/snapshots/manifest.py /// Metadata for a single image in a snapshot manifest. -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Serialize)] pub struct ImageMetadata { pub image_file_name: String, pub width: u32, @@ -30,10 +30,3 @@ pub struct ImageMetadata { #[serde(skip_serializing_if = "Option::is_none")] pub display_name: Option, } - -// Keep in sync with https://github.com/getsentry/sentry/blob/master/src/sentry/preprod/snapshots/manifest.py -/// A JSON manifest file that users can drop alongside images to provide additional metadata. -#[derive(Debug, Deserialize)] -pub struct SnapshotManifestFile { - pub images: HashMap, -} diff --git a/src/commands/build/snapshots.rs b/src/commands/build/snapshots.rs index 8ba37cf3ed..1c8ddc0b1c 100644 --- a/src/commands/build/snapshots.rs +++ b/src/commands/build/snapshots.rs @@ -12,9 +12,9 @@ use secrecy::ExposeSecret as _; use sha2::{Digest as _, Sha256}; use walkdir::WalkDir; -use crate::api::{ - Api, CreateSnapshotResponse, ImageMetadata, SnapshotManifestFile, SnapshotsManifest, -}; +use serde::Deserialize; + +use crate::api::{Api, CreateSnapshotResponse, ImageMetadata, SnapshotsManifest}; use crate::config::{Auth, Config}; use crate::utils::args::ArgExt as _; @@ -97,13 +97,8 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { style(images.len()).yellow(), if images.len() == 1 { "file" } else { "files" } ); - let mut manifest_entries = upload_images(images, &org, &project)?; - - // Parse JSON manifest files and merge metadata into discovered images - let json_manifests = collect_manifests(dir_path); - if !json_manifests.is_empty() { - merge_manifest_metadata(&mut manifest_entries, &json_manifests); - } + let display_names = collect_display_names(dir_path); + let manifest_entries = upload_images(images, &display_names, &org, &project)?; // Build manifest from discovered images let manifest = SnapshotsManifest { @@ -198,6 +193,7 @@ fn is_image_file(path: &Path) -> bool { fn upload_images( images: Vec, + display_names: &HashMap, org: &str, project: &str, ) -> Result> { @@ -255,13 +251,14 @@ fn upload_images( .unwrap_or_default() .to_string_lossy() .into_owned(); + let display_name = display_names.get(&image_file_name).cloned(); manifest_entries.insert( hash, ImageMetadata { image_file_name, width: image.width, height: image.height, - display_name: None, + display_name, }, ); } @@ -290,74 +287,61 @@ fn upload_images( } } -fn collect_manifests(dir: &Path) -> Vec { - WalkDir::new(dir) +/// Input format for user-provided JSON manifest files. +#[derive(Deserialize)] +struct ManifestFile { + images: HashMap, +} + +#[derive(Deserialize)] +struct ManifestFileEntry { + image_file_name: String, + display_name: Option, +} + +/// Collects `image_file_name -> display_name` mappings from JSON manifest files in a directory. +fn collect_display_names(dir: &Path) -> HashMap { + let mut display_names = HashMap::new(); + let entries = WalkDir::new(dir) .follow_links(true) .into_iter() - .filter_entry(|e| !is_hidden(dir, e.path())) - .filter_map(|res| match res { - Ok(entry) => Some(entry), + .filter_entry(|e| !is_hidden(dir, e.path())); + + for entry in entries.flatten() { + if !entry.file_type().is_file() { + continue; + } + let path = entry.path(); + let is_json = path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.eq_ignore_ascii_case("json")) + .unwrap_or(false); + if !is_json { + continue; + } + + debug!("Reading manifest file: {}", path.display()); + let contents = match fs::read_to_string(path) { + Ok(c) => c, Err(err) => { - warn!("Failed to access file during directory walk: {err}"); - None + warn!("Failed to read manifest file {}: {err}", path.display()); + continue; } - }) - .filter(|entry| entry.file_type().is_file()) - .filter(|entry| { - entry - .path() - .extension() - .and_then(|ext| ext.to_str()) - .map(|ext| ext.eq_ignore_ascii_case("json")) - .unwrap_or(false) - }) - .filter_map(|entry| { - let path = entry.path(); - debug!("Reading manifest file: {}", path.display()); - let contents = match fs::read_to_string(path) { - Ok(c) => c, - Err(err) => { - warn!("Failed to read manifest file {}: {err}", path.display()); - return None; - } - }; - match serde_json::from_str::(&contents) { - Ok(manifest) => Some(manifest), - Err(err) => { - warn!("Failed to parse manifest file {}: {err}", path.display()); - None - } + }; + let manifest: ManifestFile = match serde_json::from_str(&contents) { + Ok(m) => m, + Err(err) => { + warn!("Failed to parse manifest file {}: {err}", path.display()); + continue; } - }) - .collect() -} + }; -fn merge_manifest_metadata( - manifest_entries: &mut HashMap, - json_manifests: &[SnapshotManifestFile], -) { - for json_manifest in json_manifests { - for json_image in json_manifest.images.values() { - let matched = manifest_entries - .values_mut() - .find(|entry| entry.image_file_name == json_image.image_file_name); - match matched { - Some(entry) => { - if let Some(ref display_name) = json_image.display_name { - debug!( - "Setting display_name for {}: {display_name}", - entry.image_file_name - ); - entry.display_name = Some(display_name.clone()); - } - } - None => { - warn!( - "Manifest entry for '{}' does not match any discovered image", - json_image.image_file_name - ); - } + for entry in manifest.images.into_values() { + if let Some(display_name) = entry.display_name { + display_names.insert(entry.image_file_name, display_name); } } } + display_names }