Skip to content

Commit 500a177

Browse files
authored
Merge branch 'master' into feat/pe-dwarf-companion-upload-support
2 parents 612b9b8 + ccf349e commit 500a177

20 files changed

Lines changed: 776 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### New Features ✨
6+
7+
- Add `sentry-cli build download` command to download installable builds (IPA/APK) by build ID ([#3221](https://github.com/getsentry/sentry-cli/pull/3221)).
8+
39
## 3.3.3
410

511
### Internal Changes 🔧

src/api/data_types/chunking/build.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ pub struct AssembleBuildResponse {
2828
pub artifact_url: Option<String>,
2929
}
3030

31+
#[derive(Debug, Deserialize)]
32+
#[serde(rename_all = "camelCase")]
33+
pub struct BuildInstallDetails {
34+
pub is_installable: bool,
35+
pub install_url: Option<String>,
36+
}
37+
3138
/// VCS information for build app uploads
3239
#[derive(Debug, Serialize)]
3340
pub struct VcsInfo<'a> {

src/api/data_types/chunking/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ mod hash_algorithm;
1010
mod upload;
1111

1212
pub use self::artifact::{AssembleArtifactsResponse, ChunkedArtifactRequest};
13-
pub use self::build::{AssembleBuildResponse, ChunkedBuildRequest, VcsInfo};
13+
pub use self::build::{AssembleBuildResponse, BuildInstallDetails, ChunkedBuildRequest, VcsInfo};
1414
pub use self::compression::ChunkCompression;
1515
pub use self::dif::{AssembleDifsRequest, AssembleDifsResponse, ChunkedDifRequest};
1616
pub use self::file_state::ChunkedFileState;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//! Data types for the bulk code mappings API.
2+
3+
use serde::{Deserialize, Serialize};
4+
5+
#[derive(Debug, Serialize)]
6+
#[serde(rename_all = "camelCase")]
7+
pub struct BulkCodeMappingsRequest<'a> {
8+
pub project: &'a str,
9+
pub repository: &'a str,
10+
pub default_branch: &'a str,
11+
pub mappings: &'a [BulkCodeMapping],
12+
}
13+
14+
#[derive(Debug, Deserialize, Serialize)]
15+
#[serde(rename_all = "camelCase")]
16+
pub struct BulkCodeMapping {
17+
pub stack_root: String,
18+
pub source_root: String,
19+
}
20+
21+
#[derive(Debug, Deserialize)]
22+
pub struct BulkCodeMappingsResponse {
23+
pub created: u64,
24+
pub updated: u64,
25+
pub errors: u64,
26+
pub mappings: Vec<BulkCodeMappingResult>,
27+
}
28+
29+
#[derive(Debug, Deserialize)]
30+
#[serde(rename_all = "camelCase")]
31+
pub struct BulkCodeMappingResult {
32+
pub stack_root: String,
33+
pub source_root: String,
34+
pub status: String,
35+
#[serde(default)]
36+
pub detail: Option<String>,
37+
}

src/api/data_types/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
//! Data types used in the api module
22
33
mod chunking;
4+
mod code_mappings;
45
mod deploy;
56
mod snapshots;
67

78
pub use self::chunking::*;
9+
pub use self::code_mappings::*;
810
pub use self::deploy::*;
911
pub use self::snapshots::*;

src/api/mod.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ use std::borrow::Cow;
1616
use std::cell::RefCell;
1717
use std::collections::HashMap;
1818
use std::error::Error as _;
19-
#[cfg(any(target_os = "macos", not(feature = "managed")))]
2019
use std::fs::File;
2120
use std::io::{self, Read as _, Write};
2221
use std::rc::Rc;
@@ -724,6 +723,26 @@ impl AuthenticatedApi<'_> {
724723
.convert_rnf(ApiErrorKind::ProjectNotFound)
725724
}
726725

726+
pub fn get_build_install_details(
727+
&self,
728+
org: &str,
729+
build_id: &str,
730+
) -> ApiResult<BuildInstallDetails> {
731+
let url = format!(
732+
"/organizations/{}/preprodartifacts/{}/install-details/",
733+
PathArg(org),
734+
PathArg(build_id)
735+
);
736+
737+
self.get(&url)?.convert()
738+
}
739+
740+
pub fn download_installable_build(&self, url: &str, dst: &mut File) -> ApiResult<ApiResponse> {
741+
self.request(Method::Get, url)?
742+
.progress_bar_mode(ProgressBarMode::Response)
743+
.send_into(dst)
744+
}
745+
727746
/// List all organizations associated with the authenticated token
728747
/// in the given `Region`. If no `Region` is provided, we assume
729748
/// we're issuing a request to a monolith deployment.
@@ -978,6 +997,17 @@ impl AuthenticatedApi<'_> {
978997
Ok(rv)
979998
}
980999

