Publish Clojure library as private GitHub Package
Tags: clojure, github, maven, library
Published: 2022-02-06 12:54:35

Recently, I faced a problem, I was need to share a Clojure library internally, without exposing it to the public Clojars repo, and surprisingly I didn't find a good tutorial on how to do it. So I decided to describe my story here.

Since our company uses GitHub, I knew that GitHub provides a private package registry, but never used it before.

It appears that it is very simple solution to provide an internal maven package registry for sharing libraries within multiple projects, and accompanied with GitHub Actions you can easily setup an automated release pipeline.

Let's start.

## Step 1 Create a new library

I use Leiningen to start a new project, so let's first create a test library:
`$ lein new cljcloud/my-lib`

It will create a new project based on default Leiningen template, with `project.clj` file in the root.

We need to modify `project.clj` and add these lines:

```Clojure
  :repositories [["github" {:url "https://maven.pkg.github.com/your-org-name/your-repo-name"
                            :username "private-token"
                            :password :env/GITHUB_TOKEN}]]
```

Pls, update `your-org-name` and `your-repo-name` with real values accordingly.

It tells Leiningen to use GitHub private registry of your GitHub organization to push your package.

It's also possible to store packages at your personal account, you just need to use your GitHub username instead of `your-org-name`.

The line `:password :env/GITHUB_TOKEN` instructs Leiningen to read password from environment variable called `GITHUB_TOKEN`, we will get back to this latter.

I set `:username "private-token"` because we going to use GitHub private access token, and it doesn't matter what we specify for the username, it only needs to be a string.

Another thing that I changed is library version, I removed the `SNAPSHOT` from it, so that final `project.clj` looked like this:

```Clojure
(defproject cljcloud/my-lib "0.1.0"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :repositories [["github" {:url      "https://maven.pkg.github.com/cljcloud/my-lib"
                            :username "private-token"
                            :password :env/GITHUB_TOKEN}]]
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.10.3"]])

```


## Step 2 Set up GitHub Actions workflow to publish package

In order to create GitHub workflow, you need to make a new YAML file:

`.github/workflows/publish-package.yml`

with the following content:

```YAML
name: publish package
# Manually trigger
on:
  workflow_dispatch:
jobs:
  publish:
    runs-on: ubuntu-latest
    # Add permissions for GITHUB_TOKEN
    permissions: 
      contents: read
      packages: write 
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-java@v2
        with:
          java-version: '17'
          distribution: 'temurin'
      - name: Install clojure tools
        uses: DeLaGuardo/setup-clojure@3.5
        with:
          lein: 'latest'
      - name: Configure GPG Key
        run: echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --import
        env:
          GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
      - name: Publish package
        run: cd my-lib && lein deploy github
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

A few more things, by default Leiningen, requires GPG key to be present when running `lein deploy`, you can opt-in to not use it with the option: `:sign-releases false` in project.clj

```Clojure
  :repositories [["github" {:url      "https://maven.pkg.github.com/cljcloud/my-lib"
                            :username "private-token"
                            :password :env/GITHUB_TOKEN
                            :sign-releases false}]]
```
But it's not recommended, so I have created a new GPG key via:

`$ gpg --gen-key`

It will prompt for name, email, and passphrase and print out a key ID and details.
Then you can run this command to list your local keys:

`$ gpg --list-keys`

In GitHub Actions workflow we use `secrets.GPG_SIGNING_KEY`, which is our locally generated GPG signing key, exported as Base64 text.

To export your newly generated key, run:

`$ gpg --export-secret-keys YOUR_ID_HERE | base64 > private.key`

`YOUR_ID_HERE` - is a public key, long string with unique characters it will be printed after the key is generated or when you run list keys command.

`private.key` - is a file name that will be created in your current folder and will contain a Base64 representation of your private GPG key.

Next, you have to copy the `private.key` content and create a new secret in your GitHub repository with the name: `GPG_SIGNING_KEY`.

Note that we also pass environment variable: `GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}`, here we use the `secrets.GITHUB_TOKEN` which is automatically provided by GitHub Actions context, and for which we set the permissions at the top:

```YAML
    # Add permissions for GITHUB_TOKEN
    permissions: 
      contents: read
      packages: write 
```

That's it. Now when you commit and push your code to the repo, you will be able to find and run a new GitHub Actions workflow that should build and release your library as a private package to GitHub private registry.


## Step 3 Wait, so how do I use it now?

In order to use your package in your other projects, you have to add a similar line to `project.clj`:

```Clojure
  :repositories [["github" {:url      "https://maven.pkg.github.com/cljcloud/my-lib"
                            :username "private-token"
                            :password :env/GITHUB_TOKEN}]]
```

and specify your library as usual Leiningen dependency, like this:

```Clojure
  :dependencies [[org.clojure/clojure "1.10.3"]
                 [cljcloud/my-lib "0.1.20"]]
```

And when you run `lein deps` locally, Leiningen may fail, with 401 Unauthorized error, that's because you don't have a `GITHUB_TOKEN` environment variable, you can go to GitHub and generate one by following [these simple instructions](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token).

After that, you should set the environment variable via:

`$ export GITHUB_TOKEN="your personal access token"`

And retry with `lein deps` should work now.

## Related links

- [Leiningen sample project.clj](https://github.com/technomancy/leiningen/blob/master/sample.project.clj)
- [Leiningen DEPLOY command](https://github.com/technomancy/leiningen/blob/master/doc/DEPLOY.md)
- [Leiningen GPG usage](https://github.com/technomancy/leiningen/blob/master/doc/GPG.md)