CI/CD recipe: GitHub Actions
This page gives you a complete .github/workflows/deploy.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. Drop it into your repo, add 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 GitHub Actions 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 the repository (or organization/environment) settings:
- Settings → Secrets and variables → Actions → New repository secret.
- Add
UIPATH_CLIENT_IDandUIPATH_CLIENT_SECRETas secrets. - Add
UIPATH_TENANTas a variable (not a secret — it is not sensitive).
- Provision a Test Manager project and test set if you want the test job. Put
TEST_SET_KEY(e.g.PROJECT:42) andPROJECT_KEYin the repo variables. See How-to: run tests from the CLI.
.github/workflows/deploy.yml
name: Deploy UiPath Solution
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
env:
CLI_VERSION: '1.0.0'
NODE_VERSION: '20'
SOLUTION_NAME: 'my-solution'
SOLUTION_DIR: './my-solution'
OUTPUT_DIR: './dist'
jobs:
build:
name: Pack Solution
runs-on: ubuntu-latest
outputs:
solution_version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Compute version
id: version
run: echo "version=1.2.0-ci.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache npm global (@uipath/cli)
uses: actions/cache@v4
with:
path: ~/.npm-global/lib/node_modules
key: uip-${{ env.CLI_VERSION }}-${{ runner.os }}
- name: Install UiPath CLI
shell: bash
run: |
set -euo pipefail
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global"
echo "$HOME/.npm-global/bin" >> "$GITHUB_PATH"
export PATH="$HOME/.npm-global/bin:$PATH"
if ! command -v uip >/dev/null; then
npm install -g "@uipath/cli@${CLI_VERSION}"
fi
uip --version
- name: Pack Solution
id: pack
shell: bash
run: |
set -euo pipefail
mkdir -p "$OUTPUT_DIR"
uip solution pack "$SOLUTION_DIR" "$OUTPUT_DIR" \
--name "$SOLUTION_NAME" \
--version "${{ steps.version.outputs.version }}"
ARTIFACT=$(find "$OUTPUT_DIR" -maxdepth 1 -name "*.zip" | head -1)
echo "artifact=$ARTIFACT" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v4
with:
name: solution-zip
path: ${{ steps.pack.outputs.artifact }}
deploy:
name: Publish and deploy
needs: build
runs-on: ubuntu-latest
environment: uipath-prod # attach approval gates here if needed
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache npm global (@uipath/cli)
uses: actions/cache@v4
with:
path: ~/.npm-global/lib/node_modules
key: uip-${{ env.CLI_VERSION }}-${{ runner.os }}
- name: Install UiPath CLI
shell: bash
run: |
set -euo pipefail
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global"
echo "$HOME/.npm-global/bin" >> "$GITHUB_PATH"
export PATH="$HOME/.npm-global/bin:$PATH"
if ! command -v uip >/dev/null; then
npm install -g "@uipath/cli@${CLI_VERSION}"
fi
- uses: actions/download-artifact@v4
with:
name: solution-zip
path: ${{ env.OUTPUT_DIR }}
- name: Authenticate
shell: bash
env:
UIPATH_CLIENT_ID: ${{ secrets.UIPATH_CLIENT_ID }}
UIPATH_CLIENT_SECRET: ${{ secrets.UIPATH_CLIENT_SECRET }}
UIPATH_TENANT: ${{ vars.UIPATH_TENANT }}
run: |
set -euo pipefail
uip login \
--client-id env.UIPATH_CLIENT_ID \
--client-secret env.UIPATH_CLIENT_SECRET \
--tenant "$UIPATH_TENANT"
- name: Publish to tenant feed
shell: bash
run: |
set -euo pipefail
ARTIFACT=$(find "$OUTPUT_DIR" -maxdepth 1 -name "*.zip" | head -1)
uip solution publish "$ARTIFACT"
- name: Deploy to Orchestrator
shell: bash
run: |
set -euo pipefail
uip solution deploy run \
--name "${SOLUTION_NAME}-${{ github.run_number }}" \
--package-name "$SOLUTION_NAME" \
--package-version "${{ needs.build.outputs.solution_version }}" \
--folder-name MySolution \
--folder-path Shared
test:
name: Run Test Manager suite
needs: deploy
if: ${{ vars.TEST_SET_KEY != '' }}
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache npm global (@uipath/cli)
uses: actions/cache@v4
with:
path: ~/.npm-global/lib/node_modules
key: uip-${{ env.CLI_VERSION }}-${{ runner.os }}
- name: Install CLI and authenticate
shell: bash
env:
UIPATH_CLIENT_ID: ${{ secrets.UIPATH_CLIENT_ID }}
UIPATH_CLIENT_SECRET: ${{ secrets.UIPATH_CLIENT_SECRET }}
UIPATH_TENANT: ${{ vars.UIPATH_TENANT }}
run: |
set -euo pipefail
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global"
echo "$HOME/.npm-global/bin" >> "$GITHUB_PATH"
export PATH="$HOME/.npm-global/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"
- name: Launch, wait, verify
shell: bash
env:
TEST_SET_KEY: ${{ vars.TEST_SET_KEY }}
PROJECT_KEY: ${{ vars.PROJECT_KEY }}
run: |
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 "::error::test run did not finish within 30 minutes"; exit 2 ;;
*) echo "::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 "::error::$FAILED test case(s) failed"
exit 1
fi
Walkthrough
build job
actions/setup-node@v4pins Node to the major version specified byNODE_VERSION. The CLI requires Node 18+.actions/cache@v4caches the npm globalnode_modulesdirectory, keyed on the pinned CLI version and the runner OS. When the cache hits, the install step'sif ! command -v uipguard turns into a no-op.- Install step configures a user-local npm prefix (no
sudoneeded on GitHub-hosted runners), adds it toGITHUB_PATHso later steps see theuipcommand, and installs the CLI. The first invocation of a tool command (uip solution …,uip tm …) auto-installs that tool — no explicituip tools installis needed. See Installing UiPath CLI — CI/CD. - Pack step invokes
uip solution packwith an explicit--versionbuilt fromgithub.run_number(monotonic, unique per run in the repo). The step captures the produced.zippath withfindrather than hardcoding the filename — keeps the recipe stable across CLI naming-convention changes. actions/upload-artifact@v4uploads the captured.zipso thedeployjob can download it in the next run.outputs.solution_versionpropagates the computed version to thedeployjob — the simplest way to share a value between jobs in the same workflow.
deploy job
- Re-installs the CLI —
needs: buildforces the job order, but each job runs on a fresh runner. The cache should hit on the second install. actions/download-artifact@v4pulls thesolution-zipinto$OUTPUT_DIR.- Authenticate step passes secrets via the step's
env:block, thenuip loginreads them through theenv.prefix. Theenv.VAR_NAMEprefix is the supported way to keep a secret out of the command line — see Authentication — the env.VAR_NAME prefix. Do not write--client-secret "${{ secrets.UIPATH_CLIENT_SECRET }}"— that embeds the value into the rendered command and into the step log. - Publish + deploy steps use
uip solution publishanduip solution deploy run.--nameusesgithub.run_numberso each deployment is identifiable. environment: uipath-prodties the job to a deployment environment. Environments are where you configure required reviewers, wait timers, and deployment branches — keep them in the UI and leave the YAML declarative.
test job
Runs only if the TEST_SET_KEY repo variable is set — the if: guard skips it otherwise. The shell block is the canonical launch → wait → verify pattern from How-to: run tests from the CLI:
uip tm testsets runlaunches and returns anExecutionId.uip tm waitblocks until terminal state. Exit2means timeout onwait(not auth failure); exit code is reported via::error::so it surfaces in the Actions UI.uip tm report getreadsData.Failedand the step exits1when anything failed.
Common variations
Promote across environments
Add a second deploy job that depends on the first, with different secrets:
deploy-stage:
needs: build
environment: uipath-stage
# …same steps as `deploy`, using ${{ secrets.UIPATH_STAGE_CLIENT_ID }} etc.
deploy-prod:
needs: deploy-stage
environment: uipath-prod # add "Required reviewers" in the environment settings
# …same steps as `deploy`, using ${{ secrets.UIPATH_PROD_CLIENT_ID }} etc.
Approval gates live in the environment settings — the needs: chain enforces order. See How-to: pack and publish a Solution — promote one package across tenants.
Rollback
Add a workflow_dispatch input and a guarded job that re-deploys a specific version:
on:
workflow_dispatch:
inputs:
rollback_version:
description: 'Version to roll back to (e.g. 1.1.9)'
required: false
type: string
jobs:
rollback:
if: ${{ github.event.inputs.rollback_version != '' }}
runs-on: ubuntu-latest
# …install + auth…
steps:
- name: Re-deploy previous version
run: |
uip solution deploy run \
--name "${SOLUTION_NAME}-rollback" \
--package-name "$SOLUTION_NAME" \
--package-version "${{ github.event.inputs.rollback_version }}" \
--folder-name MySolution \
--folder-path Shared
Trigger with Actions → Deploy UiPath Solution → Run workflow and enter the rollback version. For destructive rollback (uninstall + solution packages delete), see How-to: pack and publish a Solution — rollback.
Skip tests
Set (or leave unset) the TEST_SET_KEY repo variable. The if: ${{ vars.TEST_SET_KEY != '' }} guard skips the whole job.
Common pitfalls
${{ secrets.X }}interpolation. It substitutes at the step-rendering layer. If you put a secret into arun:command line directly, the value becomes part of the rendered script — and the step log, unless GitHub masks it. Always route secrets throughenv:and read them withenv.VAR_NAMEinsideuip.$GITHUB_PATHvs$PATH. ExportingPATH=…in one step does not carry to the next. Useecho "…" >> "$GITHUB_PATH"to persist a PATH addition across steps on the same runner.strict shell options. Start every multi-linerun:withset -euo pipefail— without it, a faileduip solution packcan be followed by a "successful" publish of a stale artifact. See Scripting patterns — strict shell options.- Cache false-positives. If you bump
CLI_VERSIONbut the cache key does not include it, you will keep using the old CLI. The key in this YAML includes${{ env.CLI_VERSION }}exactly for this reason. - Cache path is coupled to the npm prefix. The cache block uses
~/.npm-global/lib/node_modules, which only works because the install step runsnpm config set prefix "$HOME/.npm-global". If you change the prefix (e.g. on a self-hosted Windows runner where the convention is%APPDATA%\npm\node_modules), both the cachepath:and thenpm config set prefixline must move together. On a non-ubuntu-latest runner, dump the real path first with- run: npm root -gand mirror whatever it reports.
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.
- CI/CD recipe: Azure Pipelines, Jenkins, GitLab CI — the same pipeline in other platforms.