Github Packages: the right tool for the job?

Does it meet expectations or come up short?

Github Packages: the right tool for the job?

I've been looking for a simple and inexpensive way to host my Kotlin library binaries for some time. Previously I have used JitPack, but there's no Kotlin multi-platform support. I have tried Bintray in the past but found it to be over-complicated. So, I was left with no good solution for hosting my libraries.

Then Github Packages came along and offered up some hope. I was excited about the idea of having my library source code and binaries in one place. And with Github Actions, I could automate the creation and deployment of the binaries. If this works out correctly, this would be the ideal solution. But, unfortunately, I quickly ran into issues with Github Packages that became an impediment to it's adoption. I will discuss those issues in this post.

TL;DR

Github Packages suffers from issues that prevent it from being the preferred package repository for Gradle/Maven users.

1:1 Repository and Dependency Relationship

Every Github Repository (Project) acts as a Repository for it's Packages that can be used as Dependencies in other projects. This is an awful design, especially for projects with a single package. For example, to depend on a library in Github Packages, you would need to setup the repositories and dependencies in your build.gradle.kts file like so:

repositories {
    maven {
        url = uri("https://maven.pkg.github.com/$GITHUB_USER/$GITHUB_REPO")
        ...
    }
}

dependencies {
    implementation("$GROUP:$ARTIFACT_NAME:$VERSION")
}

This problem becomes more apparent in a real project that has numerous dependencies, as each dependency from Github Packages has to redundantly specify both the repository and dependency:

repositories {
    maven { 
        url = uri("https://maven.pkg.github.com/$GITHUB_USER/$REPO_ONE")
    }
    maven {
        url = uri("https://maven.pkg.github.com/$GITHUB_USER/$REPO_TWO")
    }
    maven {
        url = uri("https://maven.pkg.github.com/$GITHUB_USER/$REPO_THREE")
    }
}

dependencies {
    implementation("$GROUP_ONE:$ARTIFACT_ONE:$VERSION_ONE")
    implementation("$GROUP_TWO:$ARTIFACT_TWO:$VERSION_TWO")
    implementation("$GROUP_THREE:$ARTIFACT_THREE:$VERSION_THREE")
}

The way it should be implemented is that the library user only has to specify one repository (probably on the Github User account), and then all packages from different projects (belonging to that Github User) will be available as dependencies.

It seems that Github made an incorrect correlation between a Github Repository (Project) and a Maven Repository. A source code repository and a package repository are two different concepts. For instance, mavenCentral() is a central package repository for Maven that contains many different dependencies originating from different source code repositories. In order to access these different dependencies, one would only need to specify mavenCentral() as a repository.

The way that Github Packages should work:

repositories {
    maven { 
        url = uri("https://maven.pkg.github.com/$GITHUB_USER")
    }
}

dependencies {
    implementation("$GROUP_ONE:$ARTIFACT_ONE:$VERSION_ONE")
    implementation("$GROUP_TWO:$ARTIFACT_TWO:$VERSION_TWO")
    implementation("$GROUP_THREE:$ARTIFACT_THREE:$VERSION_THREE")
}

Authentication Required for Installing

Github Packages requires authentication, via personal access tokens, to publish and install packages. This makes sense for publishing but seems like a hindrance for using the packages. Also, this is further complicated by the inadequate documentation which doesn't provide detail about how to use a package.

The documentation indirectly examplifies how to specify a Github Package Repository to retrieve dependencies, by a snippet on publishing a package within the authentication section (comment added):

publishing {
    repositories { // This is also how to specify a Github package repository
        maven {
            name = "GitHubPackages"
            url = uri("https://maven.pkg.github.com/OWNER/REPOSITORY")
            credentials {
                username = project.findProperty("gpr.user") ?: System.getenv("USERNAME")
                password = project.findProperty("gpr.key") ?: System.getenv("PASSWORD")
            }
        }
    }
    publications {
        gpr(MavenPublication) {
            from(components.java)
        }
    }
}

This wasn't immediately obvious that this was also used for retrieving a package, and was missing from the documentation's "Installing a package" section. This StackOverflow answer was much better in illustrating the usage of retrieving a Github Package then the official documentation.

The following illustrates how to specify a Github Package Repository:

