Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
### Features

- add option to ignore source bundle upload failures ([#209](https://github.com/getsentry/sentry-maven-plugin/pull/209))
- Deterministic Bundle Id generation ([#220](https://github.com/getsentry/sentry-maven-plugin/pull/220))
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.

m We should mention the name of the option to enable it

- to enable set reproducibleBundleId to true and add an outputTimestamp to properties
- reproducible JAR builds require maven-jar-plugin >= 3.2.0

### Dependencies

Expand Down
16 changes: 16 additions & 0 deletions examples/sentry-maven-plugin-example/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<!-- <sentry.cli.debug>true</sentry.cli.debug>-->
<project.build.outputTimestamp>2026-01-01T00:00:00Z</project.build.outputTimestamp>
</properties>
<build>
<!-- add this to your pom.xml vvv -->
Expand Down Expand Up @@ -46,6 +47,8 @@
<skip>false</skip>
<skipSourceBundle>false</skipSourceBundle>
<skipAutoInstall>false</skipAutoInstall>
<!-- reproducible Builds require maven-jar-plugin >= 3.2.0 -->
<reproducibleBundleId>false</reproducibleBundleId>
<skipTelemetry>false</skipTelemetry>
<skipValidateSdkDependencyVersions>false</skipValidateSdkDependencyVersions>
<additionalSourceDirsForSourceContext>
Expand All @@ -66,6 +69,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
Expand All @@ -87,6 +91,18 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
Expand Down
118 changes: 102 additions & 16 deletions src/main/java/io/sentry/UploadSourceBundleMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
import io.sentry.cli.SentryCliRunner;
import io.sentry.telemetry.SentryTelemetryService;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Resource;
Expand Down Expand Up @@ -73,6 +77,9 @@ public class UploadSourceBundleMojo extends AbstractMojo {
@Parameter(defaultValue = DEFAULT_IGNORE_SOURCE_BUNDLE_UPLOAD_FAILURE_STRING)
private boolean ignoreSourceBundleUploadFailure;

@Parameter(defaultValue = DEFAULT_REPRODUCIBLE_BUNDLE_ID_STRING)
private boolean reproducibleBundleId;

@SuppressWarnings("NullAway")
@Component
private @NotNull BuildPluginManager pluginManager;
Expand All @@ -85,20 +92,27 @@ public void execute() throws MojoExecutionException {
return;
}

final @NotNull String bundleId = UUID.randomUUID().toString();
final @NotNull File collectedSourcesTargetDir = new File(sentryBuildDir(), "collected-sources");
final @NotNull File sourceBundleTargetDir = new File(sentryBuildDir(), "source-bundle");
final @NotNull SentryCliRunner cliRunner =
new SentryCliRunner(
debugSentryCli, sentryCliExecutablePath, mavenProject, mavenSession, pluginManager);

collectSources(collectedSourcesTargetDir);

final @NotNull String bundleId;
if (reproducibleBundleId) {
bundleId = generateDeterministicBundleId(collectedSourcesTargetDir);
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.

m can we make the return type nullable and just use UUID generation as a fallback here in case ID is null?

There's quite a few return + UUID generations below.

} else {
bundleId = UUID.randomUUID().toString();
}

createDebugMetaPropertiesFile(bundleId);
collectSources(bundleId, collectedSourcesTargetDir);
bundleSources(cliRunner, bundleId, collectedSourcesTargetDir, sourceBundleTargetDir);
uploadSourceBundle(cliRunner, sourceBundleTargetDir);
}

private void collectSources(@NotNull String bundleId, @NotNull File outputDir) {
private void collectSources(@NotNull File outputDir) {
final @Nullable ISpan span = SentryTelemetryService.getInstance().startTask("collectSources");
logger.debug("Collecting files from source directories");

Expand Down Expand Up @@ -168,6 +182,81 @@ private void collectSources(@NotNull String bundleId, @NotNull File outputDir) {
return new File(outputDirectory, "sentry");
}

/**
* Generates a deterministic bundle ID based on the MD5 hash of all collected source files. This
* ensures reproducible builds produce the same bundle ID when the source files are identical.
*
* @param collectedSourcesDir the directory containing the collected source files
* @return a UUID v4 string derived from the hash of the source files
*/
private @NotNull String generateDeterministicBundleId(final @NotNull File collectedSourcesDir) {
final @Nullable ISpan span =
SentryTelemetryService.getInstance().startTask("generateDeterministicBundleId");
try {
final @NotNull MessageDigest digest = MessageDigest.getInstance("MD5");

if (collectedSourcesDir.exists() && collectedSourcesDir.isDirectory()) {
try (final @NotNull Stream<Path> stream = Files.walk(collectedSourcesDir.toPath())) {
final @NotNull List<Path> sortedFiles =
stream
.filter(Files::isRegularFile)
.sorted(
Comparator.comparing(
p ->
collectedSourcesDir
.toPath()
.relativize(p)
.toString()
.replace('\\', '/')))
.collect(Collectors.toList());

for (final @NotNull Path file : sortedFiles) {
final @NotNull String relativePath =
collectedSourcesDir.toPath().relativize(file).toString().replace('\\', '/');
digest.update(relativePath.getBytes(StandardCharsets.UTF_8));

// Include the file content in the hash
final byte[] fileBytes = Files.readAllBytes(file);
digest.update(fileBytes);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing delimiter between path and content in hash

Medium Severity

The hash computation feeds relativePath bytes and fileBytes into the digest without any separator or length prefix between them. This means two different file trees can produce the same hash — for example, a file named "ab" with content "cd" produces the same digest input as a file named "a" with content "bcd". Adding a delimiter (e.g. a null byte) or a length prefix between the path and content would prevent these ambiguous collisions and make the deterministic bundle ID more robust.

Fix in Cursor Fix in Web

}
}
}

final byte[] hashBytes = digest.digest();
return bytesToUuid(hashBytes);
} catch (NoSuchAlgorithmException e) {
logger.warn("MD5 algorithm not available, falling back to random UUID", e);
return UUID.randomUUID().toString();
} catch (IOException e) {
logger.warn(
"Failed to read source files for bundle ID generation, falling back to random UUID", e);
SentryTelemetryService.getInstance().captureError(e, "generateDeterministicBundleId");
return UUID.randomUUID().toString();
} catch (Throwable t) {
logger.warn("Failed to generate deterministic bundle ID, falling back to random UUID", t);
SentryTelemetryService.getInstance().captureError(t, "generateDeterministicBundleId");
return UUID.randomUUID().toString();
} finally {
SentryTelemetryService.getInstance().endTask(span);
}
}

/**
* Converts 16 bytes into a UUID v4 string format (RFC 4122).
*
* @param hashBytes the hash bytes (exactly 16 bytes expected from MD5)
* @return a UUID string in the format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
*/
private @NotNull String bytesToUuid(final byte[] hashBytes) {
// Set version 4 (bits 12-15 of time_hi_and_version to 0100)
hashBytes[6] = (byte) ((hashBytes[6] & 0x0F) | 0x40);
// Set variant to RFC 4122 (bits 6-7 of clock_seq_hi_and_reserved to 10)
hashBytes[8] = (byte) ((hashBytes[8] & 0x3F) | 0x80);

final @NotNull ByteBuffer buffer = ByteBuffer.wrap(hashBytes);
return new UUID(buffer.getLong(), buffer.getLong()).toString();
}

private void bundleSources(
final @NotNull SentryCliRunner cliRunner,
final @NotNull String bundleId,
Expand Down Expand Up @@ -280,11 +369,17 @@ private void createDebugMetaPropertiesFile(final @NotNull String bundleId)
}

final @NotNull File debugMetaFile = new File(sentryBuildDir, "sentry-debug-meta.properties");
final @NotNull Properties properties = createDebugMetaProperties(bundleId);

try (final @NotNull BufferedWriter fileWriter =
Files.newBufferedWriter(debugMetaFile.toPath(), Charset.defaultCharset())) {
properties.store(fileWriter, "Generated by sentry-maven-plugin");
Files.newBufferedWriter(debugMetaFile.toPath(), StandardCharsets.UTF_8)) {
// 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("io.sentry.build-tool=maven");
fileWriter.newLine();
fileWriter.write("io.sentry.bundle-ids=" + bundleId);
fileWriter.newLine();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Platform-dependent newLine breaks cross-OS reproducibility

Medium Severity

BufferedWriter.newLine() writes the platform-specific line separator (\r\n on Windows, \n on Unix). Since the purpose of this change is to produce reproducible builds, the properties file will differ across operating systems, resulting in different JAR contents. Using an explicit "\n" instead of newLine() would ensure byte-identical output regardless of platform.

Fix in Cursor Fix in Web


final @NotNull Resource resource = new Resource();
resource.setDirectory(sentryBuildDir.getPath());
Expand All @@ -300,13 +395,4 @@ private void createDebugMetaPropertiesFile(final @NotNull String bundleId)
SentryTelemetryService.getInstance().endTask(span);
}
}

private @NotNull Properties createDebugMetaProperties(final @NotNull String bundleId) {
final @NotNull Properties properties = new Properties();

properties.setProperty("io.sentry.bundle-ids", bundleId);
properties.setProperty("io.sentry.build-tool", "maven");

return properties;
}
}
2 changes: 2 additions & 0 deletions src/main/java/io/sentry/config/PluginConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public class PluginConfig {
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";
public static final boolean DEFAULT_DEBUG_SENTRY_CLI = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ fun basePom(
skipPlugin: Boolean = false,
skipSourceBundle: Boolean = false,
ignoreSourceBundleUploadFailure: Boolean = false,
reproducibleBundleId: Boolean = false,
sentryCliPath: String? = null,
extraSourceRoots: List<String> = listOf(),
extraSourceContextDirs: List<String> = emptyList(),
Expand Down Expand Up @@ -91,6 +92,7 @@ fun basePom(
<skip>$skipPlugin</skip>
<skipSourceBundle>$skipSourceBundle</skipSourceBundle>
<ignoreSourceBundleUploadFailure>$ignoreSourceBundleUploadFailure</ignoreSourceBundleUploadFailure>
<reproducibleBundleId>$reproducibleBundleId</reproducibleBundleId>
<skipTelemetry>true</skipTelemetry>
<org>sentry-sdks</org>
<project>sentry-maven</project>
Expand Down
Loading
Loading