Phase 3: Artifact migration

Importing packages with the CLI

The Cloudsmith CLI uploads one package per cloudsmith push command. Importing an exported repository means looping over its files and pushing each one, which is what the scripts on this page do. Most formats are one file per package and use the same script. Four situations need something more, and each has its own section: Maven groups several files into one package, Debian and RPM need a distribution, raw files need a name and version, and container images are copied registry to registry with no export at all.

Read Running a bulk import first. It covers the order to take repositories in, the service account, tags, rate limits, and verification that every section here relies on.

How it works

Exported folderone file per packagecloudsmith pushone command per file, in a loopCloudsmithupload, then sync queueRecord the identifier of every package. Tag it with its source. Verify when the queue drains.

Before you begin

To check what a push would do without uploading anything, add --dry-run to any cloudsmith push command.

Single-file formats

npm, NuGet, Python, Cargo, Composer, Ruby, Helm, Conda, and most other formats package everything into one file, and Cloudsmith reads the name and version from that file. See all supported formats.

FormatExported filesPush one package
npm.tgz, as produced by npm pack. Scope is read from the tarball.cloudsmith push npm my-org/my-repo my-package-1.0.0.tgz
NuGet.nupkgcloudsmith push nuget my-org/my-repo my.package.1.0.0.nupkg
Python.whl and .tar.gz. Each file is a separate upload; Cloudsmith groups them under one version.cloudsmith push python my-org/my-repo my_package-1.0.0-py3-none-any.whl
OthersThe format's native archivecloudsmith push FORMAT my-org/my-repo FILE

Push a folder

Save the following as push-folder.sh and make it executable with chmod +x push-folder.sh.

shell
#!/usr/bin/env bash
# Push every package file below a folder to a Cloudsmith repository with the Cloudsmith CLI,
# recording the identifier Cloudsmith assigns to each one.
#
# Usage:   ./push-folder.sh FORMAT OWNER/REPO FOLDER [EXTENSION]
# Example: ./push-folder.sh npm my-org/my-repo ./export/npm-local tgz
#
# Requires the Cloudsmith CLI and CLOUDSMITH_API_KEY in the environment.
# Set TAGS to override the tags applied to every package (default: migration).
set -u

format="$1"
repo="$2"
folder="$3"
tags="${TAGS:-migration}"
pattern="*"
[ "$#" -ge 4 ] && pattern="*.$4"   # no EXTENSION given: every file, including files without one

