How I created a JDK 19 Loom GitHub Actio ...

How I created a JDK 19 Loom GitHub Action

Mar 15, 2024

I'm currently working on a project, which I'm using to compare what Kotlin and Java provide in terms of performance.

Having said this, I want to share with you how I created a GitHub action for Project Loom. Since Project Loom is about to come out officially, this is great news, but it hasn't really yet and support for it isn't the best yet. From a quick search and following the lead on InfoQ magazine online these are currently the best predictions and the suggestions, quoting, of Mark Reinhold:

2022/06/09 Rampdown Phase One

2022/07/21 Rampdown Phase Two

2022/08/11 Initial Release Candidate

2022/08/25 Final Release Candidate

2022/09/20 General Availability

This means that, odds are, that an official release of Project Loom might only come at the end of the year. This whole thing is an SDK. Unlike coroutines which works as a DSL (Domain Specific Language) running on top of the JDK, Project Loom is a JDK on its own. It uses concepts such as Fibers, Virtual Threads, Tail-Calls and Continuation that I want to explore. By the way Continuation is described as the Co in Coroutines.


Working locally you may simply dowload the JDK from their website and then proceed as you would for other manually installed JDK's. However for GitHub pipelines, this can be quite challenging. The pipelines use actions and I couldn't find a single one that would suit my needs, and so I did one myself. To be honest I never really quite understood what an action actually does. I never really looked at the inner workings of a GitHub action and so this is why I'm sharing this with you now. I based my own action on another action called GitHub Action for GraalVM. And of course I followed the official GitHub tutorial about this.


I named it loom-action and you can find it on GitHub. There are several ways to start an action and it basically just runs code and it does follow a particular kind of architecture. You can start a Docker Container and you can run Node code as an example. Out of all available options known to me I chose to implement my action code using Node TS. In order to do that I first defined my action in a YAML format:

name: 'JEsperancinhaOrg Loom Action'
description: 'Allows usage of the Java Loom JDK in GitHub Pipelines'
author: 'João Esperancinha <[email protected]>'
branding:
 icon: 'package'
 color: 'green'
inputs:
 loom:
 required: false
 description: 'Loom version (release, latest, dev).'
 value: '19-loom+6-625'
runs:
 using: 'node16'
 main: 'dist/index.js'

What this means is that I'm creating an action that will install JDK 19 in the pipeline. I made it possible to configure just one input at the moment and that is loom with a default version 19-loom+6-625. This is of course the Loom JDK version we want to use. In the runs command I'm telling the action to run the index.js file in order to make everything happen. If you haven't seen it already the index.js exists in folder dist. This whole parameter is configurable but the code must run standalone. This means that the action does not compile code. We need to give it the completely compiled code. This is the reason that both the compiled and the source code are checked in for this repo. In order to use this action fully, we can use it in our git action like this:

- name: JEsperancinhaOrg Loom Action
 uses: JEsperancinhaOrg/[email protected]
 with:
 loom: '19-loom+6-625'

Note that my alfa versions are not yet ready to be used in Production. Not according to my criteria at least. I still need to test how this works with Gradle and Maven. But here you have just a way that this could work now.

If you know Node JS, it should be fairly easy for you to understand how I generate the compiled code. If not and because it is not the goal of this coffee session, then you can find plenty of tutorials on the web. I used NCC for this project. So this is the code:

import * as core from "@actions/core";
import http from "https";
import fs from "fs";
import path from "path";
import tar from "tar-fs";
import zlib from "zlib";

const confLoom = core.getInput("loom")
const loom = confLoom ? confLoom : "19-loom+6-625"
const file = fs.createWriteStream("openjdk-19.tar.gz");
let downloadFile = "https://download.java.net/java/early_access/loom/6/openjdk-" + loom + "_linux-x64_bin.tar.gz";
console.log("Downloading file at " + downloadFile + ".")
http.get(downloadFile, function (response) {
 response.pipe(file);
 file.on("finish", () => {
 file.close();
 console.log("Download Completed");
 fs.createReadStream("openjdk-19.tar.gz")
 .pipe(zlib.createGunzip())
 .pipe(tar.extract("loom-jdk"))
 .on("finish", () => {
 console.log("Unzipped JDK Loom");
 let loomJdkJdk19 = "loom-jdk/jdk-19";
 const absolutePath = path.resolve(loomJdkJdk19);
 console.log("Setting:")
 console.log("JAVA_HOME=" + absolutePath)
 core.exportVariable('JAVA_HOME', absolutePath);
 let newPath = absolutePath + "/bin:" + process.env.PATH;
 console.log("PATH=" + newPath)
 core.exportVariable('PATH', newPath);
 });
 });
});

And yes, I'm using console and not a better logger. I know, but that wasn't the goal of this action. I really had to invest my time learning actually how it work. Better versions will come later on 😁. For now, it's important to focus on the library @actions/core. This library is not the only one made specifically to git actions, but it is the most used one and it is essential to understand a few things about it. We get the loom value from core.getInput("loom"). It should at this point come with value 19-loom+6-625. If not, I still try to assign it with this default value. It then follows a chain of streams that will download the configured version to file openjdk-19.tar.gz. After that, we just unpack this file in the root and finally we set the matching JAVA_HOME and PATH environment values with the new values with core.exportVariable('JAVA_HOME', absolutePath); and core.exportVariable('PATH', newPath);. This is essentially it and as you can see, there is no mythology here or difficulties.

There was a small issue with the exportVariable. I noticed that this function behaved quite strangely when two JDK actions are configured in the pipeline. In my case, I was making tests with GraalVM JDK 17 at the same time I was trying to get my action to work. Quite remarkably, it always placed the JDK17 first in the PATH. So this tells me that exportVariable isn't really a setter. It does something else if there are more actions on the same pipeline that use the same environment variables. But anyways, why would I want to have two JDK's in the same pipeline? I guess I'm just curious. There are better places to install JDK's with a GitHub action, but just like many of my improvement points mentioned above, more versions will come, they will be better, and they will be improved. But no matter how I improve them, the core of the functionality is already there.


Anyways, I hope you enjoyed this coffee session and that I could give you some insights on how easy it actually is to implement a GitHub action.

Enjoy this post?

Buy João Esperancinha a coffee

More from João Esperancinha