# How-to: deploy to Orchestrator from CI

> Set up authentication, caching, tool pre-installation, and version pinning for CI pipelines that deploy UiPath Solutions to Orchestrator.

This page covers what every CI pipeline deploying a UiPath Solution needs, regardless of whether you run on Azure DevOps, GitHub Actions, Jenkins, or GitLab: **auth, caching, tool pre-install, and version pinning**. The platform-specific syntax lives in [the recipe pages](#platform-recipes) — this one gives you the moving parts so you can read any of them with confidence.

## The shape of a good CI pipeline

A production pipeline that ships a Solution always has the same five stages:

1. **Set up Node.js** (version 18 or later).
2. **Install `@uipath/cli`** at a pinned version.
3. **Pre-install the tools** you will use (so the first `uip` call is not slower than the rest).
4. **Authenticate** with an External Application using the `env.` prefix for secrets.
5. **Pack, publish, deploy** — and optionally test.

Stage 4 is the one that varies the most between platforms, because secret syntax is platform-specific. Stages 1–3 and 5 are nearly identical everywhere.

## Authentication: External Application + env. prefix

In a headless environment you authenticate with an **External Application** (client credentials). See [Authentication — Flow 2](./authentication.md#flow-2-external-application-client-credentials) for how to create one in the UiPath portal.

Store the credentials in your platform's secret store (never in source control, never in a plain env var file). Pass them to [`uip login`](./uip-login.md) with the special `env.` prefix:

```bash
uip login \
  --client-id env.UIPATH_CLIENT_ID \
  --client-secret env.UIPATH_CLIENT_SECRET \
  --tenant "$UIPATH_TENANT"
```

The `env.VAR_NAME` prefix tells `uip` to read the value from the `VAR_NAME` environment variable at runtime. This keeps the secret out of shell history and process listings — unlike `--client-secret "$UIPATH_CLIENT_SECRET"`, which expands on the command line and can leak via `ps`.

:::warning
**Do not rely on implicit env-var reading.** Setting `UIPATH_CLIENT_ID` / `UIPATH_CLIENT_SECRET` alone, without the flag, will **not** authenticate you — that feature was removed prior to CLI 1.0. Always pass the flag; use `env.VAR_NAME` when you want the value resolved from the environment.
:::

In your pipeline, inject the secrets into the step's environment under the exact variable names you reference:

| Platform | Secret syntax in YAML / Groovy | Shape passed to the step |
|---|---|---|
| GitHub Actions | `${{ secrets.UIPATH_CLIENT_ID }}` in `env:` | `UIPATH_CLIENT_ID: <value>` |
| Azure DevOps | `$(UIPATH_CLIENT_ID)` from a variable group in `env:` | `UIPATH_CLIENT_ID: $(UIPATH_CLIENT_ID)` |
| Jenkins | `credentialsId: 'UIPATH_CLIENT_ID'` inside `withCredentials` | exported as `$UIPATH_CLIENT_ID` |
| GitLab CI | `UIPATH_CLIENT_ID` CI/CD variable, marked **Protected**+**Masked** | `$UIPATH_CLIENT_ID` in `script` |

In every case, the `uip login` command itself looks identical — `--client-id env.UIPATH_CLIENT_ID --client-secret env.UIPATH_CLIENT_SECRET`. Only how the env var arrives in the step changes.

### Session storage

`uip login` persists the session inside a `.uipath/` folder. On most CI runners the working directory is already stateless, so the session ends with the job — which is the desired behaviour. If your runner is persistent, either remove the session with `uip logout` at the end of the job or set `--file` to a job-local path. See [Sessions and credentials](./concepts-sessions.md).

### Multiple organizations in one pipeline

A session holds one organization and one tenant at a time. To target **different UiPath organizations** from the same pipeline — for example, to promote a Solution from a build org into a customer org — **re-run `uip login`** between the two blocks with a different External Application. Each login overwrites the previous session.

Create one External App per org (each with its own `OR.*` scopes). Store both client IDs and secrets in the pipeline's secret store under distinct names:

```bash
set -euo pipefail

# --- Organization A ---
uip login \
  --client-id env.ORGA_CLIENT_ID \
  --client-secret env.ORGA_CLIENT_SECRET \
  --tenant Prod

uip or folders list
uip solution pack ./my-solution ./dist --version "$SOLUTION_VERSION"
uip solution publish "./dist/my-solution.$SOLUTION_VERSION.zip"

# --- Organization B — this overwrites the previous session ---
uip login \
  --client-id env.ORGB_CLIENT_ID \
  --client-secret env.ORGB_CLIENT_SECRET \
  --tenant Prod

uip solution publish "./dist/my-solution.$SOLUTION_VERSION.zip"
uip solution deploy run \
  --name "my-solution-$GIT_SHA" \
  --package-name my-solution \
  --package-version "$SOLUTION_VERSION" \
  --folder-name MySolution \
  --folder-path Shared
```

Same pattern for different tenants within a single org — except tenants don't need a second login. Pass `--tenant <name>` on any `uip or …` call to override the session tenant for a single command, without re-authenticating.

:::note
This is a **serial** pattern. Each `uip login` overwrites the stored session, so only one org is reachable at any moment. If you need to run commands against two orgs **simultaneously** (for example, parallel matrix jobs), give each job its own runner or its own `--file` / `HOME` scope — see [Sessions and credentials](./concepts-sessions.md#overriding-the-location).
:::

## Pre-install tools to keep build times deterministic

The CLI ships with no tools pre-installed. The first time you run a verb from an uninstalled tool, `uip` auto-installs it from npm — which is fine on a laptop but adds 5–10 seconds of latency to the first command on a stateless CI runner.

Install the tools you use upfront, as a separate step:

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

Add `@uipath/test-manager-tool` when you run Test Manager, `@uipath/agent-tool` when you deploy Agents, `@uipath/resource-tool` when you manage assets/queues/buckets outside of a Solution. See the [tools reference](./uip-tools.md) for the full list.

Auto-install is a no-op when the tool is already installed, so the pre-install step is the only behavior change you need — nothing else in your pipeline has to know about it.

:::note
`CI=true` does **not** disable auto-install. There is no env-var switch; pre-install is the only way to avoid it. See [Installing UiPath CLI — Controlling tool auto-install](./installing-uipath-cli.md#controlling-tool-auto-install).
:::

## Cache the npm global directory

CI runners that re-install `@uipath/cli` on every job waste 20–40 seconds of download and decompression. Caching the npm global `node_modules` directory turns that into a cache hit — usually under a second.

The directory to cache is the one reported by `npm root -g` (typically `~/.npm-global/lib/node_modules` on Linux/macOS, `%APPDATA%\npm\node_modules` on Windows). Key the cache on the CLI version you pin, so a version bump invalidates the cache cleanly:

| Platform | Cache mechanism |
|---|---|
| GitHub Actions | [`actions/cache`](https://github.com/actions/cache) with `path: ~/.npm-global/lib/node_modules` and `key: uip-${{ version }}-${{ runner.os }}` |
| Azure DevOps | [`Cache@2`](https://learn.microsoft.com/azure/devops/pipelines/tasks/reference/cache-v2) keyed on the version variable |
| Jenkins | The [Job Cacher plugin](https://plugins.jenkins.io/jobcacher/) or a manually-managed workspace folder |
| GitLab CI | Top-level `cache:` block with `key: uip-$CLI_VERSION` |

When the cache hits, you can skip both `npm install -g @uipath/cli` and `uip tools install` — the `uip` executable and its tools are already on the `PATH`. A small bash guard does this cleanly:

```bash
if ! command -v uip >/dev/null; then
  npm install -g "@uipath/cli@${CLI_VERSION}"
  uip tools install @uipath/orchestrator-tool @uipath/solution-tool
fi
```

The concrete YAML/Groovy for each platform is in the [recipe pages](#platform-recipes).

## Pin versions for reproducibility

Reproducible pipelines pin *everything*. Four versions matter:

- **Node.js** — pin the major (`20.x` on GitHub Actions' `setup-node`, `versionSpec: '20.x'` on Azure DevOps).
- **`@uipath/cli`** — pin exactly (`@1.0.0`), not a range.
- **Tools** — optional. By default they track the CLI's MAJOR.MINOR line; pin only if you need strict patch-level reproducibility (`@uipath/solution-tool@1.0.2`).
- **Your own Solution's version** — pass `--version` to `uip solution pack` explicitly. Never rely on the `1.0.0` default in CI.

```bash
# Pin these once at the top of the pipeline; reuse below
CLI_VERSION="1.0.0"
SOLUTION_VERSION="1.2.0-ci.${BUILD_NUMBER}"

npm install -g "@uipath/cli@${CLI_VERSION}"
uip tools install @uipath/solution-tool @uipath/orchestrator-tool

uip solution pack ./my-solution ./dist --version "$SOLUTION_VERSION"
```

The `.zip` path is deterministic (`./dist/my-solution.${SOLUTION_VERSION}.zip`), so downstream steps can construct it without parsing the `uip solution pack` JSON output.

See [Scripting patterns — pinning versions in CI](./scripting-patterns.md#pinning-versions-in-ci) for the tool-pinning rules in depth.

## The minimal deploy block

Every platform boils down to this:

```bash
set -euo pipefail

# Authenticate
uip login \
  --client-id env.UIPATH_CLIENT_ID \
  --client-secret env.UIPATH_CLIENT_SECRET \
  --tenant "$UIPATH_TENANT"

# Pack
uip solution pack ./my-solution ./dist \
  --name my-solution \
  --version "$SOLUTION_VERSION"

# Publish
uip solution publish "./dist/my-solution.${SOLUTION_VERSION}.zip"

# Deploy
uip solution deploy run \
  --name "my-solution-${ENVIRONMENT}" \
  --package-name my-solution \
  --package-version "$SOLUTION_VERSION" \
  --folder-name MySolution \
  --folder-path Shared
```

`set -euo pipefail` makes the script fail fast: `-e` aborts on any non-zero exit, `-u` catches undefined variables, `-o pipefail` propagates failures through pipes. This is the pattern every recipe in this documentation uses. See [Scripting patterns — strict shell options](./scripting-patterns.md#strict-shell-options).

## Optional: run tests after deploy

If the Solution includes a test set, launch → wait → verify it before marking the pipeline green. The three-step pattern is important: `uip tm testsets run` exits `0` as soon as the run is **queued**, not when tests pass — so you need [`uip tm wait`](./uip-test-manager-wait.md) to block and [`uip tm report get`](./uip-test-manager-report.md) to read the verdict.

See [How-to: run tests from the CLI](./howto-run-tests.md) for the full pattern with error handling.

## Handling re-authentication mid-pipeline

Access tokens can expire during long-running pipelines. The [scripting-patterns page](./scripting-patterns.md#re-authenticating-on-authenticationerror-exit-2) has the canonical retry pattern: branch on exit code `2` (`AuthenticationError`), re-run `uip login`, retry once, fail otherwise. In most CI pipelines this is unnecessary — jobs are short enough that the initial login's token lasts the whole run — but long test suites or multi-tenant promotion loops can benefit from it.

## Platform recipes

For complete, copy-pasteable pipelines in your platform's native syntax:

- **[CI/CD recipe: Azure DevOps](./recipes-azure-devops.md)**
- **[CI/CD recipe: GitHub Actions](./recipes-github-actions.md)**
- **[CI/CD recipe: Jenkins](./recipes-jenkins.md)**
- **[CI/CD recipe: GitLab CI](./recipes-gitlab.md)**

Each recipe shows the full pipeline (install → auth → pack → publish → deploy → test), the secret wiring, a cache entry, and variations for pinning versions and running tests.

## See also

- [Your first pipeline](./first-pipeline.md) — the minimal three-command flow.
- [How-to: pack and publish a Solution](./howto-pack-publish-solution.md) — versioning discipline, multi-environment promotion, rollback.
- [Authentication](./authentication.md) — the three flows and when to use each.
- [Scripting patterns](./scripting-patterns.md) — exit codes, retry, JSON filtering.
- [Installing UiPath CLI — CI/CD](./installing-uipath-cli.md#installing-in-cicd) — pre-install and caching details.
