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:
- Create an External Application in UiPath with the
OR.*scopes your pipeline needs. See Authentication — Flow 2. - Store the secrets in an Azure DevOps variable group:
- Project Settings → Pipelines → Library → New variable group (for example,
uipath-prod). - Add
UIPATH_CLIENT_IDandUIPATH_CLIENT_SECRET— mark both as secret (padlock icon). - Optionally add
UIPATH_TENANT(not a secret).
- Project Settings → Pipelines → Library → New variable group (for example,
- Link the variable group to the pipeline (see the
variablesblock below). - Provision a Test Manager project and test set (if you want the test stage). You will need the
TEST_SET_KEY(formatPROJECT:42) andPROJECT_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@0pins Node.js to version 20. Pin to a major; the CLI requires 18+.Cache@2caches the npm globalnode_modulesdirectory. The cache key includes$(CLI_VERSION)so a version bump invalidates cleanly.- Install step switches npm's global prefix to
$HOME/.npm-global(nosudoneeded), 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 explicituip tools installis needed. See Installing UiPath CLI — CI/CD and Scripting patterns — pinning versions. - Pack step invokes
uip solution packwith an explicit--version(never rely on the1.0.0default in CI). publish: ...uploads the entire$(OUTPUT_DIR)as a pipeline artifact namedsolution-zipso 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 loginuses theenv.VAR_NAMEprefix for both the client ID and secret. Theenv:block at the step level maps$(UIPATH_CLIENT_ID)(from the variable group) to the actual environment variableUIPATH_CLIENT_IDthat 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 andpsoutput. Theenv.prefix feature is documented in Authentication — the env.VAR_NAME prefix.download: currentpulls thesolution-zipartifact from Stage 1 into$(Pipeline.Workspace)/solution-zip.uip solution publishuploads the.zipto the tenant feed.uip solution deploy runcreates the deployment.--nameuses$(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:
uip tm testsets runlaunches the run and returns anExecutionId.uip tm waitblocks until the execution reaches a terminal state (exit2means timeout, not auth failure — a domain-specific reuse of the exit-code slot).uip tm report getreadsData.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 forenv.resolution —env:on the step exposes the variable directly.- Multi-line bash needs
set -euo pipefailat the top of everyscript:block. Without it,packcan 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 uipso the pipeline self-heals when the cache is cold.
See also
- How-to: deploy to Orchestrator from CI — platform-agnostic guidance.
- How-to: pack and publish a Solution — versioning and rollback.
- How-to: run tests from the CLI — the launch → wait → verify pattern in depth.
- CI/CD recipe: GitHub Actions, Jenkins, GitLab CI — the same pipeline in other platforms.