Skip to content

Deterministic Bundle Id generation#220

Open
lbloder wants to merge 9 commits intomainfrom
feat/deterministic-bundle-id
Open

Deterministic Bundle Id generation#220
lbloder wants to merge 9 commits intomainfrom
feat/deterministic-bundle-id

Conversation

@lbloder
Copy link
Copy Markdown
Collaborator

@lbloder lbloder commented Jan 26, 2026

📜 Description

Add the ability to calculate the bundleId based on Hashes of filepaths and file contents of the source context.
This allows for reproducible builds by having the bundleId stay the same if no files change.
This is an opt-in feature that can be enabled with:

<reproducibleBundleId>true</reproducibleBundleId>

💡 Motivation and Context

Resolves #213

💚 How did you test it?

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I reviewed the submitted code
  • I added tests to verify the changes
  • I updated the docs if needed
  • No breaking changes

🔮 Next steps

@lbloder lbloder changed the title set correct maven version in wrapper, generate bundle id from file ha… Deterministic Bundle Id generation Feb 4, 2026
@lbloder lbloder marked this pull request as ready for review February 5, 2026 21:04
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Missing delimiter between path and content in hash
    • The deterministic bundle hash now prefixes both path bytes and file bytes with their lengths before updating the digest, removing ambiguous concatenation collisions.
  • ✅ Fixed: Platform-dependent newLine breaks cross-OS reproducibility
    • The properties file writer now uses explicit "\n" line endings instead of platform-dependent newLine() to keep output byte-identical across OSes.

Create PR

Or push these changes by commenting:

@cursor push 13fbb069ed
Preview (13fbb069ed)
diff --git a/src/main/java/io/sentry/UploadSourceBundleMojo.java b/src/main/java/io/sentry/UploadSourceBundleMojo.java
--- a/src/main/java/io/sentry/UploadSourceBundleMojo.java
+++ b/src/main/java/io/sentry/UploadSourceBundleMojo.java
@@ -213,11 +213,11 @@
           for (final @NotNull Path file : sortedFiles) {
             final @NotNull String relativePath =
                 collectedSourcesDir.toPath().relativize(file).toString().replace('\\', '/');
-            digest.update(relativePath.getBytes(StandardCharsets.UTF_8));
+            updateDigestWithLengthPrefix(digest, relativePath.getBytes(StandardCharsets.UTF_8));
 
             // Include the file content in the hash
             final byte[] fileBytes = Files.readAllBytes(file);
-            digest.update(fileBytes);
+            updateDigestWithLengthPrefix(digest, fileBytes);
           }
         }
       }
@@ -257,6 +257,12 @@
     return new UUID(buffer.getLong(), buffer.getLong()).toString();
   }
 
+  private static void updateDigestWithLengthPrefix(
+      final @NotNull MessageDigest digest, final byte[] data) {
+    digest.update(ByteBuffer.allocate(Integer.BYTES).putInt(data.length).array());
+    digest.update(data);
+  }
+
   private void bundleSources(
       final @NotNull SentryCliRunner cliRunner,
       final @NotNull String bundleId,
@@ -375,11 +381,11 @@
       // Write properties without timestamp comment for reproducible builds
       // Properties are written in sorted order for consistency
       fileWriter.write("# Generated by sentry-maven-plugin");
-      fileWriter.newLine();
+      fileWriter.write("\n");
       fileWriter.write("io.sentry.build-tool=maven");
-      fileWriter.newLine();
+      fileWriter.write("\n");
       fileWriter.write("io.sentry.bundle-ids=" + bundleId);
-      fileWriter.newLine();
+      fileWriter.write("\n");
 
       final @NotNull Resource resource = new Resource();
       resource.setDirectory(sentryBuildDir.getPath());
This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

@@ -1,2 +1,2 @@
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a min version that our customers also need? If so, we should probably start mentioning it in changelog/readme/docs.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out it was actually the maven-jar-plugin that needs to be at version >= 3.2.0.
Maven 3.8.6 bundles version 2.4.0 of that plugin. I updated the changelog to reflect that this feature requires >= 3.2.0, updated the sample and reverted the maven version upgrade.

Comment on lines 109 to +113
bundleSources(cliRunner, bundleId, collectedSourcesTargetDir, sourceBundleTargetDir);
uploadSourceBundle(cliRunner, sourceBundleTargetDir);
}

