From 46c718ace22ed32f6da85b55a11bd56f5fdebcc6 Mon Sep 17 00:00:00 2001 From: Tyler Jang Date: Wed, 26 Aug 2026 00:38:13 +0000 Subject: [PATCH 1/2] feat(upload): print a short link to the upload when a collection id is passed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The end-of-run "Bundle Upload ID" line becomes a link when the run is in test-collection link mode: 🏷️ Bundle Upload: https://app.trunk.io/{org}/flaky-tests/collections/{short_id}/u/{bundle_meta_id} The webapp resolves the upload's createdAt from the id and redirects to the canonical uploads page (companion PR: trunk-io/trunk2#5440). Unlike the per-test `/t/` links there is no `?repo=` param — the collection short id fully scopes the lookup. Gated exactly as the `/t/` links are: a collection id must be passed and `--hide-test-collection-links` / `TRUNK_HIDE_TEST_COLLECTION_LINKS` must be unset. Otherwise the bare bundle upload id prints as before. The gate reads `test_collection_short_id` rather than `base_props.test_collection.short_id` because an exported-but-blank `TRUNK_TEST_COLLECTION_ID` is `Some("")`, not `None`, and would otherwise produce a malformed `/collections//u/` link. Co-Authored-By: Claude Opus 5 (1M context) --- api/src/urls.rs | 37 ++++++++++++++++++++++++++ cli/src/upload_command.rs | 51 ++++++++++++++++++++++++++++++------ cli/tests/upload.rs | 55 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 132 insertions(+), 11 deletions(-) diff --git a/api/src/urls.rs b/api/src/urls.rs index 17548348..0ace68ff 100644 --- a/api/src/urls.rs +++ b/api/src/urls.rs @@ -19,6 +19,26 @@ pub fn url_for_test_case( Ok(url.to_string()) } +/// Short link to a single upload. Unlike [`url_for_test_case`] this needs no `repo` query +/// param: the collection short id fully scopes the webapp's lookup of the upload's +/// timestamp, which is the half of the canonical URL's key that cannot fit in a short link. +pub fn url_for_upload( + public_api_address: &str, + org_url_slug: &str, + test_collection_short_id: &str, + bundle_meta_id: &str, +) -> Result { + let mut url = Url::parse(convert_to_app_url(public_api_address).as_str())?; + url.set_path( + format!( + "{}/flaky-tests/collections/{}/u/{}", + org_url_slug, test_collection_short_id, bundle_meta_id + ) + .as_str(), + ); + Ok(url.to_string()) +} + fn convert_to_app_url(public_api_address: &str) -> String { public_api_address.replace("https://api.", "https://app.") } @@ -87,6 +107,23 @@ mod tests { ); } + #[test] + fn test_upload_url_generated() { + let actual = url_for_upload( + "https://api.trunk-staging.io", + "bad-app-org", + "tc_123", + "82c6a6e5-f8ea-4d93-9a26-b8ab6ff8f6bc", + ); + + assert_eq!( + actual, + Ok(String::from( + "https://app.trunk-staging.io/bad-app-org/flaky-tests/collections/tc_123/u/82c6a6e5-f8ea-4d93-9a26-b8ab6ff8f6bc" + )), + ); + } + #[test] fn test_collection_url_generated() { let actual = url_for_test_case( diff --git a/cli/src/upload_command.rs b/cli/src/upload_command.rs index 02c7ce36..7cd843cf 100644 --- a/cli/src/upload_command.rs +++ b/cli/src/upload_command.rs @@ -4,7 +4,10 @@ use std::path::PathBuf; use std::sync::mpsc::Sender; use api::client::{ApiClient, ApiErrorEndpoint}; -use api::{client::get_api_host, urls::url_for_test_case}; +use api::{ + client::get_api_host, + urls::{url_for_test_case, url_for_upload}, +}; use bundle::{BundleMeta, BundlerUtil, QuarantineResolutionMode, Test, unzip_tarball}; use clap::{ArgAction, Args}; use codeowners::OwnersSource; @@ -384,6 +387,7 @@ pub struct UploadRunResult { pub show_failure_messages: bool, pub test_collection_short_id: Option, pub hide_test_collection_links: bool, + pub api_address: String, } pub struct RunUploadOptions { @@ -658,6 +662,7 @@ pub async fn run_upload( .test_collection_short_id .filter(|id| !id.is_empty()), hide_test_collection_links: upload_args.hide_test_collection_links, + api_address: api_client.api_host.clone(), }) } @@ -716,6 +721,31 @@ pub fn get_bundle_upload_id_message(bundle_upload_id: &str) -> String { format!("🏷️ Bundle Upload ID: {}", bundle_upload_id) } +pub fn get_bundle_upload_url_message(url: &str) -> String { + format!("🏷️ Bundle Upload: {}", url) +} + +impl UploadRunResult { + /// Short link to this upload's page, when the run is in test-collection link mode. + /// Gated on `test_collection_short_id` rather than `base_props.test_collection.short_id` + /// so an exported-but-blank collection id can't yield a malformed `/collections//u/` + /// link; the payload supplies the bundle_meta id the link is keyed on. + fn collection_upload_url(&self) -> Option { + if self.hide_test_collection_links { + return None; + } + let short_id = self.test_collection_short_id.as_deref()?; + let test_collection = self.meta.base_props.test_collection.as_ref()?; + url_for_upload( + &self.api_address, + &self.meta.base_props.org, + short_id, + &test_collection.bundle_meta_id, + ) + .ok() + } +} + impl EndOutput for UploadRunResult { fn output(&self) -> anyhow::Result> { let mut output: Vec = Vec::new(); @@ -733,15 +763,20 @@ impl EndOutput for UploadRunResult { } } - // Add the bundle upload ID message + // Add the bundle upload message: the short link when the run is in + // test-collection link mode, otherwise the bare id as before. { - let bundle_upload_id = self.meta.base_props.bundle_upload_id.clone(); - if !bundle_upload_id.is_empty() { + let bundle_upload_id = &self.meta.base_props.bundle_upload_id; + let message = match self.collection_upload_url() { + Some(url) => Some(get_bundle_upload_url_message(&url)), + None if !bundle_upload_id.is_empty() => { + Some(get_bundle_upload_id_message(bundle_upload_id)) + } + None => None, + }; + if let Some(message) = message { output.push(Line::from_iter([Span::new_styled( - style(get_bundle_upload_id_message( - &self.meta.base_props.bundle_upload_id, - )) - .attribute(Attribute::Bold), + style(message).attribute(Attribute::Bold), )?])); output.push(Line::default()); } diff --git a/cli/tests/upload.rs b/cli/tests/upload.rs index 8712dfe4..a7ffdf9d 100644 --- a/cli/tests/upload.rs +++ b/cli/tests/upload.rs @@ -292,10 +292,10 @@ async fn upload_bundle() { // HINT: View CLI output with `cargo test -- --nocapture` println!("{assert}"); - // Verify that the bundle upload ID message is printed - let bundle_upload_id = base_props.bundle_upload_id.clone(); + // This run passes a collection id, so the upload surfaces as a short link rather + // than the bare id (the bare-id form is covered by the links-hidden test below). assert.stderr(predicate::str::contains( - get_bundle_upload_id_message(&bundle_upload_id).as_str(), + "/test-org/flaky-tests/collections/tc_123/u/82c6a6e5-f8ea-4d93-9a26-b8ab6ff8f6bc", )); } @@ -322,6 +322,55 @@ async fn upload_bundle_prints_test_collection_links() { .stderr(predicate::str::contains("?repo=trunk-io%2Fanalytics-cli")); } +// NOTE: must be multi threaded to start a mock server +#[tokio::test(flavor = "multi_thread")] +async fn upload_bundle_prints_test_collection_upload_link() { + let temp_dir = tempdir().unwrap(); + generate_mock_git_repo(&temp_dir); + generate_mock_valid_junit_xmls(&temp_dir); + + let state = MockServerBuilder::new().spawn_mock_server().await; + + let assert = CommandBuilder::upload(temp_dir.path(), state.host.clone()) + .command() + .arg("--test-collection-id") + .arg("tc_123") + .assert() + .failure(); + + // the upload link is keyed on the bundle_meta id the server returned, not the + // bundle upload id, and needs no ?repo= — the short id scopes the lookup + assert + .stderr(predicate::str::contains( + "/test-org/flaky-tests/collections/tc_123/u/82c6a6e5-f8ea-4d93-9a26-b8ab6ff8f6bc", + )) + .stderr(predicate::str::contains("Bundle Upload ID:").not()); +} + +// NOTE: must be multi threaded to start a mock server +#[tokio::test(flavor = "multi_thread")] +async fn upload_bundle_prints_bare_upload_id_when_links_hidden() { + let temp_dir = tempdir().unwrap(); + generate_mock_git_repo(&temp_dir); + generate_mock_valid_junit_xmls(&temp_dir); + + let state = MockServerBuilder::new().spawn_mock_server().await; + + let assert = CommandBuilder::upload(temp_dir.path(), state.host.clone()) + .command() + .env("TRUNK_HIDE_TEST_COLLECTION_LINKS", "true") + .arg("--test-collection-id") + .arg("tc_123") + .assert() + .failure(); + + assert + .stderr(predicate::str::contains( + get_bundle_upload_id_message("test-bundle-upload-id").as_str(), + )) + .stderr(predicate::str::contains("/flaky-tests/collections/tc_123/u/").not()); +} + // NOTE: must be multi threaded to start a mock server #[tokio::test(flavor = "multi_thread")] async fn upload_bundle_hides_test_collection_links_when_env_set() { From 0eeb6c49a82a765e25e09baa856dd128283bdf9d Mon Sep 17 00:00:00 2001 From: Tyler Jang Date: Wed, 26 Aug 2026 01:14:06 +0000 Subject: [PATCH 2/2] fix(upload): shorten the upload link label to "Upload:" Co-Authored-By: Claude Opus 5 (1M context) --- cli/src/upload_command.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/upload_command.rs b/cli/src/upload_command.rs index 7cd843cf..51c400cf 100644 --- a/cli/src/upload_command.rs +++ b/cli/src/upload_command.rs @@ -722,7 +722,7 @@ pub fn get_bundle_upload_id_message(bundle_upload_id: &str) -> String { } pub fn get_bundle_upload_url_message(url: &str) -> String { - format!("🏷️ Bundle Upload: {}", url) + format!("🏷️ Upload: {}", url) } impl UploadRunResult {