Scala SBT Publishing to GitHub Packages
GitHub Packages is a natural extension of a CI/CD pipeline created in GitHub Action. It currently offers repositories
for Java (Maven), .Net (NuGet), Ruby (Gems), JavaScript (npm), and Docker images.
For a lot of users this can be a free private service if you can squeeze under the size limitation and are okay using
OAuth keys managed in GitHub.
Scala artifacts are usually stored in a Maven compatible repository, so while GitHub Packages doesn’t advertise Scala
support explicitly it can still be a great fit for your project.
Maven publishing is supported by SBT in its sbt publish
task, so let’s give it a go.
HTTP 422 Errors
There are a few nuances with configuring GitHub Packages in SBT, they can be done manually in your build.sbt
or more
easily using a purpose built SBT plugin like sbt-github-packages. It
looks like there are a lot of happy users, but sadly I couldn’t get it to work.
[error] java.io.IOException: Error writing to server
[error] at java.base/sun.net.www.protocol.http.HttpURLConnection.writeRequests(HttpURLConnection.java:718)
[error] at java.base/sun.net.www.protocol.http.HttpURLConnection.writeRequests(HttpURLConnection.java:730)
[error] at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1613)
[error] at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1520)
[error] at java.base/java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:527)
[error] at java.base/sun.net.www.protocol.https.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:334)
[error] at org.apache.ivy.util.url.BasicURLHandler.upload(BasicURLHandler.java:284)
[error] at org.apache.ivy.util.url.URLHandlerDispatcher.upload(URLHandlerDispatcher.java:82)
For an actively maintained GitHub project, I was sad to see an identical 4-month-old open issue Without change the SBT file, I get a “java.io.IOException: Error writing to server” exception. Even manually configuring SBT without the plugin couldn’t resolve the issues, there might be an incompatibility with how my project is named or versioned that I just couldn’t resolve.
Über Jar
aka: fat jar, uber jar, or executable jar
Steps to publish regular library jars is included as an appendix
To complicate the issue further, I didn’t just want to privately publish my standard artifacts, I want to publish an
über jar. Similar to how Docker creates an easy deploy with just 1 file, an über jar is similar. It is also similar to
how a Java War file is a deployable package. Normal Jar files are lean, they only contain your compiled code and publish
their dependencies in a POM file. This is great for libraries, but in the case where the jar is meant to be a standalone
executable all dependencies will need to be included. This is an über jar, it is like a regular jar but includes
the .class
or .jar
of all of your code’s dependencies. It doesn’t make sense to publish an über jar like it was a
library, since the extra code it includes will likely have conflicts or overlaps with other libraries used in a linking
project.
It does however to maintain a set of compiled releases, and here we will be using GitHub Packages.
There is an SBT plugin called sbt/sbt-assembly that will create an über jar, and
allow it to be published to Maven. It is a single-line add to project/plugins.sbt
and takes zero configuration
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0")
It should be as simple as running sbt assembly
to create, and sbt publish
to publish. But like before, GitHub
Packages is still returning HTTP 422 on any attempt, and with this new non-standard artifact it just gets more
complex.
SBT is compiling, and Maven is publishing
We have 2 tasks, compile and publish.
It looks like sbt-assembly
compiles fine, but SBT is failing at publishing the artifacts.
An alternative approach would be to set up Maven to compile, and also use it to publish the Scala project like it was Java (which I would assume works fine since it is directly supported by GitHub).
A second alternative approach would be to keep SBT, but create an SBT task to publish using an external Maven call. Looking over the Maven documentation there is a deploy:deploy-file command in Maven that can publish any file.
The command for our über jar will be something like:
mvn deploy:deploy-file \
-Durl=https://maven.pkg.github.com/user/repo \
-DrepositoryId=github \
-Dfile=target/scala-2.13/project_2.13-version.jar \
-DgroupId=com.yourcompany \
-DartifactId=yourproject \
-Dversion=1.0.0
There is an implicit file that the repositoryId=github
parameter refers to, it expects your repository credentials for
the github
repository id to be stored in your ~/.m2/settings.xml
file. Pleasantly there is way to manually specify
where this file is located using --settings=
, which will be important for us because our repository credentials for
our GitHub Action are stored in GitHub.
⚠️ Since we are creating an über jar, the artifact generated by
sbt-assembly
is namedproject-assembly
instead ofproject
⚠️ Locally we would use a GitHub OAuth token for credentials, we could also use it in the GitHub Action but they should ideally use the token provided to the action.
Creating a custom SBT task
SBT plugins and customization in .sbt
and /project/*
files are Scala code. Nothing stops you from running insanely
complicated code in an SBT task, but here we have a pretty simple two-step workflow.
We want to create a new task to:
- create an über jar using sbt-assembly
- create a
settings.xml
file - publish the jar using a call to an external
mvn
Since this is drop in task, put it either in build.sbt
or its own file called publishAssembyToGitHubPackages.sbt
.
Defining a manually executed task is pretty simple in SBT:
lazy val publishAssembyToGitHubPackages = taskKey[Unit]("Publish Über Jar to GitHub Packages")
publishAssembyToGitHubPackages := {
your scala code goes here
}
The body of the task can be any Scala. Normally SBT is complicated by different configurations existing in different scopes, here everything we need is in the Global scope. The inputs for this task, whether run locally or inside GitHub Actions will be populated from environmental variables. This is a secure way to handle OAuth tokens and prevent them from being committed to your code repo.
Create an Über Jar Using sbt-assembly
This is simple, call assembly
in SBT. The task returns a java.io.File
which is useful in the next steps.
Create a settings.xml
File
This file can be created in /target
to keep things tidy when running this task locally. The secure contents of this
file will also be populated by Maven from ENV variables, so it would be safe to commit this file into source code and
skip this step. To dynamically create this file from a String defined in our .sbt file we can use SBT’s IO.write
.
<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>github</id>
<url>https://maven.pkg.github.com/${GITHUB_REPOSITORY}</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>
<servers>
<server>
<id>github</id>
<username>${GITHUB_REPOSITORY_OWNER}</username>
<password>${GITHUB_TOKEN}</password>
</server>
</servers>
</settings>
The file references 3 ENV variables, which according to GitHub Action documentation are populated during execution:
- GitHub owner
GITHUB_REPOSITORY_OWNER
- GitHub repository
GITHUB_REPOSITORY
- GitHub OAuth token
GITHUB_TOKEN
Once settings.xml
exists, the first bit of Scala code should confirm these 3 ENV variables have been set:
val githubRepository = sys.env.get("GITHUB_REPOSITORY").getOrElse {
throw new Exception("You must set environmental variable GITHUB_REPOSITORY, eg: owner/repository")
}
if (!sys.env.keySet.contains("GITHUB_REPOSITORY_OWNER")) {
throw new Exception("You must set environmental variable GITHUB_REPOSITORY_OWNER, eg: your username")
}
if (!sys.env.keySet.contains("GITHUB_TOKEN")) {
throw new Exception("You must set environmental variable GITHUB_TOKEN")
}
Publish the Über Jar Using a Call to mvn
This last step will assume mvn
is installed can can be called from the command line. This is true for GitHub Actions,
when running this task locally ensure that mvn
is available in your $PATH
.
The name
, organization
and version
keys defined every build.sbt
are suitable for most scenarios to populate
the mvn
parameters. The sbt-assembly
task returns the file we want to publish. In SBT, it is necessary to
call .value
on setting keys, since they need to be resolved to their current value. Calling assembly.value
on
the assembly
task automatically runs it ensuring the file is in our /target
folder.
val exe =
s"""mvn deploy:deploy-file
-Durl=https://maven.pkg.github.com/$githubRepository
-DrepositoryId=github -Dfile=${assembly.value}
-DgroupId=${organization.value}
-DartifactId=${name.value}-assembly
-Dversion=${version.value}
--settings=target/settings.xml
""".stripLineEnd
println(s"Executing shell command $exe")
import scala.sys.process._
if (exe.! != 0) throw new Exception("publishAssembyToGitHubPackages failed")
GitHub Workflow
We have populated our publishAssembyToGitHubPackages
code added it to a publishAssembyToGitHubPackages.sbt
file in
the base of our project. The next step is to create a new Action in GitHub Actions. It can be created through the
website, or manually by creating a new file /.github/workflows/publish-uber-assembly-to-github.yml
.
⚠️ You may need to adjust your Personal Access Token permissions to check in this file using git since it requires
workflow
permissions._
The action YAML starts with the basics, a name:
name: Publish Über Assembly to GitHub Packages
Make it a manual execution for now, this could optionally be tied into git tag events in the future:
on:
workflow_dispatch:
The GITHUB_TOKEN
isn’t populated into ENV by default, so do this now. It will also need write privileges to packages
to push to GitHub Packages.
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
permissions:
contents: read
packages: write
The actual job is a simple setup of checkout, java, and then calling our publishAssembyToGitHubPackages
task in SBT.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Publish Über Jar to GitHub Packages
run: sbt publishAssembyToGitHubPackages
And now we have working GitHub Package publishing of an über jar by adding only 2 files to our project.
Downloading using wget
A separate article Downloading from GitHub Packages Using HTTP and Maven has instructions on how to browse and download artifacts from GitHub Packages using HTTP and Maven.
Publishing non-Über Jars
This article is a little easier since there is only 1 über jar to publish. A typical library would also want to publish a dependency pom, javadoc jar and sources jar, plus those for any subprojects.
The necessary changes for a monolithic project are very small, we will use other SBT packaging tasks instead of
the assembly
task in the plugin. There is are mvn deploy:deploy-file
parameters to specify the pom file, and
optionally source and JavaDoc jars.
val exe =
s"""mvn deploy:deploy-file
-Durl=https://maven.pkg.github.com/$githubRepository
-DrepositoryId=github
-Dfile=${(Compile / packageBin).value}
-DpomFile=${(Compile / makePom).value}
-Dsources=${(Compile / packageSrc).value}
-Djavadoc=${(Compile / packageDoc).value}
--settings=target/settings.xml
""".stripLineEnd
For monolithic libraries include publishToGitHubPackages.sbt instead of publishAssemblyToGitHubPackages.sbt and call publishToGitHubPackages instead of publishAssemblyToGitHubPackages in your GitHub Action.
Full Sources: