2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Azure PipelinesとUiPath CLIでUiPathプロジェクトのCI/CD環境を構築する

2
Last updated at Posted at 2026-10-06

はじめに

UiPathの開発プロジェクトを運用していると、開発環境で作成したパッケージを手動でパブリッシュしたり、検証環境・本番環境へ手動で反映したりする場面があるかと思います。

小規模なプロジェクトであれば手動運用でも大きな問題にならないことがありますが、開発者が増えたり、リリース頻度が高くなったりすると、次のような課題が出てきます。

  • パッケージ化やデプロイの手順が人によって異なる
  • リリース作業時の設定ミスや反映漏れが発生する
  • どのソースコードからどのパッケージが作成されたのか追跡しづらい
  • テストやレビューの実施タイミングが属人化する
  • 検証環境と本番環境でリリース手順が揃わない

このような課題に対して、CI/CDパイプラインを構築しておくことで、コードの変更からビルド、テスト、パッケージ化、デプロイまでの流れを自動化できます。

本記事では、Azure Pipelines と UiPath CLI を利用して、UiPathプロジェクトのCI/CD環境を構築する流れをまとめます。

公式ドキュメントとしては、以下のページが参考になります。

この記事でわかること

この記事では、以下の内容を扱います。

  • UiPathにおけるCI/CDの基本的な考え方
  • Azure Pipelinesを使ってUiPathプロジェクトをビルド・デプロイする構成
  • UiPath CLIをAzure Pipelines上で実行するための準備
  • 外部アプリケーションを利用した認証方法

前提

本記事では、以下の環境・知識を前提としています。

  • UiPath Automation Cloud を利用できること
  • Orchestrator上にデプロイ先のテナント・フォルダーが用意されていること
  • Azure DevOps のプロジェクトを利用できること
  • Azure Repos または GitHub 等でUiPathプロジェクトをソース管理していること

なお、本記事ではAzure PipelinesのYAMLを使ってパイプラインを定義します。

CI/CDとは

CI/CDは、ソースコードの変更を起点として、ビルド、テスト、パッケージ化、デプロイといった一連の作業を自動化するための考え方です。

CIは Continuous Integration の略で、日本語では継続的インテグレーションと呼ばれます。
開発者がコードを変更したタイミングで、自動的にビルドやテストを実行し、変更内容に問題がないかを早い段階で確認します。

CDは Continuous Delivery または Continuous Deployment の略で、日本語では継続的デリバリー、または継続的デプロイメントと呼ばれます。
CIで確認済みの成果物を、検証環境や本番環境へ安全に反映するための仕組みです。

UiPathプロジェクトに置き換えると、概ね以下のような流れになります。

なぜCI/CDが必要か

UiPathの開発では、Studio上でワークフローを作成し、パッケージをOrchestratorへパブリッシュして実行環境へ反映する流れが一般的です。

ただし、これらの作業を毎回手動で行う場合、リリース作業が属人化しやすくなります。
たとえば、ある開発者はStudioから直接パブリッシュし、別の開発者は別の手順でパッケージをアップロードする、といった状態になると、リリース手順の再現性が下がります。

CI/CDを導入することで、以下のようなメリットがあります。

  • ソースコードの変更を起点に、ビルドやデプロイを自動化できる
  • 同じ手順でパッケージ化・デプロイを実行できる
  • 手作業による設定ミスや反映漏れを減らせる
  • パイプラインの実行履歴から、いつ・誰が・どの変更をリリースしたか確認しやすくなる
  • 検証環境から本番環境への反映を承認付きで管理しやすくなる
  • テストや静的解析をリリース前の標準ステップとして組み込みやすくなる

特に複数人で開発する場合や、検証環境・本番環境を分けて運用している場合は、CI/CDの仕組みを用意しておくことで、リリース作業を安定化できます。

Azure Pipelinesとは

Azure Pipelinesは、Azure DevOpsに含まれるCI/CDサービスです。
リポジトリへのPushやPull Requestをトリガーとして、YAMLで定義したパイプラインを実行できます。

UiPathプロジェクトの場合、Azure Pipelines上でUiPath CLIを実行することで、以下のような処理を自動化できます。

  • UiPath CLIのインストール
  • UiPath Automation Cloud / Orchestratorへの認証
  • UiPathプロジェクトのパッケージ化
  • パッケージのパブリッシュ
  • Orchestrator上のプロセス更新
  • テスト実行
  • 検証環境・本番環境への段階的なデプロイ

Azure Pipelinesを利用する利点は、リポジトリの変更とリリース作業をYAMLで一元管理できる点です。
また、Azure DevOpsのVariable GroupやEnvironmentの承認機能を利用することで、シークレット管理や本番デプロイ前の承認フローも組み込みやすくなります。

本記事では、Azure Pipelines上でUiPath CLIを実行し、UiPathプロジェクトをパッケージ化してOrchestratorへデプロイする構成を扱います。

Automation OpsのPipelineとの比較

UiPathには、Automation OpsのPipeline機能も用意されています。
Automation OpsのPipelineを利用すると、UiPath側で用意されたパイプラインプロセスを使って、外部リポジトリ上のUiPathプロジェクトに対する分析、ビルド、パブリッシュ、プロセス更新などを実行できます。

一方で、Azure PipelinesとUiPath CLIを組み合わせる場合は、パイプラインの定義をAzure DevOps側のYAMLとして管理します。
そのため、既にAzure DevOpsを標準的なCI/CD基盤として利用している組織では、既存の運用ルールに合わせやすい構成になります。

大まかには、以下のような使い分けになると考えています。

観点 Azure Pipelines + UiPath CLI Automation Ops Pipeline
パイプライン定義 Azure PipelinesのYAMLで管理 UiPath Automation Ops上で管理
実行基盤 Azure DevOps UiPath Automation Cloud
向いているケース Azure DevOps中心でCI/CDを統一したい場合 UiPath上でパイプラインを完結させたい場合
カスタマイズ性 YAMLで柔軟に制御しやすい UiPathのパイプラインプロセスに沿って構成しやすい
シークレット管理 Azure DevOpsのVariable Group等を利用 UiPath側の設定・接続情報を利用
承認フロー Azure DevOps Environment等を利用 Action Center等を利用する構成が可能

