Have you ever thought about starting a new project with a release pipeline? I’m talking about automation that will deliver your application to a real environment. I know, it sounds a bit strange to kick off a project with stuff. It’s like releasing the thing that doesn’t yet exist. It’s like writing a test for a code that isn't even written. But wait, that’s a well-known technique called Test Driven Development! CI/CD So I’m talking about something similar here. Before writing any piece of code, prepare your future service for the real world. Everyone should start building their app from “core”. But please make sure your core is . Your clients/customers/whoever would like to see your service being accessible not only from your local dev machine. deployable I haven’t found any mention of ‘Release Driven Development’ on the internet (bumping it here, I’m first). Someone (read: me) has to prepare a big theoretical article describing what RDD is in detail, and how it’s related to Continuous Deployment and Agile methodologies. But now let’s have some fun. I’m going to show you how to start with RDD (I already love this acronym!) with a very simple app, , and DigitalOcean. SpringBoot Github Actions Show me the code We’ll build a minimalistic release pipeline based on GitHub Actions done right. On the main branch push, it will build and test your Gradle module, tag it with a new version, and deploy it to DigitalOcean. We are building something like this: Key points: Git is the source of truth for release info. Read for more details. Step 4 Store release version as a Git tag. No Gradle release plugin. Only a plugin that fetches the latest tag. Github Actions as a CI/CD platform. Software development should be iterative, this article is no exception. Consider this a guide. Each step is a separate commit in the repo. And of course, feel free to experiment at any step. Or go ahead and check out the project . repo at GitHub Step 1: Skeleton We start with a very simple app generated by . Add dependency (we are going to be serious) before hitting button. Spring Initializr Spring Web Generate Your project should look similar to this: Go ahead to and change to something better than . Not sure that (this is how I named mine) is better, but would love it. settings.gradle rootProject.nam e demo Deployinator Dr. Doofenshmirtz Step 2: Project version Next, we want to make sure our java application knows the version of… itself. We have to pass the Gradle project version (which is currently ) inside our application. 0.0.1-SNAPSHOT To make it happen, we need to create a under and add the next line to it: build.properties src/main/resources info.build.version = ${version} and this piece of code to : build.gradle processResources { filesMatching("build.properties") { expand(project.properties) } } Wait what? Why create a new properties file if there are already empty ? application.properties Let me explain. , to keep things separated. Use - for any business or app-related configuration. Use - for any build/Gradle-related stuff. First application.properties build.properties , Gradle properties expansion with Spring expansion. If you filter with Gradle, you have to use an escaped placeholder for any Spring injections, because the classic one is used by Gradle. It’s not efficient to use non-standard placeholders everywhere for the sake of one property from Gradle. Second clashes application.properties /${} ${} There are other options available to pass the Gradle project version into the app, like adding it to the or using . But I like the one mentioned above more, because it is the most transparent approach. Also, it works without additional magic when you do or run a Main class from an IDE. manifest buildInfo :bootRun Don’t forget to add a new file as a property source to Spring: build.properties import org.springframework.context.annotation.PropertySource; @SpringBootApplication @PropertySource("classpath:build.properties") public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } Commit with changes for the step is here Step 3: Version Controller Let’s introduce . VersionController It will print out the current app’s version: @RestController public class VersionController { private static final Logger logger = LoggerFactory.getLogger(VersionController.class); @Value("${info.build.version}") private String version; @PostConstruct public void printVersion() { // check version in logs logger.info("You deployed me well! My version is {}", version); } @GetMapping("/") public String helloWorld() { return version; } } As far as we are done with our app (yes, that’s it), let’s make sure we have everything working > ./gradlew :bootRun and find the next line in the output logs: Go to the terminal and check out HTTP: > curl localhost:8080/version > 0.0.1-SNAPSHOT Step 4: Version management Now it’s time to talk about version management. There are several approaches (of course) to storing version info: Keep it under a build tool (Gradle, Maven, etc) as a property. Like inside project.version = 0.0.1-SNAPSHOT build.gradle I remember those days when almost every project was using Maven Release Plugin. Those shiny commits and with version bumping. Such a… mess. “Prepare Release ..” “Prepare next dev iteration…” I believe responsibility should be taken away from build tools. They have to know how to test/assembly/build resources and artifacts. They should not be aware of the release, it’s a part that nowadays could be easily delegated to CI/CD platforms. Single responsibility principle. So if the build tool is not releasing, there is no point to store versions there and use them as a source of truth. release Store versions in Git. - is a version control system. At least based on the description it’s a healthy idea to store the application version there. - to have a tag that uniquely identifies a release for example. A tag named or . It’s more flexible, you can reassign a tag to another commit and easily add/remove features from an upcoming release. You can simply track release history by looking into the Git log. In this approach, a build tool is just a follower - takes info from Git and does its job. Git Store 0.0.1-SNAPSHOT v12-my-first-release It's possible to argue for different version management. If you are not concerned yet, just give it a try for a new project. In this tutorial we are keeping things simple and flexible, so Git is the best choice. Folk wisdom: If you have a problem and you want to solve it using Gradle, now you have 2 problems. Let’s make Gradle aware of what’s going on in the repo. A simple solves our problem. It’s even more awesome than I originally thought because it’s not based on , it just reads the folder. Which means less configuration and more predictable results. gradle-git-plugin JGit .git Changes in build.gradle: plugins { id 'java' id 'org.springframework.boot' version '3.0.4' id 'io.spring.dependency-management' version '1.1.0' id 'com.palantir.git-version' version '2.0.0' } group = 'com.example' version = gitVersion() sourceCompatibility = '17' Let’s see what we have for a project version in logs. Good! We see a short hash of a commit (we don’t have tags yet) + . The flag means there are uncommitted files in the repo. This is nice to have because it softly warns: you probably have something missing in your repo. But it’s not blocking the release process which gives flexibility in case of an exceptional situation (anything could happen, even releasing a midnight fix from your laptop). dirty flag After committing all the changes and adding a tag named we see in the logs: v1-manualReleaseHere Alright, now we have a from Git -> Gradle -> Java app version flow Step 5: Docker It’s hard to imagine a modern app without docker, so let’s quickly containerize what we have. The simplest could look like this: Dockerfile FROM eclipse-temurin:17 COPY build/libs/deployinator-*.jar /app/deployinator.jar ENTRYPOINT java -jar /app/deployinator.jar Build and push to make sure everything is ok: > docker build -t <your_name_here>/deployinator . > docker tag <your_name_here>/deployinator <your_name_here>/deployinator:latest > docker push <your_name_here>/deployinator:latest Step 6: Github Actions It’s time to gather everything in one place. We are going to have one pipeline under . While you can find the whole working pipeline , let’s focus on the most important parts. .github/workflows/createRelease.yml here First of all, declare the version of the current release. We need an env var for this to be able to access it later: - name: Create Release Version run: echo "RELEASE_VERSION=v${{github.run_number}}-${GITHUB_SHA::7}" >> $GITHUB_ENV The version looks like , where is a pipeline (job) execution number. Last 7 - short commit hash v12-123abcd 12 SemVer (versions like 1.2.3) is a de-facto standard in the industry. Here I propose to use of the job for several reasons. First, you don’t have to fetch previous tags at all (when you have thousands of releases it might be cumbersome). The second is simplicity. You don’t need logic to increase the latest release number (from to for example) because is monotonically increasing for free (no SMS or registration required). run_number 1.2.3 1.2.4 run_number Having a short sha in version number is a super important thing. It frequently happens when you have to quickly match your release with the corresponding Git commit to understand what exactly is deployed. When you open your cloud console and see that version is deployed - it’s pretty hard to guess whether your midnight fix is in or not. You have to take this version and go to the bug-tracking system or somewhere to get a clue about what stands for. But when you have a hash in a version you can just grab it and check what you need even locally. 1.2.10 1.2.10 Next is building a Jar file. Under the hood, Gradle Git plugin will fetch the current version from the tag and use it for naming the jar. - name: Build uses: gradle/gradle-build-action@v2 with: arguments: clean build Then docker image. Make sure to add the required secrets ( and ) to GitHub to make this step work: DOCKERHUB_USERNAME DOCKERHUB_TOKEN - name: Build & Push Docker Image uses: docker/build-push-action@v4 with: context: . tags: | ${{ secrets.DOCKERHUB_USERNAME }}/deployinator:latest ${{ secrets.DOCKERHUB_USERNAME }}/deployinator:${{env.RELEASE_VERSION}} push: true We tag the image with both - and release versions. But never rely on or deploy the , it’s just for convenience. You should always know exactly what’s deployed and the specific version of the app your clients are working with. latest latest Now, as we know our artifact is tested, built, and fully packed, we can claim the release as a success by pushing the tag to a Git repo. Yes, deployment still can fail. But if it fails because of a broken release - it’s fine from a release standpoint (obviously it’s not from a common sense standpoint). We just have a “bad” release. But it’s still a release, right? And the last step is deployment. We are going to deploy a (my university’s major was tautology) to . In a few words, is a solution that can host docker images with minimal config and infrastructure being defined. Each App can have one or multiple components, which are the building blocks of your service. In our case, one app and one component are more than enough. Deployinator DigitalOcean App Platform App Platform According to for DigitalOcean GitHub action, it works only when the app. So for the very first run you have to an App manually. So please do that and put your DigitalOcean together with the App name into GitHub secrets in advance (see var names below). official documentation updating create token - name: Deploy to DigitalOcean if: ${{ env.DEPLOY_TO_DIGITAL_OCEAN == 'true' }} uses: digitalocean/app_action@main with: app_name: ${{ secrets.DIGITALOCEAN_APP_NAME }} token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} images: '[ { "name": "deployinator-component", "image":{ "registry": "${{ secrets.DOCKERHUB_USERNAME }}", "registry_type": "DOCKER_HUB", "repository": "deployinator", "tag": "${{env.RELEASE_VERSION}}" } } ]' Now it’s time to do a final check. Go to your DigitalOcean’s App and click button Live App Ta-dam! It doesn’t show the version! Ah, that’s fine. It’s because our is on the path. So add to the URL and see something like this: VersionContoller /version /version Conclusion Everyone likes it when an application is releasable. Otherwise, it doesn’t make sense, no one needs an app that has never been deployed. But when starting a new project most of the teams are ok deploying their shiny new service manually. Why? Daily 15 mins deploy can easily turn out to be per month! Do you want to spend hours manually deploying your application, or do you want to spend those time doing something more fun, like staring at a wall? Okay, maybe it’s not that fun, but you got the idea. 5h+ So save your engineering time, right from the very beginning.