JReleaser for Distribution in Java

Comprehensive JReleaser Implementation Guide

1. JReleaser Configuration

jreleaser.yml - Main Configuration

# jreleaser.yml
project:
name: my-java-app
description: A fantastic Java application
longDescription: |
A comprehensive Java application with amazing features
and excellent performance.
website: https://my-java-app.example.com
license: Apache-2.0
authors:
- John Doe <[email protected]>
- Jane Smith <[email protected]>
tags:
- java
- spring-boot
- kubernetes
java:
version: 11
groupId: com.example
artifactId: my-java-app
mainClass: com.example.myapp.Application
release:
github:
owner: my-org
name: my-java-app
overwrite: false
skipTag: false
skipRelease: false
changelog:
formatted: ALWAYS
format: "- {{commitShortHash}} {{commitTitle}}"
contributors:
enabled: true
format: "- {{contributorName}}"
issue:
enabled: true
label:
name: release-{{projectVersion}}
milestones:
enabled: true
distributions:
app:
type: JAVA_BINARY
executable:
name: my-app
artifacts:
- path: "target/{{projectName}}-{{projectVersion}}.jar"
platform: java
java:
version: 11
mainClass: com.example.myapp.Application
mainModule:
jlink:
enabled: false
files:
- src: README.md
dest: ""
- src: LICENSE
dest: ""
- src: docs/
dest: "docs/"
native:
type: SINGLE_JAR
executable:
name: my-app-native
artifacts:
- path: "target/{{projectName}}-{{projectVersion}}-native.jar"
active: ALWAYS
docker:
type: DOCKER
active: ALWAYS
artifacts:
- image: my-org/my-java-app:{{projectVersion}}
tags:
- "latest"
- "{{projectVersion}}"
spec:
workingDir: /app
user: 1000
ports:
- 8080
deploy:
maven:
mavenCentral:
active: ALWAYS
url: https://oss.sonatype.org/service/local
stagingRepository: https://oss.sonatype.org/content/repositories/snapshots
applyMavenCentralRules: true
sonatype:
username: "{{env.SONATYPE_USERNAME}}"
password: "{{env.SONATYPE_PASSWORD}}"
gpg:
enabled: true
publicKey: "{{env.GPG_PUBLIC_KEY}}"
secretKey: "{{env.GPG_SECRET_KEY}}"
passphrase: "{{env.GPG_PASSPHRASE}}"
assemble:
jlink:
enabled: false
jpackage:
enabled: true
name: MyJavaApp
version: "{{projectVersion}}"
vendor: My Organization
copyright: "Copyright 2024 My Organization"
licenseFile: LICENSE
icon: src/main/resources/icon.ico
java:
version: 17
mainJar: "{{projectName}}-{{projectVersion}}.jar"
mainClass: com.example.myapp.Application
types:
- msi
- dmg
- deb
jdk:
url: "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.5%2B8/OpenJDK17U-jdk_x64_windows_hotspot_17.0.5_8.zip"
upload:
s3:
enabled: true
bucket: my-java-app-releases
region: us-east-1
path: "{{projectVersion}}/"
accessKeyId: "{{env.AWS_ACCESS_KEY_ID}}"
secretKey: "{{env.AWS_SECRET_ACCESS_KEY}}"
artifacts: true
files: true
checksums: true
signature: true
announce:
discord:
enabled: true
webhook: "{{env.DISCORD_WEBHOOK}}"
message: "🎉 **{{projectName}} {{projectVersion}}** has been released!\\n\\n{{releaseNotes}}"
connectTimeout: 20
readTimeout: 60
twitter:
enabled: true
consumerKey: "{{env.TWITTER_CONSUMER_KEY}}"
consumerSecret: "{{env.TWITTER_CONSUMER_SECRET}}"
accessToken: "{{env.TWITTER_ACCESS_TOKEN}}"
accessTokenSecret: "{{env.TWITTER_ACCESS_TOKEN_SECRET}}"
status: "🎉 {{projectName}} {{projectVersion}} has been released! {{projectDescription}} {{projectWebsite}}"
linkedin:
enabled: true
accessToken: "{{env.LINKEDIN_ACCESS_TOKEN}}"
message: "🎉 {{projectName}} {{projectVersion}} has been released! {{projectDescription}}"
slack:
enabled: true
webhook: "{{env.SLACK_WEBHOOK}}"
message: "🎉 *{{projectName}} {{projectVersion}}* has been released!\\n> {{projectDescription}}"
channel: "#releases"
signing:
active: ALWAYS
armored: true
verify: true
artifacts: true
files: true
checksums: true
command: "gpg"
args: "--batch --yes --armor --detach-sign --local-user {{env.GPG_KEY_ID}}"
checksum:
active: ALWAYS
algorithms:
- SHA-256
- SHA-512
artifacts: true
files: true
files:
- input: "target/{{projectName}}-{{projectVersion}}.jar"
output: "{{projectName}}-{{projectVersion}}.jar"
- input: "target/{{projectName}}-{{projectVersion}}-sources.jar"
output: "{{projectName}}-{{projectVersion}}-sources.jar"
- input: "target/{{projectName}}-{{projectVersion}}-javadoc.jar"
output: "{{projectName}}-{{projectVersion}}-javadoc.jar"
environment:
JAVA_HOME: "{{env.JAVA_HOME}}"
MAVEN_HOME: "{{env.MAVEN_HOME}}"
hooks:
preRelease:
- cmd: "mvn clean verify -Prelease"
shell: bash
- cmd: "docker build -t my-org/my-java-app:{{projectVersion}} ."
shell: bash
postRelease:
- cmd: "echo 'Release {{projectVersion}} completed successfully!'"
shell: bash

