# 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](/docs/tokens). |
| Remote/proxy repositories | These are a cache of somebody else's registry, not your artifacts. Point Packr's own [upstream proxies](/docs/operations) 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:

```bash
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

```bash
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

```bash
--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

```bash
--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

```bash
--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

```bash
--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:

```bash
# 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:

```bash
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](/docs/cli).

```ini
# .npmrc
@myorg:registry=https://packr.example.com
//packr.example.com/:_authToken=${PACKR_TOKEN}
```

```xml
<!-- settings.xml -->
<repository>
  <id>packr</id>
  <url>https://packr.example.com/maven</url>
</repository>
```

```ini
# pip.conf
[global]
index-url = https://packr.example.com/pypi/simple/
```

```xml
<!-- 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.
