Migrating to Packr
Moving off Verdaccio, Sonatype Nexus or JFrog Artifactory. One tool,
@packr/migrate, with a source adapter per system.
A migration is always the same three steps — list what the source holds, fetch each artifact, publish it here — so most of this guide is the same whichever system you are leaving. The per-source sections cover what differs: authentication, which repositories are read, and what each system does not tell us.
What moves, and what does not
Moves: every version of every package the source holds, its artifacts (all of them — a Maven release is a jar and its pom), npm package metadata including dependencies, and npm dist-tags.
Does not move:
| Why | |
|---|---|
| Download counts and statistics | Not exposed by any source's API in a form worth reconstructing. Your new counts start at zero. |
| Users, teams and permissions | Different models. Recreate them here; see the token permissions guide. |
| Remote/proxy repositories | These are a cache of somebody else's registry, not your artifacts. Point Packr's own upstream proxies at the same upstreams instead. |
| Virtual/group repositories | A view over other repositories. Migrating the members gives you the contents once; migrating the view as well gives you everything twice. |
| Build info, promotion, retention rules | Product-specific. |
The tool never deletes anything on the source. A migration is a copy, and you can run it as many times as you like.
Before you start
Take an inventory. --dry-run lists what would move and publishes nothing —
and deliberately needs no credentials on the target, because seeing what you
have should not require an account:
npx @packr/migrate --source nexus \
--from https://nexus.example.com \
--to https://packr.example.com \
--source-user admin --source-password "$NEXUS_PASSWORD" \
--dry-run
Read the summary before running it for real. Two lines matter:
Packages: N Versions: M— compare against what you believe you have. A number far too small usually means a repository was not read: check the per-source notes below.Not migrated — no package coordinate for N file(s)— files the tool could not place. Artifacts that cannot be placed are artifacts that will not migrate, so they are named rather than dropped quietly.
Running the migration
npx @packr/migrate --source <system> \
--from <source-url> --to <packr-url> \
--source-user <user> --source-password <password> \
--user <packr-user> --password <packr-password> \
--concurrency 5
--user/--password log in and mint a publish token for the run. If you
already have a token, pass --token instead. (A login JWT on its own only
publishes npm — Maven, PyPI and NuGet authenticate against stored tokens — which
is why the tool mints rather than reusing the login.)
Versions publish oldest first, so latest is sane even before tags are applied,
and dist-tags are replicated afterwards, once the versions they point at exist.
The run exits non-zero if anything failed, and prints a line per failure. A scripted migration cannot report success while packages are missing.
Re-running is safe. A version that already exists is refused by the registry and counted as a failure, not a corruption. To resume an interrupted run, run it again with the same arguments and read the failure list.
Verdaccio, npm Enterprise, or any npm registry
--source verdaccio --from http://verdaccio.example.com:4873
Uses the npm protocol, so it works against anything that speaks it, npmjs.org
included. --scope @myorg narrows to one scope. --source-token authenticates
if the source requires it.
The package list comes from the source's search endpoint. A registry with search disabled will report nothing to migrate — in that case migrate scope by scope, or use one of the other sources if the registry also exposes a listing API.
Sonatype Nexus 3
--source nexus --from https://nexus.example.com \
--source-user admin --source-password "$NEXUS_PASSWORD"
Reads the repository list and the components API, covering npm, maven2, pypi,
nuget and raw repositories. --repository npm-hosted (repeatable) narrows to
named repositories.
Notes:
- Group repositories are skipped. Migrating a group as well as its members publishes everything twice.
- npm dist-tags come from the packument, not the components API, which does not report them. Same for package metadata: the components API knows coordinates and files, not dependencies.
- Formats with no equivalent here (docker, rubygems, and so on) are left out of the listing rather than failing artifact by artifact.
Verified end to end against a real Nexus 3.96: packages/migrate/test/nexus-e2e.sh
publishes into a live instance, migrates into a live registry and compares the
bytes that land.
JFrog Artifactory
--source artifactory --from https://artifactory.example.com/artifactory \
--source-token "$ARTIFACTORY_TOKEN"
--source-token is sent as a Bearer token (an access token or API key);
--source-user/--source-password use Basic auth instead. Covers npm,
maven/gradle, pypi and nuget local repositories.
Notes:
- Only local repositories are read. Remote repositories cache somebody else's registry; virtual repositories are a view over others.
- Coordinates are reconstructed from paths. Artifactory's coordinate query language (AQL) is Pro-only, so the tool uses the file listing every edition exposes and derives coordinates from the layout. This is the source most likely to report files it cannot place — read that part of the summary.
maven-metadata.xml, checksum files and Artifactory's own dot-directories are skipped; the registry recomputes what it needs.- Edition: the adapter needs no Pro feature — it reads the repository list and the file listing, both of which OSS serves. Note that OSS itself only hosts Maven/Gradle and Generic repositories, so an OSS instance has no npm, PyPI or NuGet packages to migrate in the first place.
GitHub Packages
--source github --from my-org --source-token "$GITHUB_PAT"
--from is the organisation — or a user account with --github-user — not a
URL: GitHub Packages is one hosted service, so what varies is whose packages to
read. The token needs read:packages.
Covers npm and NuGet.
Notes:
- npm packages are published under the owner as their scope. GitHub's API
reports a package as
widgetwhile its registry serves@my-org/widget. - Tarball URLs, metadata and dist-tags come from the packument. GitHub's download paths are opaque, so constructing them is guesswork.
- Maven is not supported, and says so per package. GitHub reports a Maven
coordinate as one dotted string —
com.example.widgetcould bewidgetincom.exampleorexample.widgetincom— and its Maven registry serves no file listing, so the files of a version are unknown. Guessing the split would publish real bytes under a coordinate nobody will look for; guessing the files would silently drop sources and javadoc jars. Those packages appear in the "no package coordinate" list rather than vanishing from the run. Republish them from the build that produced them, or migrate them from a mirror that does report coordinates.
Verified against live GitHub Packages: a 12-package, 19-version org migrated
with every tarball byte-identical (sha256) to what GitHub served, and dist-tags
and dependencies intact. packages/migrate/test/github-e2e.sh runs that against
any org, given a token.
Verifying what landed
Counts first, then a spot check of something you know well:
# per-ecosystem totals on the target
curl -s https://packr.example.com/api/v1/ecosystems/stats | jq
# one package, end to end
npm view --registry https://packr.example.com @myorg/widget versions
npm view --registry https://packr.example.com @myorg/widget dist-tags
For Maven, fetch a jar and its pom — a jar without its pom resolves and then fails to build:
curl -sfI https://packr.example.com/maven/com/example/widget/3.1.0/widget-3.1.0.jar
curl -sfI https://packr.example.com/maven/com/example/widget/3.1.0/widget-3.1.0.pom
If a package matters, install it from the new registry into a clean directory before you cut over. Dependency resolution is the thing a version count cannot check for you.
Cutting over
Point toolchains at the registry, one ecosystem at a time. packr-cli config
writes most of these for you; see the CLI guide.
# .npmrc
@myorg:registry=https://packr.example.com
//packr.example.com/:_authToken=${PACKR_TOKEN}
<!-- settings.xml -->
<repository>
<id>packr</id>
<url>https://packr.example.com/maven</url>
</repository>
# pip.conf
[global]
index-url = https://packr.example.com/pypi/simple/
<!-- nuget.config -->
<add key="packr" value="https://packr.example.com/nuget/v3/index.json" />
Keep the old system readable until the first full build cycle has passed on the new one. Nothing about the migration depends on it staying up, but a rollback you did not need is cheaper than one you cannot do.