From f72d9f76c67611e370bdcb938585b01cd0fc1d0d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 2 Dec 2025 16:08:27 +0000 Subject: [PATCH] ref(react-native): Remove `appcenter` subcommand (#3004) ### Description The `react-native appcenter` subcommand has been deprecated and is no longer needed. This change removes the entire subcommand implementation, including associated utility functions and related code. ### Issues * resolves: #2489 * resolves: CLI-70 --- Open in Cursor Open in Web --- src/api/mod.rs | 2 + src/commands/react_native/appcenter.rs | 230 ------------------------- src/commands/react_native/mod.rs | 2 - src/utils/appcenter.rs | 221 ------------------------ src/utils/file_search.rs | 2 +- src/utils/mod.rs | 1 - 6 files changed, 3 insertions(+), 455 deletions(-) delete mode 100644 src/commands/react_native/appcenter.rs delete mode 100644 src/utils/appcenter.rs diff --git a/src/api/mod.rs b/src/api/mod.rs index dd77ba2e1a..d810137269 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -14,6 +14,7 @@ mod pagination; use std::borrow::Cow; use std::cell::RefCell; use std::collections::HashMap; +#[cfg(any(target_os = "macos", not(feature = "managed")))] use std::fs::File; use std::io::{self, Read as _, Write}; use std::rc::Rc; @@ -1255,6 +1256,7 @@ impl ApiRequest { } /// enables or disables redirects. The default is off. + #[cfg(any(target_os = "macos", not(feature = "managed")))] pub fn follow_location(mut self, val: bool) -> ApiResult { debug!("follow redirects: {val}"); self.handle.follow_location(val)?; diff --git a/src/commands/react_native/appcenter.rs b/src/commands/react_native/appcenter.rs deleted file mode 100644 index b5e271f008..0000000000 --- a/src/commands/react_native/appcenter.rs +++ /dev/null @@ -1,230 +0,0 @@ -#![expect(clippy::unwrap_used, reason = "deprecated command")] - -use std::env; -use std::ffi::OsStr; -use std::fs; -use std::time::Duration; - -use anyhow::{anyhow, Result}; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use console::style; -use if_chain::if_chain; - -use crate::api::Api; -use crate::config::Config; -use crate::constants::DEFAULT_MAX_WAIT; -use crate::utils::appcenter::{get_appcenter_package, get_react_native_appcenter_release}; -use crate::utils::args::{validate_distribution, ArgExt as _}; -use crate::utils::file_search::ReleaseFileSearch; -use crate::utils::file_upload::UploadContext; -use crate::utils::sourcemaps::SourceMapProcessor; - -pub fn make_command(command: Command) -> Command { - command - .about("[DEPRECATED] Upload react-native projects for AppCenter.") - .hide(true) - .org_arg() - .project_arg(false) - .arg( - Arg::new("deployment") - .long("deployment") - .value_name("DEPLOYMENT") - .help("The name of the deployment. [Production, Staging]"), - ) - .arg( - Arg::new("bundle_id") - .value_name("BUNDLE_ID") - .long("bundle-id") - .help( - "Explicitly provide the bundle ID instead of \ - parsing the source projects. This allows you to push \ - codepush releases for iOS on platforms without Xcode or \ - codepush releases for Android when you use different \ - bundle IDs for release and debug etc.", - ), - ) - .arg( - Arg::new("version_name") - .value_name("VERSION_NAME") - .long("version-name") - .help("Override version name in release name"), - ) - .arg( - Arg::new("dist") - .long("dist") - .value_name("DISTRIBUTION") - .action(ArgAction::Append) - .value_parser(validate_distribution) - .help("The names of the distributions to publish. Can be supplied multiple times."), - ) - .arg( - Arg::new("print_release_name") - .long("print-release-name") - .action(ArgAction::SetTrue) - .help("Print the release name instead."), - ) - .arg( - Arg::new("release_name") - .value_name("RELEASE_NAME") - .long("release-name") - .conflicts_with_all(["bundle_id", "version_name"]) - .help("Override the entire release-name"), - ) - .arg( - Arg::new("app_name") - .value_name("APP_NAME") - .required(true) - .help("The name of the AppCenter application."), - ) - .arg( - Arg::new("platform") - .value_name("PLATFORM") - .required(true) - .help("The name of the app platform. [ios, android]"), - ) - .arg( - Arg::new("paths") - .value_name("PATH") - .required(true) - .num_args(1..) - .action(ArgAction::Append) - .help("A list of folders with assets that should be processed."), - ) - .arg( - Arg::new("wait") - .long("wait") - .action(ArgAction::SetTrue) - .conflicts_with("wait_for") - .help("Wait for the server to fully process uploaded files."), - ) - .arg( - Arg::new("wait_for") - .long("wait-for") - .value_name("SECS") - .value_parser(clap::value_parser!(u64)) - .conflicts_with("wait") - .help( - "Wait for the server to fully process uploaded files, \ - but at most for the given number of seconds.", - ), - ) -} - -pub fn execute(matches: &ArgMatches) -> Result<()> { - eprintln!("{}", style("⚠ DEPRECATION NOTICE: This functionality will be removed in a future version of `sentry-cli`. \ - Use the `sourcemaps upload` command instead.").yellow()); - - let config = Config::current(); - let here = env::current_dir()?; - let here_str: &str = &here.to_string_lossy(); - let org = config.get_org(matches)?; - let projects = config.get_projects(matches)?; - let app = matches.get_one::("app_name").unwrap(); - let platform = matches.get_one::("platform").unwrap(); - let deployment = matches - .get_one::("deployment") - .map(String::as_str) - .unwrap_or("Staging"); - let api = Api::current(); - let print_release_name = matches.get_flag("print_release_name"); - - if !print_release_name { - println!( - "{} Fetching latest AppCenter deployment info", - style(">").dim() - ); - } - - let package = get_appcenter_package(app, deployment)?; - let release = get_react_native_appcenter_release( - &package, - platform, - matches.get_one::("bundle_id").map(String::as_str), - matches - .get_one::("version_name") - .map(String::as_str), - matches - .get_one::("release_name") - .map(String::as_str), - )?; - if print_release_name { - println!("{release}"); - return Ok(()); - } - - println!( - "{} Processing react-native AppCenter sourcemaps", - style(">").dim() - ); - - let mut processor = SourceMapProcessor::new(); - - for path in matches.get_many::("paths").unwrap() { - let entries = fs::read_dir(path) - .map_err(|e| anyhow!(e).context(format!("Failed processing path: \"{}\"", &path)))?; - - for entry in entries.flatten() { - if_chain! { - if let Some(filename) = entry.file_name().to_str(); - if let Some(ext) = entry.path().extension(); - if ext == OsStr::new("jsbundle") || - ext == OsStr::new("map") || - ext == OsStr::new("bundle"); - then { - let url = format!("~/{filename}"); - processor.add(&url, ReleaseFileSearch::collect_file(entry.path())?); - } - } - } - } - - processor.rewrite(&[here_str])?; - processor.add_sourcemap_references(); - - let chunk_upload_options = api.authenticated()?.get_chunk_upload_options(&org)?; - - let wait_for_secs = matches.get_one::("wait_for").copied(); - let wait = matches.get_flag("wait") || wait_for_secs.is_some(); - let max_wait = wait_for_secs.map_or(DEFAULT_MAX_WAIT, Duration::from_secs); - - match matches.get_many::("dist") { - None => { - println!( - "Uploading sourcemaps for release {} (no distribution value given; use --dist to set distribution value)", - &release - ); - - processor.upload(&UploadContext { - org: &org, - projects: projects.as_non_empty_slice(), - release: Some(&release), - dist: None, - note: None, - wait, - max_wait, - chunk_upload_options: &chunk_upload_options, - })?; - } - Some(dists) => { - for dist in dists { - println!( - "Uploading sourcemaps for release {} distribution {dist}", - &release - ); - - processor.upload(&UploadContext { - org: &org, - projects: projects.as_non_empty_slice(), - release: Some(&release), - dist: Some(dist), - note: None, - wait, - max_wait, - chunk_upload_options: &chunk_upload_options, - })?; - } - } - } - - Ok(()) -} diff --git a/src/commands/react_native/mod.rs b/src/commands/react_native/mod.rs index a035e87cab..a57e9ab76a 100644 --- a/src/commands/react_native/mod.rs +++ b/src/commands/react_native/mod.rs @@ -1,7 +1,6 @@ use anyhow::Result; use clap::{ArgMatches, Command}; -pub mod appcenter; pub mod gradle; #[cfg(target_os = "macos")] pub mod xcode; @@ -9,7 +8,6 @@ pub mod xcode; macro_rules! each_subcommand { ($mac:ident) => { $mac!(gradle); - $mac!(appcenter); #[cfg(target_os = "macos")] $mac!(xcode); }; diff --git a/src/utils/appcenter.rs b/src/utils/appcenter.rs deleted file mode 100644 index 484cf2705f..0000000000 --- a/src/utils/appcenter.rs +++ /dev/null @@ -1,221 +0,0 @@ -use std::env; -use std::fmt; -use std::io; -use std::path::Path; -use std::process::{Command, Output}; -use std::str; - -use anyhow::{bail, format_err, Error, Result}; -use console::strip_ansi_codes; -use glob::{glob_with, MatchOptions}; -use if_chain::if_chain; -use serde::de; -use serde::Deserialize; - -use crate::utils::releases::{get_xcode_release_name, infer_gradle_release_name}; -use crate::utils::xcode::{InfoPlist, XcodeProjectInfo}; - -#[cfg(not(windows))] -static APPCENTER_BIN_PATH: &str = "appcenter"; -#[cfg(not(windows))] -static APPCENTER_NPM_PATH: &str = "node_modules/.bin/appcenter"; - -#[cfg(windows)] -static APPCENTER_BIN_PATH: &str = "appcenter.cmd"; -#[cfg(windows)] -static APPCENTER_NPM_PATH: &str = "node_modules/.bin/appcenter.cmd"; - -static APPCENTER_NOT_FOUND: &str = "AppCenter CLI not found - -Install with `npm install -g appcenter-cli` and make sure it is on the PATH."; - -#[derive(Debug)] -pub struct AppCenterPackage { - pub label: String, -} - -#[derive(Debug, Deserialize)] -pub struct AppCenterOutput { - #[serde(rename = "errorMessage")] - pub error_message: String, -} - -impl<'de> de::Deserialize<'de> for AppCenterPackage { - fn deserialize>(deserializer: D) -> Result { - struct PackageVisitor; - - impl<'de> de::Visitor<'de> for PackageVisitor { - type Value = AppCenterPackage; - - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("a deployment history entry") - } - - fn visit_seq>( - self, - mut seq: S, - ) -> Result { - // Since we only need the package label, we can deserialize the JSON string very - // efficiently by only looking at the first element. - let label = seq - .next_element()? - .ok_or_else(|| de::Error::custom("missing package label"))?; - - // Drain the sequence, ignoring all other values. - while seq.next_element::()?.is_some() {} - - Ok(AppCenterPackage { label }) - } - } - - deserializer.deserialize_seq(PackageVisitor) - } -} - -// AppCenter CLI can throw errors in 2 different formats, based on the `--output` flag, -// and we want to handle them both (we call it with `--output json` ourselves). -// -// JSON: `{"succeeded":false,"errorCode":5,"errorMessage":"Command 'appcenter codepush deployment history' requires a logged in user. Use the 'appcenter login' command to log in."}` -// Text: `Error: Command 'appcenter codepush deployment history' requires a logged in user. Use the 'appcenter login' command to log in.` -// -// Also, starting version 2.10.8 (2022-01-10), it prints to `stderr`, where it used to use `stdout` before. -// ref: https://github.com/microsoft/appcenter-cli/commit/b3d6290afcb84affe6a4096893b1ea11d10ac3cf -pub fn get_appcenter_error(output: &Output) -> Error { - let cause = serde_json::from_slice::(&output.stderr) - .map(|o| o.error_message) - .unwrap_or_else(|_| { - str::from_utf8(&output.stderr) - .map(|o| { - let stripped = strip_ansi_codes(o); - if let Some(rest) = stripped.strip_prefix("Error: ") { - rest.to_owned() - } else { - stripped.to_string() - } - }) - .unwrap_or_else(|_| "Unknown AppCenter error".to_owned()) - }); - - format_err!(cause) -} - -pub fn get_appcenter_deployment_history( - app: &str, - deployment: &str, -) -> Result> { - let appcenter_bin = if Path::new(APPCENTER_NPM_PATH).exists() { - APPCENTER_NPM_PATH - } else { - APPCENTER_BIN_PATH - }; - - let output = Command::new(appcenter_bin) - .arg("codepush") - .arg("deployment") - .arg("history") - .arg(deployment) - .arg("--app") - .arg(app) - .arg("--output") - .arg("json") - .output() - .map_err(|e| match e.kind() { - io::ErrorKind::NotFound => Error::msg(APPCENTER_NOT_FOUND), - _ => Error::from(e).context("Failed to run AppCenter CLI"), - })?; - - if output.status.success() { - Ok(serde_json::from_slice(&output.stdout).unwrap_or_else(|_| { - let format_err = format!("Command `{appcenter_bin} codepush deployment history {deployment} --app {app} --output json` failed to produce a valid JSON output."); - panic!("{}", format_err); - })) - } else { - Err(get_appcenter_error(&output).context("Failed to load AppCenter deployment history")) - } -} - -pub fn get_appcenter_package(app: &str, deployment: &str) -> Result { - let history = get_appcenter_deployment_history(app, deployment)?; - if let Some(latest) = history.into_iter().next_back() { - Ok(latest) - } else { - bail!("Could not find deployment {deployment} for {app}"); - } -} - -pub fn get_react_native_appcenter_release( - package: &AppCenterPackage, - platform: &str, - bundle_id_override: Option<&str>, - version_name_override: Option<&str>, - release_name_override: Option<&str>, -) -> Result { - let bundle_id_ovrr = bundle_id_override.unwrap_or(""); - let version_name_ovrr = version_name_override.unwrap_or(""); - let release_name_ovrr = release_name_override.unwrap_or(""); - - if !release_name_ovrr.is_empty() { - return Ok(release_name_ovrr.to_owned()); - } - - if !bundle_id_ovrr.is_empty() && !version_name_ovrr.is_empty() { - return Ok(format!( - "{bundle_id_ovrr}@{version_name_ovrr}+codepush:{}", - package.label - )); - } - - if platform == "ios" { - if !cfg!(target_os = "macos") { - bail!("AppCenter codepush releases for iOS require macOS if no bundle ID and version name are specified"); - } - - let mut opts = MatchOptions::new(); - opts.case_sensitive = false; - - for entry in (glob_with("ios/*.xcodeproj", opts)?).flatten() { - let pi = XcodeProjectInfo::from_path(entry)?; - if let Some(ipl) = InfoPlist::from_project_info(&pi)? { - if let Some(release_name) = get_xcode_release_name(Some(ipl))? { - let vec: Vec<&str> = release_name.split('@').collect(); - let bundle_id = if bundle_id_ovrr.is_empty() { - vec[0] - } else { - bundle_id_ovrr - }; - let version_name = if version_name_ovrr.is_empty() { - vec[1] - } else { - version_name_ovrr - }; - return Ok(format!( - "{bundle_id}@{version_name}+codepush:{}", - package.label - )); - } - } - } - - bail!("Could not find plist"); - } else if platform == "android" { - if_chain! { - if let Ok(here) = env::current_dir(); - if let Ok(android_folder) = here.join("android").metadata(); - if android_folder.is_dir(); - then { - if let Some(release_name) = infer_gradle_release_name(Some(here.join("android")))? { - let vec: Vec<&str> = release_name.split('@').collect(); - let bundle_id = if bundle_id_ovrr.is_empty() { vec[0] } else { bundle_id_ovrr }; - let version_name = if version_name_ovrr.is_empty() { vec[1] } else { version_name_ovrr }; - return Ok(format!("{bundle_id}@{version_name}+codepush:{}", package.label)); - } else { - bail!("Could not parse app id from build.gradle"); - } - } - } - - bail!("Could not find AndroidManifest.xml"); - } - - bail!("Unsupported platform '{platform}'"); -} diff --git a/src/utils/file_search.rs b/src/utils/file_search.rs index dab73b4035..6e11e5a63b 100644 --- a/src/utils/file_search.rs +++ b/src/utils/file_search.rs @@ -80,7 +80,7 @@ impl ReleaseFileSearch { pub fn collect_file(path: PathBuf) -> Result { // NOTE: `collect_file` currently do not handle gzip decompression, - // as its mostly used for 3rd tools like xcode, appcenter or gradle. + // as its mostly used for 3rd tools like xcode or gradle. let mut f = fs::File::open(path.clone())?; let mut contents = Vec::new(); f.read_to_end(&mut contents)?; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index d4f75b7241..63baa14657 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,5 @@ //! Various utility functionality. pub mod android; -pub mod appcenter; pub mod args; pub mod auth_token; pub mod build;