Skip to content

Instantly share code, notes, and snippets.

@faph
Last active October 19, 2022 18:30
Show Gist options
  • Save faph/20331648cdc0b492eba0f4d83f69eaa5 to your computer and use it in GitHub Desktop.
Save faph/20331648cdc0b492eba0f4d83f69eaa5 to your computer and use it in GitHub Desktop.
Automated Docker releases using Maven and GitHub Actions

Automated Docker releases using Maven and GitHub Actions

Life is too short for tedious, manual release processes.

Here, we will use Maven's Release plugin to execute releases. Instead of triggering the release process manually, we will use a GitHub Actions workflow on the master branch. All development work is done on the dev branch (or feature branches) and a release is done when (and only when) the dev branch is merged with the master branch.

The release process includes compiling a Java app, building a Docker image and publishing the image on Docker Hub.

The POM

Let's start with this mighty pom.xml file in the root of our repository. For simplicity and consistency, we assume:

  1. The project's groupId is the same as the GitHub user name.
  2. The GitHub user name is the same as the Docker Hub user name.
  3. The project's artifactId is the same as the GitHub repository name.
  4. The project's artifactId is also the same as the Docker Hub repository name.
  5. The project uses semantic versioning, for example 1.0.
  6. During development the project's version will be the upcoming version number, appended with -SNAPSHOT, for example 1.1-SNAPSHOT.
  7. Releases in the repository will be tagged with the version number prefixed with v, for example v1.0.
  8. Docker image tags will be the same as the project's version, for example 1.0.
  9. The most recently built Docker image will also be available using the latest tag.

To build the POM, we will need to configure the following plugins:

maven-assembly-plugin

This plugin is not strictly necessary, but for now we build the app using a single assembled jar. To make the app jar executable, we add the manifest tag as shown below.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>2.3</version>
    <configuration>
        <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
        <archive>
            <manifest>
                <mainClass>my.main.Class</mainClass>
                <addClasspath>true</addClasspath>
            </manifest>
        </archive>
    </configuration>
    <executions>
        <execution>
            <id>make-assembly</id>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>

dockerfile-maven-plugin

We will use Spotify's plugin to build and publish the Docker image. We add two executions, one for building a Docker image to using the project's version as a tag, and one using the latest tag [bang-fist-on-table, surely there must be a better way of doing this!?].

To upload the image to Docker Hub, we create a personal access token on Docker Hub. We will set this later as the system environment variable DOCKER_HUB_TOKEN.

<plugin>
    <groupId>com.spotify</groupId>
    <artifactId>dockerfile-maven-plugin</artifactId>
    <version>1.4.12</version>
    <executions>
        <execution>
            <id>default</id>
            <goals>
                <goal>build</goal>
                <goal>push</goal>
            </goals>
        </execution>
        <execution>
            <id>tag-latest</id>
            <goals>
                <goal>build</goal>
                <goal>push</goal>
            </goals>
            <configuration>
                <tag>latest</tag>
            </configuration>
        </execution>
    </executions>
    <configuration>
        <username>${project.groupId}</username>
        <password>${env.DOCKER_HUB_TOKEN}</password>
        <repository>registry.hub.docker.com/${project.groupId}/${project.artifactId}</repository>
        <tag>${project.version}</tag>
    </configuration>
</plugin>

maven-deploy-plugin

We need to disable the default deploy plugin as we're use the Spotify Docker plugin.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-deploy-plugin</artifactId>
    <version>2.8.2</version>
    <configuration>
        <skip>true</skip>
    </configuration>
</plugin>

maven-release-plugin

Apache's Maven release plugin does all the heavy lifting of the release process.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-release-plugin</artifactId>
    <version>2.5.3</version>
    <configuration>
        <tagNameFormat>v@{project.version}</tagNameFormat>
    </configuration>
</plugin>

Rest of the POM

We'll add the above plugins along with any other dependencies to the skeleton POM.xml shown below. We also need to add an scm tag containing the git repository URL. This is where the release plugin will push the version changes to.

Similar to the Docker Hub password, we need to create a personal access token on GitHub. We will pass this in later using a system environment variable.

In the example below, we start the project's version with 1.1-SNAPSHOT. This means that once we run the release plugin, a version 1.1 will be released. Once the release has been deployed, the release plugin will update the version to 1.2-SNAPHOT.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>my-github-username</groupId>
    <artifactId>my-app-name</artifactId>
    <version>1.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <scm>
        <developerConnection>scm:git:https://github.com/${project.groupId}/${project.artifactId}</developerConnection>
        <tag>HEAD</tag>
    </scm>
    <dependencies>
        ...
    </dependencies>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <build>
        <plugins>
           ...
        </plugins>
    </build>
</project>

The GitHub Action workflow

name: Release workflow
on:
  push:
    branches: master
  
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
      with:
        ref: master
    - name: Set up JDK 1.8
      uses: actions/setup-java@v1
      with:
        java-version: 1.8
    - name: Configure git
      run: |
        git config --global committer.email "[email protected]"
        git config --global committer.name "GitHub"
        git config --global author.email "${GITHUB_ACTOR}@users.noreply.github.com"
        git config --global author.name "${GITHUB_ACTOR}"
    - name: Checkout master branch
      run: git checkout master
    - name: Prepare release
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: mvn --batch-mode release:prepare -Dusername=$GITHUB_ACTOR -Dpassword=$GITHUB_TOKEN
    - name: Perform release
      env:
        DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }}
      run: mvn --batch-mode release:perform
