diff --git a/CHANGELOG.md b/CHANGELOG.md index 785d770..74f1334 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,20 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). carrying an unresolvable debug-id `missing_sym`, which is what prompts an upload — an unstamped bundle is silently unsymbolicated instead. +### Fixed +- **`debug-files upload --type sourcemaps --dry-run` no longer fails on a map that has no + debug-id.** A dry run sends nothing, so an un-keyed map cannot register the unfindable symbol the + real run refuses over — but it exited 11 on the first one, which made the documented SAFE + diagnostic unusable on a freshly built directory: `sourcemaps inject --dry-run` writes nothing by + design, so every map is still un-keyed when the preview reaches it. (`@bugsee/bundler-plugin-core` + works around this by skipping the upload step entirely on a dry run, so the one safe way to + preview the flow never exercised the flow.) + + Such a map is now reported (`dry run: no debug_id — …`) and counted (`unkeyed` in the completion + log) instead. A build where NOTHING is keyed completes as a success saying so, rather than falling + into the "all 0 source maps are stylesheet maps" branch, which was plainly wrong there. A real run + still exits 11 on the first un-keyed map, and `--uuid` still keys a map on a dry run as usual. + ## [0.7.10] - 2026-09-18 ### Added diff --git a/README.md b/README.md index 4614b6e..6959e80 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,11 @@ sequential uploads whatever the ceiling says — those registrations must not ra each other. A failed upload stops the batch rather than letting the rest run into a server that has already refused one. +A `--dry-run` discovers and packs but sends nothing, so a map that carries no debug-id is reported +rather than fatal there (`unkeyed` in the completion log) — the whole flow can be previewed on a +freshly built directory, where `sourcemaps inject --dry-run` has deliberately written nothing yet. +A REAL run still refuses such a map (exit 11): uploading it would register a symbol nothing can find. + `--allow-empty` turns "nothing to upload" into success (exit 0) instead of exit 10 — a monorepo package built without maps, or a framework whose server output has none, is a legitimate no-op rather than a reason to fail the build. diff --git a/scripts/e2e_flows.py b/scripts/e2e_flows.py index 16b3e28..834ec54 100644 --- a/scripts/e2e_flows.py +++ b/scripts/e2e_flows.py @@ -398,6 +398,17 @@ def main(): results["sourcemaps_uninjected_uploads_nothing"] = not os.path.exists( cappath("sourcemaps_uninjected__symbols_posts.jsonl")) + # The same directory under --dry-run is the documented SAFE diagnostic, and it used to die on + # the first un-keyed map (exit 11) — which is why the bundler plugin skips the upload step + # entirely on a dry run. It now succeeds and names what `sourcemaps inject` would key. + results["sourcemaps_uninjected_dry_run_succeeds"] = run( + binpath, "sourcemaps_uninjected_dry", + ["debug-files", "upload", "--type", "sourcemaps", "--dry-run", + os.path.join(fix, "web-uninjected")] + v, + expect_stderr="no debug_id") + results["sourcemaps_dry_run_uploads_nothing"] = not os.path.exists( + cappath("sourcemaps_uninjected_dry__symbols_posts.jsonl")) + # --concurrency uploads several maps at once: every one of them must still be registered # exactly once. (The unit tests prove the overlap itself; this proves nothing is dropped.) results["sourcemaps_concurrent_upload"] = run( diff --git a/src/cli/debug_files.rs b/src/cli/debug_files.rs index 42d1b71..a2eeae3 100644 --- a/src/cli/debug_files.rs +++ b/src/cli/debug_files.rs @@ -1545,6 +1545,8 @@ async fn run_sourcemap_upload( // Pass 1: identify and key every map, uploading nothing. let mut planned = Vec::with_capacity(candidates.len()); let mut skipped = 0usize; + // Maps a DRY RUN found with no debug-id. Always 0 on a real run: that returns on the first one. + let mut unkeyed = 0usize; for SourcemapCandidate { path: map_path, explicit, @@ -1580,18 +1582,34 @@ async fn run_sourcemap_upload( } supplied_str } - None => identity.debug_id.clone().ok_or_else(|| { - input_invalid(format!( - "source map has no debug_id/debugId/uuid: {} — nothing was uploaded. Run \ - `bugsee-cli sourcemaps inject ` over the directory holding its JS \ - bundle first: inject stamps a `.js`/`.cjs`/`.mjs` bundle's map when it sits \ - beside the bundle as `.map`, or when the bundle's \ - `//# sourceMappingURL=` names it by a relative path inside the bundle's \ - directory. For a single map whose id you own (e.g. a React Native \ - bundle's), pass that file with --uuid", - map_path.display() - )) - })?, + None => match identity.debug_id.clone() { + Some(id) => id, + // A dry run sends nothing, so an un-keyed map cannot produce the unfindable symbol + // the real run refuses over. Failing here made the documented SAFE diagnostic + // unusable on a freshly built directory: `sourcemaps inject --dry-run` writes + // nothing by design, so every map is still un-keyed and the preview died on the + // first one. Reported and counted instead. + None if dry_run => { + unkeyed += 1; + tracing::warn!( + path = %map_path.display(), + "dry run: no debug_id — `sourcemaps inject` keys it before a real upload" + ); + continue; + } + None => { + return Err(input_invalid(format!( + "source map has no debug_id/debugId/uuid: {} — nothing was uploaded. Run \ + `bugsee-cli sourcemaps inject ` over the directory holding its JS \ + bundle first: inject stamps a `.js`/`.cjs`/`.mjs` bundle's map when it \ + sits beside the bundle as `.map`, or when the bundle's \ + `//# sourceMappingURL=` names it by a relative path inside the bundle's \ + directory. For a single map whose id you own (e.g. a React Native \ + bundle's), pass that file with --uuid", + map_path.display() + ))); + } + }, }; tracing::info!( debug_id = %resolved_id, @@ -1603,6 +1621,17 @@ async fn run_sourcemap_upload( } if planned.is_empty() { + // A dry run over a freshly built directory: every map is un-keyed because `inject --dry-run` + // wrote nothing. That is the preview working, not a failure — and the stylesheet message + // below would be plainly wrong ("all 0 source maps are stylesheet maps"). + if dry_run && unkeyed > 0 { + tracing::info!( + unkeyed, + skipped, + "dry-run complete: no map carries a debug_id yet — `sourcemaps inject` keys them" + ); + return Ok(()); + } if allow_empty { tracing::info!( skipped, @@ -1684,7 +1713,7 @@ async fn run_sourcemap_upload( } if dry_run { - tracing::info!(skipped, "dry-run complete"); + tracing::info!(skipped, unkeyed, "dry-run complete"); } else { tracing::info!(uploaded, already_existed, skipped, "upload complete"); } @@ -2894,6 +2923,107 @@ mod sourcemap_upload_tests { assert_eq!(puts(&server).await, 1); } + /// `--dry-run` is documented as the safe diagnostic, and it could not be used on a freshly built + /// output directory at all: `sourcemaps inject --dry-run` writes nothing by design, so the maps + /// still carry no debug-id and the upload then failed with exit 11 on the FIRST one. The JS + /// bundler plugin works around it by skipping the upload step entirely on a dry run, which means + /// the one safe way to preview the flow never exercises the flow. + /// + /// A dry run sends nothing, so an un-keyed map cannot produce an unfindable symbol — the reason + /// the real run refuses. It is now reported and counted instead. + #[tokio::test] + async fn a_dry_run_reports_maps_with_no_debug_id_instead_of_failing() { + let tmp = tempfile::tempdir().unwrap(); + write( + tmp.path(), + "keyed.js.map", + br#"{"version":3,"debug_id":"11111111-1111-1111-1111-111111111111","mappings":""}"#, + ); + write(tmp.path(), "bare.js.map", br#"{"version":3,"mappings":""}"#); + let server = collector(&[]).await; + + // The real run still refuses: uploading an un-keyed map registers a symbol nothing can find. + let err = upload_paths( + &[tmp.path().to_path_buf()], + &server.uri(), + SourcemapUploadTweak::default(), + ) + .await + .unwrap_err(); + assert_eq!( + crate::error::classify(&err), + crate::exit_code::ExitCode::InputInvalid + ); + assert!( + posted_ids(&server).await.is_empty(), + "nothing is registered" + ); + + // The dry run succeeds, and still packs the map that IS keyed. + upload_paths( + &[tmp.path().to_path_buf()], + &server.uri(), + SourcemapUploadTweak { + dry_run: true, + ..SourcemapUploadTweak::default() + }, + ) + .await + .unwrap(); + assert!( + posted_ids(&server).await.is_empty(), + "a dry run sends nothing" + ); + } + + /// …and when NOTHING is keyed, the dry run is still a success: there is nothing to warn about + /// twice, and failing here would put the workaround back. + #[tokio::test] + async fn a_dry_run_over_an_entirely_uninjected_build_still_succeeds() { + let tmp = tempfile::tempdir().unwrap(); + for name in ["a.js.map", "b.js.map"] { + write(tmp.path(), name, br#"{"version":3,"mappings":""}"#); + } + let server = collector(&[]).await; + + upload_paths( + &[tmp.path().to_path_buf()], + &server.uri(), + SourcemapUploadTweak { + dry_run: true, + ..SourcemapUploadTweak::default() + }, + ) + .await + .unwrap(); + assert!(puts(&server).await == 0 && posted_ids(&server).await.is_empty()); + } + + /// An explicit `--uuid` still wins on a dry run — the override is what keys the map, so it is + /// not "missing" at all. + #[tokio::test] + async fn a_dry_run_with_a_uuid_override_keys_the_map_as_usual() { + let tmp = tempfile::tempdir().unwrap(); + write(tmp.path(), "bare.js.map", br#"{"version":3,"mappings":""}"#); + let server = collector(&[]).await; + + run_sourcemap_upload( + &[tmp.path().join("bare.js.map")], + &server.uri(), + "TKN", + "1.0", + "1", + Some(Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap()), + Strategy::Zstd(11), + false, + None, + false, + true, + ) + .await + .unwrap(); + } + /// A build step that legitimately produces no maps — a monorepo package built without them, a /// framework whose server output has none — must not fail the caller's build. Opt-in, because a /// scan that finds nothing is a configuration mistake as often as it is a legitimate no-op.