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.