diff --git a/src/api/data_types/code_mappings.rs b/src/api/data_types/code_mappings.rs new file mode 100644 index 0000000000..21a8aedd55 --- /dev/null +++ b/src/api/data_types/code_mappings.rs @@ -0,0 +1,37 @@ +//! Data types for the bulk code mappings API. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BulkCodeMappingsRequest { + pub project: String, + pub repository: String, + pub default_branch: String, + pub mappings: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BulkCodeMapping { + pub stack_root: String, + pub source_root: String, +} + +#[derive(Debug, Deserialize)] +pub struct BulkCodeMappingsResponse { + pub created: u64, + pub updated: u64, + pub errors: u64, + pub mappings: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BulkCodeMappingResult { + pub stack_root: String, + pub source_root: String, + pub status: String, + #[serde(default)] + pub detail: Option, +} diff --git a/src/api/data_types/mod.rs b/src/api/data_types/mod.rs index 8f7d5dc661..899dcccf60 100644 --- a/src/api/data_types/mod.rs +++ b/src/api/data_types/mod.rs @@ -1,9 +1,11 @@ //! Data types used in the api module mod chunking; +mod code_mappings; mod deploy; mod snapshots; pub use self::chunking::*; +pub use self::code_mappings::*; pub use self::deploy::*; pub use self::snapshots::*; diff --git a/src/api/mod.rs b/src/api/mod.rs index df57300b16..8420c2aa0e 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -978,6 +978,17 @@ impl AuthenticatedApi<'_> { Ok(rv) } + /// Bulk uploads code mappings for an organization. + pub fn bulk_upload_code_mappings( + &self, + org: &str, + body: &BulkCodeMappingsRequest, + ) -> ApiResult { + let path = format!("/organizations/{}/code-mappings/bulk/", PathArg(org)); + self.post(&path, body)? + .convert_rnf(ApiErrorKind::ResourceNotFound) + } + /// Creates a preprod snapshot artifact for the given project. pub fn create_preprod_snapshot( &self, diff --git a/src/commands/code_mappings/upload.rs b/src/commands/code_mappings/upload.rs index c15da6ca9c..6e1036477b 100644 --- a/src/commands/code_mappings/upload.rs +++ b/src/commands/code_mappings/upload.rs @@ -3,18 +3,12 @@ use std::fs; use anyhow::{bail, Context as _, Result}; use clap::{Arg, ArgMatches, Command}; use log::debug; -use serde::{Deserialize, Serialize}; +use crate::api::{Api, BulkCodeMapping, BulkCodeMappingsRequest}; use crate::config::Config; +use crate::utils::formatting::Table; use crate::utils::vcs; -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct CodeMapping { - stack_root: String, - source_root: String, -} - pub fn make_command(command: Command) -> Command { command .about("Upload code mappings for a project from a JSON file.") @@ -39,11 +33,15 @@ pub fn make_command(command: Command) -> Command { } pub fn execute(matches: &ArgMatches) -> Result<()> { + let config = Config::current(); + let org = config.get_org(matches)?; + let project = config.get_project(matches)?; + #[expect(clippy::unwrap_used, reason = "path is a required argument")] let path = matches.get_one::("path").unwrap(); let data = fs::read(path).with_context(|| format!("Failed to read mappings file '{path}'"))?; - let mappings: Vec = + let mappings: Vec = serde_json::from_slice(&data).context("Failed to parse mappings JSON")?; if mappings.is_empty() { @@ -74,7 +72,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { })?; // Prefer explicit config (SENTRY_VCS_REMOTE / ini), then inspect // the repo for the best remote (upstream > origin > first). - let config = Config::current(); let configured_remote = config.get_cached_vcs_remote(); let remote_name = if vcs::git_repo_remote_url(&git_repo, &configured_remote).is_ok() { debug!("Using configured VCS remote: {configured_remote}"); @@ -120,9 +117,57 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { } }; - println!("Found {} code mapping(s) in {path}", mappings.len()); - println!("Repository: {repo_name}"); - println!("Default branch: {default_branch}"); + let mapping_count = mappings.len(); + let request = BulkCodeMappingsRequest { + project, + repository: repo_name, + default_branch, + mappings, + }; + + println!("Uploading {mapping_count} code mapping(s)..."); + + let api = Api::current(); + let response = api + .authenticated()? + .bulk_upload_code_mappings(&org, &request)?; + + // Display results + let mut table = Table::new(); + table + .title_row() + .add("Stack Root") + .add("Source Root") + .add("Status"); + + for result in &response.mappings { + let status = match result.status.as_str() { + "error" => match &result.detail { + Some(detail) => format!("error: {detail}"), + None => "error".to_owned(), + }, + s => s.to_owned(), + }; + table + .add_row() + .add(&result.stack_root) + .add(&result.source_root) + .add(&status); + } + + table.print(); + println!(); + println!( + "Created: {}, Updated: {}, Errors: {}", + response.created, response.updated, response.errors + ); + + if response.errors > 0 { + bail!( + "{} mapping(s) failed to upload. See errors above.", + response.errors + ); + } Ok(()) } diff --git a/tests/integration/_cases/code_mappings/code-mappings-upload.trycmd b/tests/integration/_cases/code_mappings/code-mappings-upload.trycmd new file mode 100644 index 0000000000..72c35d9d19 --- /dev/null +++ b/tests/integration/_cases/code_mappings/code-mappings-upload.trycmd @@ -0,0 +1,14 @@ +``` +$ sentry-cli code-mappings upload tests/integration/_fixtures/code_mappings/mappings.json --org wat-org --project wat-project --repo owner/repo --default-branch main +? success +Uploading 2 code mapping(s)... ++------------------+---------------------------------------------+---------+ +| Stack Root | Source Root | Status | ++------------------+---------------------------------------------+---------+ +| com/example/core | modules/core/src/main/java/com/example/core | created | +| com/example/maps | modules/maps/src/main/java/com/example/maps | created | ++------------------+---------------------------------------------+---------+ + +Created: 2, Updated: 0, Errors: 0 + +``` diff --git a/tests/integration/_fixtures/code_mappings/mappings.json b/tests/integration/_fixtures/code_mappings/mappings.json new file mode 100644 index 0000000000..d03581bf7e --- /dev/null +++ b/tests/integration/_fixtures/code_mappings/mappings.json @@ -0,0 +1,4 @@ +[ + {"stackRoot": "com/example/core", "sourceRoot": "modules/core/src/main/java/com/example/core"}, + {"stackRoot": "com/example/maps", "sourceRoot": "modules/maps/src/main/java/com/example/maps"} +] diff --git a/tests/integration/_responses/code_mappings/post-bulk.json b/tests/integration/_responses/code_mappings/post-bulk.json new file mode 100644 index 0000000000..4d30478f44 --- /dev/null +++ b/tests/integration/_responses/code_mappings/post-bulk.json @@ -0,0 +1,9 @@ +{ + "created": 2, + "updated": 0, + "errors": 0, + "mappings": [ + {"stackRoot": "com/example/core", "sourceRoot": "modules/core/src/main/java/com/example/core", "status": "created"}, + {"stackRoot": "com/example/maps", "sourceRoot": "modules/maps/src/main/java/com/example/maps", "status": "created"} + ] +} diff --git a/tests/integration/code_mappings/mod.rs b/tests/integration/code_mappings/mod.rs index bcfc6ec6a5..1869e71805 100644 --- a/tests/integration/code_mappings/mod.rs +++ b/tests/integration/code_mappings/mod.rs @@ -1,5 +1,7 @@ use crate::integration::TestManager; +mod upload; + #[test] fn command_code_mappings_help() { TestManager::new().register_trycmd_test("code_mappings/code-mappings-help.trycmd"); diff --git a/tests/integration/code_mappings/upload.rs b/tests/integration/code_mappings/upload.rs new file mode 100644 index 0000000000..861ed199a9 --- /dev/null +++ b/tests/integration/code_mappings/upload.rs @@ -0,0 +1,12 @@ +use crate::integration::{MockEndpointBuilder, TestManager}; + +#[test] +fn command_code_mappings_upload() { + TestManager::new() + .mock_endpoint( + MockEndpointBuilder::new("POST", "/api/0/organizations/wat-org/code-mappings/bulk/") + .with_response_file("code_mappings/post-bulk.json"), + ) + .register_trycmd_test("code_mappings/code-mappings-upload.trycmd") + .with_default_token(); +}