Skip to content

orbital migrate

orbital migrate is the cross-importer suite that drives a full migration from another artifact registry into OrbitalReg. It wraps seven source backends (JFrog, Nexus, GitHub, GitLab, Azure, AWS CodeArtifact, Google Artifact Registry) behind a single command tree with deterministic exit codes, resumable runs, and read-only previews at every step.

This page is the reference — flag tables, exit codes, and copy- paste recipes. For the strategy walkthrough and per-source playbooks, see the migration overview and the per-source guides (JFrog, Nexus, GitLab, GitHub Packages, cloud registries).

Subcommand at a glance

SubcommandPurposeMutates?Run as
planInventory the source registry; emit a JSON envelopeNoOperator (one-shot)
applyProvision OrbitalReg projects + repos from the planYes (project / repos)Operator (resumable)
verifysha256-compare migrated artifacts against the sourceNoOperator or CI gate
finalizeFlip warmed lazy-proxy repos to local-modeYes with --convertOperator or scheduled

The full suite is a thin presentation layer over four admin endpoints under /api/admin/migrate/*; every backend the bulk-import surface already supports flows through unchanged. The CLI never persists the source credential anywhere — it lives in the request body and is dropped at the end of each call.

Authentication

Every subcommand calls OrbitalReg via the active CLI profile. Run orbital auth login first; the per-call --token flag carries the source-registry credential, not the OrbitalReg one.

Supported sources

--sourceSource registryAuth shapeRequired extras
jfrogJFrog Artifactorybearer / API token--endpoint
nexusSonatype Nexus 3password or user:password--endpoint
githubGitHub PackagesPAT (read:packages)--github-org
gitlabGitLab Package RegistryPAT--gitlab-project-id xor --gitlab-group-id
azureAzure ArtifactsPAT--azure-organization
awscodeartifactAWS CodeArtifactkeys:AKIA…:secret[:session] or bearer:<token>--aws-region, --aws-domain
gcparhybridGoogle Artifact Registrysa:<service-account JSON> or bearer:<oauth>--gcp-project-id, --gcp-location

The canonical list lives in api/internal/migrate.SupportedSources. New adapters land in lock-step with the bulk-import handler, so a backend that the import dispatcher accepts is automatically reachable from orbital migrate too.

Exit codes

Every subcommand obeys the same deterministic mapping so a CI pipeline can branch on the exit code without parsing JSON:

CodeMeaning
0Success
1User error (bad flag, conflicting options, OrbitalReg returned 400)
2Auth error (no OrbitalReg token, OrbitalReg returned 401 / 403)
3Server / source error (OrbitalReg returned 5xx, source-registry unreachable)

Source-side discovery failures surface as 502 Bad Gateway from the admin endpoint, so a wrong source token maps to exit code 3 (server) rather than 1 (user). The grammar is "1 means fix your flags; 3 means the server or the source is misbehaving."

orbital migrate plan

Inventory the source registry, sample the size, and emit a JSON envelope apply will consume.

bash
orbital migrate plan \
  --source <jfrog|nexus|github|gitlab|azure|awscodeartifact|gcparhybrid> \
  --token <source-token> \
  [--endpoint <url>] \
  [--strategy big-bang|blue-green|lazy-proxy] \
  [--output-file <path>] \
  [--json] \
  <per-source extras>

Flags

FlagDefaultNotes
--source(required)One of the seven sources above
--token(required)Source-registry credential
--endpointvariesRequired for jfrog / nexus; auto-derived for cloud sources
--strategyautoOverride the recommendation (see thresholds)
--output-filePersist the JSON envelope for apply
--jsonfalseEmit JSON to stdout instead of pretty-print
--github-orgRequired for --source github
--github-package-typeOptional filter: container / npm / maven / rubygems / nuget
--gitlab-project-id xor --gitlab-group-idExactly one is required for --source gitlab
--gitlab-include-containerfalseInclude the project/group container registry
--azure-organizationRequired for --source azure
--azure-project, --azure-feedOptional Azure scope filters
--aws-region, --aws-domainRequired for --source awscodeartifact
--aws-domain-owner, --aws-repository-filterOptional AWS scope filters
--gcp-project-id, --gcp-locationRequired for --source gcparhybrid
--gcp-repositorySingle GAR repo (optional; default = enumerate the whole project)

Strategy recommendation

The planner picks one of three strategies based on the sampled total-size estimate:

Total sizeStrategyEstimated window
< 100 GBbig-bang30 min – 4 h downtime depending on size
100 GB – 10 TBblue-greensingle-change cutover, double-write warm-up
> 10 TBlazy-proxyremote-mode for 7 – 30 days, then finalize

Pass --strategy to override the heuristic when compliance, change windows, or peer-review constraints already dictate the answer.

For the long-form rationale see the migration overview "Three strategies" section and migrate plan playbook.

orbital migrate apply

Consume a plan envelope and provision the OrbitalReg-side project plus repositories. Resumable across crashes and operator-initiated retries.

bash
orbital migrate apply \
  --plan <path> \
  [--strategy big-bang|blue-green|lazy-proxy] \
  [--project-slug <slug>] \
  [--dry-run] \
  [--resume] \
  [--json]

Flags

FlagDefaultNotes
--plan(required)Path to the JSON envelope produced by migrate plan --output-file
--strategyplan recommendationOverride the strategy
--project-slug<source>-importWhere the new repositories land
--dry-runfalsePreview every provision step; never writes
--resumefalseContinue an interrupted apply for the same (source, endpoint, project) tuple
--jsonfalseEmit JSON instead of pretty-print

Strategy semantics

StrategyRepository kindUpstreamApply behaviour
big-banglocalunsetProvisions empty placeholders; the artifact walk drains via the bulk-import dispatcher
blue-greenlocalunsetSame as big-bang but consumers double-write to OrbitalReg + source during cutover
lazy-proxyremotesource URLRepository is immediately serving — first-time pulls hit the source, subsequent pulls cache locally

Apply persists per-run state in migration_runs (migration 101) so a crashed run is idempotent and --resume short-circuits already- provisioned repositories without re-creating them.

Skipped row taxonomy

Apply emits a skipped list rather than failing the whole run when an individual repo can't land. Reasons:

  • unsupported format: <format> — the source advertised a format outside the OrbitalReg repo_format enum.
  • repo key normalises to empty slug — sanitisation reduced the source key to nothing (e.g. all-special-character names).
  • repository <project>/<slug> already exists — idempotent re-runs surface this so the operator can audit collisions.
  • Server-side errors carry the underlying pgx message verbatim.

Idempotency

Apply is idempotent at the (project_slug, repo_slug) tuple. A re-run without --resume flags every row as already exists rather than crashing; with --resume the row is skipped silently. Operators sometimes intentionally re-apply with a tweaked plan after editing the JSON envelope; both shapes converge cleanly.

orbital migrate verify

Read-only sha256 spot-check between the source and OrbitalReg. Recommended after every apply and before flipping a lazy-proxy to local with finalize --convert.

bash
orbital migrate verify \
  --plan <path> \
  --token <source-token> \
  [--sample <count>] \
  [--format <maven|npm|docker|...>] \
  [--project-slug <slug>] \
  [--json]

Flags

FlagDefaultNotes
--plan(required)Same envelope apply consumed
--token(required)Source-registry credential (same shape as plan)
--sample100Total samples (split evenly across migrated repos)
--format(all)Limit to one format if you want a fast targeted check
--project-slug<source>-importMust match the apply destination
--jsonfalseEmit JSON instead of pretty-print

Decision taxonomy

Each comparison lands in one of six buckets. PASS counts only match; everything else trips the FAIL path and exit code 3.

DecisionMeaningAction
matchsha256 + size agreeNone
no_source_hashSource returned no sha256 (rare)Re-pull source manifest, retry
missing_on_orbitalApply hasn't drained this artifact yetWait for the dispatcher; re-verify
sha256_mismatchMost actionable failureRe-import the affected artifact
size_mismatchBytes differ even if hash agreesFile a bug — should not happen
orbital_repo_missingPlan references a repo that's not provisionedRe-run apply

The server caps each repo at 250 samples to keep call duration bounded. Out-of-scope synthetic format-native pulls (npm pack, mvn dependency:get, docker pull) and permission spot-checks live in follow-up work.

orbital migrate finalize

Capstone for the lazy-proxy strategy. Read-only by default — reports which repositories have warmed to a configurable cache-hit threshold; pass --convert to flip qualifying repositories from remote-mode to local-mode.

bash
orbital migrate finalize \
  --plan <path> \
  [--threshold <0..100>] \
  [--min-samples <n>] \
  [--window-days <1..365>] \
  [--convert] \
  [--project-slug <slug>] \
  [--json]

Flags

FlagDefaultNotes
--plan(required)The same envelope used by apply
--threshold95Minimum cache-hit-rate percent that triggers conversion
--min-samples100Minimum pulls in the window before the rate is trustworthy
--window-days30Look-back window for cache-hit-rate computation (max 365)
--convertfalseActually flip qualifying repos from remote to local (otherwise read-only preview)
--project-slug<source>-importMust match the apply destination
--jsonfalseEmit JSON instead of pretty-print

Decision taxonomy

DecisionMeaning
converted--convert flipped the row from remote → local
qualifiesRead-only preview: would convert with --convert
below_thresholdCache-hit rate hasn't reached --threshold yet
insufficient_samplesFewer pulls than --min-samples; rate not trustworthy
already_localRe-run after a previous --convert; no-op
repo_missingPlan references a repo that's not provisioned

Cache-hit-rate derivation

OrbitalReg does not stamp a per-pull "served from cache" flag on artifact_pulls. Finalize derives the rate from the pull pattern itself:

text
pulls_total        = COUNT(*)              FROM artifact_pulls (per repo, --window-days)
distinct_artifacts = COUNT(DISTINCT artifact_id)
cache_hit_rate     = (pulls_total - distinct_artifacts) / pulls_total * 100

Each artifact's first pull is a probable upstream miss; every subsequent pull of the same artifact is a cache hit. The proxy biases conservative — repositories with a short upstream-revalidation TTL under-report — which means finalize keeps repositories in remote-mode slightly longer than strictly necessary. That's the safe direction.

End-to-end recipes

Big-bang (< 100 GB) — JFrog

bash
# Inventory + persist plan
orbital migrate plan \
  --source jfrog \
  --endpoint https://jfrog.example.com \
  --token "$JFROG_TOKEN" \
  --output-file plan.json

# Provision projects + repos
orbital migrate apply --plan plan.json --strategy big-bang

# (bulk-import dispatcher drains — wait for the queue to empty)

# sha256 sample
orbital migrate verify --plan plan.json --token "$JFROG_TOKEN" --sample 500

Blue-green (1 TB) — Nexus

bash
orbital migrate plan \
  --source nexus \
  --endpoint https://nexus.example.com \
  --token 'admin:'"$NEXUS_PASSWORD" \
  --output-file plan.json

orbital migrate apply --plan plan.json --strategy blue-green
# CI pipelines now double-write to OrbitalReg + Nexus during the warm-up.

# Weekly verify during cutover
orbital migrate verify --plan plan.json --token 'admin:'"$NEXUS_PASSWORD" --sample 1000

Lazy-proxy (50 TB) — AWS CodeArtifact

bash
# Provision remote-mode shells
orbital migrate plan \
  --source awscodeartifact \
  --aws-region eu-west-1 --aws-domain acme \
  --token 'keys:'"$AWS_ACCESS_KEY"':'"$AWS_SECRET_KEY" \
  --output-file plan.json

orbital migrate apply --plan plan.json --strategy lazy-proxy

# Cache fills during normal pull traffic over 7-30 days.

# Daily preview — surfaces which repos have warmed
orbital migrate finalize --plan plan.json --threshold 95

# Weekly verify on the warmed subset
orbital migrate verify --plan plan.json \
  --token 'keys:'"$AWS_ACCESS_KEY"':'"$AWS_SECRET_KEY" \
  --sample 250

# When an entire format has warmed, flip it
orbital migrate finalize --plan plan.json --threshold 95 --convert

CI gate — fail PRs on byte-divergence

yaml
# .gitlab-ci.yml
verify-migration:
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
  script:
    - orbital auth login --endpoint "$ORBITAL_URL" --token "$ORBITAL_TOKEN"
    - orbital migrate verify --plan plan.json --token "$JFROG_TOKEN" --sample 500
  # Exit code 3 (server / source error or FAIL verdict) trips the GitLab job.

Tab completion

Every orbital migrate flag is registered through cobra, so orbital completion emits scripts for bash, zsh, fish, and powershell that include the migrate subtree out of the box. Install once per shell:

bash
# bash (Linux)
orbital completion bash | sudo tee /etc/bash_completion.d/orbital >/dev/null

# bash (macOS, with Homebrew bash-completion@2)
orbital completion bash > "$(brew --prefix)/etc/bash_completion.d/orbital"

# zsh (any directory in $fpath)
orbital completion zsh > "${fpath[1]}/_orbital"

# fish
orbital completion fish > ~/.config/fish/completions/orbital.fish

# PowerShell
orbital completion powershell | Out-String | Invoke-Expression

After a fresh shell, type orbital migrate <Tab> to cycle through plan, apply, verify, finalize, plus the source-registry list on --source <Tab>.

See also

Released under the Apache-2.0 License.