@rajuankilla20
Copy link

rajuankilla20 commented Sep 5, 2020

Hi,

I used the same setup and when i do this, its not pushing the image to docker hub.
Do we need to change anything in pom or gitactions.yml

POM.XML


4.0.0

<groupId>com.in28minutes.microservices</groupId>
<artifactId>04-currency-exchange-basic</artifactId>
<version>0.0.1-RELEASE</version> <!-- CHANGE -->
<packaging>jar</packaging>
<name>currency-exchange</name>

<description>Demo project for Spring Boot</description>

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.1.1.RELEASE</version>
	<relativePath /> <!-- lookup parent from repository -->
</parent>

<properties>
	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
	<java.version>1.8</java.version>
	<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
	<spring-cloud.version>Greenwich.SR3</spring-cloud.version>
</properties>

<dependencies>

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-jpa</artifactId>
	</dependency>

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-security</artifactId>
	</dependency>

	<dependency> <!-- CHANGE -->
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-sleuth</artifactId>
	</dependency>

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-actuator</artifactId>
	</dependency>
	
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-devtools</artifactId>
		<scope>runtime</scope>
	</dependency>

	<dependency>
		<groupId>com.h2database</groupId>
		<artifactId>h2</artifactId>
		<scope>runtime</scope>
	</dependency>

	<dependency>
		<groupId>javax.xml.bind</groupId>
		<artifactId>jaxb-api</artifactId>
	</dependency>
	<dependency>
		<groupId>com.sun.xml.bind</groupId>
		<artifactId>jaxb-impl</artifactId>
		<version>2.3.1</version>
	</dependency>
	<dependency>
		<groupId>org.glassfish.jaxb</groupId>
		<artifactId>jaxb-runtime</artifactId>
	</dependency>
	<dependency>
		<groupId>javax.activation</groupId>
		<artifactId>activation</artifactId>
		<version>1.1.1</version>
	</dependency>

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>

</dependencies>

<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-dependencies</artifactId>
			<version>${spring-cloud.version}</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>
</dependencyManagement>

<build>
	<finalName>currency-exchange</finalName>
	<plugins>
		<plugin>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-maven-plugin</artifactId>
		</plugin>
		<!-- Docker -->
		<plugin>
			<groupId>com.spotify</groupId>
			<artifactId>dockerfile-maven-plugin</artifactId>
			<version>1.4.6</version>
			<executions>
				<execution>
					<id>default</id>
					<goals>
						<goal>build</goal>
						<goal>push</goal>
					</goals>
				</execution>
			</executions>
			<configuration>
				<repository>rajuankilla20/${project.name}</repository>
				<tag>${project.version}</tag>
				<skipDockerInfo>true</skipDockerInfo>
			</configuration>
		</plugin>
	</plugins>
</build>

<repositories>
	<repository>
		<id>spring-snapshots</id>
		<name>Spring Snapshots</name>
		<url>https://repo.spring.io/snapshot</url>
		<snapshots>
			<enabled>true</enabled>
		</snapshots>
	</repository>
	<repository>
		<id>spring-milestones</id>
		<name>Spring Milestones</name>
		<url>https://repo.spring.io/milestone</url>
		<snapshots>
			<enabled>false</enabled>
		</snapshots>
	</repository>
</repositories>

<pluginRepositories>
	<pluginRepository>
		<id>spring-snapshots</id>
		<name>Spring Snapshots</name>
		<url>https://repo.spring.io/snapshot</url>
		<snapshots>
			<enabled>true</enabled>
		</snapshots>
	</pluginRepository>
	<pluginRepository>
		<id>spring-milestones</id>
		<name>Spring Milestones</name>
		<url>https://repo.spring.io/milestone</url>
		<snapshots>
			<enabled>false</enabled>
		</snapshots>
	</pluginRepository>
</pluginRepositories>

----gitactions.yml

  • name: Set up JDK 1.8
    uses: actions/setup-java@v1
    with:
    java-version: 1.8
    • name: Configure git
      run: |
      git config --global committer.email "[email protected]"
      git config --global committer.name "xyz"
      git config --global author.email "${GITHUB_ACTOR}@users.noreply.github.com"
      git config --global author.name "${GITHUB_ACTOR}"
    • name: Checkout master branch
      run: git checkout master
    • name: Prepare release
      env:
      GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }}
      run: mvn --batch-mode release:prepare -Dusername=$GITHUB_ACTOR -Dpassword=$GITHUB_TOKEN
    • name: Perform release
      env:
      DOCKER_HUB_TOKEN: ${{ secrets.MY_DOCKER_HUB_TOKEN }}
      run: mvn --batch-mode release:perform

@simbo1905
Copy link

This gist misses off how to set the secrets using a GitHub Environment which seems to be required as at Jan 2021. I have forked the gist at https://gist.github.com/simbo1905/495400fb708fdc91b75ce9b1bc4666cc and there is a working example of a spring-boot application over at https://github.com/simbo1905/bigquery-graphql/

@faph
Copy link
Author

faph commented Jan 18, 2021

Thanks!

@snazzybytes
Copy link

thanks fellas for original @faph as well as updated @simbo1905 versions, came in handy 🚀!!!!
sometimes condensed README is better than giant verbose doc 😃

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