どちらが常に優れているというものではなく、既存の開発基盤や運用ルールに合わせて選択するのがよいと思います。

本記事では、Azure DevOpsをCI/CD基盤として利用する前提で、Azure PipelinesとUiPath CLIを使った構成を紹介します。

Automation OpsをGitHubと連携したパイプラインの構築は、以下の記事が参考になります。

今回構築するパイプライン

ここからは、Pythonで実装したUiPath Coded Functionを例に、Azure PipelinesからUiPathへパブリッシュする構成を見ていきます。

今回使用するYAMLでは、main ブランチへの変更を起点として、次の処理を実行します。

  1. ソースコードをチェックアウトする
  2. Node.jsとUiPath CLIを準備する
  3. 外部アプリケーションのクライアント資格情報でUiPathへログインする
  4. Pythonと依存関係を準備する
  5. パッケージのバージョンを更新する
  6. Coded Functionをパッケージ化してテナントフィードへパブリッシュする
  7. Orchestratorにプロセスがなければ作成し、既にあれば新しいバージョンへ更新する

以降の各節は、後述する完成版サンプルYAMLからの抜粋です。抜粋どうしはそのまま連結して動作します。

今回のサンプルはパブリッシュとプロセス更新までを対象としています。静的解析、単体テスト、Test Managerのテストセット実行、本番環境への承認付きデプロイは含んでいません。これらは後述する拡張ポイントとして追加できます。

また、Coded Functionおよび関連するCLIの仕様は変更される可能性があります。利用時には、固定したUiPath CLIのバージョンに対応するコマンドリファレンスも確認してください。

なお、UiPath CLIではCoded Functionのパッケージ化・パブリッシュに uip codedagent コマンド群を使用します。本記事の変数名やコマンドに codedagent / CODED_AGENT_ が現れるのはこのためで、対象はPython Coded Functionです。

リポジトリの構成

サンプルでは、パイプラインのYAMLとCoded Functionを同じリポジトリで管理します。

repository/
├── azure-pipeline-coded-function.yml
└── my-function/
    ├── main.py
    ├── pyproject.toml
    ├── uv.lock
    ├── uipath.json
    └── requirements.txt  # 利用する場合

サンプルYAMLは uv.lock があればuvで、なければ requirements.txt / pyproject.toml でpipにフォールバックする構成にしています。本記事では uv.lock を含む構成を基本として説明します。

複数のUiPathプロジェクトを一つのリポジトリに配置するMonorepo構成では、共通スクリプトやYAMLテンプレートを再利用しやすい一方、どの変更でどのパイプラインを動かすかを明確にする必要があります。

今回のYAMLでは paths.include を使い、my-function 配下またはパイプライン定義自体が変更されたときだけ実行します。

