Skip to main content

CI/CD recipe: Azure Pipelines

This page gives you a complete azure-pipelines.yml 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 against the deployment. The pipeline is self-contained: copy it into the root of your repo, wire two secrets, and it runs.

For the underlying principles — auth, caching, tool pre-install, version pinning — see How-to: deploy to Orchestrator from CI. This page focuses on the Azure Pipelines syntax.

Prerequisites

Before copying the YAML below:

  1. Create an External Application in UiPath with the OR.* scopes your pipeline needs. See Authentication — Flow 2.
  2. Store the secrets in an Azure DevOps variable group:
    • Project Settings → Pipelines → Library → New variable group (for example, uipath-prod).
    • Add UIPATH_CLIENT_ID and UIPATH_CLIENT_SECRET — mark both as secret (padlock icon).
    • Optionally add UIPATH_TENANT (not a secret).
  3. Link the variable group to the pipeline (see the variables block below).
  4. Provision a Test Manager project and test set (if you want the test stage). You will need the TEST_SET_KEY (format PROJECT:42) and PROJECT_KEY. See How-to: run tests from the CLI.

azure-pipelines.yml

trigger:
branches:
include: [ main ]

pr:
branches:
include: [ main ]

variables:
- group: uipath-prod # contains UIPATH_CLIENT_ID, UIPATH_CLIENT_SECRET
- name: CLI_VERSION
value: '1.0.0'
- name: NODE_VERSION
value: '20.x'
- name: SOLUTION_NAME
value: 'my-solution'
- name: SOLUTION_DIR
value: '$(Build.SourcesDirectory)/my-solution'
- name: OUTPUT_DIR
value: '$(Build.ArtifactStagingDirectory)'
- name: SOLUTION_VERSION
value: '1.2.0-ci.$(Build.BuildId)'

# Path where npm places globally-installed packages. Used to cache the CLI + tools.
- name: NPM_GLOBAL_CACHE
value: '$(HOME)/.npm-global/lib/node_modules'

stages:

- stage: Build
displayName: 'Pack the Solution'
jobs:
- job: pack
pool:
vmImage: 'ubuntu-latest'
steps:

- task: NodeTool@0
displayName: 'Use Node.js $(NODE_VERSION)'
inputs:
versionSpec: $(NODE_VERSION)

- task: Cache@2
displayName: 'Cache npm global (@uipath/cli)'
inputs:
key: 'uip | "$(Agent.OS)" | $(CLI_VERSION)'
path: $(NPM_GLOBAL_CACHE)

- script: |
set -euo pipefail
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global"
export PATH="$HOME/.npm-global/bin:$PATH"
echo "##vso[task.prependpath]$HOME/.npm-global/bin"

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

uip --version
displayName: 'Install UiPath CLI'

- script: |
set -euo pipefail
uip solution pack "$(SOLUTION_DIR)" "$(OUTPUT_DIR)" \
--name "$(SOLUTION_NAME)" \
--version "$(SOLUTION_VERSION)"
displayName: 'Pack Solution'

- publish: '$(OUTPUT_DIR)'
artifact: solution-zip
displayName: 'Publish artifact'

- stage: Deploy
displayName: 'Publish to feed and deploy to Orchestrator'
dependsOn: Build
jobs:
- deployment: deploy
environment: 'uipath-prod' # Azure DevOps environment — gates / approvals live here
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:

- task: NodeTool@0
displayName: 'Use Node.js $(NODE_VERSION)'
inputs:
versionSpec: $(NODE_VERSION)

- task: Cache@2
displayName: 'Cache npm global (@uipath/cli)'
inputs:
key: 'uip | "$(Agent.OS)" | $(CLI_VERSION)'
path: $(NPM_GLOBAL_CACHE)

- script: |
set -euo pipefail
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global"
export PATH="$HOME/.npm-global/bin:$PATH"
echo "##vso[task.prependpath]$HOME/.npm-global/bin"

