Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions scripts/e2e_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
156 changes: 143 additions & 13 deletions src/cli/debug_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 <dir>` over the directory holding its JS \
bundle first: inject stamps a `.js`/`.cjs`/`.mjs` bundle's map when it sits \
beside the bundle as `<bundle>.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 <dir>` over the directory holding its JS \
bundle first: inject stamps a `.js`/`.cjs`/`.mjs` bundle's map when it \
sits beside the bundle as `<bundle>.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,
Expand All @@ -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,
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -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.
Expand Down
Loading