Documentation

Migrating to Packr

Moving off Verdaccio, Nexus or Artifactory: what moves, what does not, and how to verify.

Source: api.packr.blueforge.studio/-/docs/migrating

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 statisticsNot exposed by any source's API in a form worth reconstructing. Your new counts start at zero.
Users, teams and permissionsDifferent models. Recreate them here; see the token permissions guide.
Remote/proxy repositoriesThese are a cache of somebody else's registry, not your artifacts. Point Packr's own upstream proxies at the same upstreams instead.
Virtual/group repositoriesA 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 rulesProduct-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 widget while 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.widget could be widget in com.example or example.widget in com — 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.