From b1d8cdaec344c1cf2bb3b8d5fcb467e050e55431 Mon Sep 17 00:00:00 2001 From: Ryan Brooks Date: Thu, 7 Aug 2025 11:10:04 -0700 Subject: [PATCH 01/14] Add new VCS params to mobile-app command --- src/api/data_types/chunking/mobile_app.rs | 17 +++- src/api/mod.rs | 18 +++- src/commands/mobile_app/upload.rs | 103 +++++++++++++++++++--- src/utils/vcs.rs | 8 ++ 4 files changed, 130 insertions(+), 16 deletions(-) diff --git a/src/api/data_types/chunking/mobile_app.rs b/src/api/data_types/chunking/mobile_app.rs index 6223e78c30..4cb6f96bfb 100644 --- a/src/api/data_types/chunking/mobile_app.rs +++ b/src/api/data_types/chunking/mobile_app.rs @@ -9,9 +9,24 @@ pub struct ChunkedMobileAppRequest<'a> { pub checksum: Digest, pub chunks: &'a [Digest], #[serde(skip_serializing_if = "Option::is_none")] + pub build_configuration: Option<&'a str>, + // VCS fields + #[serde(skip_serializing_if = "Option::is_none")] pub head_sha: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] - pub build_configuration: Option<&'a str>, + pub base_sha: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub provider: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub head_repo_name: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub base_repo_name: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub head_ref: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub base_ref: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub pr_number: Option<&'a str>, } #[derive(Debug, Deserialize)] diff --git a/src/api/mod.rs b/src/api/mod.rs index 33c175bfd2..ea2d15ee0d 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1036,8 +1036,15 @@ impl<'a> AuthenticatedApi<'a> { project: &str, checksum: Digest, chunks: &[Digest], - head_sha: Option<&str>, build_configuration: Option<&str>, + head_sha: Option<&str>, + base_sha: Option<&str>, + provider: Option<&str>, + head_repo_name: Option<&str>, + base_repo_name: Option<&str>, + head_ref: Option<&str>, + base_ref: Option<&str>, + pr_number: Option<&str>, ) -> ApiResult { let url = format!( "/projects/{}/{}/files/preprodartifacts/assemble/", @@ -1049,8 +1056,15 @@ impl<'a> AuthenticatedApi<'a> { .with_json_body(&ChunkedMobileAppRequest { checksum, chunks, - head_sha, build_configuration, + head_sha, + base_sha, + provider, + head_repo_name, + base_repo_name, + head_ref, + base_ref, + pr_number, })? .send()? .convert_rnf(ApiErrorKind::ProjectNotFound) diff --git a/src/commands/mobile_app/upload.rs b/src/commands/mobile_app/upload.rs index 55ccad7c64..179e7d7f13 100644 --- a/src/commands/mobile_app/upload.rs +++ b/src/commands/mobile_app/upload.rs @@ -44,9 +44,44 @@ pub fn make_command(command: Command) -> Command { .required(true), ) .arg( - Arg::new("sha") - .long("sha") - .help("The git commit sha to use for the upload. If not provided, the current commit sha will be used.") + Arg::new("head_sha") + .long("head-sha") + .help("The VCS commit sha to use for the upload. If not provided, the current commit sha will be used.") + ) + .arg( + Arg::new("base_sha") + .long("base-sha") + .help("The VCS commit's base sha to use for the upload. If not provided, the merge-base of the current and remote branch will be used.") + ) + .arg( + Arg::new("vcs_provider") + .long("vcs-provider") + .help("The VCS provider to use for the upload. If not provided, the current provider will be used.") + ) + .arg( + Arg::new("head_repo_name") + .long("head-repo-name") + .help("The name of the git repository to use for the upload (e.g. organization/repository). If not provided, the current repository will be used.") + ) + .arg( + Arg::new("base_repo_name") + .long("base-repo-name") + .help("The name of the git repository to use for the upload (e.g. organization/repository). If not provided, the current repository will be used.") + ) + .arg( + Arg::new("head_ref") + .long("head-ref") + .help("The reference (branch) to use for the upload. If not provided, the current reference will be used.") + ) + .arg( + Arg::new("base_ref") + .long("base-ref") + .help("The reference (branch) to use for the upload. If not provided, the current reference will be used.") + ) + .arg( + Arg::new("pr_number") + .long("pr-number") + .help("The pull request number to use for the upload. If not provided, the current pull request number will be used.") ) .arg( Arg::new("build_configuration") @@ -60,12 +95,21 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { .get_many::("paths") .expect("paths argument is required"); - let sha = matches - .get_one("sha") + let head_sha = matches + .get_one("head_sha") .map(String::as_str) .map(Cow::Borrowed) .or_else(|| vcs::find_head().ok().map(Cow::Owned)); + // TODO: Implement default values + let base_sha = matches.get_one("base_sha").map(String::as_str); + let vcs_provider = matches.get_one("vcs_provider").map(String::as_str); + let head_repo_name = matches.get_one("head_repo_name").map(String::as_str); + let base_repo_name = matches.get_one("base_repo_name").map(String::as_str); + let head_ref = matches.get_one("head_ref").map(String::as_str); + let base_ref = matches.get_one("base_ref").map(String::as_str); + let pr_number = matches.get_one("pr_number").map(String::as_str); + let build_configuration = matches.get_one("build_configuration").map(String::as_str); let api = Api::current(); @@ -129,8 +173,15 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { &bytes, &org, &project, - sha.as_deref(), build_configuration, + head_sha.as_deref(), + base_sha, + vcs_provider, + head_repo_name, + base_repo_name, + head_ref, + base_ref, + pr_number, ) { Ok(artifact_id) => { info!("Successfully uploaded file: {}", path.display()); @@ -289,17 +340,31 @@ fn upload_file( bytes: &[u8], org: &str, project: &str, - sha: Option<&str>, build_configuration: Option<&str>, + head_sha: Option<&str>, + base_sha: Option<&str>, + vcs_provider: Option<&str>, + head_repo_name: Option<&str>, + base_repo_name: Option<&str>, + head_ref: Option<&str>, + base_ref: Option<&str>, + pr_number: Option<&str>, ) -> Result { const SELF_HOSTED_ERROR_HINT: &str = "If you are using a self-hosted Sentry server, \ update to the latest version of Sentry to use the mobile-app upload command."; debug!( - "Uploading file to organization: {}, project: {}, sha: {}, build_configuration: {}", + "Uploading file to organization: {}, project: {}, head_sha: {}, base_sha: {}, vcs_provider: {}, head_repo_name: {}, base_repo_name: {}, head_ref: {}, base_ref: {}, pr_number: {}, build_configuration: {}", org, project, - sha.unwrap_or("unknown"), + head_sha.unwrap_or("unknown"), + base_sha.unwrap_or("unknown"), + vcs_provider.unwrap_or("unknown"), + head_repo_name.unwrap_or("unknown"), + base_repo_name.unwrap_or("unknown"), + head_ref.unwrap_or("unknown"), + base_ref.unwrap_or("unknown"), + pr_number.unwrap_or("unknown"), build_configuration.unwrap_or("unknown") ); @@ -345,10 +410,22 @@ fn upload_file( // In the case where something went wrong (which could be on either // iteration of the loop) we get: // n. state=err, artifact_id unset - let result = loop { - let response = - api.assemble_mobile_app(org, project, checksum, &checksums, sha, build_configuration)?; - chunks.retain(|Chunk((digest, _))| response.missing_chunks.contains(digest)); + let result = loop {let response = api.assemble_mobile_app( + org, + project, + checksum, + &checksums, + build_configuration, + head_sha, + base_sha, + vcs_provider, + head_repo_name, + base_repo_name, + head_ref, + base_ref, + pr_number, + )?; + chunks.retain(|Chunk((digest, _))| response.missing_chunks.contains(digest)); if !chunks.is_empty() { let upload_progress_style = ProgressStyle::default_bar().template( diff --git a/src/utils/vcs.rs b/src/utils/vcs.rs index d32ac34078..e96a5e305b 100644 --- a/src/utils/vcs.rs +++ b/src/utils/vcs.rs @@ -211,6 +211,7 @@ fn is_matching_url(a: &str, b: &str) -> bool { VcsUrl::parse(a) == VcsUrl::parse(b) } +// TODO: This is not used anywhere. pub fn get_repo_from_remote(repo: &str) -> String { let obj = VcsUrl::parse(repo); obj.id @@ -398,6 +399,13 @@ pub fn find_head() -> Result { Ok(head.id().to_string()) } +pub fn find_base_sha(repo: &Repository, branch: &str) -> Result { + let head = repo.revparse_single(branch)?; + Ok(head.id().to_string()) +} + + + /// Given commit specs, repos and remote_name this returns a list of head /// commits from it. pub fn find_heads( From cd79a6479b48dac0a69f6ca02c3af3a5c3d7f648 Mon Sep 17 00:00:00 2001 From: Ryan Brooks Date: Thu, 7 Aug 2025 14:29:40 -0700 Subject: [PATCH 02/14] Tweaks --- src/api/data_types/chunking/mobile_app.rs | 2 +- src/api/mod.rs | 2 +- src/commands/mobile_app/upload.rs | 9 ++++++--- src/utils/vcs.rs | 7 ------- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/api/data_types/chunking/mobile_app.rs b/src/api/data_types/chunking/mobile_app.rs index 4cb6f96bfb..6a35a4afc5 100644 --- a/src/api/data_types/chunking/mobile_app.rs +++ b/src/api/data_types/chunking/mobile_app.rs @@ -26,7 +26,7 @@ pub struct ChunkedMobileAppRequest<'a> { #[serde(skip_serializing_if = "Option::is_none")] pub base_ref: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] - pub pr_number: Option<&'a str>, + pub pr_number: Option, } #[derive(Debug, Deserialize)] diff --git a/src/api/mod.rs b/src/api/mod.rs index ea2d15ee0d..47a7d200ca 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1044,7 +1044,7 @@ impl<'a> AuthenticatedApi<'a> { base_repo_name: Option<&str>, head_ref: Option<&str>, base_ref: Option<&str>, - pr_number: Option<&str>, + pr_number: Option, ) -> ApiResult { let url = format!( "/projects/{}/{}/files/preprodartifacts/assemble/", diff --git a/src/commands/mobile_app/upload.rs b/src/commands/mobile_app/upload.rs index 179e7d7f13..a5dc61ddcc 100644 --- a/src/commands/mobile_app/upload.rs +++ b/src/commands/mobile_app/upload.rs @@ -108,7 +108,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { let base_repo_name = matches.get_one("base_repo_name").map(String::as_str); let head_ref = matches.get_one("head_ref").map(String::as_str); let base_ref = matches.get_one("base_ref").map(String::as_str); - let pr_number = matches.get_one("pr_number").map(String::as_str); + let pr_number = matches.get_one("pr_number").map(String::as_str).and_then(|s| s.parse::().ok()); let build_configuration = matches.get_one("build_configuration").map(String::as_str); @@ -348,7 +348,7 @@ fn upload_file( base_repo_name: Option<&str>, head_ref: Option<&str>, base_ref: Option<&str>, - pr_number: Option<&str>, + pr_number: Option, ) -> Result { const SELF_HOSTED_ERROR_HINT: &str = "If you are using a self-hosted Sentry server, \ update to the latest version of Sentry to use the mobile-app upload command."; @@ -364,7 +364,10 @@ fn upload_file( base_repo_name.unwrap_or("unknown"), head_ref.unwrap_or("unknown"), base_ref.unwrap_or("unknown"), - pr_number.unwrap_or("unknown"), + pr_number + .map(|n| n.to_string()) + .as_deref() + .unwrap_or("unknown"), build_configuration.unwrap_or("unknown") ); diff --git a/src/utils/vcs.rs b/src/utils/vcs.rs index e96a5e305b..dec63ac855 100644 --- a/src/utils/vcs.rs +++ b/src/utils/vcs.rs @@ -399,13 +399,6 @@ pub fn find_head() -> Result { Ok(head.id().to_string()) } -pub fn find_base_sha(repo: &Repository, branch: &str) -> Result { - let head = repo.revparse_single(branch)?; - Ok(head.id().to_string()) -} - - - /// Given commit specs, repos and remote_name this returns a list of head /// commits from it. pub fn find_heads( From 74c11528aa93730bb0a7a65abbbaa1a1ff6bec21 Mon Sep 17 00:00:00 2001 From: Ryan Brooks Date: Fri, 8 Aug 2025 10:57:06 -0700 Subject: [PATCH 03/14] Remove comment --- src/utils/vcs.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/vcs.rs b/src/utils/vcs.rs index dec63ac855..d32ac34078 100644 --- a/src/utils/vcs.rs +++ b/src/utils/vcs.rs @@ -211,7 +211,6 @@ fn is_matching_url(a: &str, b: &str) -> bool { VcsUrl::parse(a) == VcsUrl::parse(b) } -// TODO: This is not used anywhere. pub fn get_repo_from_remote(repo: &str) -> String { let obj = VcsUrl::parse(repo); obj.id From ebb5fcb4a8e74d7e4caeedfc5e3cf4075663a774 Mon Sep 17 00:00:00 2001 From: Ryan Brooks Date: Fri, 8 Aug 2025 11:10:47 -0700 Subject: [PATCH 04/14] fmt --- src/commands/mobile_app/upload.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/commands/mobile_app/upload.rs b/src/commands/mobile_app/upload.rs index a5dc61ddcc..99c174e4bc 100644 --- a/src/commands/mobile_app/upload.rs +++ b/src/commands/mobile_app/upload.rs @@ -108,7 +108,10 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { let base_repo_name = matches.get_one("base_repo_name").map(String::as_str); let head_ref = matches.get_one("head_ref").map(String::as_str); let base_ref = matches.get_one("base_ref").map(String::as_str); - let pr_number = matches.get_one("pr_number").map(String::as_str).and_then(|s| s.parse::().ok()); + let pr_number = matches + .get_one("pr_number") + .map(String::as_str) + .and_then(|s| s.parse::().ok()); let build_configuration = matches.get_one("build_configuration").map(String::as_str); From f01a03e66dbf6f0d937125dd50eaeb93a06027af Mon Sep 17 00:00:00 2001 From: Ryan Brooks Date: Fri, 8 Aug 2025 14:17:53 -0700 Subject: [PATCH 05/14] Clippy --- src/api/mod.rs | 40 +++++++++++++---------- src/commands/mobile_app/upload.rs | 53 ++++++++++--------------------- 2 files changed, 40 insertions(+), 53 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 47a7d200ca..393d47fc63 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1037,14 +1037,7 @@ impl<'a> AuthenticatedApi<'a> { checksum: Digest, chunks: &[Digest], build_configuration: Option<&str>, - head_sha: Option<&str>, - base_sha: Option<&str>, - provider: Option<&str>, - head_repo_name: Option<&str>, - base_repo_name: Option<&str>, - head_ref: Option<&str>, - base_ref: Option<&str>, - pr_number: Option, + vcs_info: &VcsInfo<'_>, ) -> ApiResult { let url = format!( "/projects/{}/{}/files/preprodartifacts/assemble/", @@ -1057,14 +1050,14 @@ impl<'a> AuthenticatedApi<'a> { checksum, chunks, build_configuration, - head_sha, - base_sha, - provider, - head_repo_name, - base_repo_name, - head_ref, - base_ref, - pr_number, + head_sha: vcs_info.head_sha, + base_sha: vcs_info.base_sha, + provider: vcs_info.vcs_provider, + head_repo_name: vcs_info.head_repo_name, + base_repo_name: vcs_info.base_repo_name, + head_ref: vcs_info.head_ref, + base_ref: vcs_info.base_ref, + pr_number: vcs_info.pr_number, })? .send()? .convert_rnf(ApiErrorKind::ProjectNotFound) @@ -2532,6 +2525,21 @@ struct LogsResponse { data: Vec, } +/// VCS information for mobile app uploads +#[derive(Debug)] +// This is not dead code because it is used in the mobile app upload command +#[expect(dead_code)] +pub struct VcsInfo<'a> { + pub head_sha: Option<&'a str>, + pub base_sha: Option<&'a str>, + pub vcs_provider: Option<&'a str>, + pub head_repo_name: Option<&'a str>, + pub base_repo_name: Option<&'a str>, + pub head_ref: Option<&'a str>, + pub base_ref: Option<&'a str>, + pub pr_number: Option, +} + /// Log entry structure from the logs API #[derive(Debug, Deserialize)] pub struct LogEntry { diff --git a/src/commands/mobile_app/upload.rs b/src/commands/mobile_app/upload.rs index 99c174e4bc..5dc10c2362 100644 --- a/src/commands/mobile_app/upload.rs +++ b/src/commands/mobile_app/upload.rs @@ -10,7 +10,7 @@ use symbolic::common::ByteView; use zip::write::SimpleFileOptions; use zip::{DateTime, ZipWriter}; -use crate::api::{Api, AuthenticatedApi, ChunkUploadCapability, ChunkedFileState}; +use crate::api::{Api, AuthenticatedApi, ChunkUploadCapability, ChunkedFileState, VcsInfo}; use crate::config::Config; use crate::utils::args::ArgExt as _; use crate::utils::chunks::{upload_chunks, Chunk}; @@ -171,13 +171,8 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { for (path, zip) in normalized_zips { info!("Uploading file: {}", path.display()); let bytes = ByteView::open(zip.path())?; - match upload_file( - &authenticated_api, - &bytes, - &org, - &project, - build_configuration, - head_sha.as_deref(), + let vcs_info = VcsInfo { + head_sha: head_sha.as_deref(), base_sha, vcs_provider, head_repo_name, @@ -185,6 +180,14 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { head_ref, base_ref, pr_number, + }; + match upload_file( + &authenticated_api, + &bytes, + &org, + &project, + build_configuration, + &vcs_info, ) { Ok(artifact_id) => { info!("Successfully uploaded file: {}", path.display()); @@ -344,34 +347,17 @@ fn upload_file( org: &str, project: &str, build_configuration: Option<&str>, - head_sha: Option<&str>, - base_sha: Option<&str>, - vcs_provider: Option<&str>, - head_repo_name: Option<&str>, - base_repo_name: Option<&str>, - head_ref: Option<&str>, - base_ref: Option<&str>, - pr_number: Option, + vcs_info: &VcsInfo<'_>, ) -> Result { const SELF_HOSTED_ERROR_HINT: &str = "If you are using a self-hosted Sentry server, \ update to the latest version of Sentry to use the mobile-app upload command."; debug!( - "Uploading file to organization: {}, project: {}, head_sha: {}, base_sha: {}, vcs_provider: {}, head_repo_name: {}, base_repo_name: {}, head_ref: {}, base_ref: {}, pr_number: {}, build_configuration: {}", + "Uploading file to organization: {}, project: {}, build_configuration: {}, vcs_info: {:?}", org, project, - head_sha.unwrap_or("unknown"), - base_sha.unwrap_or("unknown"), - vcs_provider.unwrap_or("unknown"), - head_repo_name.unwrap_or("unknown"), - base_repo_name.unwrap_or("unknown"), - head_ref.unwrap_or("unknown"), - base_ref.unwrap_or("unknown"), - pr_number - .map(|n| n.to_string()) - .as_deref() - .unwrap_or("unknown"), - build_configuration.unwrap_or("unknown") + build_configuration.unwrap_or("unknown"), + vcs_info, ); let chunk_upload_options = api.get_chunk_upload_options(org)?.ok_or_else(|| { @@ -422,14 +408,7 @@ fn upload_file( checksum, &checksums, build_configuration, - head_sha, - base_sha, - vcs_provider, - head_repo_name, - base_repo_name, - head_ref, - base_ref, - pr_number, + vcs_info, )?; chunks.retain(|Chunk((digest, _))| response.missing_chunks.contains(digest)); From e64d0aa1c82abc544ea8beb21313d2d40fc1fc4f Mon Sep 17 00:00:00 2001 From: Ryan Brooks Date: Fri, 8 Aug 2025 14:23:54 -0700 Subject: [PATCH 06/14] Adjust --- src/api/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 393d47fc63..cb9a4cd9fb 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2527,8 +2527,6 @@ struct LogsResponse { /// VCS information for mobile app uploads #[derive(Debug)] -// This is not dead code because it is used in the mobile app upload command -#[expect(dead_code)] pub struct VcsInfo<'a> { pub head_sha: Option<&'a str>, pub base_sha: Option<&'a str>, From 8665fa67ade89b111b75031626ad979a664273cb Mon Sep 17 00:00:00 2001 From: Ryan Brooks Date: Fri, 8 Aug 2025 14:27:47 -0700 Subject: [PATCH 07/14] unstable flag --- src/api/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/mod.rs b/src/api/mod.rs index cb9a4cd9fb..4aab424234 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2526,6 +2526,7 @@ struct LogsResponse { } /// VCS information for mobile app uploads +#[cfg(feature = "unstable-mobile-app")] #[derive(Debug)] pub struct VcsInfo<'a> { pub head_sha: Option<&'a str>, From c9dedf6e43631d237356be3f3846354fa8828577 Mon Sep 17 00:00:00 2001 From: Ryan Brooks Date: Fri, 8 Aug 2025 14:54:11 -0700 Subject: [PATCH 08/14] Update tests --- .../mobile_app/mobile_app-upload-apk.trycmd | 4 +-- .../mobile_app-upload-help-macos.trycmd | 31 ++++++++++++++++--- .../mobile_app/mobile_app-upload-ipa.trycmd | 2 +- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/tests/integration/_cases/mobile_app/mobile_app-upload-apk.trycmd b/tests/integration/_cases/mobile_app/mobile_app-upload-apk.trycmd index 1f2259717b..4f0821cf5a 100644 --- a/tests/integration/_cases/mobile_app/mobile_app-upload-apk.trycmd +++ b/tests/integration/_cases/mobile_app/mobile_app-upload-apk.trycmd @@ -1,6 +1,6 @@ ``` -$ sentry-cli mobile-app upload tests/integration/_fixtures/mobile_app/apk.apk --sha test_sha -? success +$ sentry-cli mobile-app upload tests/integration/_fixtures/mobile_app/apk.apk --head-sha test_head_sha +? 1 [..]WARN[..]EXPERIMENTAL: The mobile-app subcommand is experimental. The command is subject to breaking changes and may be removed without notice in any release. Successfully uploaded 1 file to Sentry - tests/integration/_fixtures/mobile_app/apk.apk (http[..]/wat-org/preprod/wat-project/42) diff --git a/tests/integration/_cases/mobile_app/mobile_app-upload-help-macos.trycmd b/tests/integration/_cases/mobile_app/mobile_app-upload-help-macos.trycmd index 2364c49980..9d8ff4d33a 100644 --- a/tests/integration/_cases/mobile_app/mobile_app-upload-help-macos.trycmd +++ b/tests/integration/_cases/mobile_app/mobile_app-upload-help-macos.trycmd @@ -19,17 +19,38 @@ Options: The project ID or slug. --auth-token Use the given Sentry auth token. - --sha - The git commit sha to use for the upload. If not provided, the current commit sha will be + --head-sha + The VCS commit sha to use for the upload. If not provided, the current commit sha will be used. - --build-configuration - The build configuration to use for the upload. If not provided, the current version will - be used. + --base-sha + The VCS commit's base sha to use for the upload. If not provided, the merge-base of the + current and remote branch will be used. --log-level Set the log output verbosity. [possible values: trace, debug, info, warn, error] + --vcs-provider + The VCS provider to use for the upload. If not provided, the current provider will be + used. + --head-repo-name + The name of the git repository to use for the upload (e.g. organization/repository). If + not provided, the current repository will be used. --quiet Do not print any output while preserving correct exit code. This flag is currently implemented only for selected subcommands. [aliases: silent] + --base-repo-name + The name of the git repository to use for the upload (e.g. organization/repository). If + not provided, the current repository will be used. + --head-ref + The reference (branch) to use for the upload. If not provided, the current reference will + be used. + --base-ref + The reference (branch) to use for the upload. If not provided, the current reference will + be used. + --pr-number + The pull request number to use for the upload. If not provided, the current pull request + number will be used. + --build-configuration + The build configuration to use for the upload. If not provided, the current version will + be used. -h, --help Print help diff --git a/tests/integration/_cases/mobile_app/mobile_app-upload-ipa.trycmd b/tests/integration/_cases/mobile_app/mobile_app-upload-ipa.trycmd index aafd34cafb..fb7a9d5590 100644 --- a/tests/integration/_cases/mobile_app/mobile_app-upload-ipa.trycmd +++ b/tests/integration/_cases/mobile_app/mobile_app-upload-ipa.trycmd @@ -1,5 +1,5 @@ ``` -$ sentry-cli mobile-app upload tests/integration/_fixtures/mobile_app/ipa.ipa --sha test_sha +$ sentry-cli mobile-app upload tests/integration/_fixtures/mobile_app/ipa.ipa --head-sha test_head_sha ? success [..]WARN[..]EXPERIMENTAL: The mobile-app subcommand is experimental. The command is subject to breaking changes and may be removed without notice in any release. Successfully uploaded 1 file to Sentry From c96cfc62c2c8d9a83b21c73993ca28a5566542dc Mon Sep 17 00:00:00 2001 From: Ryan Brooks Date: Fri, 8 Aug 2025 15:49:16 -0700 Subject: [PATCH 09/14] update test --- .../integration/_cases/mobile_app/mobile_app-upload-apk.trycmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/_cases/mobile_app/mobile_app-upload-apk.trycmd b/tests/integration/_cases/mobile_app/mobile_app-upload-apk.trycmd index 4f0821cf5a..1da11a9ee6 100644 --- a/tests/integration/_cases/mobile_app/mobile_app-upload-apk.trycmd +++ b/tests/integration/_cases/mobile_app/mobile_app-upload-apk.trycmd @@ -1,6 +1,6 @@ ``` $ sentry-cli mobile-app upload tests/integration/_fixtures/mobile_app/apk.apk --head-sha test_head_sha -? 1 +? success [..]WARN[..]EXPERIMENTAL: The mobile-app subcommand is experimental. The command is subject to breaking changes and may be removed without notice in any release. Successfully uploaded 1 file to Sentry - tests/integration/_fixtures/mobile_app/apk.apk (http[..]/wat-org/preprod/wat-project/42) From 64c77c8255203c3fdd043f8e2903b0ad17fb086a Mon Sep 17 00:00:00 2001 From: Ryan Brooks Date: Fri, 8 Aug 2025 15:53:51 -0700 Subject: [PATCH 10/14] Fix tests --- tests/integration/mobile_app/upload.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/mobile_app/upload.rs b/tests/integration/mobile_app/upload.rs index 613be3b42d..f9e2c6e4e1 100644 --- a/tests/integration/mobile_app/upload.rs +++ b/tests/integration/mobile_app/upload.rs @@ -159,7 +159,7 @@ fn command_mobile_app_upload_apk_chunked() { "/api/0/projects/wat-org/wat-project/files/preprodartifacts/assemble/", ) .with_header_matcher("content-type", "application/json") - .with_matcher(r#"{"checksum":"18e40e6e932d0b622d631e887be454cc2003dbb5","chunks":["18e40e6e932d0b622d631e887be454cc2003dbb5"],"head_sha":"test_sha"}"#) + .with_matcher(r#"{"checksum":"18e40e6e932d0b622d631e887be454cc2003dbb5","chunks":["18e40e6e932d0b622d631e887be454cc2003dbb5"],"head_sha":"test_head_sha"}"#) .with_response_fn(move |_| { if is_first_assemble_call.swap(false, Ordering::Relaxed) { r#"{ @@ -214,7 +214,7 @@ fn command_mobile_app_upload_ipa_chunked() { "/api/0/projects/wat-org/wat-project/files/preprodartifacts/assemble/", ) .with_header_matcher("content-type", "application/json") - .with_matcher(r#"{"checksum":"ed9da71e3688261875db21b266da84ffe004a8a4","chunks":["ed9da71e3688261875db21b266da84ffe004a8a4"],"head_sha":"test_sha"}"#) + .with_matcher(r#"{"checksum":"ed9da71e3688261875db21b266da84ffe004a8a4","chunks":["ed9da71e3688261875db21b266da84ffe004a8a4"],"head_sha":"test_head_sha"}"#) .with_response_fn(move |_| { if is_first_assemble_call.swap(false, Ordering::Relaxed) { r#"{ From 3ebad1923303ab0e000c074586e782f0650755d6 Mon Sep 17 00:00:00 2001 From: Ryan Brooks Date: Fri, 8 Aug 2025 16:01:37 -0700 Subject: [PATCH 11/14] Fix non macos test --- .../mobile_app-upload-help-not-macos.trycmd | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/tests/integration/_cases/mobile_app/mobile_app-upload-help-not-macos.trycmd b/tests/integration/_cases/mobile_app/mobile_app-upload-help-not-macos.trycmd index e77c625a01..7468ce9246 100644 --- a/tests/integration/_cases/mobile_app/mobile_app-upload-help-not-macos.trycmd +++ b/tests/integration/_cases/mobile_app/mobile_app-upload-help-not-macos.trycmd @@ -1,12 +1,12 @@ ``` $ sentry-cli mobile-app upload --help -? success [EXPERIMENTAL] Upload mobile app files to a project. Usage: sentry-cli[EXE] mobile-app upload [OPTIONS] ... Arguments: - ... The path to the mobile app files to upload. Supported files include Apk, and Aab. + ... The path to the mobile app files to upload. Supported files include Apk, Aab, + XCArchive, and IPA. Options: -o, --org @@ -18,17 +18,38 @@ Options: The project ID or slug. --auth-token Use the given Sentry auth token. - --sha - The git commit sha to use for the upload. If not provided, the current commit sha will be + --head-sha + The VCS commit sha to use for the upload. If not provided, the current commit sha will be used. - --build-configuration - The build configuration to use for the upload. If not provided, the current version will - be used. + --base-sha + The VCS commit's base sha to use for the upload. If not provided, the merge-base of the + current and remote branch will be used. --log-level Set the log output verbosity. [possible values: trace, debug, info, warn, error] + --vcs-provider + The VCS provider to use for the upload. If not provided, the current provider will be + used. + --head-repo-name + The name of the git repository to use for the upload (e.g. organization/repository). If + not provided, the current repository will be used. --quiet Do not print any output while preserving correct exit code. This flag is currently implemented only for selected subcommands. [aliases: silent] + --base-repo-name + The name of the git repository to use for the upload (e.g. organization/repository). If + not provided, the current repository will be used. + --head-ref + The reference (branch) to use for the upload. If not provided, the current reference will + be used. + --base-ref + The reference (branch) to use for the upload. If not provided, the current reference will + be used. + --pr-number + The pull request number to use for the upload. If not provided, the current pull request + number will be used. + --build-configuration + The build configuration to use for the upload. If not provided, the current version will + be used. -h, --help Print help From 2603a611b750e7f8249a7880fa8b0d05cd6fa6f3 Mon Sep 17 00:00:00 2001 From: Ryan Brooks Date: Fri, 8 Aug 2025 16:05:28 -0700 Subject: [PATCH 12/14] non macos --- .../_cases/mobile_app/mobile_app-upload-help-not-macos.trycmd | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/integration/_cases/mobile_app/mobile_app-upload-help-not-macos.trycmd b/tests/integration/_cases/mobile_app/mobile_app-upload-help-not-macos.trycmd index 7468ce9246..92910a2b5b 100644 --- a/tests/integration/_cases/mobile_app/mobile_app-upload-help-not-macos.trycmd +++ b/tests/integration/_cases/mobile_app/mobile_app-upload-help-not-macos.trycmd @@ -5,8 +5,7 @@ $ sentry-cli mobile-app upload --help Usage: sentry-cli[EXE] mobile-app upload [OPTIONS] ... Arguments: - ... The path to the mobile app files to upload. Supported files include Apk, Aab, - XCArchive, and IPA. + ... The path to the mobile app files to upload. Supported files include Apk, and Aab. Options: -o, --org From c5ecf09660c0c3c3a6c81805553100f4a33582d8 Mon Sep 17 00:00:00 2001 From: Ryan Brooks Date: Mon, 11 Aug 2025 09:23:32 -0700 Subject: [PATCH 13/14] Rebase and PR feedback --- src/api/data_types/chunking/mobile_app.rs | 2 +- src/api/mod.rs | 2 +- src/commands/mobile_app/upload.rs | 26 +++++++++++------------ 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/api/data_types/chunking/mobile_app.rs b/src/api/data_types/chunking/mobile_app.rs index 6a35a4afc5..abcbe0bf95 100644 --- a/src/api/data_types/chunking/mobile_app.rs +++ b/src/api/data_types/chunking/mobile_app.rs @@ -26,7 +26,7 @@ pub struct ChunkedMobileAppRequest<'a> { #[serde(skip_serializing_if = "Option::is_none")] pub base_ref: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] - pub pr_number: Option, + pub pr_number: Option<&u32>, } #[derive(Debug, Deserialize)] diff --git a/src/api/mod.rs b/src/api/mod.rs index 4aab424234..047ff10504 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2536,7 +2536,7 @@ pub struct VcsInfo<'a> { pub base_repo_name: Option<&'a str>, pub head_ref: Option<&'a str>, pub base_ref: Option<&'a str>, - pub pr_number: Option, + pub pr_number: Option<&u32>, } /// Log entry structure from the logs API diff --git a/src/commands/mobile_app/upload.rs b/src/commands/mobile_app/upload.rs index 5dc10c2362..62acb4224e 100644 --- a/src/commands/mobile_app/upload.rs +++ b/src/commands/mobile_app/upload.rs @@ -81,6 +81,7 @@ pub fn make_command(command: Command) -> Command { .arg( Arg::new("pr_number") .long("pr-number") + .value_parser(clap::value_parser!(u32)) .help("The pull request number to use for the upload. If not provided, the current pull request number will be used.") ) .arg( @@ -101,17 +102,13 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { .map(Cow::Borrowed) .or_else(|| vcs::find_head().ok().map(Cow::Owned)); - // TODO: Implement default values let base_sha = matches.get_one("base_sha").map(String::as_str); let vcs_provider = matches.get_one("vcs_provider").map(String::as_str); let head_repo_name = matches.get_one("head_repo_name").map(String::as_str); let base_repo_name = matches.get_one("base_repo_name").map(String::as_str); let head_ref = matches.get_one("head_ref").map(String::as_str); let base_ref = matches.get_one("base_ref").map(String::as_str); - let pr_number = matches - .get_one("pr_number") - .map(String::as_str) - .and_then(|s| s.parse::().ok()); + let pr_number = matches.get_one::("pr_number"); let build_configuration = matches.get_one("build_configuration").map(String::as_str); @@ -402,15 +399,16 @@ fn upload_file( // In the case where something went wrong (which could be on either // iteration of the loop) we get: // n. state=err, artifact_id unset - let result = loop {let response = api.assemble_mobile_app( - org, - project, - checksum, - &checksums, - build_configuration, - vcs_info, - )?; - chunks.retain(|Chunk((digest, _))| response.missing_chunks.contains(digest)); + let result = loop { + let response = api.assemble_mobile_app( + org, + project, + checksum, + &checksums, + build_configuration, + vcs_info, + )?; + chunks.retain(|Chunk((digest, _))| response.missing_chunks.contains(digest)); if !chunks.is_empty() { let upload_progress_style = ProgressStyle::default_bar().template( From d76b5fb97461c6b5cb46b8f8547bf222b35a21fa Mon Sep 17 00:00:00 2001 From: Ryan Brooks Date: Mon, 11 Aug 2025 09:28:55 -0700 Subject: [PATCH 14/14] lint --- src/api/data_types/chunking/mobile_app.rs | 2 +- src/api/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/data_types/chunking/mobile_app.rs b/src/api/data_types/chunking/mobile_app.rs index abcbe0bf95..5879a87383 100644 --- a/src/api/data_types/chunking/mobile_app.rs +++ b/src/api/data_types/chunking/mobile_app.rs @@ -26,7 +26,7 @@ pub struct ChunkedMobileAppRequest<'a> { #[serde(skip_serializing_if = "Option::is_none")] pub base_ref: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] - pub pr_number: Option<&u32>, + pub pr_number: Option<&'a u32>, } #[derive(Debug, Deserialize)] diff --git a/src/api/mod.rs b/src/api/mod.rs index 047ff10504..397f3b603a 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2536,7 +2536,7 @@ pub struct VcsInfo<'a> { pub base_repo_name: Option<&'a str>, pub head_ref: Option<&'a str>, pub base_ref: Option<&'a str>, - pub pr_number: Option<&u32>, + pub pr_number: Option<&'a u32>, } /// Log entry structure from the logs API