if ! command -v uip >/dev/null; then
npm install -g "@uipath/cli@$(CLI_VERSION)"
fi
displayName: 'Install UiPath CLI'

- script: |
set -euo pipefail
uip login \
--client-id env.UIPATH_CLIENT_ID \
--client-secret env.UIPATH_CLIENT_SECRET \
--tenant "$UIPATH_TENANT"
displayName: 'Authenticate'
env:
UIPATH_CLIENT_ID: $(UIPATH_CLIENT_ID)
UIPATH_CLIENT_SECRET: $(UIPATH_CLIENT_SECRET)
UIPATH_TENANT: $(UIPATH_TENANT)

- download: current
artifact: solution-zip

- script: |
set -euo pipefail
ARTIFACT=$(find "$(Pipeline.Workspace)/solution-zip" -maxdepth 1 -name "*.zip" | head -1)
uip solution publish "$ARTIFACT"
displayName: 'Publish to tenant feed'

- script: |
set -euo pipefail
uip solution deploy run \
--name "$(SOLUTION_NAME)-$(Build.BuildId)" \
--package-name "$(SOLUTION_NAME)" \
--package-version "$(SOLUTION_VERSION)" \
--folder-name MySolution \
--folder-path Shared
displayName: 'Deploy to Orchestrator'

- stage: Test
displayName: 'Run Test Manager suite'
dependsOn: Deploy
condition: and(succeeded(), ne(variables['TEST_SET_KEY'], ''))
jobs:
- job: test
pool:
vmImage: 'ubuntu-latest'
steps:

- task: NodeTool@0
displayName: 'Use Node.js $(NODE_VERSION)'
inputs:
versionSpec: $(NODE_VERSION)

- task: Cache@2
displayName: 'Cache npm global (@uipath/cli)'
inputs:
key: 'uip | "$(Agent.OS)" | $(CLI_VERSION)'
path: $(NPM_GLOBAL_CACHE)

- script: |
set -euo pipefail
export PATH="$HOME/.npm-global/bin:$PATH"
echo "##vso[task.prependpath]$HOME/.npm-global/bin"
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"
displayName: 'Install + authenticate'
env:
UIPATH_CLIENT_ID: $(UIPATH_CLIENT_ID)
UIPATH_CLIENT_SECRET: $(UIPATH_CLIENT_SECRET)
UIPATH_TENANT: $(UIPATH_TENANT)

- script: |
set -euo pipefail

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 "##vso[task.logissue type=error]test run did not finish within 30 minutes"; exit 2 ;;
*) echo "##vso[task.logissue type=error]wait failed (exit $code)"; 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 "##vso[task.logissue type=error]$FAILED test case(s) failed"
exit 1
fi
displayName: 'Launch, wait, verify'
env:
TEST_SET_KEY: $(TEST_SET_KEY)
PROJECT_KEY: $(PROJECT_KEY)

Walkthrough

Stage 1 — Build

  • NodeTool@0 pins Node.js to version 20. Pin to a major; the CLI requires 18+.
  • Cache@2 caches the npm global node_modules directory. The cache key includes $(CLI_VERSION) so a version bump invalidates cleanly.
  • Install step switches npm's global prefix to $HOME/.npm-global (no sudo needed), prepends it to the PATH, and — only if the cache missed — installs @uipath/cli. The first invocation of a tool command (uip solution …, uip tm …) auto-installs that tool, so no explicit uip tools install is needed. See Installing UiPath CLI — CI/CD and Scripting patterns — pinning versions.
  • Pack step invokes uip solution pack with an explicit --version (never rely on the 1.0.0 default in CI).
  • publish: ... uploads the entire $(OUTPUT_DIR) as a pipeline artifact named solution-zip so the Deploy stage can consume it. Publishing the directory rather than a hardcoded filename keeps the recipe stable across CLI naming-convention changes.