repository {
    maven {
        name = "GithubPackages" // Name is Optional
        url = uri("https://maven.pkg.github.com/$GITHUB_USER/$GITHUB_REPO")
        credentials {
            // "grp.user" and "gpr.key" are added to your LOCAL gradle properties file
            username = project.findProperty("gpr.user") as? String // Github Username
            password = project.findProperty("gpr.key") as? String // Personal Access Token
        }
    }
}

As illustrated above, accessing a Github Package Registry as a Maven repository requires authentication with a Github Personal Access Token. This can become very verbose, especially with the 1:1 relationship of repositories and dependencies. For private repositories, this is a necessity but for public open source projects, this shouldn't be required.

Incorrect group and artifact names

Consider a project with multiple modules:

root
  moduleOne
  moduleTwo

When using the Maven Publish Plugin, these modules are correctly resolved to the following:

group.root:module:version

// Example:
com.chrynan.aaaah:core:0.3.3

However, this isn't the case when publishing packages to Github Packages. The Root Module is ignored, and the package name becomes:

group:module:version

// Example:
com.chrynan:core:0.3.3

This obviously is problematic because it can easily lead to name clashes. However, there is a way around this, by explicitly including the root module in the group name or prefacing the root module into the artifact name (note that I haven't verified that these approaches fully work).

Artifact Approach:

publishing {
    group = "$GROUP" // com.chrynan
    ...
    publications {
        gpr(MavenPublication) {
            from(components.android)
            artifactId = "$ROOT_MODULE-$SUB_MODULE" // aaaah-core
        }
    }
    // The dependency could then be accessed like so:
    // implementation("$GROUP:$ROOT_MODULE-$SUB_MODULE:$VERSION")
    // Ex: implementation("com.chrynan.aaaah-core:0.3.3")
}

Group Approach:

publishing {
    group = "$GROUP.$ROOT_MODULE" // com.chrynan.aaaah
    ...
    publications {
        gpr(MavenPublication) {
            from(components.android)
            artifactId = "$SUB_MODULE" // core
        }
    }
    // The dependency could then be accessed like so:
    // implementation("$GROUP.$ROOT_MODULE:$SUB_MODULE:$VERSION")
    // Ex: implementation("com.chrynan.aaaah:core:0.3.3")
}

Multi-platform projects require additional configuration

The documentation uses the following example for publishing multiple packages in the same repository (multiple module projects):

plugins {
    `maven-publish` apply false
}

subprojects {
    apply(plugin = "maven-publish")
    configure {
        repositories {
            maven {
                name = "GitHubPackages"
                url = uri("https://maven.pkg.github.com/OWNER/REPOSITORY")
                credentials {
                    username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME")
                    password = project.findProperty("gpr.key") as String? ?: System.getenv("PASSWORD")
                }
            }
        }
        publications {
            register("gpr") {
                from(components["java"])
            }
        }
    }
}

But this only works for Java modules. The Java Component, components["java"], is only available in Java projects. For Android Projects, an additional plugin is required (or manual configuration), to provide an Android Component. Then the component can be registered like so:

// Note that this example is in Groovy
publications {
    mavenAar(MavenPublication) {
        from components.android
    }
}

For Kotlin Multi-platform there is no Java or Android Component and the components block is handled by the system. Though I'm not positive if this resolves correctly with Github Packages.

Since each component is handled differently, each module's build file will have to explicitly handle the publications block for itself.

Cannot Delete Packages

According to the documentation, you cannot delete a package:

To avoid breaking projects that may depend on your packages, you cannot  delete an entire public package or specific versions of a public  package.

However, you can delete a version of a private package using the Github GraphQL API:

Anyone with admin permissions to a repository can delete a version of a private package in that repository.

This is an annoying limitation on Github Packages which creates more problems than the one they sought to solve. For instance, when experimenting with Github Packages on a public repository, I published numerous versions of packages which are not valid. These were created when trying different approaches to get the packages to resolve correctly (using more explicit artifactIds). But now, since I can't delete any of the invalid packages, I have numerous packages polluting my repository.

Conclusion

The idea behind Github Packages (package registry stored along with the source code repository) is great. Unfortunately, it's shortfalls for modern Gradle/Maven usage are obstacles to it's adoption. Hopefully in the future these will be resolved. But until then, my desire for a modern package repository, with Kotlin Multi-platform in mind, remains.