Skip to content

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:

  1. Create the app (and, for a solution, the solution), once, on your machine.
  2. Develop the app locally and commit your changes.
  3. 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/cli 1.205.0 or later, with the codedapp tool (and the solution tool 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.

  1. Create a Confidential External Application in the UiPath Admin portal with the application scopes Apps, OR.Folders.Read, and OR.Execution.
  2. Assign the external app to the Orchestrator folder you deploy to (Settings → Manage Access → Assign external app).
  3. Store UIPATH_CLIENT_ID and UIPATH_CLIENT_SECRET as 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.

  1. 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 with uip admin pat list, uip admin pat regenerate, and uip admin pat revoke.

    Scope Needed for
    Apps Publishing, deploying, and exporting apps
    OR.Folders, OR.Execution Folder lookups, package upload and download
    OR.AutomationSolutions.Access Adding a deployed Coded App to a solution (uip solution resources add)
    Solutions.Packages, Solutions.Deployments Publishing and deploying the solution
    RCS.FolderAuthorization Finding deployed apps by name (uip solution resources add)
  2. Find your organization and tenant IDs: run uip login status --output json and read OrganizationId and TenantId.

  3. 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.
  4. Expose them as environment variables to the deploy step. UIPATH_URL is 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.

  1. Create the solution and the app inside it. uip codedapp init registers the app in the surrounding solution:

    uip solution init my-solution
    cd my-solution
    uip codedapp init my-app                 # add --type Action for a Coded Action App
    
    my-solution/
    ├── my-solution.uipx
    ├── resources/
    └── my-app/
        ├── project.uiproj
        ├── webAppManifest.json
        └── source/
            ├── package.json
            ├── uipath.json
            └── dist/          # packed into the solution
    
  2. Develop the app in my-app/source with the framework of your choice. Configure uipath.json and local development as described in Getting Started, and make the build write to my-app/source/dist.

  3. Ignore generated files in the solution's .gitignore:

    out/
    userProfile/
    deploy-config*.json
    node_modules/
    */source/dist/
    
  4. 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.

  1. Add the app to the solution. Run this once on your machine, signed in with uip login, and commit the files it writes under resources/:

    uip solution resources add --source remote --kind App --name my-action-app --folder-path Shared
    

    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 with No remote resource found. An app deployed earlier is found on the first attempt.

    - name: Add the Coded Action App
      run: |
        for attempt in 1 2 3 4 5; do
          uip solution resources add --source remote --kind App --name my-action-app --folder-path Shared && exit 0
          sleep 30
        done
        exit 1
    
  2. 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:

      uip solution deploy config link deploy-config.json my-action-app --name my-action-app --folder-path Shared
      
    • 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:

on:
  push:
    branches: [main]

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.