blog podcast

Github Package

We recently encountered the problem that we needed to patch a 3rd party library. Now this library lives in an eco-system of many dependencies and we just wanted to do some small changes to two of the modules. The question is, how do you patch a library and then make it available for your other projects to use?

Well, in our case we’re using JVM. Our projects are using Gradle, and the library we want to patch is using Maven.

The simplest scenario is to solve this problem on the local developer machine. If you run the command mvn install in a project folder, you publish that project into your local maven repository. If you then in gradle make sure to have the following code:

repositories {
    mavenLocal()
    mavenCentral()
}

Note that the order here is important, this makes sure we try to find our packages first in your local maven and then in the central maven repository.

You might wonder why we do the install. Wouldn’t it be possible to just run mvn package and then import the jar file into your project? If that would be possible it would be really nice, then you could have the dependency checked into your repository and it would work wherever cloned. Well, gradle supports this as well, it looks like this:

repositories {
    flatDir {
        dirs 'lib'
    }
    mavenCentral()
}

where lib would be the place where we store our jar files. This works, BUT. There’s a difference between how gradle handle imports maven imports and flatDir dependencies. In the case of flatDir gradle doesn’t resolve any transitive dependencies. Which means you’d have to manually reference all of the transitive dependencies in your build.

What if you bundled all the dependencies into a fat jar? It does seem like a plausible solution, When I tried it I ran into problems with Quarkus though, which performs some optimizations on the code compile time.

Okay. Back to our dependency again. Now it’s working on your developer machine (cool!). But you problably have a build system that also needs to run this build, and you probably can’t or don’t want to run mvn install on that machine. So how will this build system get access to your edited depdency?

You could deploy the dependency to Maven Central under a different name. But (I believe) getting an artifact published there requires some review process. I mean that would make sense because if you can publish any code there you might really be able to cause to harm if unknowing users start using your potentially malignant code.

Github supports publishing packages, and that’s the route that I selected. First you then need to configure your maven environment to publish to your github project. In the ~/.m2/settings.xml you add something like:

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                      http://maven.apache.org/xsd/settings-1.0.0.xsd">

  <activeProfiles>
    <activeProfile>github</activeProfile>
  </activeProfiles>

  <profiles>
    <profile>
      <id>github</id>
      <repositories>
        <repository>
          <id>central</id>
          <url>https://repo1.maven.org/maven2</url>
        </repository>
        <repository>
          <id>github</id>
          <url>https://maven.pkg.github.com/OWNER/REPO</url>
          <snapshots>
            <enabled>true</enabled>
          </snapshots>
        </repository>
      </repositories>
    </profile>
  </profiles>

  <servers>
    <server>
      <id>github</id>
      <username>USER_NAME</username>
      <password>TOKEN</password>
    </server>
  </servers>
</settings>

Where you configure OWNER to be the owner of the repo, REPO is the repository itself. USER_NAME is your username, and TOKEN is a token you need to configure to enable access to the repository. It makes sense that you need the token to publish artifacts. Unfortunately Github doesn’t support unauthorized pulling of packages, which means you also need to have this token when pulling from the repository.

For gradle on your local machine you can do this by providing the token in a gradle.properties file in your gradle folder:

gpr.token=TOKEN

The name gpr.token could be anything, it’s short for github private repository. In your build.gradle file you then add:

repositories {
    maven {
        url("https://maven.pkg.github.com/OWNER/REPO")
        credentials(HttpHeaderCredentials) {
            name = "Authorization"
            value = "Bearer ${project.findProperty("gpr.token")}"
        }
        authentication {
            register("header", HttpHeaderAuthentication)
        }
    }
    mavenCentral()
}

This will provide the token into the authorization and your now get the package from the new place. But what about Jenkins? It doesn’t have a gradle.properties file. Instead, by using the Credentials and Credentials Bindings plugins you can inject the token using an environment variable. Your gradle code changes to :

repositories {
    maven {
        url("https://maven.pkg.github.com/OWNER/REPO")
        credentials(HttpHeaderCredentials) {
            name = "Authorization"
            value = "Bearer ${project.findProperty("gpr.token") ?: System.getenv("GPR_TOKEN")}"
        }
        authentication {
            register("header", HttpHeaderAuthentication)
        }
    }
    mavenCentral()
}

And your Jenkinsfile adds the following:

    pipeline {
        agent any

        environment {
            GPR_TOKEN = credentials('gpr.token')
        }

where you have to save your credentials as a secret text with the name gpr.token