Stage 2 — Deploy

Uses a deployment job bound to a named environment. Environments are where you configure gates and approvals in Azure DevOps — the YAML itself stays simple.

  • Re-installs the CLI (the cache should hit on the second stage of the same run).
  • uip login uses the env.VAR_NAME prefix for both the client ID and secret. The env: block at the step level maps $(UIPATH_CLIENT_ID) (from the variable group) to the actual environment variable UIPATH_CLIENT_ID that the flag resolves. Do not pass the secret literally on the command line (--client-secret "$(UIPATH_CLIENT_SECRET)") — that leaks it into the build log and ps output. The env. prefix feature is documented in Authentication — the env.VAR_NAME prefix.
  • download: current pulls the solution-zip artifact from Stage 1 into $(Pipeline.Workspace)/solution-zip.
  • uip solution publish uploads the .zip to the tenant feed.
  • uip solution deploy run creates the deployment. --name uses $(Build.BuildId) so each run is identifiable and re-deploys do not clobber each other's deployment record.

Stage 3 — Test

Only runs if TEST_SET_KEY is set at the variable-group level — the condition: guard skips the stage cleanly if you don't have a test suite configured.

The shell block is the canonical launch → wait → verify flow from How-to: run tests from the CLI:

  1. uip tm testsets run launches the run and returns an ExecutionId.
  2. uip tm wait blocks until the execution reaches a terminal state (exit 2 means timeout, not auth failure — a domain-specific reuse of the exit-code slot).
  3. uip tm report get reads Data.Failed; the step fails with ##vso[task.logissue type=error] so the outcome surfaces cleanly in the Azure DevOps UI.

Common variations

Promote across environments

Link two variable groups — uipath-stage and uipath-prod — and turn each into a separate deployment: job. Use environment-specific gates/approvals to decide when prod runs:

- stage: DeployStage
dependsOn: Build
jobs:
- deployment: deploy-stage
variables: [ { group: uipath-stage } ]
environment: 'uipath-stage'
# …same steps as above, but with stage's tenant

- stage: DeployProd
dependsOn: DeployStage
jobs:
- deployment: deploy-prod
variables: [ { group: uipath-prod } ]
environment: 'uipath-prod' # attach a manual approval gate here
# …same steps as above

Deeper coverage in How-to: pack and publish a Solution — promote one package across tenants.

Rollback

Azure DevOps does not roll back automatically; add a manual stage that re-deploys the previous version:

- stage: Rollback
dependsOn: [] # run on demand, not from Deploy
jobs:
- job: rollback
pool: { vmImage: 'ubuntu-latest' }
steps:
# …install + auth…
- script: |
uip solution deploy run \
--name "$(SOLUTION_NAME)-rollback" \
--package-name "$(SOLUTION_NAME)" \
--package-version "$(ROLLBACK_VERSION)" \
--folder-name MySolution \
--folder-path Shared
displayName: 'Re-deploy previous version'

Pass ROLLBACK_VERSION at queue time. For destructive rollback (uninstall + delete artifact), see How-to: pack and publish a Solution — rollback.

Skipping tests

To run without the test stage, delete the Test stage block (or leave TEST_SET_KEY unset — the condition: on the stage will skip it).

Common pitfalls

  • env. prefix vs literal secret. Always --client-secret env.UIPATH_CLIENT_SECRET, never --client-secret "$(UIPATH_CLIENT_SECRET)". The literal form embeds the value into the rendered command line. See the auth warning.
  • ##vso[task.setvariable] is not needed for env. resolution — env: on the step exposes the variable directly.
  • Multi-line bash needs set -euo pipefail at the top of every script: block. Without it, pack can fail while later steps keep running. See Scripting patterns — strict shell options.
  • Cache hits are not guaranteed. Always wrap the install commands in if ! command -v uip so the pipeline self-heals when the cache is cold.

See also