# How-to: pack and publish a Solution

> This page picks up where [Your first pipeline](./first-pipeline.md) stops. It assumes you already have the three-command flow working on a single tenant and now want to ship the same Solution across **dev → stage → prod**, pin versions for reproducibility, and know exactly how to back out a bad release.

This page picks up where [Your first pipeline](./first-pipeline.md) stops. It assumes you already have the three-command flow working on a single tenant and now want to ship the same Solution across **dev → stage → prod**, pin versions for reproducibility, and know exactly how to back out a bad release.

If you have not yet packed a Solution end-to-end, read [Your first pipeline](./first-pipeline.md) first — everything here builds on `uip solution pack` / `publish` / `deploy run`.

## What this guide covers

- A **versioning discipline** that plays well with the Orchestrator feed.
- **Promoting the same package** through multiple tenants without repacking.
- **Pinning** the CLI and its tools so every build is reproducible.
- **Rolling back** a broken release with [`uip solution packages delete`](./uip-solution-packages.md#uip-solution-packages-delete) and [`uip solution deploy uninstall`](./uip-solution-deploy.md#uip-solution-deploy-uninstall).

## Pick a version, then freeze it

`uip solution pack --version` controls both the `.zip` filename and the `packageVersion` that every downstream step consumes. The default is `1.0.0`; a real pipeline should pass it explicitly.

Semantic versioning (`MAJOR.MINOR.PATCH`) is the scheme the Orchestrator feed sorts best. Two concrete rules make the difference between a clean history and an unsortable one:

1. **Never reuse a version.** [`uip solution publish`](./uip-solution-publish.md) rejects a `name+version` pair that already exists in the feed. Treat a rejected publish as a build bug, not as something to "fix" by re-running `pack`.
2. **Use build metadata, not timestamps in the version core.** Prefer `1.2.0-rc.3` over `1.2.0.20260424`. The former sorts correctly in the feed; the latter is technically valid semver but harder to read at a glance.

A typical CI configuration:

```bash
# Build number from the pipeline, tag from git, combined for a valid pre-release
VERSION="1.2.0-ci.${BUILD_NUMBER}"

uip solution pack ./my-solution ./dist \
  --name my-solution \
  --version "$VERSION"

uip solution publish "./dist/my-solution.${VERSION}.zip"
```

The `.zip` path is deterministic (`<outputDir>/<name>.<version>.zip`) — see [`uip solution pack`](./uip-solution-pack.md) — so scripts can compute it without parsing JSON.

## Promote one package across tenants

**Pack and publish once per version**, then re-deploy the same artifact into each tenant. Re-packing per environment risks drift; re-publishing the same version is also idempotent per `(name, version)`, so if you publish twice by mistake, nothing breaks. Even so, one build = one publish is the cleanest contract.

`uip solution publish` and every `uip solution deploy *` subcommand accept `--tenant <tenant-name>` (short: `-t`) to override the tenant selected by the active session. For a CI promotion job:

```bash
VERSION="$1"   # e.g. 1.2.0-ci.456

for tenant in dev stage prod; do
  uip solution publish "./dist/my-solution.${VERSION}.zip" --tenant "$tenant"

  uip solution deploy run \
    --name "my-solution-${tenant}" \
    --package-name my-solution \
    --package-version "$VERSION" \
    --folder-name MySolution \
    --folder-path Shared \
    --tenant "$tenant"
done
```

Two notes:

- **`publish` is idempotent per tenant.** Publishing the same `(name, version)` twice returns the existing `PackageVersionKey` instead of duplicating — see [`uip solution publish`](./uip-solution-publish.md).
- **Deployment names should be tenant-scoped.** Using `my-solution-prod` rather than `my-solution` makes [`uip solution deploy list`](./uip-solution-deploy.md#uip-solution-deploy-list) readable and prevents accidental cross-environment `deploy uninstall` calls. `--name` identifies the deployment record, not the folder.

### Parameterizing per-environment config

If your Solution has resources (queues, assets) that differ per tenant, generate the config file once and edit it per environment with [`deploy config set`](./uip-solution-deploy.md#uip-solution-deploy-config-set):

```bash
uip solution deploy config get my-solution --package-version "$VERSION" -d ./deploy-config.json

# Production uses a bigger retry count
uip solution deploy config set ./deploy-config.json MyQueue maxNumberOfRetries 5

uip solution deploy run \
  --name my-solution-prod \
  --package-name my-solution \
  --package-version "$VERSION" \
  --folder-name MySolution \
  --folder-path Shared \
  --config-file ./deploy-config.json \
  --tenant prod
```

Pass an existing Orchestrator resource instead of a freshly-created one with [`deploy config link`](./uip-solution-deploy.md#uip-solution-deploy-config-link):

```bash
uip solution deploy config link ./deploy-config.json MyQueue \
  --name SharedProductionQueue \
  --folder-path "Shared/Production"
```

## Pin the CLI and its tools

Reproducible pipelines pin every tool they depend on. The CLI is distributed on npm as `@uipath/cli`; `uip solution …` is provided by `@uipath/solution-tool`. Both follow semver.

```bash
# Pin the CLI exactly
npm install -g @uipath/cli@1.0.0

# Pre-install tools explicitly so the first command is not slower than the rest
uip tools install @uipath/solution-tool @uipath/orchestrator-tool
```

Tool versions default to tracking the CLI's MAJOR.MINOR line, so pinning the CLI alone usually suffices. For strict per-patch reproducibility, pin the tool too:

```bash
uip tools install @uipath/solution-tool@1.0.2
```

See [Scripting patterns — pinning versions in CI](./scripting-patterns.md#pinning-versions-in-ci) and [Installing UiPath CLI — CI/CD](./installing-uipath-cli.md#installing-in-cicd) for the full story.

## Rollback

Breaking production is rare; rolling back when you do matters. There are two levels of rollback — **redeploy a known-good version** (fast) and **delete the bad artifact** (clean).

### Fast: re-deploy the previous version

If the bad deployment is live, install the previous version *over* it instead of uninstalling first. The deployment name stays the same; only `--package-version` changes:

```bash
uip solution deploy run \
  --name my-solution-prod \
  --package-name my-solution \
  --package-version 1.1.9 \
  --folder-name MySolution \
  --folder-path Shared \
  --tenant prod
```

This is the common case. Orchestrator re-activates the new deployment for the configuration key and leaves the folder's resources intact.

### Clean: uninstall the deployment

If the Solution provisioned resources (queues, assets, triggers) that you do not want, call [`uip solution deploy uninstall`](./uip-solution-deploy.md#uip-solution-deploy-uninstall):

```bash
uip solution deploy uninstall my-solution-prod --tenant prod
```

This removes all provisioned resources and the Solution folder. It is a destructive operation — confirm the deployment name with [`deploy list`](./uip-solution-deploy.md#uip-solution-deploy-list) before running it, especially in a loop over tenants.

### Retire the artifact

Once nothing references a bad version, remove it from the tenant feed with [`uip solution packages delete`](./uip-solution-packages.md#uip-solution-packages-delete):

```bash
uip solution packages delete my-solution 1.2.0-ci.456 --tenant prod
```

There is no soft-delete; this is permanent. Keep the deletion to a small window — the command accepts exactly one `packageVersion` at a time, so scripting bulk cleanups requires a `list` → `filter` → `xargs` pattern. See the `packages delete` reference for a full example.

### Checking what you have

Before any of the above, get the ground truth:

```bash
# What is in the feed?
uip solution packages list --take 50 \
  --output-filter "Data[?packageName=='my-solution']"

# What is deployed?
uip solution deploy list --folder-path Shared --take 50
```

## CI-ready snippet

Combining the steps — a shell script that any CI system can run as-is:

```bash
#!/usr/bin/env bash
set -euo pipefail

VERSION="$1"                          # e.g. 1.2.0-ci.456
SOLUTION_DIR="./my-solution"
OUT_DIR="./dist"

# 1. Log in (External App in CI)
uip login \
  --client-id env.UIPATH_CLIENT_ID \
  --client-secret env.UIPATH_CLIENT_SECRET \
  --tenant "$UIPATH_TENANT_DEV"

# 2. Pack once
uip solution pack "$SOLUTION_DIR" "$OUT_DIR" \
  --name my-solution \
  --version "$VERSION"

PKG="${OUT_DIR}/my-solution.${VERSION}.zip"

# 3. Publish + deploy to each tenant
for env_name in dev stage prod; do
  tenant_var="UIPATH_TENANT_$(echo "$env_name" | tr a-z A-Z)"
  tenant="${!tenant_var}"

  uip solution publish "$PKG" --tenant "$tenant"
  uip solution deploy run \
    --name "my-solution-${env_name}" \
    --package-name my-solution \
    --package-version "$VERSION" \
    --folder-name MySolution \
    --folder-path Shared \
    --tenant "$tenant"
done
```

With `set -euo pipefail`, any failure aborts the loop at the failing tenant. Later tenants are unaffected — the partial promotion can then be re-run or rolled back explicitly.

## Next steps

- **[How-to: deploy to Orchestrator from CI](./howto-deploy-from-ci.md)** — platform-agnostic deep-dive on auth, caching, and tool pre-install.
- **[CI/CD recipes](./recipes-azure-devops.md)** — copy-pasteable pipelines for Azure DevOps, GitHub Actions, Jenkins, GitLab.
- **[`uip solution` reference](./uip-solution.md)** — every subcommand.
- **[Scripting patterns](./scripting-patterns.md)** — exit-code branching, idempotent pipelines, auth retry.
