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
| Subcommand | Purpose | Mutates? | Run as |
|---|---|---|---|
plan | Inventory the source registry; emit a JSON envelope | No | Operator (one-shot) |
apply | Provision OrbitalReg projects + repos from the plan | Yes (project / repos) | Operator (resumable) |
verify | sha256-compare migrated artifacts against the source | No | Operator or CI gate |
finalize | Flip warmed lazy-proxy repos to local-mode | Yes with --convert | Operator 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
--source | Source registry | Auth shape | Required extras |
|---|---|---|---|
jfrog | JFrog Artifactory | bearer / API token | --endpoint |
nexus | Sonatype Nexus 3 | password or user:password | --endpoint |
github | GitHub Packages | PAT (read:packages) | --github-org |
gitlab | GitLab Package Registry | PAT | --gitlab-project-id xor --gitlab-group-id |
azure | Azure Artifacts | PAT | --azure-organization |
awscodeartifact | AWS CodeArtifact | keys:AKIA…:secret[:session] or bearer:<token> | --aws-region, --aws-domain |
gcparhybrid | Google Artifact Registry | sa:<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:
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | User error (bad flag, conflicting options, OrbitalReg returned 400) |
| 2 | Auth error (no OrbitalReg token, OrbitalReg returned 401 / 403) |
| 3 | Server / 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.
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
| Flag | Default | Notes |
|---|---|---|
--source | (required) | One of the seven sources above |
--token | (required) | Source-registry credential |
--endpoint | varies | Required for jfrog / nexus; auto-derived for cloud sources |
--strategy | auto | Override the recommendation (see thresholds) |
--output-file | — | Persist the JSON envelope for apply |
--json | false | Emit JSON to stdout instead of pretty-print |
--github-org | — | Required for --source github |
--github-package-type | — | Optional filter: container / npm / maven / rubygems / nuget |
--gitlab-project-id xor --gitlab-group-id | — | Exactly one is required for --source gitlab |
--gitlab-include-container | false | Include the project/group container registry |
--azure-organization | — | Required for --source azure |
--azure-project, --azure-feed | — | Optional Azure scope filters |
--aws-region, --aws-domain | — | Required for --source awscodeartifact |
--aws-domain-owner, --aws-repository-filter | — | Optional AWS scope filters |
--gcp-project-id, --gcp-location | — | Required for --source gcparhybrid |
--gcp-repository | — | Single 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 size | Strategy | Estimated window |
|---|---|---|
| < 100 GB | big-bang | 30 min – 4 h downtime depending on size |
| 100 GB – 10 TB | blue-green | single-change cutover, double-write warm-up |
| > 10 TB | lazy-proxy | remote-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.
orbital migrate apply \
--plan <path> \
[--strategy big-bang|blue-green|lazy-proxy] \
[--project-slug <slug>] \
[--dry-run] \
[--resume] \
[--json]Flags
| Flag | Default | Notes |
|---|---|---|
--plan | (required) | Path to the JSON envelope produced by migrate plan --output-file |
--strategy | plan recommendation | Override the strategy |
--project-slug | <source>-import | Where the new repositories land |
--dry-run | false | Preview every provision step; never writes |
--resume | false | Continue an interrupted apply for the same (source, endpoint, project) tuple |
--json | false | Emit JSON instead of pretty-print |
Strategy semantics
| Strategy | Repository kind | Upstream | Apply behaviour |
|---|---|---|---|
big-bang | local | unset | Provisions empty placeholders; the artifact walk drains via the bulk-import dispatcher |
blue-green | local | unset | Same as big-bang but consumers double-write to OrbitalReg + source during cutover |
lazy-proxy | remote | source URL | Repository 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 OrbitalRegrepo_formatenum.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
pgxmessage 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.
orbital migrate verify \
--plan <path> \
--token <source-token> \
[--sample <count>] \
[--format <maven|npm|docker|...>] \
[--project-slug <slug>] \
[--json]Flags
| Flag | Default | Notes |
|---|---|---|
--plan | (required) | Same envelope apply consumed |
--token | (required) | Source-registry credential (same shape as plan) |
--sample | 100 | Total samples (split evenly across migrated repos) |
--format | (all) | Limit to one format if you want a fast targeted check |
--project-slug | <source>-import | Must match the apply destination |
--json | false | Emit 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.
| Decision | Meaning | Action |
|---|---|---|
match | sha256 + size agree | None |
no_source_hash | Source returned no sha256 (rare) | Re-pull source manifest, retry |
missing_on_orbital | Apply hasn't drained this artifact yet | Wait for the dispatcher; re-verify |
sha256_mismatch | Most actionable failure | Re-import the affected artifact |
size_mismatch | Bytes differ even if hash agrees | File a bug — should not happen |
orbital_repo_missing | Plan references a repo that's not provisioned | Re-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.
orbital migrate finalize \
--plan <path> \
[--threshold <0..100>] \
[--min-samples <n>] \
[--window-days <1..365>] \
[--convert] \
[--project-slug <slug>] \
[--json]Flags
| Flag | Default | Notes |
|---|---|---|
--plan | (required) | The same envelope used by apply |
--threshold | 95 | Minimum cache-hit-rate percent that triggers conversion |
--min-samples | 100 | Minimum pulls in the window before the rate is trustworthy |
--window-days | 30 | Look-back window for cache-hit-rate computation (max 365) |
--convert | false | Actually flip qualifying repos from remote to local (otherwise read-only preview) |
--project-slug | <source>-import | Must match the apply destination |
--json | false | Emit JSON instead of pretty-print |
Decision taxonomy
| Decision | Meaning |
|---|---|
converted | --convert flipped the row from remote → local |
qualifies | Read-only preview: would convert with --convert |
below_threshold | Cache-hit rate hasn't reached --threshold yet |
insufficient_samples | Fewer pulls than --min-samples; rate not trustworthy |
already_local | Re-run after a previous --convert; no-op |
repo_missing | Plan 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:
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 * 100Each 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
# 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 500Blue-green (1 TB) — Nexus
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 1000Lazy-proxy (50 TB) — AWS CodeArtifact
# 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 --convertCI gate — fail PRs on byte-divergence
# .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 (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-ExpressionAfter a fresh shell, type orbital migrate <Tab> to cycle through plan, apply, verify, finalize, plus the source-registry list on --source <Tab>.
See also
- Migration overview — strategy ladder and per-source playbooks
migrate planwalkthrough — long-form examplesmigrate applywalkthroughmigrate verifywalkthroughmigrate finalizewalkthroughorbitalCLI overview — install, authenticate, configure profiles