Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### New Features ✨

- Add `sentry-cli build download` command to download installable builds (IPA/APK) by build ID ([#3221](https://github.com/getsentry/sentry-cli/pull/3221)).

## 3.3.3

### Internal Changes 🔧
Expand Down
7 changes: 7 additions & 0 deletions src/api/data_types/chunking/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ pub struct AssembleBuildResponse {
pub artifact_url: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildInstallDetails {
pub is_installable: bool,
pub install_url: Option<String>,
}

/// VCS information for build app uploads
#[derive(Debug, Serialize)]
pub struct VcsInfo<'a> {
Expand Down
2 changes: 1 addition & 1 deletion src/api/data_types/chunking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ mod hash_algorithm;
mod upload;

pub use self::artifact::{AssembleArtifactsResponse, ChunkedArtifactRequest};
pub use self::build::{AssembleBuildResponse, ChunkedBuildRequest, VcsInfo};
pub use self::build::{AssembleBuildResponse, BuildInstallDetails, ChunkedBuildRequest, VcsInfo};
pub use self::compression::ChunkCompression;
pub use self::dif::{AssembleDifsRequest, AssembleDifsResponse, ChunkedDifRequest};
pub use self::file_state::ChunkedFileState;
Expand Down
21 changes: 20 additions & 1 deletion src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashMap;
use std::error::Error as _;
#[cfg(any(target_os = "macos", not(feature = "managed")))]
use std::fs::File;
use std::io::{self, Read as _, Write};
use std::rc::Rc;
Expand Down Expand Up @@ -724,6 +723,26 @@ impl AuthenticatedApi<'_> {
.convert_rnf(ApiErrorKind::ProjectNotFound)
}

pub fn get_build_install_details(
&self,
org: &str,
build_id: &str,
) -> ApiResult<BuildInstallDetails> {
let url = format!(
"/organizations/{}/preprodartifacts/{}/install-details/",
PathArg(org),
PathArg(build_id)
);

self.get(&url)?.convert()
}

pub fn download_installable_build(&self, url: &str, dst: &mut File) -> ApiResult<ApiResponse> {
self.request(Method::Get, url)?
.progress_bar_mode(ProgressBarMode::Response)
.send_into(dst)
}

/// List all organizations associated with the authenticated token
/// in the given `Region`. If no `Region` is provided, we assume
/// we're issuing a request to a monolith deployment.
Expand Down
100 changes: 100 additions & 0 deletions src/commands/build/download.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use std::fs;
use std::path::PathBuf;

use anyhow::{bail, Result};
use clap::{Arg, ArgMatches, Command};
use log::info;

use crate::api::Api;
use crate::config::Config;
use crate::utils::args::ArgExt as _;
use crate::utils::fs::TempFile;

pub fn make_command(command: Command) -> Command {
command
.about("Download a build from a project.")
.long_about("Download a build from a project.\n\nThis feature only works with Sentry SaaS.")
.org_arg()
.project_arg(false)
.arg(
Arg::new("build_id")
.long("build-id")
.short('b')
.required(true)
.help("The ID of the build to download."),
)
.arg(Arg::new("output").long("output").help(
"The output file path. Defaults to \
'preprod_artifact_<build_id>.<ext>' in the current directory, \
where ext is ipa or apk depending on the platform.",
))
}

/// For iOS builds, the install URL points to a plist manifest.
/// Replace the response_format to download the actual IPA binary instead.
fn ensure_binary_format(url: &str) -> String {
url.replace("response_format=plist", "response_format=ipa")
}

/// Extract the file extension from the response_format query parameter.
fn extension_from_url(url: &str) -> Result<&str> {
if url.contains("response_format=ipa") {
Ok("ipa")
} else if url.contains("response_format=apk") {
Ok("apk")
} else {
bail!("Unsupported build format in download URL.")
}
}

pub fn execute(matches: &ArgMatches) -> Result<()> {
let config = Config::current();
let org = config.get_org(matches)?;
let build_id = matches
.get_one::<String>("build_id")
.expect("build_id is required");

let api = Api::current();
let authenticated_api = api.authenticated()?;

info!("Fetching install details for build {build_id}");
let details = authenticated_api.get_build_install_details(&org, build_id)?;

if !details.is_installable {
bail!("Build {build_id} is not installable.");
}

let install_url = details
.install_url
.ok_or_else(|| anyhow::anyhow!("Build {build_id} has no install URL."))?;

let download_url = ensure_binary_format(&install_url);

let output_path = match matches.get_one::<String>("output") {
Some(path) => PathBuf::from(path),
None => {
let ext = extension_from_url(&download_url)?;
PathBuf::from(format!("preprod_artifact_{build_id}.{ext}"))
}
};

info!("Downloading build {build_id} to {}", output_path.display());

let tmp = TempFile::create()?;
let mut file = tmp.open()?;
let response = authenticated_api.download_installable_build(&download_url, &mut file)?;

if response.failed() {
bail!(
"Failed to download build (server returned status {}).",
response.status()
);
}

drop(file);
fs::copy(tmp.path(), &output_path)?;

println!("Successfully downloaded build to {}", output_path.display());

Ok(())
}
2 changes: 2 additions & 0 deletions src/commands/build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ use clap::{ArgMatches, Command};

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

pub mod download;
pub mod snapshots;
pub mod upload;

macro_rules! each_subcommand {
($mac:ident) => {
$mac!(download);
$mac!(snapshots);
$mac!(upload);
};
Expand Down
1 change: 1 addition & 0 deletions tests/integration/_cases/build/build-help.trycmd
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Manage builds.
Usage: sentry-cli[EXE] build [OPTIONS] <COMMAND>

Commands:
download Download a build from a project.
snapshots [EXPERIMENTAL] Upload build snapshots to a project.
upload Upload builds to a project.
help Print this message or the help of the given subcommand(s)
Expand Down
Loading