2. Maven Configuration with JReleaser

pom.xml with JReleaser Profile

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-java-app</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>My Java Application</name>
<description>A fantastic Java application</description>
<url>https://my-java-app.example.com</url>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<id>johndoe</id>
<name>John Doe</name>
<email>[email protected]</email>
<roles>
<role>Project Lead</role>
</roles>
</developer>
<developer>
<id>janesmith</id>
<name>Jane Smith</name>
<email>[email protected]</email>
<roles>
<role>Developer</role>
</roles>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/my-org/my-java-app.git</connection>
<developerConnection>scm:git:ssh://github.com:my-org/my-java-app.git</developerConnection>
<url>https://github.com/my-org/my-java-app</url>
<tag>HEAD</tag>
</scm>
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>ossrh</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
<properties>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- JReleaser -->
<jreleaser.version>1.6.0</jreleaser.version>
<!-- Maven Plugins -->
<maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
<maven-surefire-plugin.version>3.0.0</maven-surefire-plugin.version>
<maven-javadoc-plugin.version>3.5.0</maven-javadoc-plugin.version>
<maven-source-plugin.version>3.2.1</maven-source-plugin.version>
<maven-gpg-plugin.version>3.0.1</maven-gpg-plugin.version>
<nexus-staging-maven-plugin.version>1.6.13</nexus-staging-maven-plugin.version>
<!-- Spring Boot -->
<spring-boot.version>2.7.0</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Spring Boot Maven Plugin -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<executable>true</executable>
<mainClass>com.example.myapp.Application</mainClass>
<layout>JAR</layout>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<!-- Surefire Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<includes>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
<!-- Source Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>${maven-source-plugin.version}</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Javadoc Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>${maven-javadoc-plugin.version}</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<doclint>none</doclint>
<source>${java.version}</source>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<!-- Release Profile -->
<profile>
<id>release</id>
<build>
<plugins>
<!-- GPG Signing -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>${maven-gpg-plugin.version}</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
<configuration>
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
</execution>
</executions>
</plugin>
<!-- Nexus Staging -->
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>${nexus-staging-maven-plugin.version}</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<!-- JReleaser -->
<plugin>
<groupId>org.jreleaser</groupId>
<artifactId>jreleaser-maven-plugin</artifactId>
<version>${jreleaser.version}</version>
<configuration>
<jreleaser>
<configFile>${project.basedir}/jreleaser.yml</configFile>
</jreleaser>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<!-- Native Image Profile -->
<profile>
<id>native</id>
<properties>
<native.maven.plugin.version>0.9.20</native.maven.plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native.maven.plugin.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native.maven.plugin.version}</version>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<imageName>${project.artifactId}-native</imageName>
<mainClass>${start-class}</mainClass>
<buildArgs>
<buildArg>--no-fallback</buildArg>
<buildArg>--enable-http</buildArg>
<buildArg>--enable-https</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

