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 usernameMVN_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:
|
|
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
.
|
|
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:
|
|
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
.
|
|
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:
|
|
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:
|
|
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>
indistributionManagement
must equal the<server><id>
insettings.xml
. - Missing scopes. Use
repo
,write:packages
, andread: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
andartifactId
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.