diff --git a/src/api/data_types/code_mappings.rs b/src/api/data_types/code_mappings.rs new file mode 100644 index 0000000000..0f66a1504b --- /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<'a> { + pub project: &'a str, + pub repository: &'a str, + pub default_branch: &'a str, + pub mappings: &'a [BulkCodeMapping], +} + +#[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..5810e3c7de 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::OrganizationNotFound) + } + /// 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 3bd9f2fef9..5718ab56d6 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; +use crate::api::{Api, BulkCodeMapping, BulkCodeMappingResult, BulkCodeMappingsRequest}; use crate::config::Config; +use crate::utils::formatting::Table; use crate::utils::vcs; -#[derive(Debug, Deserialize)] -#[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. Each mapping pairs a stack trace root (e.g. com/example/module) with the corresponding source path in your repository (e.g. modules/module/src/main/java/com/example/module).") @@ -39,12 +33,16 @@ 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)?; + let path = matches .get_one::("path") .expect("path is a required argument"); 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() { @@ -73,9 +71,33 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { git_repo.as_ref(), )?; - 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: &project, + repository: &repo_name, + default_branch: &default_branch, + mappings: &mappings, + }; + + println!("Uploading {mapping_count} code mapping(s)..."); + + let api = Api::current(); + let response = api + .authenticated()? + .bulk_upload_code_mappings(&org, &request)?; + + print_results_table(response.mappings); + 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(()) } @@ -174,6 +196,30 @@ fn infer_default_branch(git_repo: Option<&git2::Repository>, remote_name: Option }) } +fn print_results_table(mappings: Vec) { + let mut table = Table::new(); + table + .title_row() + .add("Stack Root") + .add("Source Root") + .add("Status"); + + for result in mappings { + let status = match result.detail { + Some(detail) if result.status == "error" => format!("error: {detail}"), + _ => result.status, + }; + table + .add_row() + .add(&result.stack_root) + .add(&result.source_root) + .add(&status); + } + + table.print(); + println!(); +} + #[cfg(test)] mod tests { use super::*; 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(); +}