Making Private Maven Packages Work on GitHub

Publishing private Java libraries to GitHub Packages sounds simple until everything is private. Default repo tokens help in a few places, but a clean setup needs a Personal Access Token, a couple of small Maven tweaks, and two short GitHub Actions workflows. Here is the end‑to‑end recipe that has been reliable across multiple repositories without extra infrastructure.

1) Create a Personal Access Token

You will need a GitHub Personal Access Token (PAT) with the following scopes:

  • repo (for private repositories)
  • write:packages
  • read:packages

Create it under Settings → Developer settings → Personal access tokens. Copy it once and store it as an organization secret if possible, so multiple repos can use the same credentials safely. Name suggestions:

  • MVN_USER → your GitHub username
  • MVN_TOKEN → the PAT you just created

2) Tell Maven where to publish

In your library’s pom.xml, point distributionManagement at the package feed for the repository that will own the package:

1
2
3
4
5
6
7
<distributionManagement>
  <repository>
    <id>github</id>
    <name>GitHub Packages</name>
    <url>https://maven.pkg.github.com/OWNER/REPO</url>
  </repository>
</distributionManagement>

Replace OWNER with your user or org, and REPO with the repository that hosts the package. The <id> value must match the server id you will define in settings.xml.

3) Configure settings.xml

Run mvn -X to see which settings.xml Maven is using. Add a server and a profile. The server id must match the one in distributionManagement.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<settings>
  <servers>
    <server>
      <id>github</id>
      <username>${env.MVN_USER}</username>
      <password>${env.MVN_TOKEN}</password>
    </server>
  </servers>

  <profiles>
    <profile>
      <id>github</id>
      <repositories>
        <repository>
          <id>github</id>
          <url>https://maven.pkg.github.com/OWNER/*</url>
          <snapshots><enabled>true</enabled></snapshots>
        </repository>
        <repository>
          <id>central</id>
          <url>https://repo1.maven.org/maven2</url>
        </repository>
      </repositories>
    </profile>
  </profiles>

  <activeProfiles>
    <activeProfile>github</activeProfile>
  </activeProfiles>
</settings>

Notes:

  • The OWNER/* wildcard lets Maven resolve packages from any repository under that owner. This avoids hard‑coding a single repo per dependency.
  • Using ${env.MVN_USER} and ${env.MVN_TOKEN} keeps secrets out of files; the CI job will provide them via environment variables.

4) Dry‑run a local deploy

From the library project, run:

1
mvn -B -DskipTests deploy

If you see a 401 Unauthorized, check scopes on the PAT and that the server id matches distributionManagement. If you see a conflict, bump the version before redeploying.

5) Publish automatically with GitHub Actions

Add a workflow to the library repository to publish on every push to main (or on tagged releases if you prefer). This uses the built‑in GITHUB_TOKEN for publishing to the current repo’s package feed, plus a self‑contained settings.xml written by actions/setup-java.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
name: Build and Deploy Package

on:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          server-id: github
          cache: 'maven'
          settings-path: ${{ github.workspace }}

      - name: Publish to GitHub Packages
        run: mvn -B -DskipTests deploy -s $GITHUB_WORKSPACE/settings.xml
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Why this works: the workflow publishes to the same repo that hosts the workflow, which GitHub allows using GITHUB_TOKEN. No PAT needed here.

6) Consume the package in another private project

In the consumer project’s pom.xml, point repositories at the owner’s feed and add the dependency:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<repositories>
  <repository>
    <id>github</id>
    <name>GitHub Packages</name>
    <url>https://maven.pkg.github.com/OWNER/*</url>
  </repository>
</repositories>

<dependency>
  <groupId>com.example</groupId>
  <artifactId>my-package</artifactId>
  <version>1.0.0</version>
</dependency>

For local builds, re‑use the same settings.xml pattern from step 3 so Maven can authenticate with your PAT.

7) Build consumers in CI with a PAT

When a different repository needs to download your private package inside GitHub Actions, the default token usually is not enough. Use your org secrets instead:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
name: Build Consumer

on: [ push, pull_request ]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: read

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: 'maven'
          server-id: github
          server-username: MVN_USER
          server-password: MVN_TOKEN

      - name: Build with Maven
        run: mvn -B clean verify -Pgithub
        env:
          MVN_USER: ${{ secrets.MVN_USER }}
          MVN_TOKEN: ${{ secrets.MVN_TOKEN }}

This injects credentials into the generated settings.xml, allowing Maven to fetch your private packages during the build.

8) Common pitfalls to avoid

  • Wrong owner or repo in URLs. https://maven.pkg.github.com/OWNER/REPO for deploys, https://maven.pkg.github.com/OWNER/* for resolving across many repos.
  • Server id mismatch. The <id> in distributionManagement must equal the <server><id> in settings.xml.
  • Missing scopes. Use repo, write:packages, and read:packages on the PAT.
  • Publishing snapshots vs releases. Keep versions moving; GitHub will reject overwrites. Use -SNAPSHOT during development.
  • GroupId drift. Consumers must declare the exact groupId and artifactId that you publish.
  • Using org name as username. Authentication uses your personal GitHub username, not the organization name.
  • Caching confusion. If a build pulls the wrong version, clear the Maven cache on the runner or bump the version to force a refresh.

That is the complete loop: one PAT for cross‑repo reads, a safe settings.xml pattern, and two small workflows that publish and consume packages without extra servers. It is simple, repeatable, and friendly to private repositories.

Thanks for reading. If you have a leaner pattern for consumer CI without a PAT, share it so others can benefit.


↤ Previous Post
Next Post ↦