# CI/CD recipe: Jenkins

> This page gives you a complete **declarative `Jenkinsfile`** that installs the CLI, authenticates with an External Application, packs and publishes a UiPath Solution, deploys it to Orchestrator, and runs a Test Manager suite. Place it in the root of your repo, create a matching Jenkins credentials entry, and the pipeline runs.

This page gives you a complete **declarative `Jenkinsfile`** that installs the CLI, authenticates with an External Application, packs and publishes a UiPath Solution, deploys it to Orchestrator, and runs a Test Manager suite. Place it in the root of your repo, create a matching Jenkins credentials entry, and the pipeline runs.

For the underlying principles — auth, caching, tool pre-install, version pinning — see [How-to: deploy to Orchestrator from CI](./howto-deploy-from-ci.md). This page focuses on the Jenkins syntax, including the bits (`withCredentials`, `stash` / `unstash`, agent selection) that are Jenkins-specific.

## Prerequisites

Before copying the Jenkinsfile:

1. **Create an External Application** in UiPath with the `OR.*` scopes your pipeline needs. See [Authentication — Flow 2](./authentication.md#flow-2-external-application-client-credentials).
2. **Store the secrets** in Jenkins credentials:
   - **Manage Jenkins → Credentials → System → Global credentials (unrestricted)**.
   - Add two **Secret text** entries with IDs `UIPATH_CLIENT_ID` and `UIPATH_CLIENT_SECRET`.
   - Add a **Plain text** or **Secret text** entry with ID `UIPATH_TENANT` (the tenant name; not strictly secret, but credentials are the most portable place).
3. **Agent requirements.** The pipeline assumes an agent labelled `linux` with Node.js 18+ and `npm` on the PATH. See the [alternative agent options](#agent-variations) below if you run on Windows or in a container.
4. **Provision a Test Manager project and test set** if you want the test stage. Add `TEST_SET_KEY` and `PROJECT_KEY` as additional credentials (or as pipeline parameters).

## Jenkinsfile

```groovy
pipeline {
  agent none

  options {
    timestamps()
    buildDiscarder(logRotator(numToKeepStr: '30'))
    disableConcurrentBuilds()
  }

  environment {
    CLI_VERSION      = '1.0.0'
    SOLUTION_NAME    = 'my-solution'
    SOLUTION_DIR     = 'my-solution'
    OUTPUT_DIR       = 'dist'
    SOLUTION_VERSION = "1.2.0-ci.${env.BUILD_NUMBER}"

    // npm global prefix for a user-local, no-sudo install
    NPM_PREFIX       = "${env.WORKSPACE}/.npm-global"
  }

  stages {

    stage('Build') {
      agent { label 'linux' }
      steps {
        checkout scm
        sh '''#!/usr/bin/env bash
          set -euo pipefail

          mkdir -p "$NPM_PREFIX"
          npm config set prefix "$NPM_PREFIX"
          export PATH="$NPM_PREFIX/bin:$PATH"

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

          uip --version

          mkdir -p "$OUTPUT_DIR"
          uip solution pack "$SOLUTION_DIR" "$OUTPUT_DIR" \
            --name "$SOLUTION_NAME" \
            --version "$SOLUTION_VERSION"
        '''

        // Carry the .zip to the Deploy stage, which runs on a fresh agent.
        stash name: 'solution-zip', includes: "${OUTPUT_DIR}/*.zip"

        archiveArtifacts artifacts: "${OUTPUT_DIR}/*.zip", fingerprint: true
      }
    }

    stage('Deploy') {
      agent { label 'linux' }
      steps {
        unstash 'solution-zip'

        withCredentials([
          string(credentialsId: 'UIPATH_CLIENT_ID',     variable: 'UIPATH_CLIENT_ID'),
          string(credentialsId: 'UIPATH_CLIENT_SECRET', variable: 'UIPATH_CLIENT_SECRET'),
          string(credentialsId: 'UIPATH_TENANT',        variable: 'UIPATH_TENANT')
        ]) {
          sh '''#!/usr/bin/env bash
            set -euo pipefail

            mkdir -p "$NPM_PREFIX"
            npm config set prefix "$NPM_PREFIX"
            export PATH="$NPM_PREFIX/bin:$PATH"

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

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

            ARTIFACT=$(find "$OUTPUT_DIR" -maxdepth 1 -name "*.zip" | head -1)
            uip solution publish "$ARTIFACT"

            uip solution deploy run \
              --name "${SOLUTION_NAME}-${BUILD_NUMBER}" \
              --package-name "$SOLUTION_NAME" \
              --package-version "$SOLUTION_VERSION" \
              --folder-name MySolution \
              --folder-path Shared
          '''
        }
      }
    }

    stage('Test') {
      when {
        expression { return env.TEST_SET_KEY?.trim() }
      }
      agent { label 'linux' }
      steps {
        withCredentials([
          string(credentialsId: 'UIPATH_CLIENT_ID',     variable: 'UIPATH_CLIENT_ID'),
          string(credentialsId: 'UIPATH_CLIENT_SECRET', variable: 'UIPATH_CLIENT_SECRET'),
          string(credentialsId: 'UIPATH_TENANT',        variable: 'UIPATH_TENANT'),
          string(credentialsId: 'TEST_SET_KEY',         variable: 'TEST_SET_KEY'),
          string(credentialsId: 'PROJECT_KEY',          variable: 'PROJECT_KEY')
        ]) {
          sh '''#!/usr/bin/env bash
            set -euo pipefail

            mkdir -p "$NPM_PREFIX"
            npm config set prefix "$NPM_PREFIX"
            export PATH="$NPM_PREFIX/bin:$PATH"

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

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

            EXECUTION_ID=$(uip tm testsets run \
              --test-set-key "$TEST_SET_KEY" \
              --output-filter "Data.ExecutionId" \
              --output plain)

            echo "started execution $EXECUTION_ID"

            if ! uip tm wait \
              --execution-id "$EXECUTION_ID" \
              --project-key "$PROJECT_KEY" \
              --timeout 1800; then
              code=$?
              case "$code" in
                2) echo "test run did not finish within 30 minutes" >&2; exit 2 ;;
                *) echo "wait failed (exit $code)" >&2; exit "$code" ;;
              esac
            fi

            uip tm report get \
              --execution-id "$EXECUTION_ID" \
              --project-key "$PROJECT_KEY"

            FAILED=$(uip tm report get \
              --execution-id "$EXECUTION_ID" \
              --project-key "$PROJECT_KEY" \
              --output-filter "Data.Failed" \
              --output plain)

            if [ "$FAILED" -gt 0 ]; then
              echo "$FAILED test case(s) failed" >&2
              exit 1
            fi
          '''
        }
      }
    }
  }

  post {
    always {
      // Best-effort cleanup; uip logout only matters on a persistent agent.
      node('linux') {
        sh '''
          if command -v uip >/dev/null; then
            uip logout || true
          fi
        '''
      }
    }
  }
}
```

## Walkthrough

### Top-level setup

- **`agent none`** at the top declares that the pipeline has no default agent; each stage picks its own with `agent { label 'linux' }`. This lets you put the build on a high-capacity agent and the deploy on a locked-down agent if your infrastructure separates them that way.
- **`options`** — `timestamps()` adds `[2026-04-24T10:30:12]` prefixes to every console line, which makes debugging long runs much easier. `disableConcurrentBuilds()` prevents two deploys from racing into the same Orchestrator folder.
- **`environment` block** — the same pattern as the other recipes: pin `CLI_VERSION`, compute `SOLUTION_VERSION` from `BUILD_NUMBER`, and route npm's global prefix into the workspace so installs are per-build and need no `sudo`.

### Build stage

- **`checkout scm`** — Jenkins needs this explicitly in a declarative pipeline.
- **Install step** — same `if ! command -v uip` guard as other platforms. The workspace-local npm prefix (`$NPM_PREFIX`) means every build starts clean; no caching across runs. If your Jenkins agents are persistent and you want to cache the CLI, either use the [Job Cacher plugin](https://plugins.jenkins.io/jobcacher/) or set `NPM_PREFIX` to a path on the agent rather than `${env.WORKSPACE}`.
- **Pack step** — straight [`uip solution pack`](./uip-solution-pack.md) with an explicit version.
- **`stash name: 'solution-zip'`** — this is the Jenkins-specific piece. Unlike GitHub Actions artifacts or Azure Pipelines `publish`, a `stash` is scoped to the pipeline run and survives across stages without needing the workspace to persist. The stash uses a glob (`${OUTPUT_DIR}/*.zip`) so it picks up whatever filename `uip solution pack` produces. Every stage that needs the `.zip` calls `unstash 'solution-zip'` at its start. See the Jenkins docs on [`stash` / `unstash`](https://www.jenkins.io/doc/pipeline/steps/workflow-basic-steps/#stash-stash-some-files-to-be-used-later-in-the-build).
- **`archiveArtifacts`** — a copy of the `.zip` also appears on the build page for humans to download. `fingerprint: true` lets Jenkins track it across jobs.

### Deploy stage

- **`unstash 'solution-zip'`** restores the `.zip` to the workspace. Combined with a fresh agent, this cleanly separates "build context" from "deploy context".
- **`withCredentials`** binds three credential entries to environment variables visible to the `sh` block. The closure is the credentials-safe boundary — values are masked in the console log and not leaked to any step outside this block. Use this for every `uip` call that talks to Orchestrator.
- **`uip login`** uses `env.UIPATH_CLIENT_ID` / `env.UIPATH_CLIENT_SECRET` — the `env.VAR_NAME` prefix reads the variable set by `withCredentials`. See [Authentication — the env.VAR_NAME prefix](./authentication.md#the-envvar_name-prefix). **Never** interpolate the secret into the command line directly (`--client-secret "$UIPATH_CLIENT_SECRET"`) — that embeds it into the rendered shell command and, under some logging configurations, into the console output.
- **Publish + deploy** — [`uip solution publish`](./uip-solution-publish.md) then [`uip solution deploy run`](./uip-solution-deploy.md). `--name "${SOLUTION_NAME}-${BUILD_NUMBER}"` makes each deployment traceable back to its Jenkins build.

### Test stage

Gated by `when { expression { return env.TEST_SET_KEY?.trim() } }` — if you do not set the `TEST_SET_KEY` credential (or leave it empty), the stage is skipped. The shell block is the canonical **launch → wait → verify** pattern from [How-to: run tests from the CLI](./howto-run-tests.md):

1. [`uip tm testsets run`](./uip-test-manager-testsets.md#uip-tm-testsets-run) launches and returns an `ExecutionId`.
2. [`uip tm wait`](./uip-test-manager-wait.md) blocks until the execution reaches a terminal state. On `wait`, exit code `2` means **timeout** (not authentication failure — it is a domain-specific reuse of the code).
3. [`uip tm report get`](./uip-test-manager-report.md) reads `Data.Failed` and the shell exits `1` when any test failed.

### post { always }

Best-effort `uip logout`. On ephemeral agents this is unnecessary; on a persistent agent it clears the workspace-local `.uipath/` folder so a later job on the same workspace does not reuse a stale session. The `|| true` swallows the error if `uip` was never installed (the build failed before installation).

## Common variations

### Agent variations

- **Windows agent** — replace `sh` with `bat`, translate the bash scripts to `cmd.exe` or PowerShell. The `env.` prefix and every `uip` command are identical; only the surrounding shell changes. See [Installing UiPath CLI — Windows](./installing-uipath-cli.md#windows).
- **Docker agent** — `agent { docker { image 'node:20' } }` gives you a fresh Node runtime per build with zero agent config. Add `args '-u root'` if you need sudo-free global npm installs in a rootful image; otherwise configure the NPM prefix to `$WORKSPACE/.npm-global` as above and skip the privilege dance.
- **Kubernetes plugin** — use a pod template with a `node:20` container and mount `/root/.npm-global` as a PVC to cache the CLI between builds.

### Promote across environments

Turn each environment into its own stage, reusing the same `stash`:

```groovy
stage('Deploy stage') {
  agent { label 'linux' }
  steps {
    unstash 'solution-zip'
    withCredentials([string(credentialsId: 'UIPATH_STAGE_CLIENT_ID', variable: 'UIPATH_CLIENT_ID'), ...]) {
      sh '...deploy to stage tenant...'
    }
  }
}

stage('Approval') {
  steps {
    input message: 'Deploy to production?', ok: 'Deploy'
  }
}

stage('Deploy prod') {
  agent { label 'linux' }
  steps {
    unstash 'solution-zip'
    withCredentials([string(credentialsId: 'UIPATH_PROD_CLIENT_ID', variable: 'UIPATH_CLIENT_ID'), ...]) {
      sh '...deploy to prod tenant...'
    }
  }
}
```

The `input` step pauses the pipeline until a human clicks **Deploy**. Use Jenkins' [Matrix Authorization Strategy](https://plugins.jenkins.io/matrix-auth/) to restrict who can approve. Deeper coverage in [How-to: pack and publish a Solution — promote one package across tenants](./howto-pack-publish-solution.md#promote-one-package-across-tenants).

### Rollback

Add a parameterized job — **Jenkins → New Item → Pipeline → This project is parameterized → Add Parameter (String) → "ROLLBACK_VERSION"** — and add a guarded stage:

```groovy
stage('Rollback') {
  when { expression { return params.ROLLBACK_VERSION?.trim() } }
  agent { label 'linux' }
  steps {
    withCredentials([/* UIPATH_* credentials */]) {
      sh '''#!/usr/bin/env bash
        set -euo pipefail
        # …install + login…

        uip solution deploy run \
          --name "${SOLUTION_NAME}-rollback" \
          --package-name "$SOLUTION_NAME" \
          --package-version "$ROLLBACK_VERSION" \
          --folder-name MySolution \
          --folder-path Shared
      '''
    }
  }
  environment {
    ROLLBACK_VERSION = "${params.ROLLBACK_VERSION}"
  }
}
```

Trigger from the build page with **Build with Parameters**. For destructive rollback (uninstall + delete artifact), see [How-to: pack and publish a Solution — rollback](./howto-pack-publish-solution.md#rollback).

## Common pitfalls

- **`withCredentials` scoping.** Credentials are only available inside the closure. If you call `uip login` inside `withCredentials` and `uip solution publish` outside, the second command has no session. Keep the whole CLI block inside one `withCredentials`.
- **`stash` before changing agents.** A `stash` declared in one stage is usable in later stages only if the stash step ran to completion before the agent switched. If the `Build` stage's shell block exits non-zero **before** `stash`, later stages will fail at `unstash`. Putting `stash` outside the `sh` block — as in this recipe — ensures it runs on a clean agent regardless of shell outcome.
- **`|| true` at the wrong time.** The `uip logout || true` in `post` is safe because the only failure mode is "uip was never installed". Do not pepper `|| true` through earlier stages — it hides real failures.
- **`sh` strict mode.** Start every `sh` block's bash script with `#!/usr/bin/env bash` and `set -euo pipefail`. The default Jenkins shell is `/bin/sh`, which does not support `pipefail`. See [Scripting patterns — strict shell options](./scripting-patterns.md#strict-shell-options).
- **Multiline strings in Groovy.** Prefer triple-single-quoted strings (`'''...'''`) for shell bodies — they do not interpolate `$VAR`, so the variable is expanded by bash rather than by Groovy. This avoids a common class of "variable is empty" bugs. For values you want Groovy to fill in at pipeline-compile time, use `${env.SOME_VAR}` explicitly.

## See also

- [How-to: deploy to Orchestrator from CI](./howto-deploy-from-ci.md) — platform-agnostic guidance.
- [How-to: pack and publish a Solution](./howto-pack-publish-solution.md) — versioning and rollback.
- [How-to: run tests from the CLI](./howto-run-tests.md) — the launch → wait → verify pattern.
- [CI/CD recipe: Azure Pipelines](./recipes-azure-devops.md), [GitHub Actions](./recipes-github-actions.md), [GitLab CI](./recipes-gitlab.md) — the same pipeline in other platforms.
