Publish Scala Library To Maven Central With GitHub Actions
Setting up a GitHub Action to publish your Scala library to Maven Central might seem daunting, but it's totally achievable with the right steps. This comprehensive guide breaks down the process, making it easy to automate your releases. We'll be using the sbt-ci-release plugin, a powerful tool designed to streamline the entire process. So, let's dive in and get your library out there!
1. One-Time Sonatype & PGP Setup
Before we even think about GitHub Actions or our sbt build, there are a few essential one-time setups we need to tackle. These steps involve creating a Sonatype account and generating a PGP key. Think of this as laying the groundwork for a smooth publishing process. Skipping these steps is like trying to build a house without a foundation – it just won't work!
1.1. Create a Sonatype Central Account
Your journey to Maven Central starts with Sonatype Central Portal. Maven Central pulls its artifacts from Sonatype, so you'll need an account to get your library in the mix. It's like getting a key to the city, but for your code.
- First things first, sign up at the Sonatype Central Portal. This is your gateway to the Maven Central world. It's a straightforward process, just like signing up for any other online service.
- Next, you'll need to claim your namespace, also known as a "groupId." This is a unique identifier for your library, typically a reverse domain you own (e.g.,
io.github.your-username). It's like staking your claim in the digital world. If you don't have a domain, don't sweat it! Usingio.github.<your-github-username>is the standard practice for open-source projects. Think of it as your digital neighborhood.- Proving ownership is key here. Sonatype will have specific instructions, often involving creating a temporary public repository on GitHub with a specific name. This is their way of making sure you're the real deal.
- Now, patience is a virtue. This approval process can take a business day or two, so don't panic if you don't see immediate results. You can't publish until the approval is complete, so grab a coffee and maybe binge-watch some coding tutorials while you wait.
1.2. Generate a PGP Key
Security is paramount in the world of software publishing. Maven Central requires all published artifacts to be signed with a PGP key. This is your digital signature, ensuring the authenticity and integrity of your library. Think of it as a notary public for your code.
-
First up, install GnuPG (or GPG). If you're on macOS,
brew install gpgis your friend. This is the tool that will help you create and manage your PGP key. -
Now, let's generate a new key:
gpg --full-gen-key ``` 3. Follow the prompts carefully. This is where you'll define the characteristics of your key.
* Choose **`RSA and RSA`** (the default). It's a robust and widely used encryption algorithm.
* Opt for a key size of **`4096`** bits. This provides a good balance between security and performance. It's like choosing the right lock for your digital vault.
* Set an expiration date (e.g., `2y` for 2 years) or `0` for no expiration. Consider how long you want your key to be valid. If you're unsure, a 2-year expiration is a good starting point.
* Enter your real name and email address. This information will be public, so make sure it's accurate. It's like putting your name on your work.
* **Crucially, enter a secure passphrase.** This is the password that protects your key, so make it strong and memorable (but don't forget it!). It's the equivalent of the combination to your digital safe.
-
Time to find your key ID. Run
gpg --list-secret-keys. The output will look something like this:
sec rsa4096/ABC12345DEF67890 2025-11-10 [SC] [expires: 2027-11-10] A1B2C3D4E5F6A1B2C3D4E5F6ABC12345DEF67890 uid [ultimate] Your Name your.email@example.com ```
Your key ID is the long string: `A1B2C3D4E5F6A1B2C3D4E5F6ABC12345DEF67890`. Copy this down; you'll need it later. It's like your key's fingerprint.
-
Finally, publish your public key to a keyserver. This allows Sonatype to verify your signature. Think of it as making your key publicly available for verification.
gpg --keyserver hkp://keyserver.ubuntu.com --send-keys A1B2C3D4E5F6A1B2C3D4E5F6ABC12345DEF67890 ```
(Replace `A1B2C3D4E5F6A1B2C3D4E5F6ABC12345DEF67890` with your key ID, of course!). This step makes your public key accessible to the world.
2. Configure Your sbt Project
With the one-time setup out of the way, it's time to turn our attention to your Scala project. We'll be adding the necessary plugins and settings to your build.sbt file. This is where we tell sbt how to build and publish your library.
2.1. Add the sbt-ci-release Plugin
The sbt-ci-release plugin is the star of the show. It simplifies the release process by bundling other necessary plugins (sbt-sonatype, sbt-pgp, and sbt-dynver) and automating the release logic. It's like having a dedicated release manager built into your sbt build.
In your project's project/plugins.sbt file, add:
addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") // Check for the latest version
Make sure to check for the latest version of the plugin to take advantage of the latest features and bug fixes. It's like keeping your tools sharp and up-to-date.
2.2. Configure build.sbt
Now, let's configure your build.sbt file. Maven Central has specific metadata requirements, and sbt-ci-release conveniently populates some of this for you (like scmInfo) from your Git repository. This is where we provide the information that identifies your library and its creators. Think of it as filling out the paperwork for your library's debut.
Here's a typical build.sbt configuration:
// Your organization must match the namespace you registered with Sonatype
ThisBuild / organization := "io.github.your-username"
ThisBuild / organizationName := "your-username"
ThisBuild / organizationHomepage := Some(url("https://github.com/your-username"))
// The license for your project
ThisBuild / licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0"))
// Your developer information
ThisBuild / developers := List(
Developer(
id = "your-username",
name = "Your Name",
email = "your.email@example.com",
url = url("https://github.com/your-username")
)
)
// This is required by sbt-ci-release to publish to the new Sonatype Central portal
ThisBuild / sonatypeCredentialHost := "central.sonatype.com"
ThisBuild / sonatypeRepository := "https://central.sonatype.com/api/v1"
// Your project's name and other settings
lazy val root = (project in file("."))
.settings(
name := "my-awesome-scala-library",
// ... other settings like scalaVersion, libraryDependencies, etc.
)
Let's break down some key parts:
organization: This must match the namespace you registered with Sonatype. It's your library's unique identifier.organizationName: Your organization's name.organizationHomepage: A link to your organization's website (usually your GitHub profile).licenses: The license under which your library is released. Open-source licenses like Apache-2.0 are common.developers: Information about the developers of the library. This helps give credit where it's due.sonatypeCredentialHostandsonatypeRepository: These settings are required bysbt-ci-releaseto publish to the new Sonatype Central portal.
Don't forget to commit these changes (plugins.sbt and build.sbt) to your repository. It's like saving your work before moving on.
3. Set Up GitHub Actions Secrets
Now, let's talk security. Your GitHub workflow needs to authenticate with Sonatype and sign the artifacts, but never hardcode credentials directly into your workflow files! That's a big no-no. Instead, we'll use GitHub's encrypted "Secrets." Think of these as secure storage for sensitive information.
Navigate to your repository on GitHub, then Settings > Secrets and variables > Actions. This is where you'll manage your secrets.
Click New repository secret and add the following four secrets:
SONATYPE_USERNAME: Your Sonatype Central username. This is how GitHub Actions identifies you to Sonatype.SONATYPE_PASSWORD: Your Sonatype Central password. Important: It's highly recommended to generate a User Token on Sonatype and use that as the password. This is more secure than using your actual password.PGP_PASSPHRASE: The passphrase you created for your GPG key in step 1.2. This is the key to unlocking your digital signature.PGP_SECRET: Your full private key, base64 encoded. This is the most sensitive piece of information, so handle it with care.
Let's dive into how to get that PGP_SECRET:
-
Run the following command in your terminal (replacing
A1B2C3D4E5F6A1B2C3D4E5F6ABC12345DEF67890with your actual key ID) and copy the entire output block, including the-----BEGIN...and-----END...lines:
gpg --export-secret-keys --armor A1B2C3D4E5F6A1B2C3D4E5F6ABC12345DEF67890 ```
This command extracts your private key in an armored (text-based) format.
-
On macOS:
gpg --export-secret-keys --armor YOUR_KEY_ID | pbcopy ```
This command copies the output to your clipboard.
-
On Linux (with xclip):
gpg --export-secret-keys --armor YOUR_KEY_ID | xclip -selection clipboard ```
This command also copies the output to your clipboard.
- Paste this value directly into the
PGP_SECRETfield in GitHub. Treat this secret like gold!
4. Create the GitHub Actions Workflow
Alright, we're in the home stretch! Now, we'll create the GitHub Actions workflow file that will automate the release process. This is where we define the steps that will be executed whenever we push a new tag.
- Create a directory path in your repository:
.github/workflows/. This is where GitHub Actions looks for workflow files. - Inside that directory, create a new file named
release.yml. This is where we'll define our release workflow.
Copy and paste the following content into .github/workflows/release.yml:
name: Release
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+" # Triggers on tags like v1.0.0
jobs:
publish:
name: Publish to Maven Central
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v4
with:
# This is necessary to let sbt-dynver determine the version
fetch-depth: 0
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'temurin'
cache: 'sbt'
- name: Publish release
run: sbt ci-release
env:
# These secrets are provided by GitHub Actions
PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
PGP_SECRET: ${{ secrets.PGP_SECRET }}
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
Let's break down this workflow file to understand what's happening:
name: Release: This is the name of your workflow, which will be displayed in the GitHub Actions UI.on: push: tags:: This is the trigger that starts the workflow. In this case, it will only run when you push a new Git tag that matches the patternv*.*.*(e.g.,v1.0.1,v0.2.0). This ensures that releases are triggered by tagged commits.jobs: publish:: This defines a job calledpublishthat will be executed. Jobs are collections of steps that run in a specific environment.name: Publish to Maven Central: The name of the job.runs-on: ubuntu-latest: This specifies that the job should run on the latest version of Ubuntu. It's like choosing the operating system for your build environment.steps:: This is where the magic happens! This section defines the individual steps that will be executed in the job.name: Check out: This step checks out your code from the repository.uses: actions/checkout@v4: This uses the officialcheckoutaction from GitHub.with: fetch-depth: 0: This is crucial forsbt-dynver(part ofsbt-ci-release) to see your Git history and set the version correctly. It ensures that the entire Git history is fetched.
name: Set up JDK 11: This step sets up Java Development Kit (JDK) version 11.uses: actions/setup-java@v4: This uses the officialsetup-javaaction from GitHub.with:: This configures the action.java-version: '11': Specifies the Java version to use.distribution: 'temurin': Specifies the JDK distribution (Temurin is a popular open-source distribution).cache: 'sbt': Enables caching for sbt dependencies, which speeds up builds.
name: Publish release: This is the core step that publishes your library.run: sbt ci-release: This executes thesbt ci-releasecommand, which triggers the release process.env:: This section securely injects the secrets you set up into the build environment, allowing sbt to sign the artifacts and log in to Sonatype.PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}: Passes the PGP passphrase from GitHub Secrets.PGP_SECRET: ${{ secrets.PGP_SECRET }}: Passes the PGP private key from GitHub Secrets.SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}: Passes the Sonatype username from GitHub Secrets.SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}: Passes the Sonatype password (or user token) from GitHub Secrets.
How This Workflow Works: A Deep Dive
on: push: tags:: This workflow is designed to be triggered only when you push a new Git tag that follows thev*.*.*pattern (likev1.0.1orv0.2.0). This is a common practice for releases, as it clearly marks specific versions of your library.actions/checkout@v4: This action is the foundation of the workflow. It checks out your code from your GitHub repository, making it available for the subsequent steps. Thefetch-depth: 0setting is crucial forsbt-dynver, a key component ofsbt-ci-release.sbt-dynveranalyzes your Git history to automatically determine the version number of your library. By settingfetch-depth: 0, we ensure that the entire Git history is fetched, allowingsbt-dynverto accurately determine the version.actions/setup-java@v4: This action sets up the Java Development Kit (JDK) environment that's needed to build your Scala library. We're specifying Java version 11, a widely used and stable version. Thedistribution: 'temurin'setting indicates that we're using the Temurin distribution, a reliable and open-source option. Thecache: 'sbt'setting is a performance booster. It enables caching for sbt dependencies, meaning that sbt will download dependencies once and then store them for future builds. This significantly speeds up subsequent workflow runs.run: sbt ci-release: This is the heart of the workflow! This command executes thesbt ci-releaseplugin, which orchestrates the entire release process. Thesbt-ci-releaseplugin is smart; it detects that it's running in a GitHub Action environment triggered by a tag push. Based on this context, it automatically performs a series of actions:- Version Setting: It sets the project version based on the Git tag. For example, if you push a tag named
v1.0.1, the plugin will automatically set the version of your library to1.0.1. This eliminates manual version management. - Cross-building (if configured): If your project is configured to cross-build for multiple Scala versions, the plugin will handle building your library for each of those versions. This ensures compatibility across different Scala environments.
- Signing and Uploading: It runs the
+publishSignedcommand, which builds your library, signs the artifacts with your PGP key (using the secrets you configured), and uploads them to Sonatype's staging repository. This is where your library is prepared for its debut on Maven Central. - Bundle Release: It runs the
sonatypeBundleReleasecommand, which
- Version Setting: It sets the project version based on the Git tag. For example, if you push a tag named