Skip to content

Commit 5720ec2

Browse files
committed
feat(cli): Wire up API integration for code-mappings upload
Add bulk_upload_code_mappings() API method and BulkCodeMappingsRequest/ Response data types. Wire up the upload command to authenticate, call the bulk endpoint, and display results in a table with summary counts. Exit with code 1 if any mappings fail.
1 parent 95e8814 commit 5720ec2

File tree

9 files changed

+163
-25
lines changed

9 files changed

+163
-25
lines changed
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 {
8+
pub project: String,
9+
pub repository: String,
10+
pub default_branch: String,
11+
pub mappings: Vec<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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,17 @@ impl AuthenticatedApi<'_> {
978978
Ok(rv)
979979
}
980980

981+
/// Bulk uploads code mappings for an organization.
982+
pub fn bulk_upload_code_mappings(
983+
&self,
984+
org: &str,
985+
body: &BulkCodeMappingsRequest,
986+
) -> ApiResult<BulkCodeMappingsResponse> {
987+
let path = format!("/organizations/{}/code-mappings/bulk/", PathArg(org));
988+
self.post(&path, body)?
989+
.convert_rnf(ApiErrorKind::ResourceNotFound)
990+
}
991+
981992
/// Creates a preprod snapshot artifact for the given project.
982993
pub fn create_preprod_snapshot<S: Serialize>(
983994
&self,

src/commands/code_mappings/upload.rs

Lines changed: 69 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,12 @@ use std::fs;
33
use anyhow::{bail, Context as _, Result};
44
use clap::{Arg, ArgMatches, Command};
55
use log::debug;
6-
use serde::{Deserialize, Serialize};
76

7+
use crate::api::{Api, BulkCodeMapping, BulkCodeMappingsRequest};
88
use crate::config::Config;
9+
use crate::utils::formatting::Table;
910
use crate::utils::vcs;
1011

11-
#[derive(Debug, Deserialize, Serialize)]
12-
#[serde(rename_all = "camelCase")]
13-
struct CodeMapping {
14-
stack_root: String,
15-
source_root: String,
16-
}
17-
1812
pub fn make_command(command: Command) -> Command {
1913
command
2014
.about("Upload code mappings for a project from a JSON file.")
@@ -39,11 +33,15 @@ pub fn make_command(command: Command) -> Command {
3933
}
4034

4135
pub fn execute(matches: &ArgMatches) -> Result<()> {
36+
let config = Config::current();
37+
let org = config.get_org(matches)?;
38+
let project = config.get_project(matches)?;
39+
4240
#[expect(clippy::unwrap_used, reason = "path is a required argument")]
4341
let path = matches.get_one::<String>("path").unwrap();
4442
let data = fs::read(path).with_context(|| format!("Failed to read mappings file '{path}'"))?;
4543

46-
let mappings: Vec<CodeMapping> =
44+
let mappings: Vec<BulkCodeMapping> =
4745
serde_json::from_slice(&data).context("Failed to parse mappings JSON")?;
4846

4947
if mappings.is_empty() {
@@ -74,21 +72,19 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
7472
})?;
7573
// Prefer explicit config (SENTRY_VCS_REMOTE / ini), then inspect
7674
// the repo for the best remote (upstream > origin > first).
77-
let config = Config::current();
7875
let configured_remote = config.get_cached_vcs_remote();
79-
let remote_name =
80-
if vcs::git_repo_remote_url(&git_repo, &configured_remote).is_ok() {
81-
debug!("Using configured VCS remote: {configured_remote}");
82-
configured_remote
83-
} else if let Some(best) = vcs::find_best_remote(&git_repo)? {
84-
debug!("Configured remote '{configured_remote}' not found, using: {best}");
85-
best
86-
} else {
87-
bail!(
88-
"No remotes found in the git repository. \
76+
let remote_name = if vcs::git_repo_remote_url(&git_repo, &configured_remote).is_ok() {
77+
debug!("Using configured VCS remote: {configured_remote}");
78+
configured_remote
79+
} else if let Some(best) = vcs::find_best_remote(&git_repo)? {
80+
debug!("Configured remote '{configured_remote}' not found, using: {best}");
81+
best
82+
} else {
83+
bail!(
84+
"No remotes found in the git repository. \
8985
Use --repo and --default-branch to specify manually."
90-
);
91-
};
86+
);
87+
};
9288

9389
let repo_name = match explicit_repo {
9490
Some(r) => r.to_owned(),
@@ -121,9 +117,57 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
121117
}
122118
};
123119

124-
println!("Found {} code mapping(s) in {path}", mappings.len());
125-
println!("Repository: {repo_name}");
126-
println!("Default branch: {default_branch}");
120+
let mapping_count = mappings.len();
121+
let request = BulkCodeMappingsRequest {
122+
project,
123+
repository: repo_name,
124+
default_branch,
125+
mappings,
126+
};
127+
128+
println!("Uploading {mapping_count} code mapping(s)...");
129+
130+
let api = Api::current();
131+
let response = api
132+
.authenticated()?
133+
.bulk_upload_code_mappings(&org, &request)?;
134+
135+
// Display results
136+
let mut table = Table::new();
137+
table
138+
.title_row()
139+
.add("Stack Root")
140+
.add("Source Root")
141+
.add("Status");
142+
143+
for result in &response.mappings {
144+
let status = match result.status.as_str() {
145+
"error" => match &result.detail {
146+
Some(detail) => format!("error: {detail}"),
147+
None => "error".to_owned(),
148+
},
149+
s => s.to_owned(),
150+
};
151+
table
152+
.add_row()
153+
.add(&result.stack_root)
154+
.add(&result.source_root)
155+
.add(&status);
156+
}
157+
158+
table.print();
159+
println!();
160+
println!(
161+
"Created: {}, Updated: {}, Errors: {}",
162+
response.created, response.updated, response.errors
163+
);
164+
165+
if response.errors > 0 {
166+
bail!(
167+
"{} mapping(s) failed to upload. See errors above.",
168+
response.errors
169+
);
170+
}
127171

128172
Ok(())
129173
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
```
2+
$ sentry-cli code-mappings upload tests/integration/_fixtures/code_mappings/mappings.json --org wat-org --project wat-project --repo owner/repo --default-branch main
3+
? success
4+
Uploading 2 code mapping(s)...
5+
+------------------+---------------------------------------------+---------+
6+
| Stack Root | Source Root | Status |
7+
+------------------+---------------------------------------------+---------+
8+
| com/example/core | modules/core/src/main/java/com/example/core | created |
9+
| com/example/maps | modules/maps/src/main/java/com/example/maps | created |
10+
+------------------+---------------------------------------------+---------+
11+
12+
Created: 2, Updated: 0, Errors: 0
13+
14+
```
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[
2+
{"stackRoot": "com/example/core", "sourceRoot": "modules/core/src/main/java/com/example/core"},
3+
{"stackRoot": "com/example/maps", "sourceRoot": "modules/maps/src/main/java/com/example/maps"}
4+
]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"created": 2,
3+
"updated": 0,
4+
"errors": 0,
5+
"mappings": [
6+
{"stackRoot": "com/example/core", "sourceRoot": "modules/core/src/main/java/com/example/core", "status": "created"},
7+
{"stackRoot": "com/example/maps", "sourceRoot": "modules/maps/src/main/java/com/example/maps", "status": "created"}
8+
]
9+
}

tests/integration/code_mappings/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use crate::integration::TestManager;
22

3+
mod upload;
4+
35
#[test]
46
fn command_code_mappings_help() {
57
TestManager::new().register_trycmd_test("code_mappings/code-mappings-help.trycmd");
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use crate::integration::{MockEndpointBuilder, TestManager};
2+
3+
#[test]
4+
fn command_code_mappings_upload() {
5+
TestManager::new()
6+
.mock_endpoint(
7+
MockEndpointBuilder::new(
8+
"POST",
9+
"/api/0/organizations/wat-org/code-mappings/bulk/",
10+
)
11+
.with_response_file("code_mappings/post-bulk.json"),
12+
)
13+
.register_trycmd_test("code_mappings/code-mappings-upload.trycmd")
14+
.with_default_token();
15+
}

0 commit comments

Comments
 (0)