Publish private Java library to Github Packages

Mustafa Bahaa SAMISM
MobileAction Technology
8 min readJul 24, 2023

Creating a shared Java library for a project involves packaging reusable code, functions, or components into a separate module that can be used by multiple projects or teams. The library promotes code reuse, reduces duplication, and simplifies maintenance.

GitHub Packages is a package hosting service provided by GitHub. It allows developers to publish and distribute packages within their organization or to the broader GitHub community. While GitHub Packages initially focused on hosting Docker images and npm packages, it has expanded to support other package formats, including Maven, Gradle, NuGet, and RubyGems.

Here’s what GitHub Packages offers and why it can be a good choice for distributing internal private artifacts:

  1. Hosting for Packages
  2. Dependency Resolution and Management
  3. Access Control and Permissions
  4. Versioning and Release Management
  5. Integration with GitHub Ecosystem
  6. Familiar Collaboration Platform

Overall, GitHub Packages offers a convenient and integrated solution for hosting and distributing internal private artifacts. It combines package hosting, dependency management, access control, and integration with existing GitHub workflows, making it a compelling choice for our organization which relies on GitHub for its development processes.

Simple Java library with Spring Boot

First of all, we are going to create a dummy spring project which contains just a Test class “TestMethods.java” and one simple static method “testPrint”:

/**
* This is a Javadoc comment for a class or method.
* This class has a function to test the print from a dummy library.
*/
public class TestMethods {

/**
* This is a Javadoc comment for a method.
* This function just prints some bla bla text.
* @param text The first parameter description.
* @return return some string.
*/
public static String testPrint(String text) {
System.out.println("this is test print from dummy library!");
System.out.println("Your text: " + text);
return "nice text: " + text;
}
}

Also because we are not going to run the library directly we need to delete the main class and its associated test class as there is no need for them.
Also, we need to disable bootJar task and enable just jar task to prevent Gradle from trying to create Jar with ​​Spring Boot application executable dependencies because we don’t have Main class in our library, We do that by adding the following lines to build.gradle:

bootJar {
enabled = false
}

jar {
enabled = true
}

Also to create the source and javadoc jar files for IDE lookup add the following lines to build.gradle:

java {
withJavadocJar()
withSourcesJar()
}

To build the library locally we just need to run ./gradlew build and this will create the library jar files.

Now we can use the library from another project by adding its jar file as dependency:

dependencies {
implementation files('/path-to-lib/dummyLibrary-0.0.1.jar')
}

Publishing Package to GitHub Package

We discussed that early Github Packages will be the ideal solution for our organization, so let's publish our dummy library to Github packages.

In build.gradle file we need to add ‘maven-publish’ plugin:

plugins {
id 'maven-publish'
}

Then configure the host repository and artifact as:

publishing {
publications {
maven(MavenPublication) {
groupId = "co.mobileaction"
artifactId = "mobileaction-dummy-library"
version = "3.0.3"
from components.java
versionMapping {
usage('java-api') {
fromResolutionOf('runtimeClasspath')
}
usage('java-runtime') {
fromResolutionResult()
}
}
}
}
repositories {
maven {
name = "GitHubPackages"
url = "https://maven.pkg.github.com/GITHUB_ACTOR/REPO_NAME"
credentials {
username = <GITHUB_ACTOR>
password = <GITHUB_TOKEN>
}
}
}
}

Now for Automate package creation and deployment, we need to create GitHub action for that we create the folder .github/workflows in the root directory of our project. And inside it, we create the file release-package.yaml. This file defines our workflow and will be like this:

name: Publish package to GitHub Packages
on:
push:
branches:
- master
release:
types: [ created ]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Set up Java
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@ccb4328a959376b642e027874838f60f8e596de3
- name: Publish package
uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629
with:
arguments: publish

In the first part “on” we defined when this action will be triggered and set as when pushing to master or create a new release.

In the jobs part we define the publish job steps and config in order:

  1. actions/checkout@v3: to fetch the source code of a repository into the GitHub Actions workflow environment.
  2. actions/setup-java@v3: to set up java.
  3. gradle/wrapper-validation-action: to validate Gradle wrapper.
  4. gradle/gradle-build-action with argument publish: to build and package.

When push to master or create a new release New action will start automatically and create a new package version.

We can find the package after publish by clicking on packages on the right side of the repository home page.

To manage the package from the package page in the right bottom side click ‘Package settings’ from there you can delete the package or click on ‘Manage versions’ to delete specific versions.

To include the library in another project we just need to copy the dependency from package page and paste to build.gradle:

dependencies {

implementation 'co.mobileaction:mobileaction-dummy-library:1.0.0'

}