private void collectSources(@NotNull String bundleId, @NotNull File outputDir) {
private void collectSources(@NotNull File outputDir) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: When reproducibleBundleId is true, the build silently falls back to a random UUID if generateDeterministicBundleId() fails, instead of failing the build.
Severity: MEDIUM

Suggested Fix

Modify generateDeterministicBundleId to throw an exception upon failure instead of returning null. This will cause the build to fail as expected when reproducibility cannot be guaranteed. Alternatively, introduce a new configuration option, similar to ignoreSourceBundleUploadFailure, to allow users to explicitly control the failure behavior.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: src/main/java/io/sentry/UploadSourceBundleMojo.java#L109-L113

Potential issue: When the `reproducibleBundleId` option is enabled, any exception during
the deterministic bundle ID generation (e.g., an `IOException` from file system issues)
is caught, and the `generateDeterministicBundleId` method returns `null`. This causes
the logic to fall back to generating a `UUID.randomUUID()`, silently producing a
non-reproducible build. This behavior violates the user's explicit configuration for
reproducibility, as they would expect the build to fail in such a scenario rather than
silently degrade.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Unused boolean constant added to PluginConfig
    • Removed the unused DEFAULT_REPRODUCIBLE_BUNDLE_ID constant from PluginConfig while keeping the string default that is actually referenced.

Create PR

Or push these changes by commenting:

@cursor push 04f3b28ab3
Preview (04f3b28ab3)
diff --git a/src/main/java/io/sentry/config/PluginConfig.java b/src/main/java/io/sentry/config/PluginConfig.java
--- a/src/main/java/io/sentry/config/PluginConfig.java
+++ b/src/main/java/io/sentry/config/PluginConfig.java
@@ -14,7 +14,6 @@
   public static final @NotNull String DEFAULT_SKIP_SOURCE_BUNDLE_STRING = "false";
   public static final boolean DEFAULT_IGNORE_SOURCE_BUNDLE_UPLOAD_FAILURE = false;
   public static final @NotNull String DEFAULT_IGNORE_SOURCE_BUNDLE_UPLOAD_FAILURE_STRING = "false";
-  public static final boolean DEFAULT_REPRODUCIBLE_BUNDLE_ID = false;
   public static final @NotNull String DEFAULT_REPRODUCIBLE_BUNDLE_ID_STRING = "false";
   public static final boolean DEFAULT_SKIP_TELEMETRY = false;
   public static final @NotNull String DEFAULT_SKIP_TELEMETRY_STRING = "false";

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 814cc7b. Configure here.

public static final @NotNull String DEFAULT_SKIP_SOURCE_BUNDLE_STRING = "false";
public static final boolean DEFAULT_IGNORE_SOURCE_BUNDLE_UPLOAD_FAILURE = false;
public static final @NotNull String DEFAULT_IGNORE_SOURCE_BUNDLE_UPLOAD_FAILURE_STRING = "false";
public static final boolean DEFAULT_REPRODUCIBLE_BUNDLE_ID = false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused boolean constant added to PluginConfig

Low Severity

DEFAULT_REPRODUCIBLE_BUNDLE_ID (the boolean constant) is defined but never referenced anywhere in the codebase. Only DEFAULT_REPRODUCIBLE_BUNDLE_ID_STRING is used (in UploadSourceBundleMojo's @Parameter annotation). Unlike every other DEFAULT_* boolean constant in PluginConfig, there's no corresponding instance variable or usage for reproducibleBundleId in this class, making it dead code.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 814cc7b. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Re-running sentry plugin breaks binary-reproducible builds

2 participants