From be51270430f42aca5264afb071979489941817ca Mon Sep 17 00:00:00 2001 From: "Daniel Szoke (via Pi Coding Agent)" Date: Tue, 3 Mar 2026 14:15:39 +0000 Subject: [PATCH] =?UTF-8?q?feat(proguard):=20`upload-proguard`=20=E2=86=92?= =?UTF-8?q?=20`proguard=20upload`=20(#3174)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the `sentry-cli upload-proguard` command to `sentry-cli proguard upload`. This new API matches the API for the `debug-files upload` and `sourcemap upload` commands. This API will also enable us to add new subcommands, which will be useful for #3173. `sentry-cli upload-proguard` is retained as an alias to `sentry-cli proguard upload`, and we intend to keep it around for the foreseeable future, so no one needs to migrate. --- CHANGELOG.md | 4 + src/commands/mod.rs | 2 + src/commands/proguard/mod.rs | 43 +++ .../upload.rs} | 0 src/commands/upload_proguard/mod.rs | 11 + .../_cases/help/help-windows.trycmd | 1 + tests/integration/_cases/help/help.trycmd | 1 + .../_cases/proguard/proguard-help.trycmd | 23 ++ .../proguard/proguard-upload-help.trycmd | 37 +++ .../proguard/proguard-upload-no-upload.trycmd | 7 + .../upload}/chunk_upload_needs_upload.bin | Bin .../upload}/chunk_upload_two_files.bin | Bin .../upload}/mapping-2.txt | 0 .../upload}/mapping.txt | 0 tests/integration/mod.rs | 1 + tests/integration/proguard/mod.rs | 1 + tests/integration/proguard/upload.rs | 288 ++++++++++++++++++ tests/integration/upload_proguard.rs | 12 +- 18 files changed, 425 insertions(+), 6 deletions(-) create mode 100644 src/commands/proguard/mod.rs rename src/commands/{upload_proguard.rs => proguard/upload.rs} (100%) create mode 100644 src/commands/upload_proguard/mod.rs create mode 100644 tests/integration/_cases/proguard/proguard-help.trycmd create mode 100644 tests/integration/_cases/proguard/proguard-upload-help.trycmd create mode 100644 tests/integration/_cases/proguard/proguard-upload-no-upload.trycmd rename tests/integration/_expected_requests/{upload_proguard => proguard/upload}/chunk_upload_needs_upload.bin (100%) rename tests/integration/_expected_requests/{upload_proguard => proguard/upload}/chunk_upload_two_files.bin (100%) rename tests/integration/_fixtures/{upload_proguard => proguard/upload}/mapping-2.txt (100%) rename tests/integration/_fixtures/{upload_proguard => proguard/upload}/mapping.txt (100%) create mode 100644 tests/integration/proguard/mod.rs create mode 100644 tests/integration/proguard/upload.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 76313397ad..cdae4a1607 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Improvements + +- Moved `sentry-cli upload-proguard` to `sentry-cli proguard upload`, aligning the API with similar upload commands like `debug-files upload` and `sourcemaps upload` ([#3174](https://github.com/getsentry/sentry-cli/pull/3174)). `sentry-cli upload-proguard` remains supported as an alias, so no migration is required. + ### Experimental Feature 🧑‍🔬 (internal-only) - Print snapshot URL after successful upload ([#3167](https://github.com/getsentry/sentry-cli/pull/3167)). diff --git a/src/commands/mod.rs b/src/commands/mod.rs index dac94c33c7..bf74f76d42 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -32,6 +32,7 @@ mod login; mod logs; mod monitors; mod organizations; +mod proguard; mod projects; mod react_native; mod releases; @@ -60,6 +61,7 @@ macro_rules! each_subcommand { $mac!(logs); $mac!(monitors); $mac!(organizations); + $mac!(proguard); $mac!(projects); $mac!(react_native); $mac!(releases); diff --git a/src/commands/proguard/mod.rs b/src/commands/proguard/mod.rs new file mode 100644 index 0000000000..d7f3bad30a --- /dev/null +++ b/src/commands/proguard/mod.rs @@ -0,0 +1,43 @@ +use anyhow::Result; +use clap::{ArgMatches, Command}; + +pub mod upload; + +macro_rules! each_subcommand { + ($mac:ident) => { + $mac!(upload); + }; +} + +pub fn make_command(mut command: Command) -> Command { + macro_rules! add_subcommand { + ($name:ident) => {{ + command = command.subcommand(crate::commands::proguard::$name::make_command( + Command::new(stringify!($name).replace('_', "-")), + )); + }}; + } + + command = command + .about("Manage ProGuard mapping files.") + .subcommand_required(true) + .arg_required_else_help(true); + + each_subcommand!(add_subcommand); + command +} + +pub fn execute(matches: &ArgMatches) -> Result<()> { + macro_rules! execute_subcommand { + ($name:ident) => {{ + if let Some(sub_matches) = + matches.subcommand_matches(&stringify!($name).replace('_', "-")) + { + return crate::commands::proguard::$name::execute(&sub_matches); + } + }}; + } + + each_subcommand!(execute_subcommand); + unreachable!(); +} diff --git a/src/commands/upload_proguard.rs b/src/commands/proguard/upload.rs similarity index 100% rename from src/commands/upload_proguard.rs rename to src/commands/proguard/upload.rs diff --git a/src/commands/upload_proguard/mod.rs b/src/commands/upload_proguard/mod.rs new file mode 100644 index 0000000000..411d339584 --- /dev/null +++ b/src/commands/upload_proguard/mod.rs @@ -0,0 +1,11 @@ +use anyhow::Result; +use clap::{ArgMatches, Command}; + +pub fn make_command(command: Command) -> Command { + // Retained as a top-level command for backward compatibility. + crate::commands::proguard::upload::make_command(command) +} + +pub fn execute(matches: &ArgMatches) -> Result<()> { + crate::commands::proguard::upload::execute(matches) +} diff --git a/tests/integration/_cases/help/help-windows.trycmd b/tests/integration/_cases/help/help-windows.trycmd index 7b98475db6..e634561ea7 100644 --- a/tests/integration/_cases/help/help-windows.trycmd +++ b/tests/integration/_cases/help/help-windows.trycmd @@ -21,6 +21,7 @@ Commands: logs [BETA] Manage logs in Sentry monitors Manage cron monitors on Sentry. organizations Manage organizations on Sentry. + proguard Manage ProGuard mapping files. projects Manage projects on Sentry. react-native Upload build artifacts for react-native projects. releases Manage releases on Sentry. diff --git a/tests/integration/_cases/help/help.trycmd b/tests/integration/_cases/help/help.trycmd index 5061a03fc8..aa51cd0222 100644 --- a/tests/integration/_cases/help/help.trycmd +++ b/tests/integration/_cases/help/help.trycmd @@ -21,6 +21,7 @@ Commands: logs [BETA] Manage logs in Sentry monitors Manage cron monitors on Sentry. organizations Manage organizations on Sentry. + proguard Manage ProGuard mapping files. projects Manage projects on Sentry. react-native Upload build artifacts for react-native projects. releases Manage releases on Sentry. diff --git a/tests/integration/_cases/proguard/proguard-help.trycmd b/tests/integration/_cases/proguard/proguard-help.trycmd new file mode 100644 index 0000000000..ffb4317cdc --- /dev/null +++ b/tests/integration/_cases/proguard/proguard-help.trycmd @@ -0,0 +1,23 @@ +``` +$ sentry-cli proguard --help +? success +Manage ProGuard mapping files. + +Usage: sentry-cli[EXE] proguard [OPTIONS] + +Commands: + upload Upload ProGuard mapping files to a project. + help Print this message or the help of the given subcommand(s) + +Options: + --header Custom headers that should be attached to all requests + in key:value format. + --auth-token Use the given Sentry auth token. + --log-level Set the log output verbosity. [possible values: trace, debug, info, + warn, error] + --quiet Do not print any output while preserving correct exit code. This + flag is currently implemented only for selected subcommands. + [aliases: --silent] + -h, --help Print help + +``` diff --git a/tests/integration/_cases/proguard/proguard-upload-help.trycmd b/tests/integration/_cases/proguard/proguard-upload-help.trycmd new file mode 100644 index 0000000000..9e5a586089 --- /dev/null +++ b/tests/integration/_cases/proguard/proguard-upload-help.trycmd @@ -0,0 +1,37 @@ +``` +$ sentry-cli proguard upload --help +? success +Upload ProGuard mapping files to a project. + +Usage: sentry-cli[EXE] proguard upload [OPTIONS] [PATH]... + +Arguments: + [PATH]... The path to the mapping files. + +Options: + -o, --org The organization ID or slug. + --header Custom headers that should be attached to all requests + in key:value format. + -p, --project The project ID or slug. + --auth-token Use the given Sentry auth token. + --no-upload Disable the actual upload. + This runs all steps for the processing but does not trigger the + upload. This is useful if you just want to verify the mapping + files and write the proguard UUIDs into a properties file. + --log-level Set the log output verbosity. [possible values: trace, debug, info, + warn, error] + --write-properties Write the UUIDs for the processed mapping files into the given + properties file. + --quiet Do not print any output while preserving correct exit code. This + flag is currently implemented only for selected subcommands. + [aliases: --silent] + --require-one Requires at least one file to upload or the command will error. + -u, --uuid Explicitly override the UUID of the mapping file with another one. + This should be used with caution as it means that you can upload + multiple mapping files if you don't take care. This however can be + useful if you have a build process in which you need to know the + UUID of the proguard file before it was created. If you upload a + file with a forced UUID you can only upload a single proguard file. + -h, --help Print help + +``` diff --git a/tests/integration/_cases/proguard/proguard-upload-no-upload.trycmd b/tests/integration/_cases/proguard/proguard-upload-no-upload.trycmd new file mode 100644 index 0000000000..b2c1e0a483 --- /dev/null +++ b/tests/integration/_cases/proguard/proguard-upload-no-upload.trycmd @@ -0,0 +1,7 @@ +``` +$ sentry-cli proguard upload tests/integration/_fixtures/proguard.txt --no-upload +? success +warning: ignoring proguard mapping 'tests/integration/_fixtures/proguard.txt': Proguard mapping does not contain line information +> skipping upload. + +``` diff --git a/tests/integration/_expected_requests/upload_proguard/chunk_upload_needs_upload.bin b/tests/integration/_expected_requests/proguard/upload/chunk_upload_needs_upload.bin similarity index 100% rename from tests/integration/_expected_requests/upload_proguard/chunk_upload_needs_upload.bin rename to tests/integration/_expected_requests/proguard/upload/chunk_upload_needs_upload.bin diff --git a/tests/integration/_expected_requests/upload_proguard/chunk_upload_two_files.bin b/tests/integration/_expected_requests/proguard/upload/chunk_upload_two_files.bin similarity index 100% rename from tests/integration/_expected_requests/upload_proguard/chunk_upload_two_files.bin rename to tests/integration/_expected_requests/proguard/upload/chunk_upload_two_files.bin diff --git a/tests/integration/_fixtures/upload_proguard/mapping-2.txt b/tests/integration/_fixtures/proguard/upload/mapping-2.txt similarity index 100% rename from tests/integration/_fixtures/upload_proguard/mapping-2.txt rename to tests/integration/_fixtures/proguard/upload/mapping-2.txt diff --git a/tests/integration/_fixtures/upload_proguard/mapping.txt b/tests/integration/_fixtures/proguard/upload/mapping.txt similarity index 100% rename from tests/integration/_fixtures/upload_proguard/mapping.txt rename to tests/integration/_fixtures/proguard/upload/mapping.txt diff --git a/tests/integration/mod.rs b/tests/integration/mod.rs index f009aa72c7..fde0647603 100644 --- a/tests/integration/mod.rs +++ b/tests/integration/mod.rs @@ -12,6 +12,7 @@ mod logs; mod monitors; mod org_tokens; mod organizations; +mod proguard; mod projects; #[cfg(target_os = "macos")] mod react_native; diff --git a/tests/integration/proguard/mod.rs b/tests/integration/proguard/mod.rs new file mode 100644 index 0000000000..f8802ec1d8 --- /dev/null +++ b/tests/integration/proguard/mod.rs @@ -0,0 +1 @@ +mod upload; diff --git a/tests/integration/proguard/upload.rs b/tests/integration/proguard/upload.rs new file mode 100644 index 0000000000..2e9be130b1 --- /dev/null +++ b/tests/integration/proguard/upload.rs @@ -0,0 +1,288 @@ +use std::fs; +use std::sync::atomic::{AtomicU8, Ordering}; + +use mockito::Matcher; +use serde_json::json; + +use crate::integration::test_utils::{chunk_upload, AssertCommand}; +use crate::integration::{MockEndpointBuilder, TestManager}; + +#[test] +fn command_proguard() { + TestManager::new().register_trycmd_test("proguard/*.trycmd"); +} + +#[test] +fn command_proguard_upload_no_upload_no_auth_token() { + TestManager::new().register_trycmd_test("proguard/proguard-upload-no-upload.trycmd"); +} + +#[test] +fn chunk_upload_already_there() { + TestManager::new() + .mock_endpoint( + MockEndpointBuilder::new("GET", "/api/0/organizations/wat-org/chunk-upload/") + .with_response_file("debug_files/get-chunk-upload.json"), + ) + .mock_endpoint( + MockEndpointBuilder::new( + "POST", + "/api/0/projects/wat-org/wat-project/files/difs/assemble/", + ) + .with_header_matcher("content-type", "application/json") + .with_matcher(Matcher::Json(json!({ + "297ecd9143fc2882e4b6758c1ccd13ea82930eeb": { + "name": "/proguard/c038584d-c366-570c-ad1e-034fa0d194d7.txt", + "chunks": ["297ecd9143fc2882e4b6758c1ccd13ea82930eeb"] + } + }))) + .with_response_body( + json!({ + "297ecd9143fc2882e4b6758c1ccd13ea82930eeb": { + "state": "ok", + "detail": null, + "missingChunks": [], + "dif": { + "id": "12", + "uuid": "c038584d-c366-570c-ad1e-034fa0d194d7", + "debugId": "c038584d-c366-570c-ad1e-034fa0d194d7", + "codeId": null, + "cpuName": "any", + "objectName": "proguard-mapping", + "symbolType": "proguard", + "headers": {"Content-Type": "text/x-proguard+plain"}, + "size": 155, + "sha1": "297ecd9143fc2882e4b6758c1ccd13ea82930eeb", + "dateCreated": "1776-07-04T12:00:00.000Z", + "data": {"features": ["mapping"]} + } + } + }) + .to_string(), + ), + ) + .assert_cmd([ + "proguard", + "upload", + "tests/integration/_fixtures/proguard/upload/mapping.txt", + ]) + .with_default_token() + .run_and_assert(AssertCommand::Success) +} + +#[test] +fn chunk_upload_needs_upload() { + const EXPECTED_CHUNKS_BOUNDARY: &str = "------------------------w2uOUUnuLEYTmQorc0ix48"; + + let call_count = AtomicU8::new(0); + let expected_chunk_body = fs::read( + "tests/integration/_expected_requests/proguard/upload/chunk_upload_needs_upload.bin", + ) + .expect("expected chunk upload request file should be readable"); + + TestManager::new() + .mock_endpoint( + MockEndpointBuilder::new("GET", "/api/0/organizations/wat-org/chunk-upload/") + .with_response_file("debug_files/get-chunk-upload.json"), + ) + .mock_endpoint( + MockEndpointBuilder::new("POST", "/api/0/organizations/wat-org/chunk-upload/") + .with_response_fn(move |request| { + let boundary = chunk_upload::boundary_from_request(request) + .expect("content-type header should be a valid multipart/form-data header"); + + let body = request.body().expect("body should be readable"); + + let chunks = chunk_upload::split_chunk_body(body, boundary) + .expect("body should be a valid multipart/form-data body"); + + let expected_chunks = chunk_upload::split_chunk_body( + &expected_chunk_body, + EXPECTED_CHUNKS_BOUNDARY, + ) + .expect("expected body is valid multipart form data"); + + // Using assert! because in case of failure, the output with assert_eq! + // is too long to be useful. + assert_eq!( + chunks, expected_chunks, + "Uploaded chunks differ from the expected chunks" + ); + + vec![] + }), + ) + .mock_endpoint( + MockEndpointBuilder::new( + "POST", + "/api/0/projects/wat-org/wat-project/files/difs/assemble/", + ) + .with_header_matcher("content-type", "application/json") + .with_matcher(Matcher::Json(json!({ + "297ecd9143fc2882e4b6758c1ccd13ea82930eeb": { + "name": "/proguard/c038584d-c366-570c-ad1e-034fa0d194d7.txt", + "chunks": ["297ecd9143fc2882e4b6758c1ccd13ea82930eeb"] + } + }))) + .with_response_fn(move |_| { + match call_count.fetch_add(1, Ordering::Relaxed) { + 0 => { + // First call: The file is not found since it still needs to be uploaded. + json!({ + "297ecd9143fc2882e4b6758c1ccd13ea82930eeb": { + "state": "not_found", + "missingChunks": ["297ecd9143fc2882e4b6758c1ccd13ea82930eeb"] + } + }) + .to_string() + .into_bytes() + } + 1 => { + // Second call: The file has been uploaded, assemble job created. + json!({ + "297ecd9143fc2882e4b6758c1ccd13ea82930eeb": { + "state": "created", + "missingChunks": [] + } + }) + .to_string() + .into_bytes() + } + n => panic!( + "Only 2 calls to the assemble endpoint expected, but there were {}.", + n + 1 + ), + } + }) + .expect(2), + ) + .assert_cmd([ + "proguard", + "upload", + "tests/integration/_fixtures/proguard/upload/mapping.txt", + ]) + .with_default_token() + .run_and_assert(AssertCommand::Success) +} + +#[test] +fn chunk_upload_two_files() { + const EXPECTED_CHUNKS_BOUNDARY: &str = "------------------------HNdDRjCgjkRtu3COUTCcJV"; + + let call_count = AtomicU8::new(0); + let expected_chunk_body = + fs::read("tests/integration/_expected_requests/proguard/upload/chunk_upload_two_files.bin") + .expect("expected chunk upload request file should be readable"); + + TestManager::new() + .mock_endpoint( + MockEndpointBuilder::new("GET", "/api/0/organizations/wat-org/chunk-upload/") + .with_response_file("debug_files/get-chunk-upload.json"), + ) + .mock_endpoint( + MockEndpointBuilder::new("POST", "/api/0/organizations/wat-org/chunk-upload/") + .with_response_fn(move |request| { + let boundary = chunk_upload::boundary_from_request(request) + .expect("content-type header should be a valid multipart/form-data header"); + + let body = request.body().expect("body should be readable"); + + let chunks = chunk_upload::split_chunk_body(body, boundary) + .expect("body should be a valid multipart/form-data body"); + + let expected_chunks = chunk_upload::split_chunk_body( + &expected_chunk_body, + EXPECTED_CHUNKS_BOUNDARY, + ) + .expect("expected body is valid multipart form data"); + + // Using assert! because in case of failure, the output with assert_eq! + // is too long to be useful. + assert_eq!( + chunks, expected_chunks, + "Uploaded chunks differ from the expected chunks" + ); + + vec![] + }), + ) + .mock_endpoint( + MockEndpointBuilder::new( + "POST", + "/api/0/projects/wat-org/wat-project/files/difs/assemble/", + ) + .with_header_matcher("content-type", "application/json") + .with_matcher(Matcher::AnyOf( + [ + Matcher::Json(json!({ + "297ecd9143fc2882e4b6758c1ccd13ea82930eeb": { + "name": "/proguard/c038584d-c366-570c-ad1e-034fa0d194d7.txt", + "chunks": ["297ecd9143fc2882e4b6758c1ccd13ea82930eeb"] + }, + "e5329624a8d06e084941f133c75b5874f793ee7c": { + "name": "/proguard/747e1d76-509b-5225-8a5b-db7b7d4067d4.txt", + "chunks": ["e5329624a8d06e084941f133c75b5874f793ee7c"] + } + })), + Matcher::Json(json!({ + "e5329624a8d06e084941f133c75b5874f793ee7c": { + "name": "/proguard/747e1d76-509b-5225-8a5b-db7b7d4067d4.txt", + "chunks": ["e5329624a8d06e084941f133c75b5874f793ee7c"] + }, + "297ecd9143fc2882e4b6758c1ccd13ea82930eeb": { + "name": "/proguard/c038584d-c366-570c-ad1e-034fa0d194d7.txt", + "chunks": ["297ecd9143fc2882e4b6758c1ccd13ea82930eeb"] + } + })), + ] + .into(), + )) + .with_response_fn(move |_| { + match call_count.fetch_add(1, Ordering::Relaxed) { + 0 => { + // First call: The file is not found since it still needs to be uploaded. + json!({ + "297ecd9143fc2882e4b6758c1ccd13ea82930eeb": { + "state": "not_found", + "missingChunks": ["297ecd9143fc2882e4b6758c1ccd13ea82930eeb"] + }, + "e5329624a8d06e084941f133c75b5874f793ee7c": { + "state": "not_found", + "missingChunks": ["e5329624a8d06e084941f133c75b5874f793ee7c"] + } + }) + .to_string() + .into_bytes() + } + 1 => { + // Second call: The file has been uploaded, assemble job created. + json!({ + "297ecd9143fc2882e4b6758c1ccd13ea82930eeb": { + "state": "created", + "missingChunks": [] + }, + "e5329624a8d06e084941f133c75b5874f793ee7c": { + "state": "created", + "missingChunks": [] + } + }) + .to_string() + .into_bytes() + } + n => panic!( + "Only 2 calls to the assemble endpoint expected, but there were {}.", + n + 1 + ), + } + }) + .expect(2), + ) + .assert_cmd([ + "proguard", + "upload", + "tests/integration/_fixtures/proguard/upload/mapping.txt", + "tests/integration/_fixtures/proguard/upload/mapping-2.txt", + ]) + .with_default_token() + .run_and_assert(AssertCommand::Success) +} diff --git a/tests/integration/upload_proguard.rs b/tests/integration/upload_proguard.rs index d6c148bc80..6f5d1ee6ab 100644 --- a/tests/integration/upload_proguard.rs +++ b/tests/integration/upload_proguard.rs @@ -63,7 +63,7 @@ fn chunk_upload_already_there() { ) .assert_cmd([ "upload-proguard", - "tests/integration/_fixtures/upload_proguard/mapping.txt", + "tests/integration/_fixtures/proguard/upload/mapping.txt", ]) .with_default_token() .run_and_assert(AssertCommand::Success) @@ -75,7 +75,7 @@ fn chunk_upload_needs_upload() { let call_count = AtomicU8::new(0); let expected_chunk_body = fs::read( - "tests/integration/_expected_requests/upload_proguard/chunk_upload_needs_upload.bin", + "tests/integration/_expected_requests/proguard/upload/chunk_upload_needs_upload.bin", ) .expect("expected chunk upload request file should be readable"); @@ -156,7 +156,7 @@ fn chunk_upload_needs_upload() { ) .assert_cmd([ "upload-proguard", - "tests/integration/_fixtures/upload_proguard/mapping.txt", + "tests/integration/_fixtures/proguard/upload/mapping.txt", ]) .with_default_token() .run_and_assert(AssertCommand::Success) @@ -168,7 +168,7 @@ fn chunk_upload_two_files() { let call_count = AtomicU8::new(0); let expected_chunk_body = - fs::read("tests/integration/_expected_requests/upload_proguard/chunk_upload_two_files.bin") + fs::read("tests/integration/_expected_requests/proguard/upload/chunk_upload_two_files.bin") .expect("expected chunk upload request file should be readable"); TestManager::new() @@ -277,8 +277,8 @@ fn chunk_upload_two_files() { ) .assert_cmd([ "upload-proguard", - "tests/integration/_fixtures/upload_proguard/mapping.txt", - "tests/integration/_fixtures/upload_proguard/mapping-2.txt", + "tests/integration/_fixtures/proguard/upload/mapping.txt", + "tests/integration/_fixtures/proguard/upload/mapping-2.txt", ]) .with_default_token() .run_and_assert(AssertCommand::Success)