find "$folder" -type f -name "$pattern" -print0 |
  while IFS= read -r -d '' file; do
    echo "Pushing ${file}"
    if result="$(cloudsmith push "$format" "$repo" "$file" --no-wait-for-sync --tags "$tags" -F json 2>>push.log)"; then
      slug="$(printf '%s' "$result" | sed -n 's/.*"slug_perm": *"\([^"]*\)".*/\1/p')"
      printf '%s\t%s\t%s\n' "$file" "$slug" "$repo" >> pushed.tsv
    else
      echo "$file" >> push-failures.log
    fi
  done

The script pushes every file below the folder that matches the extension, uses --no-wait-for-sync so packages queue in Cloudsmith rather than blocking the script, and tags every package with migration or with whatever TAGS is set to. For each upload it appends the file path, the identifier Cloudsmith assigned (slug_perm), and the target repository to pushed.tsv; that file is your record of exactly what went where, and the verification step reads it. Runs against different repositories can share one manifest. Files the CLI could not upload go to push-failures.log, and the CLI's own output goes to push.log. Always give the extension: exports contain index files and checksum sidecars (.sha1, .md5, Packages, repodata/) that are not packages.

Note

Every cloudsmith push prints the new package's identifiers, even with --no-wait-for-sync, as Created: OWNER/REPO/SLUG (SLUG_PERM). With -F json the same appears on stdout as {"data": {"slug": ..., "slug_perm": ..., "status": "OK"}}, with progress on stderr, which is what the script captures. slug_perm is permanent; slug can change if the package is renamed. Keep the slug_perm.

Run it once per source repository, tagging each run with where it came from:

shell
TAGS=migration,npm-local ./push-folder.sh npm my-org/my-repo ./export/npm-local tgz
TAGS=migration,nuget-hosted ./push-folder.sh nuget my-org/my-repo ./export/nuget-hosted nupkg
TAGS=migration,pypi-local ./push-folder.sh python my-org/my-repo ./export/pypi-local whl
TAGS=migration,pypi-local ./push-folder.sh python my-org/my-repo ./export/pypi-local tar.gz

Re-running

Running the script twice over the same folder is safe. A package that already exists with the same name and version uploads, then fails to synchronize with a duplicate error, and the original is left untouched. To overwrite instead, add --republish to the cloudsmith push line. To retry only what failed, run the same cloudsmith push command over each path in push-failures.log.

Pushing in parallel

One push at a time is usually fast enough, because the upload is the slow part and Cloudsmith processes the queue in the background. For many small files, xargs -P runs several pushes at once. The CLI respects your rate limit and backs off automatically, so more than about four workers rarely helps.

The worker below does the same as the script above, so pushed.tsv and push-failures.log are written the same way and the verification step works unchanged.

shell
find ./export/npm-local -type f -name '*.tgz' -print0 |
  xargs -0 -P 4 -I{} sh -c '
    if result="$(cloudsmith push npm my-org/my-repo "$1" --no-wait-for-sync --tags migration -F json 2>>push.log)"; then
      printf "%s\t%s\t%s\n" "$1" "$(printf "%s" "$result" | sed -n "s/.*\"slug_perm\": *\"\([^\"]*\)\".*/\1/p")" my-org/my-repo >> pushed.tsv
    else
      echo "$1" >> push-failures.log
    fi' _ {}

Exporting from a NuGet feed

If your platform has no export tooling, NuGet packages can be pulled out through the feed itself with PowerShell's Save-Package after registering the feed as a package source, or with choco download for Chocolatey. Disable any upstream on the source feed first, or you also download everything it can resolve from nuget.org. The result is a folder of .nupkg files for push-folder.sh.

Maven, Gradle, and sbt

A Maven package is several files that belong together: the POM, the main artifact, and often -sources.jar and -javadoc.jar classifiers. Gradle and sbt publish into the same layout. Cloudsmith needs the POM to read the group, artifact, and version, so each version folder is pushed as one package with the POM attached.

shell
cloudsmith push maven my-org/my-repo my-library-1.0.0.jar \
  --pom-file my-library-1.0.0.pom \
  --sources-file my-library-1.0.0-sources.jar \
  --javadoc-file my-library-1.0.0-javadoc.jar

# A parent or BOM POM has no JAR. Push it on its own.
cloudsmith push maven my-org/my-repo my-parent-1.0.0.pom

Important

Cloudsmith reads the group, artifact, and version from the POM as written. Property placeholders such as ${project.version} are not resolved, and packages whose POM uses them for these coordinates fail to synchronize. Check status_reason after the import and fix those POMs by hand.

The generic folder script cannot pair files, so use this one. It walks the exported groupId/artifactId/version/ tree, finds every POM, and pushes it with the matching main artifact and classifiers when they exist. Checksum sidecars and maven-metadata.xml are ignored.

shell
#!/usr/bin/env bash
# Push an exported Maven repository layout (groupId/artifactId/version/files) to Cloudsmith.
# Each POM becomes one package, together with the main artifact (.jar, .war, .aar, or .zip)
# and any sources or javadoc JARs that share its name.
#
# Usage:   ./push-maven-folder.sh OWNER/REPO FOLDER
# Example: ./push-maven-folder.sh my-org/my-repo ./export/maven-releases
#
# Requires the Cloudsmith CLI and CLOUDSMITH_API_KEY in the environment.
# Set TAGS to override the tags applied to every package (default: migration).
set -u

repo="$1"
folder="$2"
tags="${TAGS:-migration}"

# Sorted so that snapshot builds upload oldest first and the newest becomes the current snapshot.
find "$folder" -type f -name '*.pom' -print0 | sort -z |
  while IFS= read -r -d '' pom; do
    base="${pom%.pom}"
    main=""
    for ext in jar war aar zip; do
      [ -f "${base}.${ext}" ] && main="${base}.${ext}" && break
    done
    args=()
    [ -f "${base}-sources.jar" ] && args+=(--sources-file "${base}-sources.jar")
    [ -f "${base}-javadoc.jar" ] && args+=(--javadoc-file "${base}-javadoc.jar")
    if [ -n "$main" ]; then
      echo "Pushing ${main}"
      result="$(cloudsmith push maven "$repo" "$main" --pom-file "$pom" "${args[@]}" --no-wait-for-sync --tags "$tags" -F json 2>>push.log)"
    else
      echo "Pushing ${pom}"
      result="$(cloudsmith push maven "$repo" "$pom" --no-wait-for-sync --tags "$tags" -F json 2>>push.log)"
    fi
    if [ $? -eq 0 ]; then
      printf '%s\t%s\t%s\n' "$pom" "$(printf '%s' "$result" | sed -n 's/.*"slug_perm": *"\([^"]*\)".*/\1/p')" "$repo" >> pushed.tsv
    else
      echo "$pom" >> push-failures.log
    fi
  done
shell
TAGS=migration,maven-releases ./push-maven-folder.sh my-org/my-repo ./export/maven-releases

Artifacts with other packaging or classifiers, such as -tests.jar, are not picked up. Push those individually with --pom-file and --extra-files, or extend the list of extensions in the script. See Maven repository for every option.

Note

Snapshot versions are re-timestamped on import. A snapshot uploaded as 1.0-20250418.082811-1 is stored under a new timestamp such as 1.0-20260902.155153-1, and the last one uploaded becomes the current snapshot. The script sorts POM paths before pushing so snapshot builds go up oldest first and the newest export ends up current. Builds that resolve 1.0-SNAPSHOT keep working; builds pinned to a specific snapshot timestamp need updating. If you only need the latest build of each snapshot, delete the older ones from the export before importing.

Debian, RPM, and Alpine

These packages are single files, but Cloudsmith also needs the distribution and release each one is for. It is passed as part of the repository argument, OWNER/REPO/DISTRO/RELEASE, so the import is the generic folder script with a longer repository argument and one decision first. List the values Cloudsmith accepts:

shell
cloudsmith list distros deb
cloudsmith list distros rpm
cloudsmith list distros alpine

Push each package to the distribution and release it was built and tested on, such as debian/bookworm, ubuntu/jammy, el/9, fedora/40, or alpine/v3.20. The alternative, any-distro/any-version, marks a package as installable everywhere.

Caution

A package built on Debian 11 against libssl1.1 installs without error on Debian 12, which ships libssl3, and then fails at runtime. The same happens with an RPM built against a newer glibc than the target has. Uploading to any-distro/any-version removes that protection. Use it only for packages with no compiled dependencies, such as scripts or configuration.

shell
cloudsmith push deb my-org/my-repo/debian/bookworm hello_2.10-3_amd64.deb
cloudsmith push rpm my-org/my-repo/el/9 hello-2.12.2-1.el9.x86_64.rpm

Export each distribution into its own folder and run the folder script once per folder. Exports also contain the generated dists/ or repodata/ index tree and checksum files; the extension filter skips them.

shell
TAGS=migration,deb-bookworm ./push-folder.sh deb my-org/my-repo/debian/bookworm ./export/deb-bookworm deb
TAGS=migration,rpm-el9 ./push-folder.sh rpm my-org/my-repo/el/9 ./export/rpm-el9 rpm

The same package cannot be uploaded to two distributions in one repository unless the repository allows overwrites; the second upload fails to synchronize as a duplicate. See Debian repository and RPM repository for source packages, components, and client setup.

Raw files

Generic repositories hold files in no package format: installers, archives, build outputs. Cloudsmith stores them as raw packages. The one decision is whether each file carries a name and version.

Pushed by filename only, a file is available at a filename URL, and features that depend on a version, such as retention rules, version sorting, and latest URLs, have nothing to work with:

shell
cloudsmith push raw my-org/my-repo app-config-1.2.3.zip
# https://dl.cloudsmith.io/TOKEN/OWNER/REPOSITORY/raw/files/app-config-1.2.3.zip

With a name and version, files are grouped in the web app, queryable by version, and served at versioned URLs including a latest alias:

shell
cloudsmith push raw my-org/my-repo app-config-1.2.3.zip --name app-config --version 1.2.3
# https://dl.cloudsmith.io/TOKEN/OWNER/REPOSITORY/raw/names/app-config/versions/1.2.3/app-config-1.2.3.zip
# https://dl.cloudsmith.io/TOKEN/OWNER/REPOSITORY/raw/names/app-config/versions/latest/app-config-1.2.3.zip

For filename-only pushes use push-folder.sh with the raw format. To set names and versions, derive them from your naming convention as you loop: for NAME-VERSION.EXTENSION files such as app-config-1.2.3.zip, the name is everything before the first hyphen followed by a digit and the version is the rest, minus the extension. Pass them with --name and --version on each push, and skip files that do not match rather than uploading them without a version.

Files in nested folders on the source platform lose their folder path on import, since Cloudsmith addresses raw packages by name and version rather than by path. If the path carries meaning, fold it into the name.

Verify

When a run finishes, check push-failures.log, then check the sync status of every package you pushed, by identifier, from pushed.tsv. Each line carries the repository it was pushed to, so one manifest can span runs against different repositories:

shell
while IFS=$'\t' read -r file slug repo; do
  status="$(cloudsmith status "$repo/$slug" -F json 2>/dev/null | sed -n 's/.*"status_str": *"\([^"]*\)".*/\1/p')"
  [ "$status" = "Completed" ] || echo "${status:-unknown}  ${repo}/${slug}  ${file}"
done < pushed.tsv

Anything printed is not yet installable: In Progress or Queued means wait, Failed means run cloudsmith status my-org/my-repo/SLUG to read the reason, then fix the package or resync it. The two reasons you will see most are a duplicate, when the same version already exists and overwrites are off, and a package the format parser could not read. An empty result means the package no longer exists, which after 24 hours is what a failed sync looks like.

A tag query is the coarser check, useful when you no longer have the manifest:

shell
cloudsmith list packages my-org/my-repo -q 'tag:npm-local status:failed'

See Verify the import for comparing counts with the source and what the statuses mean.

Next steps

Point clients at the new repositories using the setup guide for each format, then continue with Phase 4: Validation and decommissioning.