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
Before you begin
- Install the Cloudsmith CLI.
- Export the API key of the migration service account as
CLOUDSMITH_API_KEY. - Create the target repository in Cloudsmith.
- Export the source repository to a folder, keeping its layout. See JFrog Artifactory or Sonatype Nexus.
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.
| Format | Exported files | Push 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 | .nupkg | cloudsmith 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 |
| Others | The format's native archive | cloudsmith 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.
#!/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
doneThe 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 pushprints the new package's identifiers, even with--no-wait-for-sync, asCreated: OWNER/REPO/SLUG (SLUG_PERM). With-F jsonthe same appears on stdout as{"data": {"slug": ..., "slug_perm": ..., "status": "OK"}}, with progress on stderr, which is what the script captures.slug_permis permanent;slugcan change if the package is renamed. Keep theslug_perm.
Run it once per source repository, tagging each run with where it came from:
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.gzRe-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.
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.
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.pomImportant
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. Checkstatus_reasonafter 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.
#!/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
doneTAGS=migration,maven-releases ./push-maven-folder.sh my-org/my-repo ./export/maven-releasesArtifacts 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-1is stored under a new timestamp such as1.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 resolve1.0-SNAPSHOTkeep 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:
cloudsmith list distros deb
cloudsmith list distros rpm
cloudsmith list distros alpinePush 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.1installs without error on Debian 12, which shipslibssl3, and then fails at runtime. The same happens with an RPM built against a newerglibcthan the target has. Uploading toany-distro/any-versionremoves that protection. Use it only for packages with no compiled dependencies, such as scripts or configuration.
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.rpmExport 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.
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 rpmThe 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:
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.zipWith a name and version, files are grouped in the web app, queryable by version, and served at versioned URLs
including a latest alias:
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.zipFor 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:
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.tsvAnything 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:
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.