1000+
/// Bulk uploads code mappings for an organization.
1001+
pub fn bulk_upload_code_mappings(
1002+
&self,
1003+
org: &str,
1004+
body: &BulkCodeMappingsRequest,
1005+
) -> ApiResult<BulkCodeMappingsResponse> {
1006+
let path = format!("/organizations/{}/code-mappings/bulk/", PathArg(org));
1007+
self.post(&path, body)?
1008+
.convert_rnf(ApiErrorKind::OrganizationNotFound)
1009+
}
1010+
9811011
/// Creates a preprod snapshot artifact for the given project.
9821012
pub fn create_preprod_snapshot<S: Serialize>(
9831013
&self,

src/commands/build/download.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use std::fs;
2+
use std::path::PathBuf;
3+
4+
use anyhow::{bail, Result};
5+
use clap::{Arg, ArgMatches, Command};
6+
use log::info;
7+
8+
use crate::api::Api;
9+
use crate::config::Config;
10+
use crate::utils::args::ArgExt as _;
11+
use crate::utils::fs::TempFile;
12+
13+
const EXPERIMENTAL_WARNING: &str =
14+
"[EXPERIMENTAL] The \"build download\" command is experimental. \
15+
The command is subject to breaking changes, including removal, in any Sentry CLI release.";
16+
17+
pub fn make_command(command: Command) -> Command {
18+
command
19+
.about("[EXPERIMENTAL] Download a build artifact.")
20+
.long_about(format!(
21+
"Download a build artifact.\n\n{EXPERIMENTAL_WARNING}"
22+
))
23+
.org_arg()
24+
.arg(
25+
Arg::new("build_id")
26+
.long("build-id")
27+
.short('b')
28+
.required(true)
29+
.help("The ID of the build to download."),
30+
)
31+
.arg(Arg::new("output").long("output").help(
32+
"The output file path. Defaults to \
33+
'preprod_artifact_<build_id>.<ext>' in the current directory, \
34+
where ext is ipa or apk depending on the platform.",
35+
))
36+
}
37+
38+
/// For iOS builds, the install URL points to a plist manifest.
39+
/// Replace the response_format to download the actual IPA binary instead.
40+
fn ensure_binary_format(url: &str) -> String {
41+
url.replace("response_format=plist", "response_format=ipa")
42+
}
43+
44+
/// Extract the file extension from the response_format query parameter.
45+
fn extension_from_url(url: &str) -> Result<&str> {
46+
if url.contains("response_format=ipa") {
47+
Ok("ipa")
48+
} else if url.contains("response_format=apk") {
49+
Ok("apk")
50+
} else {
51+
bail!("Unsupported build format in download URL.")
52+
}
53+
}
54+
55+
pub fn execute(matches: &ArgMatches) -> Result<()> {
56+
eprintln!("{EXPERIMENTAL_WARNING}");
57+
let config = Config::current();
58+
let org = config.get_org(matches)?;
59+
let build_id = matches
60+
.get_one::<String>("build_id")
61+
.expect("build_id is required");
62+
63+
let api = Api::current();
64+
let authenticated_api = api.authenticated()?;
65+
66+
info!("Fetching install details for build {build_id}");
67+
let details = authenticated_api.get_build_install_details(&org, build_id)?;
68+
69+
if !details.is_installable {
70+
bail!("Build {build_id} is not installable.");
71+
}
72+
73+
let install_url = details
74+
.install_url
75+
.ok_or_else(|| anyhow::anyhow!("Build {build_id} has no install URL."))?;
76+
77+
let download_url = ensure_binary_format(&install_url);
78+
79+
let output_path = match matches.get_one::<String>("output") {
80+
Some(path) => PathBuf::from(path),
81+
None => {
82+
let ext = extension_from_url(&download_url)?;
83+
PathBuf::from(format!("preprod_artifact_{build_id}.{ext}"))
84+
}
85+
};
86+
87+
info!("Downloading build {build_id} to {}", output_path.display());
88+
89+
let tmp = TempFile::create()?;
90+
let mut file = tmp.open()?;
91+
let response = authenticated_api.download_installable_build(&download_url, &mut file)?;
92+
93+
if response.failed() {
94+
bail!(
95+
"Failed to download build (server returned status {}).",
96+
response.status()
97+
);
98+
}
99+
100+
drop(file);
101+
fs::copy(tmp.path(), &output_path)?;
102+
103+
println!("Successfully downloaded build to {}", output_path.display());
104+
105+
Ok(())
106+
}

src/commands/build/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ use clap::{ArgMatches, Command};
33

44
use crate::utils::args::ArgExt as _;
55

6+
pub mod download;
67
pub mod snapshots;
78
pub mod upload;
89

910
macro_rules! each_subcommand {
1011
($mac:ident) => {
12+
$mac!(download);
1113
$mac!(snapshots);
1214
$mac!(upload);
1315
};

0 commit comments

Comments
 (0)