3. GitHub Actions Workflow

.github/workflows/release.yml

name: Release with JReleaser
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Release version'
required: true
default: '1.0.0'
env:
JAVA_VERSION: '11'
MAVEN_VERSION: '3.8.6'
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: 'maven'
- name: Set up Maven
uses: stCarolas/setup-maven@v4
with:
maven-version: ${{ env.MAVEN_VERSION }}
- name: Build project
run: mvn -B clean package -DskipTests
- name: Run tests
run: mvn -B test
- name: Verify release
run: mvn -B verify -Prelease
- name: Setup JReleaser
uses: jreleaser/release-action@v2
with:
version: '1.6.0'
- name: Full release
run: jreleaser full-release
env:
JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JRELEASER_MAVEN_CENTRAL_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
JRELEASER_MAVEN_CENTRAL_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.GPG_PUBLIC_KEY }}
JRELEASER_GPG_SECRET_KEY: ${{ secrets.GPG_SECRET_KEY }}
JRELEASER_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
JRELEASER_GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }}
JRELEASER_DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
JRELEASER_SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
JRELEASER_TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
JRELEASER_TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
JRELEASER_TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
JRELEASER_TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
JRELEASER_LINKEDIN_ACCESS_TOKEN: ${{ secrets.LINKEDIN_ACCESS_TOKEN }}
JRELEASER_AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
JRELEASER_AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Upload release artifacts
uses: actions/upload-artifact@v4
with:
name: release-artifacts
path: |
target/jreleaser/output/**/*
retention-days: 30
native-build:
name: Native Build
runs-on: ubuntu-latest
needs: release
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up GraalVM
uses: graalvm/setup-graalvm@v1
with:
version: '22.3.2'
java-version: '17'
components: 'native-image'
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build native image
run: mvn -B clean package -Pnative -DskipTests
- name: Upload native artifacts
uses: actions/upload-artifact@v4
with:
name: native-artifacts
path: target/*-native
retention-days: 30

4. Docker Configuration

Dockerfile

# Multi-stage build for Java application
FROM eclipse-temurin:11-jre AS runtime
# Install necessary packages
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# Create application user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Create application directory
WORKDIR /app
# Copy JAR file
COPY target/my-java-app-*.jar app.jar
# Create necessary directories and set permissions
RUN mkdir -p /app/logs && \
chown -R appuser:appuser /app
# Switch to non-root user
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# JVM options
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -XX:+ExitOnOutOfMemoryError"
# Expose port
EXPOSE 8080
# Entry point
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

docker-compose.yml

version: '3.8'
services:
my-java-app:
build:
context: .
dockerfile: Dockerfile
image: my-org/my-java-app:latest
ports:
- "8080:8080"
environment:
- JAVA_OPTS=-Xmx512m -Xms256m
- SPRING_PROFILES_ACTIVE=docker
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
volumes:
- ./logs:/app/logs
networks:
- app-network
postgres:
image: postgres:13
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- app-network
volumes:
postgres_data:
networks:
app-network:
driver: bridge

5. Application Release Information

src/main/resources/META-INF/build-info.properties

# build-info.properties
build.group=com.example
build.artifact=my-java-app
build.version=1.0.0
build.name=My Java Application
build.time=2024-01-15T10:30:00Z
build.java.version=11
build.os.name=Linux
build.os.version=5.15.0

Application Info Contributor

// BuildInfoContributor.java
package com.example.myapp.info;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Properties;
@Component
public class BuildInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
Properties buildInfo = loadBuildInfo();
if (buildInfo != null) {
builder.withDetail("build", buildInfo);
}
// Add additional release information
builder.withDetail("release", java.util.Map.of(
"distributedBy", "JReleaser",
"releaseDate", java.time.Instant.now().toString(),
"supportedPlatforms", java.util.List.of("linux/amd64", "windows/amd64", "darwin/amd64")
));
}
private Properties loadBuildInfo() {
try {
ClassPathResource resource = new ClassPathResource("META-INF/build-info.properties");
if (resource.exists()) {
Properties properties = new Properties();
properties.load(resource.getInputStream());
return properties;
}
} catch (IOException e) {
// Ignore if build info is not available
}
return null;
}
}

6. Release Notes Template

.jreleaser/templates/CHANGELOG.md.tpl

# Changelog
All notable changes to {{projectName}} will be documented in this file.
## [{{projectVersion}}] - {{now}}
### 🚀 Features
{{#each commitFeatures}}
- {{this}}
{{/each}}
### 🐛 Bug Fixes
{{#each commitBugFixes}}
- {{this}}
{{/each}}
### 📚 Documentation
{{#each commitDocumentation}}
- {{this}}
{{/each}}
### 🔧 Maintenance
{{#each commitMaintenance}}
- {{this}}
{{/each}}
### 👥 Contributors
{{#each contributors}}
- {{this.name}} ({{this.email}})
{{/each}}
**Full Changelog**: {{projectLink}}/compare/{{previousTag}}...{{projectVersion}}

7. Custom JReleaser Distribution Service

// JReleaserDistributionService.java
package com.example.myapp.distribution;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.*;
import java.nio.file.*;
import java.util.*;
@Service
public class JReleaserDistributionService {
private static final Logger logger = LoggerFactory.getLogger(JReleaserDistributionService.class);
@Value("${app.version:1.0.0}")
private String appVersion;
@Value("${app.distribution.dir:target/distribution}")
private String distributionDir;
public void prepareDistribution() throws IOException {
logger.info("Preparing distribution for version: {}", appVersion);
Path distPath = Paths.get(distributionDir);
Files.createDirectories(distPath);
// Create distribution structure
createDirectoryStructure(distPath);
copyArtifacts(distPath);
generateInstallScripts(distPath);
generateDocumentation(distPath);
logger.info("Distribution prepared at: {}", distPath.toAbsolutePath());
}
private void createDirectoryStructure(Path basePath) throws IOException {
Paths.get(basePath.toString(), "bin").toFile().mkdirs();
Paths.get(basePath.toString(), "lib").toFile().mkdirs();
Paths.get(basePath.toString(), "config").toFile().mkdirs();
Paths.get(basePath.toString(), "docs").toFile().mkdirs();
}
private void copyArtifacts(Path basePath) throws IOException {
// Copy JAR file
Path sourceJar = Paths.get("target/my-java-app-" + appVersion + ".jar");
Path targetJar = Paths.get(basePath.toString(), "lib", "my-java-app.jar");
if (Files.exists(sourceJar)) {
Files.copy(sourceJar, targetJar, StandardCopyOption.REPLACE_EXISTING);
logger.info("Copied JAR to: {}", targetJar);
}
// Copy configuration files
copyConfigurationFiles(basePath);
}
private void copyConfigurationFiles(Path basePath) throws IOException {
Path sourceConfig = Paths.get("src/main/resources/application.yml");
Path targetConfig = Paths.get(basePath.toString(), "config", "application.yml");
if (Files.exists(sourceConfig)) {
Files.copy(sourceConfig, targetConfig, StandardCopyOption.REPLACE_EXISTING);
}
// Create default configuration
createDefaultConfiguration(basePath);
}
private void createDefaultConfiguration(Path basePath) throws IOException {
Path defaultConfig = Paths.get(basePath.toString(), "config", "application-default.yml");
String defaultConfigContent = 
"app:\n" +
"  name: my-java-app\n" +
"  version: " + appVersion + "\n" +
"  description: My Java Application\n" +
"\n" +
"server:\n" +
"  port: 8080\n" +
"\n" +
"spring:\n" +
"  profiles:\n" +
"    active: default\n" +
"\n" +
"logging:\n" +
"  level:\n" +
"    com.example: INFO\n" +
"  file:\n" +
"    name: logs/application.log";
Files.writeString(defaultConfig, defaultConfigContent);
}
private void generateInstallScripts(Path basePath) throws IOException {
generateUnixInstallScript(basePath);
generateWindowsInstallScript(basePath);
}
private void generateUnixInstallScript(Path basePath) throws IOException {
Path unixScript = Paths.get(basePath.toString(), "bin", "install.sh");
String scriptContent = 
"#!/bin/bash\n" +
"\n" +
"echo \"Installing My Java Application v" + appVersion + "\"\n" +
"echo \"================================\"\n" +
"\n" +
"# Create installation directory\n" +
"INSTALL_DIR=\"/opt/my-java-app\"\n" +
"sudo mkdir -p \"$INSTALL_DIR\"\n" +
"\n" +
"# Copy files\n" +
"sudo cp -r lib/ \"$INSTALL_DIR/\"\n" +
"sudo cp -r config/ \"$INSTALL_DIR/\"\n" +
"sudo cp -r docs/ \"$INSTALL_DIR/\"\n" +
"\n" +
"# Create service user\n" +
"sudo useradd -r -s /bin/false myjavaapp 2>/dev/null || true\n" +
"\n" +
"# Set permissions\n" +
"sudo chown -R myjavaapp:myjavaapp \"$INSTALL_DIR\"\n" +
"sudo chmod +x \"$INSTALL_DIR/bin/start.sh\"\n" +
"\n" +
"# Create systemd service\n" +
"cat << EOF | sudo tee /etc/systemd/system/my-java-app.service\n" +
"[Unit]\n" +
"Description=My Java Application\n" +
"After=network.target\n" +
"\n" +
"[Service]\n" +
"Type=simple\n" +
"User=myjavaapp\n" +
"WorkingDirectory=$INSTALL_DIR\n" +
"ExecStart=$INSTALL_DIR/bin/start.sh\n" +
"Restart=on-failure\n" +
"RestartSec=10\n" +
"\n" +
"[Install]\n" +
"WantedBy=multi-user.target\n" +
"EOF\n" +
"\n" +
"echo \"Installation completed!\"\n" +
"echo \"Start service with: sudo systemctl start my-java-app\"\n" +
"echo \"Enable service with: sudo systemctl enable my-java-app\"\n";
Files.writeString(unixScript, scriptContent);
unixScript.toFile().setExecutable(true);
}
private void generateWindowsInstallScript(Path basePath) throws IOException {
Path windowsScript = Paths.get(basePath.toString(), "bin", "install.bat");
String scriptContent = 
"@echo off\n" +
"echo Installing My Java Application v" + appVersion + "\n" +
"echo ================================\n" +
"\n" +
"set INSTALL_DIR=%ProgramFiles%\\MyJavaApp\n" +
"\n" +
":: Create installation directory\n" +
"mkdir \"%INSTALL_DIR%\" 2>nul\n" +
"\n" +
":: Copy files\n" +
"xcopy lib \"%INSTALL_DIR%\\lib\" /E /I /Y\n" +
"xcopy config \"%INSTALL_DIR%\\config\" /E /I /Y\n" +
"xcopy docs \"%INSTALL_DIR%\\docs\" /E /I /Y\n" +
"\n" +
"echo Installation completed!\n" +
"echo Start the application with: \"%INSTALL_DIR%\\bin\\start.bat\"\n";
Files.writeString(windowsScript, scriptContent);
}
private void generateDocumentation(Path basePath) throws IOException {
Path readmePath = Paths.get(basePath.toString(), "docs", "README.md");
String readmeContent = 
"# My Java Application\n" +
"\n" +
"Version: " + appVersion + "\n" +
"\n" +
"## Overview\n" +
"A fantastic Java application with amazing features.\n" +
"\n" +
"## Installation\n" +
"\n" +
"### Linux/macOS\n" +
"```bash\n" +
"chmod +x bin/install.sh\n" +
"sudo ./bin/install.sh\n" +
"```\n" +
"\n" +
"### Windows\n" +
"```cmd\n" +
"bin\\install.bat\n" +
"```\n" +
"\n" +
"## Usage\n" +
"\n" +
"### Starting the Application\n" +
"```bash\n" +
"bin/start.sh\n" +
"```\n" +
"\n" +
"### Configuration\n" +
"Edit `config/application.yml` to customize settings.\n" +
"\n" +
"## Support\n" +
"\n" +
"- Documentation: https://my-java-app.example.com/docs\n" +
"- Issues: https://github.com/my-org/my-java-app/issues\n" +
"- Releases: https://github.com/my-org/my-java-app/releases\n";
Files.writeString(readmePath, readmeContent);
}
public Map<String, Object> getDistributionInfo() {
return Map.of(
"version", appVersion,
"distributionDir", distributionDir,
"artifacts", List.of(
"my-java-app-" + appVersion + ".jar",
"my-java-app-" + appVersion + "-sources.jar",
"my-java-app-" + appVersion + "-javadoc.jar"
),
"platforms", List.of("linux", "windows", "macos"),
"formats", List.of("jar", "docker", "native")
);
}
}

8. Release Validation Service

// ReleaseValidationService.java
package com.example.myapp.distribution;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.io.*;
import java.nio.file.*;
import java.security.MessageDigest;
import java.util.*;
@Service
public class ReleaseValidationService {
private static final Logger logger = LoggerFactory.getLogger(ReleaseValidationService.class);
public boolean validateReleaseArtifacts(String version) throws Exception {
logger.info("Validating release artifacts for version: {}", version);
boolean isValid = true;
// Check required files
isValid &= validateRequiredFiles(version);
// Verify checksums
isValid &= validateChecksums(version);
// Validate signatures
isValid &= validateSignatures(version);
// Test artifact integrity
isValid &= testArtifactIntegrity(version);
logger.info("Release validation completed: {}", isValid ? "PASSED" : "FAILED");
return isValid;
}
private boolean validateRequiredFiles(String version) {
List<String> requiredFiles = Arrays.asList(
"target/my-java-app-" + version + ".jar",
"target/my-java-app-" + version + "-sources.jar",
"target/my-java-app-" + version + "-javadoc.jar",
"target/my-java-app-" + version + ".jar.asc",
"target/my-java-app-" + version + "-sources.jar.asc",
"target/my-java-app-" + version + "-javadoc.jar.asc"
);
for (String filePath : requiredFiles) {
File file = new File(filePath);
if (!file.exists()) {
logger.error("Required file missing: {}", filePath);
return false;
}
}
logger.info("All required files present");
return true;
}
private boolean validateChecksums(String version) throws Exception {
List<String> artifacts = Arrays.asList(
"my-java-app-" + version + ".jar",
"my-java-app-" + version + "-sources.jar",
"my-java-app-" + version + "-javadoc.jar"
);
for (String artifact : artifacts) {
String filePath = "target/" + artifact;
String sha256Path = "target/" + artifact + ".sha256";
String sha512Path = "target/" + artifact + ".sha512";
if (!validateChecksum(filePath, sha256Path, "SHA-256")) {
logger.error("SHA-256 checksum validation failed for: {}", artifact);
return false;
}
if (!validateChecksum(filePath, sha512Path, "SHA-512")) {
logger.error("SHA-512 checksum validation failed for: {}", artifact);
return false;
}
}
logger.info("All checksums validated successfully");
return true;
}
private boolean validateChecksum(String filePath, String checksumPath, String algorithm) throws Exception {
File file = new File(filePath);
File checksumFile = new File(checksumPath);
if (!file.exists() || !checksumFile.exists()) {
return false;
}
String expectedChecksum = Files.readString(Paths.get(checksumPath)).split(" ")[0].trim();
String actualChecksum = calculateChecksum(filePath, algorithm);
return expectedChecksum.equalsIgnoreCase(actualChecksum);
}
private String calculateChecksum(String filePath, String algorithm) throws Exception {
MessageDigest digest = MessageDigest.getInstance(algorithm);
byte[] buffer = new byte[8192];
try (InputStream is = Files.newInputStream(Paths.get(filePath))) {
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
}
byte[] hash = digest.digest();
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
hexString.append(String.format("%02x", b));
}
return hexString.toString();
}
private boolean validateSignatures(String version) {
// This would typically use GPG verification
// For now, we'll just check that signature files exist
List<String> signatureFiles = Arrays.asList(
"target/my-java-app-" + version + ".jar.asc",
"target/my-java-app-" + version + "-sources.jar.asc",
"target/my-java-app-" + version + "-javadoc.jar.asc"
);
for (String sigFile : signatureFiles) {
if (!new File(sigFile).exists()) {
logger.error("Signature file missing: {}", sigFile);
return false;
}
}
logger.info("All signature files present");
return true;
}
private boolean testArtifactIntegrity(String version) {
try {
// Test that the JAR file can be read
String jarPath = "target/my-java-app-" + version + ".jar";
java.util.jar.JarFile jarFile = new java.util.jar.JarFile(jarPath);
jarFile.close();
logger.info("JAR file integrity check passed");
return true;
} catch (IOException e) {
logger.error("JAR file integrity check failed: {}", e.getMessage());
return false;
}
}
public Map<String, Object> getValidationReport(String version) throws Exception {
Map<String, Object> report = new HashMap<>();
report.put("version", version);
report.put("timestamp", new Date());
report.put("requiredFiles", validateRequiredFiles(version));
report.put("checksums", validateChecksums(version));
report.put("signatures", validateSignatures(version));
report.put("integrity", testArtifactIntegrity(version));
boolean overallStatus = (boolean) report.get("requiredFiles") &&
(boolean) report.get("checksums") &&
(boolean) report.get("signatures") &&
(boolean) report.get("integrity");
report.put("overallStatus", overallStatus ? "PASSED" : "FAILED");
return report;
}
}

This comprehensive JReleaser setup provides:

  • Multi-platform distribution (JAR, Docker, Native)
  • Automated release process with GitHub Actions
  • Maven Central deployment with signing
  • Multiple announcement channels (Discord, Twitter, Slack, LinkedIn)
  • Comprehensive validation and integrity checks
  • Custom distribution packaging with install scripts
  • Release notes generation with templates
  • Docker image publishing
  • Native image builds with GraalVM
  • Artifact signing and checksum verification

The setup ensures professional, automated distribution of your Java applications across multiple platforms and channels.

Pyroscope Profiling in Java
Explains how to use Pyroscope for continuous profiling in Java applications, helping developers analyze CPU and memory usage patterns to improve performance and identify bottlenecks.
https://macronepal.com/blog/pyroscope-profiling-in-java/

OpenTelemetry Metrics in Java: Comprehensive Guide
Provides a complete guide to collecting and exporting metrics in Java using OpenTelemetry, including counters, histograms, gauges, and integration with monitoring tools. (MACRO NEPAL)
https://macronepal.com/blog/opentelemetry-metrics-in-java-comprehensive-guide/

OTLP Exporter in Java: Complete Guide for OpenTelemetry
Explains how to configure OTLP exporters in Java to send telemetry data such as traces, metrics, and logs to monitoring systems using HTTP or gRPC protocols. (MACRO NEPAL)
https://macronepal.com/blog/otlp-exporter-in-java-complete-guide-for-opentelemetry/

Thanos Integration in Java: Global View of Metrics
Explains how to integrate Thanos with Java monitoring systems to create a scalable global metrics view across multiple Prometheus instances.

https://macronepal.com/blog/thanos-integration-in-java-global-view-of-metrics

Time Series with InfluxDB in Java: Complete Guide (Version 2)
Explains how to manage time-series data using InfluxDB in Java applications, including storing, querying, and analyzing metrics data.

https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide-2

Time Series with InfluxDB in Java: Complete Guide
Provides an overview of integrating InfluxDB with Java for time-series data handling, including monitoring applications and managing performance metrics.

https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide

Implementing Prometheus Remote Write in Java (Version 2)
Explains how to configure Java applications to send metrics data to Prometheus-compatible systems using the remote write feature for scalable monitoring.

https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide-2

Implementing Prometheus Remote Write in Java: Complete Guide
Provides instructions for sending metrics from Java services to Prometheus servers, enabling centralized monitoring and real-time analytics.

https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide

Building a TileServer GL in Java: Vector and Raster Tile Server
Explains how to build a TileServer GL in Java for serving vector and raster map tiles, useful for geographic visualization and mapping applications.

https://macronepal.com/blog/building-a-tileserver-gl-in-java-vector-and-raster-tile-server

Indoor Mapping in Java
Explains how to create indoor mapping systems in Java, including navigation inside buildings, spatial data handling, and visualization techniques.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper