CI/CD: GitHub Actions¶
Build, pack, publish, and deploy a Coded App from a CI/CD pipeline with the uip CLI. The lifecycle has three stages:
- Create the app (and, for a solution, the solution), once, on your machine.
- Develop the app locally and commit your changes.
- Build and deploy in the pipeline. Every run packs a new version, publishes it, and deploys it: the first run installs the app, and later runs upgrade it. No run needs manual input.
The examples use GitHub Actions. The uip commands are the same in any CI/CD system; only the way secrets reach them differs (see Other CI/CD systems).
This page covers:
Requirements
@uipath/cli1.205.0 or later, with thecodedapptool (and thesolutiontool for apps inside a solution).- Node.js 22 or later, on your machine and in the workflow. The CLI does not run on older versions.
Authentication¶
A pipeline cannot sign in through a browser, so it authenticates with credentials stored in your CI/CD system's secret store.
Confidential External Application¶
Used by the standalone workflow.
- Create a Confidential External Application in the UiPath Admin portal with the application scopes
Apps,OR.Folders.Read, andOR.Execution. - Assign the external app to the Orchestrator folder you deploy to (Settings → Manage Access → Assign external app).
- Store
UIPATH_CLIENT_IDandUIPATH_CLIENT_SECRETas secrets. In GitHub, use the repository settings (Settings → Secrets and variables → Actions).
The workflow signs in with uip login --client-id env.UIPATH_CLIENT_ID --client-secret env.UIPATH_CLIENT_SECRET, which reads both values from the step's environment and keeps the secret out of the command line.
Personal Access Token¶
Used by the solution workflow. A Personal Access Token (PAT) is created once and passed to every uip command through environment variables, so no login step runs in the pipeline.
-
Create the token on your machine, signed in with
uip login:uip admin pat create \ --description "GitHub Actions deploy" \ --expiration 2027-03-31 \ --scope "Apps,OR.Folders,OR.Execution,OR.AutomationSolutions.Access,Solutions.Packages,Solutions.Deployments,RCS.FolderAuthorization"The token is printed once, in
Data.Token. Manage it later withuip admin pat list,uip admin pat regenerate, anduip admin pat revoke.Scope Needed for AppsPublishing, deploying, and exporting apps OR.Folders,OR.ExecutionFolder lookups, package upload and download OR.AutomationSolutions.AccessAdding a deployed Coded App to a solution ( uip solution resources add)Solutions.Packages,Solutions.DeploymentsPublishing and deploying the solution RCS.FolderAuthorizationFinding deployed apps by name ( uip solution resources add) -
Find your organization and tenant IDs: run
uip login status --output jsonand readOrganizationIdandTenantId. -
Store the values with your CI/CD system's secret mechanism: the token as a secret, the organization and tenant as plain variables. In GitHub, use the repository settings (Settings → Secrets and variables → Actions):
- Secret
UIPATH_PAT: the token. - Variables
UIPATH_ORG,UIPATH_ORG_ID,UIPATH_TENANT,UIPATH_TENANT_ID.
- Secret
-
Expose them as environment variables to the deploy step.
UIPATH_URLis required because a PAT carries no server information. In GitHub Actions:env: UIPATH_CLI_ENABLE_ENV_AUTH: "true" UIPATH_CLI_AUTH_TOKEN: ${{ secrets.UIPATH_PAT }} UIPATH_URL: "https://cloud.uipath.com" UIPATH_CLI_ORGANIZATION_NAME: ${{ vars.UIPATH_ORG }} UIPATH_CLI_ORGANIZATION_ID: ${{ vars.UIPATH_ORG_ID }} UIPATH_CLI_TENANT_NAME: ${{ vars.UIPATH_TENANT }} UIPATH_CLI_TENANT_ID: ${{ vars.UIPATH_TENANT_ID }}
A PAT acts as you
Commands run with your user's folder permissions. A PAT is never refreshed: create a new one before it expires, because an expired or revoked token fails with 401 without any earlier warning. If a command reports missing credentials, uip login which names the variables that are not set.
Standalone Coded App¶
Deploy a Coded App on its own, without a solution, with uip codedapp and a Confidential External Application.
.github/workflows/deploy.yml¶
name: Deploy Coded App
permissions:
contents: read
on:
workflow_dispatch:
inputs:
app-name:
description: "App name to deploy"
required: true
path-name:
description: "URL path name (https://<org>.uipath.host/<path-name>)"
required: true
org:
description: "UiPath organization name"
required: true
tenant:
description: "UiPath tenant name"
required: true
folder-key:
description: "Orchestrator folder key (UUID)"
required: true
version:
description: "App package version (default: 1.0.<run number>)"
required: false
env:
NODE_VERSION: "22"
# Or pin an exact version for reproducible builds
UIP_CLI_VERSION: ">=1.205.0"
VERSION: ${{ inputs.version || format('1.0.{0}', github.run_number) }}
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Install uip CLI
run: |
npm install -g "@uipath/cli@${UIP_CLI_VERSION}"
uip tools install codedapp
- name: Login to UiPath
env:
UIPATH_CLIENT_ID: ${{ secrets.UIPATH_CLIENT_ID }}
UIPATH_CLIENT_SECRET: ${{ secrets.UIPATH_CLIENT_SECRET }}
run: |
uip login \
--organization "${{ inputs.org }}" \
--tenant "${{ inputs.tenant }}" \
--client-id env.UIPATH_CLIENT_ID \
--client-secret env.UIPATH_CLIENT_SECRET \
--scope "Apps OR.Folders.Read OR.Execution"
- name: Pack
run: uip codedapp pack ./dist --name "${{ inputs.app-name }}" --version "$VERSION"
- name: Publish
run: uip codedapp publish --name "${{ inputs.app-name }}"
# A Coded Action App needs no --path-name
- name: Deploy
run: |
uip codedapp deploy \
--name "${{ inputs.app-name }}" \
--path-name "${{ inputs.path-name }}" \
--folder-key "${{ inputs.folder-key }}"
To deploy a new version, run the workflow again: each run packs 1.0.<run number> unless you pass version, and uip codedapp deploy upgrades the existing app.
Coded App inside Solution¶
Build the app inside a solution with uip codedapp init and deploy it with uip solution, authenticated with a Personal Access Token.
One-time setup¶
Run these on your machine and commit the result.
-
Create the solution and the app inside it.
uip codedapp initregisters the app in the surrounding solution: -
Develop the app in
my-app/sourcewith the framework of your choice. Configureuipath.jsonand local development as described in Getting Started, and make the build write tomy-app/source/dist. -
Ignore generated files in the solution's
.gitignore: -
Commit the solution folder.
.github/workflows/deploy-solution.yml¶
The build job has no access to UiPath credentials. The deploy job holds the PAT; give it a GitHub environment with required reviewers to approve each deployment before it runs.
name: Deploy solution
permissions:
contents: read
on:
workflow_dispatch:
inputs:
version:
description: "Solution package version (default: 1.0.<run number>)"
required: false
env:
NODE_VERSION: "22"
# Or pin an exact version for reproducible builds
UIP_CLI_VERSION: ">=1.205.0"
SOLUTION: "my-solution"
APP: "my-app"
# The app is served at https://<org>.uipath.host/<routing-name>
ROUTING_NAME: "my-app"
PARENT_FOLDER: "Shared"
VERSION: ${{ inputs.version || format('1.0.{0}', github.run_number) }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Build the app
working-directory: ${{ env.SOLUTION }}/${{ env.APP }}/source
run: |
npm ci
npm run build
- name: Upload the build
uses: actions/upload-artifact@v4
with:
name: app-dist
path: ${{ env.SOLUTION }}/${{ env.APP }}/source/dist
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
env:
UIPATH_CLI_ENABLE_ENV_AUTH: "true"
UIPATH_CLI_AUTH_TOKEN: ${{ secrets.UIPATH_PAT }}
UIPATH_URL: "https://cloud.uipath.com"
UIPATH_CLI_ORGANIZATION_NAME: ${{ vars.UIPATH_ORG }}
UIPATH_CLI_ORGANIZATION_ID: ${{ vars.UIPATH_ORG_ID }}
UIPATH_CLI_TENANT_NAME: ${{ vars.UIPATH_TENANT }}
UIPATH_CLI_TENANT_ID: ${{ vars.UIPATH_TENANT_ID }}
# Non-confidential external app the deployed app signs users in with
APP_CLIENT_ID: ${{ vars.UIPATH_APP_CLIENT_ID }}
defaults:
run:
working-directory: ${{ env.SOLUTION }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download the build
uses: actions/download-artifact@v4
with:
name: app-dist
path: ${{ env.SOLUTION }}/${{ env.APP }}/source/dist
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install uip CLI
run: |
npm install -g "@uipath/cli@${UIP_CLI_VERSION}"
uip tools install solution
uip tools install codedapp
- name: Pack
run: uip solution pack . ./out --version "$VERSION"
- name: Publish
run: uip solution publish "./out/${SOLUTION}_${VERSION}.zip" --wait
# The default config has no client ID and the routing name "default-app": set both on every run (a Coded Action App needs neither)
- name: Prepare the deploy config
run: |
uip solution deploy config get "$SOLUTION" --package-version "$VERSION" -d deploy-config.json
uip solution deploy config set deploy-config.json "$APP" routingName "$ROUTING_NAME"
uip solution deploy config set deploy-config.json "$APP" externalClientId "$APP_CLIENT_ID"
# Installs on the first run and upgrades in place afterwards; the deployment is matched by --name
- name: Deploy
run: |
uip solution deploy run \
--name "$SOLUTION" \
--package-name "$SOLUTION" \
--package-version "$VERSION" \
--folder-name "$SOLUTION" \
--parent-folder-path "$PARENT_FOLDER" \
--config-file deploy-config.json \
--yes
deploy run also activates the solution. After the job succeeds, the app is live at https://<org>.uipath.host/<routing-name>. To deploy a new version, run the workflow again: each run packs 1.0.<run number> unless you pass version, and deploy run upgrades the existing deployment in place.
Approval gates
Gate the deploy job, as above, rather than activation. deploy run --skip-activate followed by uip solution deploy activate <name> holds back only the first install; on an upgrade the new version goes live during deploy run.
Reference an existing Coded Action App¶
A solution can use a Coded Action App that is already deployed outside it.
-
Add the app to the solution. Run this once on your machine, signed in with
uip login, and commit the files it writes underresources/:It is safe to run again: an app that is already in the solution is reported as
Unchanged. To run it in the pipeline instead, allow for a short delay when the app was deployed moments earlier, for example by the same pipeline. A newly deployed app takes up to a couple of minutes to become findable, and until then the command fails withNo remote resource found. An app deployed earlier is found on the first attempt. -
Choose how it is deployed:
-
Link it to the deployed app. The solution uses the existing deployment and installs no copy. Add this to the Prepare the deploy config step:
-
Install the solution's own copy. Skip the link. The copy lives in the solution's folder and follows the solution's version; the original deployment is not changed.
-
Deploy on push to main¶
Either workflow can run on every push instead of on demand. Replace the trigger:
Other CI/CD systems¶
The build and deploy steps above are plain shell commands, so they run unchanged in Azure DevOps, Jenkins, or any other CI/CD system. Store the credentials in that system's secret store and map them to the same environment variables for the deploy steps. Use its approval mechanism for the gate: environment approvals in Azure DevOps, an input step in Jenkins. The examples below pass a PAT to the solution workflow.
Secret variables are not exposed to scripts automatically, so map them in env::
- script: |
uip solution pack . ./out --version "$VERSION"
uip solution publish "./out/${SOLUTION}_${VERSION}.zip" --wait
# ... deploy config and deploy run, as in the GitHub Actions workflow
workingDirectory: my-solution
env:
UIPATH_CLI_ENABLE_ENV_AUTH: "true"
UIPATH_CLI_AUTH_TOKEN: $(UIPATH_PAT)
UIPATH_URL: "https://cloud.uipath.com"
UIPATH_CLI_ORGANIZATION_NAME: $(UIPATH_ORG)
UIPATH_CLI_ORGANIZATION_ID: $(UIPATH_ORG_ID)
UIPATH_CLI_TENANT_NAME: $(UIPATH_TENANT)
UIPATH_CLI_TENANT_ID: $(UIPATH_TENANT_ID)
Store the PAT as a Secret text credential and bind it with credentials():
stage('Deploy') {
environment {
UIPATH_CLI_ENABLE_ENV_AUTH = 'true'
UIPATH_CLI_AUTH_TOKEN = credentials('uipath-pat')
UIPATH_URL = 'https://cloud.uipath.com'
UIPATH_CLI_ORGANIZATION_NAME = 'my-org'
UIPATH_CLI_ORGANIZATION_ID = '<organization-id>'
UIPATH_CLI_TENANT_NAME = 'my-tenant'
UIPATH_CLI_TENANT_ID = '<tenant-id>'
}
steps {
dir('my-solution') {
sh 'uip solution pack . ./out --version "$VERSION"'
sh 'uip solution publish "./out/${SOLUTION}_${VERSION}.zip" --wait'
// ... deploy config and deploy run, as in the GitHub Actions workflow
}
}
}
Troubleshooting¶
| Error | Cause and fix |
|---|---|
Property 'externalClientId' is required |
The deploy config has no client ID for the app. Set it with uip solution deploy config set deploy-config.json <app> externalClientId <id>. |
| The routing name is already in use | Another app owns the URL path. Choose a different ROUTING_NAME. |
No remote resource found from uip solution resources add |
The app is not findable yet, or the token's user cannot see its folder. Retry after a couple of minutes, and check the folder path. |
deploy run stops because the package is already deployed |
Pass --yes. With the same --name, the existing deployment is upgraded in place. |
401 on every command |
The PAT expired or was revoked. Create a new one and update the UIPATH_PAT secret. |