trigger:
  branches:
    include:
      - main
  paths:
    include:
      - my-function/**
      - azure-pipeline-coded-function.yml

pr: none

pr: none としているため、この定義だけではPull Request作成時の検証は実行されません。実運用では、次のように役割を分けると扱いやすくなります。

  • Pull Request時: 静的解析、テスト、パッケージ化できることの確認
  • main へのマージ時: パッケージのパブリッシュ、検証環境へのデプロイ
  • 本番リリース時: Azure DevOps Environmentの承認後に本番環境へデプロイ

プロジェクトごとに権限やリリースサイクルを分離したい場合は、Multi Repo構成も選択肢になります。

事前準備

UiPathの外部アプリケーションを作成する

Azure PipelinesからUiPath Automation Cloudのリソースにアクセスするため、UiPathの外部アプリケーションのクライアント資格情報フローを利用します。

UiPath側で外部アプリケーションを作成し、パイプラインから実行する操作に必要なスコープを付与します。サンプルではOrchestratorのプロセス一覧取得、作成、バージョン更新を行うため、利用するCLIコマンドに対応した OR.* スコープが必要です。

権限は必要以上に広くせず、検証環境用と本番環境用の外部アプリケーションも分けておくと安全です。

作成後に得られる次の値を控えておきます。

  • Client ID
  • Client Secret
  • 接続先のテナント名

Client Secretは作成直後にしか確認できない場合があるため、安全な場所に保管します。

Azure DevOpsのVariable Groupを作成する

YAMLファイルにClient IDやClient Secretを直接記述しないようにします。Azure DevOpsの Pipelines > Library からVariable Groupを作成し、次のように登録します。

変数名 内容 Secretとして登録
UIPATH_CLIENT_ID 外部アプリケーションのClient ID はい
UIPATH_CLIENT_SECRET 外部アプリケーションのClient Secret はい
UIPATH_TENANT 接続先テナント名 いいえ

サンプルYAMLのVariable Group名は TestVariableGroup です。

variables:
  - group: TestVariableGroup

Variable Groupを作成しただけでは、パイプラインから利用できない場合があります。Pipeline permissionsで対象のパイプラインでの利用を許可しておきます。

規模が大きい場合や、複数システムで同じ資格情報を管理する場合は、Azure Key VaultとVariable Groupを連携する方法も検討できます。

Azure Pipelineを作成する

Azure DevOpsで対象リポジトリを選択し、既存のAzure Pipelines YAMLファイルを指定してパイプラインを作成します。YAML自体もソースコードと一緒にレビュー対象にできるため、処理内容や接続先の変更履歴を追跡できます。

YAMLの基本構造

Azure PipelinesのYAMLは、主に次の要素で構成されます。

要素 役割
trigger / pr パイプラインをいつ実行するか
variables 共通値やVariable Groupを定義する
stages Build、Test、Deployなどの大きな工程
jobs エージェント上で実行される作業単位
steps task、script、checkoutなどの具体的な処理

構造は次のような入れ子になります。

Pipeline
└── Stage
    └── Job
        └── Steps
            ├── Task
            └── Script

今回のサンプルは PublishAndDeploy という一つのStageと、publish_agent という一つのJobで構成しています。

stages:
  - stage: PublishAndDeploy
    displayName: Publish Coded Function
    jobs:
      - job: publish_agent
        displayName: Publish and deploy Coded Function
        pool:
          vmImage: $(VM_IMAGE)
        # 対話プロンプトで停止した場合に無限に待たないようにする
        timeoutInMinutes: 30

vmImage も変数にしています。ubuntu-latest はMicrosoft側の更新で実体のOSが切り替わるため、再現性を重視する場合は ubuntu-24.04 のようにバージョンを明示します。

timeoutInMinutes は念のためのタイムアウト値です。CLIが対話プロンプトを表示して入力待ちになった場合、ジョブがエージェントを占有し続けてしまうため、上限を設けています。

最初は一つのStageでも構いませんが、本番運用ではBuildとDeployを分け、Buildで作成したパッケージをPipeline Artifactとして保存する構成が適しています。同じ成果物を検証環境から本番環境へ昇格でき、環境ごとの再ビルドによる差異を避けやすくなるためです。

task と script の使い分け

Azure Pipelinesでは、組み込みの task と任意のコマンドを実行する script を組み合わせます。

観点 task script
役割 Azure Pipelinesが用意した処理を呼び出す シェルコマンドを直接実行する
設定 inputs にタスク固有の値を書く コマンドの引数や環境変数として渡す
向いている処理 Node.jsやPythonの準備、キャッシュ、Artifact操作 uip コマンドやプロジェクト固有の処理
例 UseNode@1、UsePythonVersion@0、Cache@2、PublishPipelineArtifact@1 uip login、uip codedagent pack

サンプルの script ステップでは、パイプライン変数を $(VAR) の形でスクリプト本文に直接埋め込まず、env: で環境変数にマップしてから $VAR として参照しています。

パイプラインの実装

変数を定義する

環境やバージョンによって変わる値は、YAMLの先頭で変数として定義します。

variables:
  - group: TestVariableGroup

  # --- ツールチェーンのバージョン(再現性のためパッチまで固定) ---
  - name: VM_IMAGE
    value: 'ubuntu-24.04'
  - name: CLI_VERSION
    value: '1.195.0'
  - name: NODE_VERSION
    value: '20.19.2'
  - name: PYTHON_VERSION
    value: '3.13'

  # --- プロジェクト固有の値(環境に合わせて変更してください) ---
  - name: CODED_AGENT_DIR
    value: '$(Build.SourcesDirectory)/my-function'
  - name: CODED_AGENT_VERSION
    value: '0.1.$(Build.BuildId)'
  - name: PROCESS_NAME
    value: 'hello-world-coded-function'  # デプロイしているプロセス名があれば、それと一致させます
  - name: PACKAGE_ID
    value: 'helloworld-codedfunction'   # pyproject.toml の name と一致させます
  - name: FOLDER_PATH
    value: 'ExampleFolder'

  # --- 内部用 ---
  - name: NPM_GLOBAL_CACHE
    value: '$(Pipeline.Workspace)/.npm-global'

$(Build.SourcesDirectory) と $(Build.BuildId) はAzure Pipelinesの定義済み変数です。Build.BuildId をパッケージのバージョンへ含めることで、実行ごとに一意なバージョンを生成し、パイプラインの実行履歴とパッケージを対応付けられます。

CLI_VERSION も明示的に固定しています。常に最新版を取得する構成は、CLI更新によって同じYAMLの挙動が変わる可能性があります。バージョンを更新するときは、変更内容を確認したうえでPull Requestとしてレビューするのが安全です。

PROCESS_NAME、PACKAGE_ID、FOLDER_PATH、テナント名は環境固有の値です。サンプルを利用する際は、自分のUiPath環境に合わせて置き換えてください。

Node.jsとUiPath CLIを準備する

UiPath CLIはnpmパッケージとしてインストールします。最初に UseNode@1 で利用するNode.jsのバージョンを揃えます。

- task: UseNode@1
  displayName: Use Node.js $(NODE_VERSION)
  inputs:
    version: $(NODE_VERSION)

続いて、キャッシュ対象のディレクトリを先に作成してから Cache@2 を設定します。

# Cache@2 の path は事前に作成しておく
- script: mkdir -p "$(NPM_GLOBAL_CACHE)"
  displayName: Prepare npm global cache directory

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

キャッシュキーにOSとCLIのバージョンを含めることで、実行環境またはCLIのバージョンを変更したときに古いキャッシュを誤って利用しないようにしています。

そのうえで、キャッシュがヒットしなかった場合だけCLIをインストールします。

- script: |
    set -euo pipefail

    npm config set prefix "$NPM_GLOBAL_CACHE"
    export PATH="$NPM_GLOBAL_CACHE/bin:$PATH"
    # 後続 Step でも uip を実行できるよう PATH を追加する
    echo "##vso[task.prependpath]$NPM_GLOBAL_CACHE/bin"

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

    uip --version
  displayName: Install UiPath CLI
  env:
    NPM_GLOBAL_CACHE: $(NPM_GLOBAL_CACHE)
    CLI_VERSION: $(CLI_VERSION)

##vso[task.prependpath]... は、後続のStepでも uip を実行できるようPATHを追加するAzure PipelinesのLogging Commandです。最後に uip --version を出力しておくと、実行ログから使用したCLIのバージョンを確認できます。

UiPathへログインする

認証情報はVariable GroupからStepの環境変数へ渡します。

- script: |
    set -euo pipefail

    uip login \
      --client-id env.UIPATH_CLIENT_ID \
      --client-secret env.UIPATH_CLIENT_SECRET \
      --scope "OR.Default" \
      --tenant "$UIPATH_TENANT"

    # --- フォールバック(env. 形式が使えない場合)---
    # uip login \
    #   --client-id "$UIPATH_CLIENT_ID" \
    #   --client-secret "$UIPATH_CLIENT_SECRET" \
    #   --scope "OR.Default" \
    #   --tenant "$UIPATH_TENANT"
  displayName: Authenticate to UiPath
  env:
    # Secret 変数は自動では環境変数に公開されないため、明示的にマップする
    UIPATH_CLIENT_ID: $(UIPATH_CLIENT_ID)
    UIPATH_CLIENT_SECRET: $(UIPATH_CLIENT_SECRET)
    UIPATH_TENANT: $(UIPATH_TENANT)

以下の点が重要です。

  1. Azure DevOpsのSecret変数は自動では環境変数に展開されないため、env: で明示的にマップする必要があります
  2. SecretをYAMLへ直接記述しません
  3. uip login へ env.UIPATH_CLIENT_SECRET の形式で渡します。この形式ではUiPath CLIが環境変数から値を解決するため、展開したSecretをコマンドライン引数へ載せずに済みます

3点目の env. 形式は、利用するCLIのバージョンでサポートされているかを uip login --help で確認してください。サポートされていない場合は、上記コメントアウトしているフォールバックのように値を直接渡す形になります。その場合、プロセス一覧からコマンドライン引数が見えうる点に注意し、共有のself-hosted agentでは特に取り扱いを検討してください。

--scope は、実際にパイプラインが実行する操作に必要な範囲に絞ることを推奨します。サンプルでは OR.Default としていますが、外部アプリケーション側で付与したスコープと合わせて見直してください。OR.Defaultはきめ細かいアクセス権管理を利用した場合に利用するスコープです。外部アプリケーションに設定したスコープを直接ここに記載する方法でも問題ありません。きめ細かいアクセス権管理では、Orchestrator側でもアクセス権設定が必要になります。詳細については以下の記事をご参照ください。

サンプルYAMLではテナント名をVariable Groupの UIPATH_TENANT から取得しています。検証環境・本番環境で別のVariable Groupを割り当てれば、YAMLを変えずに接続先を切り替えられます。

Microsoft-hosted agentはJobごとに破棄されますが、self-hosted agentでは認証セッションが残るため、サンプルでは最後に uip logout を実行しています(後述)。

Python と uv を準備する

Python Coded Functionをパッケージ化するため、UsePythonVersion@0 でPythonのバージョンを指定します。

- task: UsePythonVersion@0
  displayName: Use Python $(PYTHON_VERSION)
  inputs:
    versionSpec: $(PYTHON_VERSION)

- script: |
    set -euo pipefail

    python --version
    python -m pip install --upgrade pip uv
    uv --version
  displayName: Install uv

uv.lock を使ってロックファイルの検証と依存関係のインストールを行うため、この時点でuvを入れています。

Python Coded Functionでは、pyproject.toml にプロジェクト名、バージョン、Pythonの要求バージョン、依存パッケージなどを定義します。uipath.json には、呼び出し可能な関数のエントリーポイントを定義します。

パッケージのバージョンを更新する

同じバージョンのパッケージを何度もパブリッシュしないよう、パイプライン実行時に pyproject.toml のバージョンを CODED_AGENT_VERSION(= 0.1.$(Build.BuildId))へ書き換えます。

- script: |
    set -euo pipefail
    cd "$CODED_AGENT_DIR"

    python - <<'PY'
    import os
    import re
    from pathlib import Path

    version = os.environ["PACKAGE_VERSION"]
    path = Path("pyproject.toml")
    text = path.read_text(encoding="utf-8")

    pattern = r'(?m)^version\s*=\s*"[^"]+"'
    if not re.search(pattern, text):
        raise SystemExit("version field was not found in pyproject.toml")

    path.write_text(
        re.sub(pattern, f'version = "{version}"', text, count=1),
        encoding="utf-8",
    )
    print(f"Updated pyproject.toml version to {version}")
    PY

    grep -n '^version' pyproject.toml
  displayName: Update pyproject.toml version
  env:
    CODED_AGENT_DIR: $(CODED_AGENT_DIR)
    PACKAGE_VERSION: $(CODED_AGENT_VERSION)

version 行が見つからない場合はその場で失敗させ、置換したつもりで元のバージョンのままパッケージ化されることを防いでいます。最後の grep は、実際に書き換わった値をログへ残すためのものです。

uv.lockを更新して検証する

pyproject.toml のバージョンを変更すると、uv.lock の内容とずれます。そのため、ロックファイルを更新したうえで、期待したバージョンになっていることを確認します。

- script: |
    set -euo pipefail
    cd "$CODED_AGENT_DIR"

    # UsePythonVersion@0 が導入したインタプリタを明示的に使う
    PYTHON_BIN="$(python -c 'import sys; print(sys.executable)')"

    uv lock --python "$PYTHON_BIN"
    uv lock --locked --python "$PYTHON_BIN"

    python - <<'PY'
    import os
    import re
    from pathlib import Path

    version = os.environ["PACKAGE_VERSION"]
    package_id = os.environ["PACKAGE_ID"]

    text = Path("uv.lock").read_text(encoding="utf-8")
    pattern = (
        rf'(?ms)^\[\[package\]\]\nname = "{re.escape(package_id)}"\n'
        rf'version = "([^"]+)"'
    )
    match = re.search(pattern, text)
    if not match:
        raise SystemExit(
            f"{package_id} was not found in uv.lock. "
            "PACKAGE_ID と pyproject.toml の name が一致しているか確認してください。"
        )
    if match.group(1) != version:
        raise SystemExit(f"uv.lock has {match.group(1)}, expected {version}")

    print(f"uv.lock is synchronized for {package_id} {version}")
    PY
  displayName: Refresh and validate uv.lock
  env:
    CODED_AGENT_DIR: $(CODED_AGENT_DIR)
    PACKAGE_VERSION: $(CODED_AGENT_VERSION)
    PACKAGE_ID: $(PACKAGE_ID)

依存関係をインストールする

uv.lock がある場合はuvで依存関係をインストールし、pipとの混在を避けます。

- script: |
    set -euo pipefail
    cd "$CODED_AGENT_DIR"

    PYTHON_BIN="$(python -c 'import sys; print(sys.executable)')"

    if [ -f "uv.lock" ]; then
      uv sync --frozen --python "$PYTHON_BIN"

      # uip codedagent は .venv があると VIRTUAL_ENV の設定(activate 済み)を要求する
      # shellcheck disable=SC1091
      source .venv/bin/activate
      # 後続 Step(pack / publish)でも同じ venv を有効にする
      echo "##vso[task.setvariable variable=VIRTUAL_ENV]$VIRTUAL_ENV"
      echo "##vso[task.prependpath]$VIRTUAL_ENV/bin"
    elif [ -f "requirements.txt" ]; then
      python -m pip install -r requirements.txt
    elif [ -f "pyproject.toml" ]; then
      python -m pip install .
    else
      echo "No dependency definition found." >&2
      exit 1
    fi

    uip codedagent setup --force
  displayName: Install dependencies and configure Coded Function
  env:
    CODED_AGENT_DIR: $(CODED_AGENT_DIR)

uv sync はプロジェクト直下に .venv を作成しますが、有効化(activate)はしません。uip codedagent setup は、カレントディレクトリに .venv があるのに環境変数 VIRTUAL_ENV が設定されていない場合、Found .venv in current directory but no virtual environment is activated. というエラーで失敗するため、uv sync の直後に source .venv/bin/activate を実行しています。

また、Azure PipelinesではStepごとに新しいシェルが起動するため、activateの効果は後続のStepへ引き継がれません。pack / publishのStepでも同じvenvが使われるよう、##vso[task.setvariable] で VIRTUAL_ENV を、##vso[task.prependpath] でvenvの bin をPATHに設定しています。

パッケージ化する

準備ができたら、プロジェクトのディレクトリでパッケージ化します。

- script: |
    set -euo pipefail
    cd "$CODED_AGENT_DIR"

    uip codedagent pack

    echo "--- generated packages ---"
    find . -name '*.nupkg' -newermt '-10 minutes'
  displayName: Pack Python Coded Function
  env:
    CODED_AGENT_DIR: $(CODED_AGENT_DIR)

# トレーサビリティとロールバックのため、成果物を保存する
# Build / Deploy を分離する際は、Deploy 側でこの Artifact を取得する
- task: PublishPipelineArtifact@1
  displayName: Publish package artifact
  inputs:
    targetPath: $(CODED_AGENT_DIR)/.uipath
    # 失敗ジョブの再実行でも Build.BuildId は変わらないため、試行回数を付けて名前の重複を避ける
    artifact: "uipath-package-$(Build.BuildId)-$(System.JobAttempt)"
    publishLocation: pipeline

pack はソースコードとメタデータ、依存関係をUiPathで扱えるパッケージにまとめます。生成物は .uipath ディレクトリ配下に出力されるため、内容をログへ出しておくと、想定したバージョンのパッケージが生成されたかを実行ログから確認できます。

生成したパッケージは、トレーサビリティとロールバックのためPipeline Artifactとしても保存しています。パッケージのバージョンに Build.BuildId を含めているので、どのビルドがどのパッケージを作ったかはバージョン番号からも辿れます。BuildとDeployを別Stageに分ける場合は、Deploy側でこのArtifactを取得します(後述)。

Artifact名には $(System.JobAttempt)(ジョブの試行回数)を含めています。「Rerun failed jobs」で再実行しても Build.BuildId は変わらないため、uipath-package-$(Build.BuildId) だけでは1回目にアップロードしたArtifactと名前が重複し、Artifact uipath-package-398 already exists for build 398. のエラーになるためです。

テナントフィードへパブリッシュする

- script: |
    set -euo pipefail
    cd "$CODED_AGENT_DIR"

    uip codedagent publish --tenant
  displayName: Publish Python Coded Function
  env:
    CODED_AGENT_DIR: $(CODED_AGENT_DIR)

publish は作成したパッケージをUiPathのフィードへ登録します。

ここでの --tenant はテナント名を渡すオプションではなく、テナントのパッケージフィードをパブリッシュ先として選ぶための指定です。接続先テナント自体は、先に実行した uip login のセッションによって決まります。

publish の引数や、パブリッシュ先フィードの指定方法はCLIのバージョンによって異なる可能性があります。対話形式の選択が発生するとCIが停止するため、利用中のバージョンのヘルプを確認し、フィードを非対話で一意に指定してください。

uip codedagent publish --help

なお、現在のPython Coded FunctionsのドキュメントではPython SDK側の uipath pack、uipath publish も案内されています。今回のYAMLは固定した @uipath/cli の uip codedagent コマンドを使用しているため、両者を混在させず、採用するツールチェーンをプロジェクト内で統一します。

プロセスを作成または更新する

パッケージをパブリッシュしただけでは実行できないため、Orchestrator上のプロセスを作成または更新します。

サンプルでは、最初にプロセス一覧をJSON形式で取得し、名前またはパッケージIDが一致するプロセスのKeyを抽出します。

- script: |
    set -euo pipefail

    # uip は失敗時もエラー JSON を stdout に出すため、
    # 変数に取り込んだまま set -e で終了しないよう明示的に判定して出力する
    if ! PROCESS_JSON=$(uip or processes list \
      --folder-path "$FOLDER_PATH" \
      --name "$PROCESS_NAME" \
      --process-type Function \
      --output json \
      --output-filter "[?Name=='$PROCESS_NAME' || ProcessKey=='$PACKAGE_ID'].Key | [0]"); then
      echo "##[error]Failed to list processes in folder '$FOLDER_PATH'."
      echo "$PROCESS_JSON"
      exit 1
    fi
    echo "$PROCESS_JSON"

    # 見つかった場合: {"Data": {"Value": "<GUID>"}} / 見つからない場合: {"Data": []}
    # stdin はヒアドキュメントで使われるため、JSON は環境変数で渡す
    PROCESS_GUID_KEY=$(PROCESS_JSON="$PROCESS_JSON" python - <<'PY'
    import json
    import os

    data = json.loads(os.environ["PROCESS_JSON"]).get("Data")
    value = data.get("Value") if isinstance(data, dict) else data
    print(value if isinstance(value, str) else "", end="")
    PY
    )

    if [ -z "$PROCESS_GUID_KEY" ]; then
      echo "Process not found. Creating a new process."
      uip or processes create \
        --folder-path "$FOLDER_PATH" \
        --name "$PROCESS_NAME" \
        --package-key "$PACKAGE_ID" \
        --package-version "$CODED_AGENT_VERSION" \
        --output json
    else
      echo "Process found ($PROCESS_GUID_KEY). Updating version."
      uip or processes update-version "$PROCESS_GUID_KEY" \
        --folder-path "$FOLDER_PATH" \
        --package-version "$CODED_AGENT_VERSION" \
        --output json
    fi
  displayName: Create or Update Process
  env:
    FOLDER_PATH: $(FOLDER_PATH)
    PROCESS_NAME: $(PROCESS_NAME)
    PACKAGE_ID: $(PACKAGE_ID)
    CODED_AGENT_VERSION: $(CODED_AGENT_VERSION)

このように「存在しなければ作成、存在すれば更新」としておくことで、初回と2回目以降の実行を同じYAMLで扱えます。パイプラインを再実行しても意図した状態へ収束しやすい実装にしています。

--output json と --output-filter を利用すると、人間向けの表形式出力を文字列処理するより安定してKeyを取得できます。プロセスが見つかった場合は {"Data": {"Value": "<GUID>"}}、見つからなかった場合は {"Data": []} の形で返ります。

uip は失敗時もエラー内容をJSONとしてstdoutに出力し、exit code 1を返します。PROCESS_JSON=$(uip ...) のように出力を変数へ取り込むだけだと、set -e によってエラー内容が表示されないまま Bash exited with code '1'. で終了してしまうため、if ! ...; then で失敗を判定し、エラーのJSONを出力してから終了するようにしています。

また、JSONは printf ... | python - <<'PY' のようにパイプで渡すのではなく、環境変数で渡しています。python - のstdinはヒアドキュメント(スクリプト本体)で上書きされるため、パイプで渡したJSONを sys.stdin から読めず、プロセスが存在していても常に「見つからない」と判定されてしまうためです。

サンプルファイル

上記を反映した構成ファイルは以下のようになります。ご利用の環境や対象のプロジェクトに応じて適宜ご変更ください。

# ------------------------------------------------------------
# UiPath Python Coded Function を Azure Pipelines からパブリッシュする
#
# 前提:
#   - Variable Group "TestVariableGroup" に以下を登録済みであること
#       UIPATH_CLIENT_ID     (Secret)
#       UIPATH_CLIENT_SECRET (Secret)
#       UIPATH_TENANT        (通常の変数)
#   - Variable Group の Pipeline permissions で、本パイプラインの利用を許可済みであること
#   - my-function/pyproject.toml の name が、下記 PACKAGE_ID と一致していること
#
# 注意:
#   UiPath CLI のコマンド体系はバージョンによって変わります。
#   採用するバージョンで `uip login --help` / `uip codedagent publish --help` を
#   必ず確認してください。
# ------------------------------------------------------------

trigger:
  branches:
    include:
      - main
  paths:
    include:
      - my-function/**
      - azure-pipeline-coded-function.yml

# PR 時の検証は別パイプライン(静的解析・テスト・pack のみ)で行うものとします
pr: none

variables:
  - group: TestVariableGroup

  # --- ツールチェーンのバージョン(再現性のためパッチまで固定) ---
  - name: VM_IMAGE
    value: "ubuntu-24.04"
  - name: CLI_VERSION
    value: "1.195.0"
  - name: NODE_VERSION
    value: "20.19.2"
  - name: PYTHON_VERSION
    value: "3.13"

  # --- プロジェクト固有の値(環境に合わせて変更してください) ---
  - name: CODED_AGENT_DIR
    value: "$(Build.SourcesDirectory)/my-function"
  - name: CODED_AGENT_VERSION
    value: "0.1.$(Build.BuildId)"
  - name: PROCESS_NAME
    value: "hello-world-coded-function"
  - name: PACKAGE_ID
    value: "helloworld-codedfunction" # pyproject.toml の name と一致させること
  - name: FOLDER_PATH
    value: "ExampleFolder"

  # --- 内部用 ---
  - name: NPM_GLOBAL_CACHE
    value: "$(Pipeline.Workspace)/.npm-global"

stages:
  - stage: PublishAndDeploy
    displayName: Publish Coded Function
    jobs:
      - job: publish_agent
        displayName: Publish and deploy Coded Function
        pool:
          vmImage: $(VM_IMAGE)
        # 対話プロンプトで停止した場合に無限に待たないようにする
        timeoutInMinutes: 30

        steps:
          - checkout: self

          # ------------------------------------------------------------
          # 1. Node.js と UiPath CLI
          # ------------------------------------------------------------
          - task: UseNode@1
            displayName: Use Node.js $(NODE_VERSION)
            inputs:
              version: $(NODE_VERSION)

          # Cache@2 は path が存在しないと復元に失敗するため、先に作成する
          - script: mkdir -p "$(NPM_GLOBAL_CACHE)"
            displayName: Prepare npm global cache directory

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

          - script: |
              set -euo pipefail

              npm config set prefix "$NPM_GLOBAL_CACHE"
              export PATH="$NPM_GLOBAL_CACHE/bin:$PATH"
              # 後続 Step でも uip を実行できるよう PATH を追加する
              echo "##vso[task.prependpath]$NPM_GLOBAL_CACHE/bin"

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

              uip --version
            displayName: Install UiPath CLI
            env:
              NPM_GLOBAL_CACHE: $(NPM_GLOBAL_CACHE)
              CLI_VERSION: $(CLI_VERSION)

          # ------------------------------------------------------------
          # 2. UiPath への認証
          #
          # Secret を展開してコマンドライン引数に載せないため、
          # UiPath CLI に環境変数名を渡す `env.XXX` 形式を使用する。
          # ------------------------------------------------------------
          - script: |
              set -euo pipefail

              uip login \
                --client-id env.UIPATH_CLIENT_ID \
                --client-secret env.UIPATH_CLIENT_SECRET \
                --scope "OR.Default" \
                --tenant "$UIPATH_TENANT"
            displayName: Authenticate to UiPath
            env:
              UIPATH_CLIENT_ID: $(UIPATH_CLIENT_ID)
              UIPATH_CLIENT_SECRET: $(UIPATH_CLIENT_SECRET)
              UIPATH_TENANT: $(UIPATH_TENANT)

          # ------------------------------------------------------------
          # 3. Python と uv
          # ------------------------------------------------------------
          - task: UsePythonVersion@0
            displayName: Use Python $(PYTHON_VERSION)
            inputs:
              versionSpec: $(PYTHON_VERSION)

          - script: |
              set -euo pipefail

              python --version
              python -m pip install --upgrade pip uv
              uv --version
            displayName: Install uv

          # ------------------------------------------------------------
          # 4. パッケージバージョンの更新とロックファイルの検証
          # ------------------------------------------------------------
          - script: |
              set -euo pipefail
              cd "$CODED_AGENT_DIR"

              python - <<'PY'
              import os
              import re
              from pathlib import Path

              version = os.environ["PACKAGE_VERSION"]
              path = Path("pyproject.toml")
              text = path.read_text(encoding="utf-8")

              pattern = r'(?m)^version\s*=\s*"[^"]+"'
              if not re.search(pattern, text):
                  raise SystemExit("version field was not found in pyproject.toml")

              path.write_text(
                  re.sub(pattern, f'version = "{version}"', text, count=1),
                  encoding="utf-8",
              )
              print(f"Updated pyproject.toml version to {version}")
              PY

              grep -n '^version' pyproject.toml
            displayName: Update pyproject.toml version
            env:
              CODED_AGENT_DIR: $(CODED_AGENT_DIR)
              PACKAGE_VERSION: $(CODED_AGENT_VERSION)

          - script: |
              set -euo pipefail
              cd "$CODED_AGENT_DIR"

              # UsePythonVersion@0 が導入したインタプリタを明示的に使う
              PYTHON_BIN="$(python -c 'import sys; print(sys.executable)')"

              uv lock --python "$PYTHON_BIN"
              uv lock --locked --python "$PYTHON_BIN"

              python - <<'PY'
              import os
              import re
              from pathlib import Path

              version = os.environ["PACKAGE_VERSION"]
              package_id = os.environ["PACKAGE_ID"]

              text = Path("uv.lock").read_text(encoding="utf-8")
              pattern = (
                  rf'(?ms)^\[\[package\]\]\nname = "{re.escape(package_id)}"\n'
                  rf'version = "([^"]+)"'
              )
              match = re.search(pattern, text)
              if not match:
                  raise SystemExit(
                      f"{package_id} was not found in uv.lock. "
                      "PACKAGE_ID と pyproject.toml の name が一致しているか確認してください。"
                  )
              if match.group(1) != version:
                  raise SystemExit(f"uv.lock has {match.group(1)}, expected {version}")

              print(f"uv.lock is synchronized for {package_id} {version}")
              PY
            displayName: Refresh and validate uv.lock
            env:
              CODED_AGENT_DIR: $(CODED_AGENT_DIR)
              PACKAGE_VERSION: $(CODED_AGENT_VERSION)
              PACKAGE_ID: $(PACKAGE_ID)

          # ------------------------------------------------------------
          # 5. 依存関係のインストール
          # uv.lock がある場合は uv に統一し、pip との混在を避ける
          # ------------------------------------------------------------
          - script: |
              set -euo pipefail
              cd "$CODED_AGENT_DIR"

              PYTHON_BIN="$(python -c 'import sys; print(sys.executable)')"

              if [ -f "uv.lock" ]; then
                uv sync --frozen --python "$PYTHON_BIN"

                # uip codedagent は .venv があると VIRTUAL_ENV の設定(activate 済み)を要求する
                # shellcheck disable=SC1091
                source .venv/bin/activate
                # 後続 Step(pack / publish)でも同じ venv を有効にする
                echo "##vso[task.setvariable variable=VIRTUAL_ENV]$VIRTUAL_ENV"
                echo "##vso[task.prependpath]$VIRTUAL_ENV/bin"
              elif [ -f "requirements.txt" ]; then
                python -m pip install -r requirements.txt
              elif [ -f "pyproject.toml" ]; then
                python -m pip install .
              else
                echo "No dependency definition found." >&2
                exit 1
              fi

              uip codedagent setup --force
            displayName: Install dependencies and configure Coded Function
            env:
              CODED_AGENT_DIR: $(CODED_AGENT_DIR)

          # ------------------------------------------------------------
          # 6. パッケージ化
          # ------------------------------------------------------------
          - script: |
              set -euo pipefail
              cd "$CODED_AGENT_DIR"

              uip codedagent pack

              echo "--- generated packages ---"
              find . -name '*.nupkg' -newermt '-10 minutes'
            displayName: Pack Python Coded Function
            env:
              CODED_AGENT_DIR: $(CODED_AGENT_DIR)

          # トレーサビリティとロールバックのため、成果物を保存する
          # Build / Deploy を分離する際は、Deploy 側でこの Artifact を取得する
          - task: PublishPipelineArtifact@1
            displayName: Publish package artifact
            inputs:
              targetPath: $(CODED_AGENT_DIR)/.uipath
              # 失敗ジョブの再実行でも Build.BuildId は変わらないため、試行回数を付けて名前の重複を避ける
              artifact: "uipath-package-$(Build.BuildId)-$(System.JobAttempt)"
              publishLocation: pipeline

          # ------------------------------------------------------------
          # 7. テナントフィードへのパブリッシュ
          # --tenant はテナント名ではなく、パブリッシュ先フィードの指定
          # ------------------------------------------------------------
          - script: |
              set -euo pipefail
              cd "$CODED_AGENT_DIR"

              uip codedagent publish --tenant
            displayName: Publish Python Coded Function
            env:
              CODED_AGENT_DIR: $(CODED_AGENT_DIR)

          # ------------------------------------------------------------
          # 8. プロセスの作成 / 更新(冪等)
          # ------------------------------------------------------------
          - script: |
              set -euo pipefail

              # uip は失敗時もエラー JSON を stdout に出すため、
              # 変数に取り込んだまま set -e で終了しないよう明示的に判定して出力する
              if ! PROCESS_JSON=$(uip or processes list \
                --folder-path "$FOLDER_PATH" \
                --name "$PROCESS_NAME" \
                --process-type Function \
                --output json \
                --output-filter "[?Name=='$PROCESS_NAME' || ProcessKey=='$PACKAGE_ID'].Key | [0]"); then
                echo "##[error]Failed to list processes in folder '$FOLDER_PATH'."
                echo "$PROCESS_JSON"
                exit 1
              fi
              echo "$PROCESS_JSON"

              # 見つかった場合: {"Data": {"Value": "<GUID>"}} / 見つからない場合: {"Data": []}
              # stdin はヒアドキュメントで使われるため、JSON は環境変数で渡す
              PROCESS_GUID_KEY=$(PROCESS_JSON="$PROCESS_JSON" python - <<'PY'
              import json
              import os

              data = json.loads(os.environ["PROCESS_JSON"]).get("Data")
              value = data.get("Value") if isinstance(data, dict) else data
              print(value if isinstance(value, str) else "", end="")
              PY
              )

              if [ -z "$PROCESS_GUID_KEY" ]; then
                echo "Process not found. Creating a new process."
                uip or processes create \
                  --folder-path "$FOLDER_PATH" \
                  --name "$PROCESS_NAME" \
                  --package-key "$PACKAGE_ID" \
                  --package-version "$CODED_AGENT_VERSION" \
                  --output json
              else
                echo "Process found ($PROCESS_GUID_KEY). Updating version."
                uip or processes update-version "$PROCESS_GUID_KEY" \
                  --folder-path "$FOLDER_PATH" \
                  --package-version "$CODED_AGENT_VERSION" \
                  --output json
              fi
            displayName: Create or Update Process
            env:
              FOLDER_PATH: $(FOLDER_PATH)
              PROCESS_NAME: $(PROCESS_NAME)
              PACKAGE_ID: $(PACKAGE_ID)
              CODED_AGENT_VERSION: $(CODED_AGENT_VERSION)

          # ------------------------------------------------------------
          # 9. 後始末
          # Microsoft-hosted agent では不要だが、self-hosted agent で
          # セッションを残さないために実行する
          # ------------------------------------------------------------
          - script: uip logout || true
            displayName: Logout from UiPath
            condition: always()

本番運用に向けた拡張

今回のサンプルをそのまま本番へ適用するのではなく、運用要件に応じて以下のような項目を追加することを検討してください。

BuildとDeployを分離する

Build Stageでパッケージを作成し、Pipeline Artifactとして保存します。Deploy StageではArtifactをダウンロードし、同じパッケージを検証環境・本番環境へ順番に反映します。

これにより、「検証したものと本番へ出したものが同じ」という状態を作りやすくなります。

Environmentに承認を設定する

本番用のDeploy処理を deployment Jobにし、Azure DevOps Environmentを関連付けます。EnvironmentのApprovals and checksを設定することで、本番反映前に担当者の承認を要求できます。

YAMLの変更だけで承認を迂回されないよう、承認設定をYAMLの外側にあるEnvironmentで管理できる点もメリットです。

テストと静的解析を追加する

Pull Request時には、UiPathプロジェクトの静的解析や単体テストを実行し、失敗した場合はマージできないようBranch Policyを設定します。

デプロイ後の確認が必要な場合は、Test Managerのテストセットを起動し、完了を待って結果を判定するStageを追加できます。テストに失敗したときに後続の本番デプロイを実行しないよう、dependsOn と condition で制御します。

環境ごとに設定を分離する

検証環境と本番環境では、少なくとも次の値を分けます。

  • Variable Group
  • Client IDとClient Secret
  • UiPathのテナント、フォルダー
  • Azure DevOps Environment
  • デプロイの承認者

YAMLテンプレートへ共通処理を切り出し、環境ごとの差分だけをパラメーターとして渡すと、処理の重複を抑えられます。

CLIとツールのバージョンを固定する

再現性を保つため、Node.js、Python、UiPath CLIなどのバージョンを明示します。更新は自動的に追従させるのではなく、検証用のPull Requestで動作確認してから取り込みます。

よくあるエラーと確認ポイント

UiPathへのログインに失敗する

次の点を確認します。

  • Variable Groupがパイプラインに許可されているか
  • YAMLの変数名とVariable Groupの変数名が一致しているか
  • Client Secretの有効期限が切れていないか
  • 外部アプリケーションに必要なスコープが付与されているか
  • テナント名が正しいか

Secretの値そのものをデバッグログへ出力しないよう注意してください。

パッケージのバージョンが重複する

Build.BuildId やGitタグを利用し、パイプライン実行ごとに一意なバージョンを設定します。pyproject.toml と uv.lock のバージョンが一致しているかも確認します。

パブリッシュ後にプロセスが更新されない

PACKAGE_ID、PROCESS_NAME、FOLDER_PATH が実際のOrchestrator上の値と一致しているか確認します。また、外部アプリケーションにプロセスの参照・作成・更新に必要な権限があることも確認します。

想定していない変更でパイプラインが動く

Monorepo構成の場合は trigger.paths.include と exclude を見直します。共通ファイルの変更時に複数のパイプラインを動かす必要があるかも合わせて整理します。

まとめ

いかがだったでしょうか。本記事ではAzure PipelinesとUiPath CLIによるCI/CD環境の構築の例について紹介しました。これらを組み合わせることで、UiPathプロジェクトのパッケージ化、パブリッシュ、Orchestrator上のプロセス更新をYAMLで自動化できます。

構築時のポイントは次のとおりです。

  • パイプライン定義もリポジトリで管理し、Pull Requestのレビュー対象にする
  • 外部アプリケーションを使い、SecretはVariable GroupやKey Vaultで管理する
  • CLIやランタイムのバージョンを固定し、実行結果の再現性を確保する
  • パッケージのバージョンとパイプライン実行を対応付ける
  • 初回作成と更新の両方を扱える処理にする
  • Build、Test、Deployを分離し、本番反映には承認を組み込む

最初からすべてを自動化する必要はありません。まずはパッケージ化と検証環境へのデプロイを自動化し、運用が安定してからテストや本番承認を追加していくと導入しやすいと思います。

免責事項

  • 本記事は 2026年10月時点で公開されているドキュメントをもとに作成しています。仕様変更時は最新ドキュメントをご確認ください
  • 本記事の内容に関するサポートは受け付けていないため、予めご了承ください
  • ここで記載している内容はあくまで個人の検証結果に基づく内容であり、所属団体を代表するものではありません

参考文献

UiPath公式ドキュメント

Microsoft公式ドキュメント

CI/CD・開発プラクティス

関連記事

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?