Add Github packages repository with credentials:

repositories {
mavenCentral()
maven {
name = "GitHubPackages"
url = "https://maven.pkg.github.com/GITHUB_ACTOR/REPO_NAME"
credentials {
username = <GITHUB_ACTOR>
password = <GITHUB_TOKEN>
}
}
}

And after refreshing Gradle dependency will be fetched and resolved.

Authentication

GitHub Packages only supports authentication using a personal access token (classic), To write and delete package we need admin permissions over the repository that will publish the packages because selecting write:packages permission need full control of private repositories.

Fortunately, we can also take advantage of environment variables set in our CI workflow run. Where Github workflow generates Access Token scoped to the repository before each job begins, The GITHUB_TOKEN expires when a job finishes or after a maximum of 24 hours.

To do that we should first read GITHUB_ACTOR and GITHUB_TOKEN from workflow secrets and set them as environment variables.

- name: Publish package
uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629
with:
arguments: publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_ACTOR: ${{ secrets.GITHUB_ACTOR }}

Then from build.gradle we can use this env variables as credential:

credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}

In other hand we can create Access Token with just packages read permission and use it to download packages from Github package registry.

go to Settings/Developer Settings/Personal access tokens (classic) then generate new token(classic) with just read:packages permission. For more information about creating access token, see “Managing your personal access tokens.”

Delete & Restore Package Versions

As I mentioned earlier we can manage packages and versions deletion from package settings page with respect to the following rules:

  • an entire private package
  • an entire public package, if there’s not more than 5000 downloads of any version of the package
  • a specific version of a private package
  • a specific version of a public package, if the package version doesn’t have more than 5,000 downloads

Also we can restore deleted version if:

  • You restore the package within 30 days of its deletion
  • The same package namespace is still available and not used for a new package.

In other hand Github provide actions/delete-package-versions@v4 action to automate versions deletion. But it can only delete a maximum of 100 versions in one run.

And we can use this action to:

  • Create a retention policy (delete all except n most recent pre-release versions)
  • Delete all package versions except n most recent versions
  • Delete oldest version(s)
  • Ignore version(s) from deletion through regex
  • Delete version(s) of a package that is hosted from a repo having access to package
  • Delete version(s) of a package that is hosted from a repo not having access to package
  • Delete a single version
  • Delete multiple versions
  • Delete specific version(s)

Usage example:

- uses: actions/delete-package-versions@v4
with:
# Can be a single package version id, or a comma separated list of package version ids.
# Defaults to an empty string.
package-version-ids:

# Owner of the package.
# Defaults to the owner of the repo executing the workflow.
# Required if deleting a version from a package hosted in a different org than the one executing the workflow.
owner:

# Name of the package.
# Required
package-name:

# Type of the package. Can be one of container, maven, npm, nuget, or rubygems.
# Required
package-type:

# The number of old versions to delete starting from the oldest version.
# Defaults to 1.
num-old-versions-to-delete:

# The number of latest versions to keep.
# This cannot be specified with `num-old-versions-to-delete`. By default, `min-versions-to-keep` takes precedence over `num-old-versions-to-delete`.
# When set to 0, all deletable versions will be deleted.
# When set greater than 0, all deletable package versions except the specified number will be deleted.
min-versions-to-keep:

# The package versions to exclude from deletion.
# Takes regex for the version name as input.
# By default nothing is ignored. This is ignored when `delete-only-pre-release-versions` is true
ignore-versions:

# If true it will delete only the pre-release versions.
# The number of pre-release versions to keep can be set by using `min-versions-to-keep` value with this.
# When `min-versions-to-keep` is 0, all pre-release versions get deleted.
# Defaults to false.
# Cannot be used with `num-old-versions-to-delete` and `ignore-versions`.
delete-only-pre-release-versions:

# If true it will delete only the untagged versions in case of container package.
# Does not work for other package types and will be ignored.
# The number of untagged versions to keep can be set by using `min-versions-to-keep` value with this.
# When `min-versions-to-keep` is 0, all untagged versions get deleted.
# Defaults to false.
# Cannot be used with `num-old-versions-to-delete`.
delete-only-untagged-versions:

# The token used to authenticate with GitHub Packages.
# Defaults to github.token.
# Required if the repo running the workflow does not have access to delete the package.
# For rubygems and maven package, repo has access if package is hosted in the same repo as the workflow.
# For container, npm and nuget package, repo has access if assigned **Admin** role under Package Settings > Manage Actions Access.
# If `package-version-ids` is given the token only needs the delete packages scope.
# If `package-version-ids` is not given the token needs the delete packages scope and the read packages scope
token:

for more examples see Delete Package Versions.

--

--