外部システムからの連絡でケースを進めよう(返答編)

折り返し編では、GitHub Actions から q_result のような名前のパラメータを送ってもらいました。送信する側の GitHub ワークフローを自分で書けたので、名前をこちらの都合に合わせられたわけです。

相手がそうしてくれるとは限りません。送信する側を自分で決められないとき、[受信タスク (HTTP)]では足りなくなります。

この記事では[受信タスク (Webhook)]を使います。届いたリクエストボディをそのまま Questetra BPM Suite のデータ項目に保存し、業務データを差し込んだ応答を返します。ビルドを終えた GitHub ワークフローが、公開に使うバージョンとリリースノートを取りに来る、という作りにします。

この記事は連載「外部システムからの連絡でケースを進めよう」(全 4 回)の第 3 回です。

前回:折り返し編

[受信タスク (Webhook)]を選ぶ場面

使う場面は 3 つあります。

パラメータを細かく制御できません。 折り返し編で説明したとおり、[受信タスク (HTTP)]では、受け取る値の名前をデータ項目のフィールド名に合わせてもらう必要があります。外部サービスからの通知は、そのサービスが決めた構造の JSON で届きます。名前も入れ物の形も変えられないので、ボディをそのまま受け取ることになります。

相手が応答の内容を必要としています。 [受信タスク (HTTP)]が返すのは HTTP 200 だけです。[受信タスク (Webhook)]には[HTTP レスポンスの内容]の設定があり、業務データを差し込んだ応答を返せます。処理の途中で必要な値を問い合わせてくる相手や、「この処理を続けてよいか」を聞いてくる相手には、こちらしか使えません。

値を渡すのではなく、取りに来てもらえます。 処理を投げるときにまとめて渡す(PUSH)のではなく、相手が必要になった時点で問い合わせて受け取る(PULL)形にできます。渡す形では、値は投げた時点のものに固定され、渡し方によっては相手側の記録に残ります。取りに来てもらう形なら、そのときのデータ項目の値が渡ります。相手に渡せる値の数や長さに上限があっても、その上限に縛られずに値を渡せます。送信する側を自分で書ける場合でも選べます。 この記事の作りがそれです。高度な使い方ですが、応答を返せる[受信タスク (Webhook)]でないとできません。

場面とは別に、知っておきたい違いが 2 つあります。

届いたリクエストボディが、そのままデータ項目に残ります。[受信タスク (HTTP)]は、工程で「編集可」にしたデータ項目のフィールド名と一致した名前の値だけを書き込むので、名前が合わないものはどこにも残りません。[受信タスク (Webhook)]なら、相手が送ってきた JSON をケースの画面でそのまま読めます。解析がエラーになったときに、届いた内容から原因を確かめられます。

もうひとつ、API キーの扱いが違います。[受信タスク (HTTP)]の API キーは工程に設定する固定値で、全ケースで共通です。[受信タスク (Webhook)]は API キーをデータ項目から取るので、固定にも、ケースごとに生成することもできます。ケースごとに変えれば、1 件の受信 URL が漏れても他のケースには使えません。この点では[受信タスク (Webhook)]の方が安全です。

この記事で足す工程

折り返し編で作ったアプリに、次の工程を足します。

  • [データ更新]「API キーの生成」— 準備編で置いた「リクエストボディの作成」の前に置きます
  • [受信タスク (Webhook)]「リリース情報の問い合わせの受信」— 折り返し編で置いた[分岐 XOR ゲートウェイ]「成功か失敗か」の success の経路に、ヒューマンタスク「リリースの承認」より前に置きます
  • [スクリプトタスク]「リクエストボディの解析」— [受信タスク (Webhook)]の直後、「リリースの承認」より前に置きます

[受信タスク (HTTP)]の直後に[受信タスク (Webhook)]が来ます。 間に人の工程は入りません。ビルド結果の折り返しでトークンが動いた数秒後に、GitHub が次の問い合わせを送ってくる、という並びです。人の判断を待たせないので、GitHub のジョブが長く動き続けることもありません。

準備編で置いた「リクエストボディの作成」にも手を入れ、起動のリクエストにこのケースの API キーを足します。GitHub ワークフローの起動は準備編の 1 回だけです。

データ項目は 4 つ追加します。次の表の「第 3 回」の行です。上から、人が入力する項目、受信で埋まる項目、通信のために使う裏方の項目の順に並べています。[受信タスク (Webhook)]には[編集の可否]の設定がありません。「問い合わせ内容」は、工程のプロパティで[リクエストボディ保存先のデータ項目]に指定します。

フィールド名データ項目名データタイプ追加した回リリース申請ビルド結果の受信リリースの承認
q_release_tag公開バージョン文字 (単一行)第 3 回編集可表示なし表示のみ
q_release_noteリリースノート文字 (複数行)第 3 回編集可表示なし表示のみ
q_resultビルド結果文字 (単一行)第 2 回表示なし編集可表示のみ
q_log_url実行ログ URL文字 (単一行)第 2 回表示なし編集可表示のみ
q_reply_json問い合わせ内容文字 (複数行)第 3 回表示なし表示なし表示のみ
q_dispatch_body起動リクエストボディ文字 (複数行)第 1 回表示なし表示なし表示なし
q_error起動エラー内容文字 (複数行)第 1 回表示なし表示なし表示なし
q_api_keyAPI キー文字 (単一行)第 3 回表示なし表示なし表示なし

GitHub ワークフローは、ビルドの結果を折り返したあとも動き続けます。折り返しでトークンが[受信タスク (HTTP)]を通過すると、次の[受信タスク (Webhook)]で待ち受けが始まります。そこへ「公開に使うバージョンとリリースノートを教えてほしい」と問い合わせ、応答で受け取った値でドラフトのリリースを作ります。

なぜ起動のときに渡さないのか。 workflow_dispatch の inputs には、項目の数と全体の長さに上限があります(ワークフローをトリガーする)。この記事で渡すのは 2 つだけなのでまだ余裕がありますが、ここでは取りに来てもらう形にします。

この作りでは、応答の内容がなければ処理を進められません。 [受信タスク (HTTP)]が返すのは HTTP 200 だけなので、同じことはできません。

「API キーの生成」と「リクエストボディの作成」は、準備編で置いた「GitHub ワークフローの起動」([メッセージ送信中間イベント (HTTP)])より前に置きます。鍵が決まっていないと、起動のリクエストに載せられません。

この記事では、相手が送ってくるリクエストボディの形式は決まっているものとして進めます。 実際の練習では GitHub ワークフローを自分で書くので、パラメータ形式で送ることもできてしまいます。ここでは外部サービスからの通知を受けるときと同じ条件を作るために、次の形式の JSON が届くものとします。この記事の GitHub ワークフローは、あとで示すステップでこの形を送ります。

{
  "action": "build_succeeded",
  "repository": { "full_name": "yourname/workflow-practice" },
  "workflow_run": { "id": 1234567890, "html_url": "https://github.com/..." }
}

フィールド名を q_ で始まる名前に合わせてもらうことも、入れ子を平らにしてもらうこともできない、という想定です。この形のまま受け取って、必要な値はあとで取り出します。

受信 URL はケースごとに違う

折り返し編では、受信 URL に含まれるのはアプリ ID とノード番号までで、API キーとケース ID は POST のパラメータで送りました。API キーは全ケース共通の固定値なので、GitHub 側ではリポジトリのシークレットに登録できました。[受信タスク (Webhook)]はリクエストボディをそのまま受け取るので、パラメータを足す場所がありません。そのため API キーとケース ID は受信 URL のパスに含まれ、受信 URL はケースごとに異なります。

[受信タスク (HTTP)][受信タスク (Webhook)]
受信 URL に含まれるものアプリ ID、ノード番号アプリ ID、ノード番号、ケース ID、API キー
API キーの渡し方クエリ文字列 ?key=、または POST のパラメータ key受信 URL のパス
ケースの特定POST のパラメータ processInstanceId受信 URL のパス
API キーの設定場所工程に直接設定文字 (単一行) のデータ項目を指定
API キーの値全ケース共通の固定値固定にも、ケースごとに生成も可能

受信 URL は、工程のプロパティ画面の URL 欄に、ケース ID と API キーの部分を (ケースID) (API キー) と置いたひな形で表示されます。次の形式です。

https://{ドメイン}/System/ReceiveTask/Http/{アプリID}/{ノード番号}/{ケースID}/{APIキー}/receive<

API キーは、工程に直接設定するのではなく、文字 (単一行) のデータ項目(この記事では「API キー」)を指定します。実際の鍵はそのデータ項目の値で、半角英数 100 文字以内です。トークンが工程に届いた時点で値が空だと、工程はエラーになります。

[IPアドレス制限]の登録も、この工程の分が別に要ります。 受信 URL のパスが[受信タスク (HTTP)]と違うので、折り返し編で登録した行はこの工程には効きません。[システム設定]>[IPアドレス制限]に、[パスプレフィックス]の選択肢[受信タスク (Webhook) – 特定ノード]で入る /System/ReceiveTask/Http/(processModelInfoId)/(nodeNumber)/ の括弧をアプリ ID とノード番号に置き換えた行を足し、[接続が許可されているネットワーク]には折り返し編と同じく 0.0.0.0/0 を登録します。受信 URL を守るのは API キーだけになるので、ケースごとに生成する作りが効いてきます。登録内容は、工程のプロパティ画面の[IP アドレス制限、自動処理ログ]を押すと開く画面の[接続が許可されているネットワーク]で確認できます。

API キーを相手に渡す

受信 URL は、外部サービスの管理画面にあらかじめ登録できません。処理を投げるときに、一緒に渡します。 ただし URL をまるごと渡す必要はありません。アプリ ID とノード番号は工程ごとに決まった値ですし、ケース ID は準備編から case_id として渡しています。足りないのは API キーだけです。

まず「API キーの生成」で、「API キー」に次の式を設定します。ケースごとに 32 文字の英数字を生成します。

#{#randomString(32)}

続けて、準備編で作った[データ更新]「リクエストボディの作成」に手を入れます。「起動リクエストボディ」の inputs に api_key を足します。値は SpEL 式で「API キー」を参照するだけです。英数字しか入らないので escapeJson は要りません。

{
  "ref": "main",
  "inputs": {
    "case_id": "#{processInstanceId}",
    "api_key": "#{#q_api_key}"
  }
}

yml とリクエストボディは、両方を直してから起動してください。 api_key は GitHub 側で required: true として宣言します。宣言する前にこのボディを送っても、宣言したあとに古いボディを送っても、起動のリクエストは失敗します。yml の書き換えは、後の「GitHub ワークフローに問い合わせのステップを足す」で行います。

鍵の生成とリクエストボディの組み立てを 2 つの[データ更新]に分けているのは、同じ[データ更新]の中では、先に設定した値を後の式から参照できないためです。式はどれも、その工程に入った時点のデータ項目の値で評価されます。

API キーは inputs で渡すことになります。 折り返し編では、API キーを inputs に載せずリポジトリのシークレットに登録しました。ここでは鍵がケースごとに変わるので、シークレットには登録できません。シークレットの値はログに出ても伏せ字になりますが、inputs の値は伏せられません。 送り先のベース URL はシークレットに置いているので、出るとしても鍵だけです。それでも鍵が見えることに変わりはないので、このやり方が取れるのは、リポジトリの閲覧範囲を管理できる場合だけです。 相手が社外のサービスであれば、鍵を渡す経路そのものが保護されているかを確認してください。

受信 HTTP メソッドとリクエストボディの保存先

[受信タスク (Webhook)]では、待ち受ける HTTP メソッドを GET・POST・PUT・PATCH・DELETE から選びます。Content-Type は application/json・text/xml・application/x-www-form-urlencoded から選びます(直接入力もできます)。この記事では POST と application/json です。

リクエストボディ保存先のデータ項目には、文字 (複数行) または文字 (Markdown) のデータ項目を指定します。この記事では「問い合わせ内容」に保存します。

保存先の文字型データ項目に入る長さ(1,000,000 文字)を超えるリクエストボディは保存できず、受信は 400 番台のエラーになります。トークンは工程に留まったままです。

応答に業務データを差し込む

[HTTP レスポンスの内容]に、返す内容を書きます。データ項目の値を SpEL 式で差し込めます。

設定項目
HTTP レスポンスの Content-Typeapplication/json;charset=UTF-8
HTTP レスポンスの内容下記
{
  "caseId": "#{processInstanceId}",
  "tag": "#{#escaper.escapeJson(#q_release_tag)}",
  "note": "#{#escaper.escapeJson(#q_release_note)}"
}

「リリースノート」は複数行です。 改行や引用符がそのまま入ると JSON が壊れるので、escapeJson を通します。準備編でリクエストボディを組み立てたときと同じ関数です。

応答は、リクエストが届いた時点のデータ項目の値で作られます。申請の工程で入力された内容が、そのまま GitHub 側へ渡ります。

リリース情報を問い合わせる

GitHub ワークフローに問い合わせのステップを足す

準備編で作った release-check.yml に手を入れます。新しいファイルは作りません。[メッセージ送信中間イベント (HTTP)]のアクセス URL も準備編のままです。

受信 URL のうちノード番号までの部分は、どのケースでも同じです。https://{ドメイン}/System/ReceiveTask/Http/{アプリID}/{ノード番号} までをリポジトリのシークレット QUESTETRA_WEBHOOK_URL に登録します。折り返し編で登録した 3 つに、1 つ足す形です。GitHub 側は、これに case_id と api_key をつないで受信 URL を組み立てます。

この記事には 2 つの鍵が出てきます。[受信タスク (HTTP)]の鍵は全ケース共通で、折り返し編でリポジトリのシークレット QUESTETRA_API_KEY に登録したものです。[受信タスク (Webhook)]の鍵はケースごとに違い、データ項目「API キー」の値を inputs の api_key として受け取ります。次の yml には両方が並びます。

足すのは、inputs の api_key、ジョブの permissionssteps: の最後の 3 つです。折り返し編で最後に置いた「結果を Questetra BPM Suite へ折り返す」の後ろに続きます。release-check.yml の全体は次のようになります。

name: release-check
on:
  workflow_dispatch:
    inputs:
      case_id:
        description: Questetra BPM Suite のケース ID
        required: true
        type: string
      api_key:
        description: [受信タスク (Webhook)]の API キー
        required: true
        type: string

jobs:
  check:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: 受信タスクが受け取れる状態になるまで待つ
        run: |
          for i in $(seq 1 30); do
            if curl -sf "${{ secrets.QUESTETRA_CHECK_URL }}?key=${{ secrets.QUESTETRA_API_KEY }}&processInstanceId=${{ inputs.case_id }}"; then
              exit 0
            fi
            echo "まだ受け取れないので 10 秒待ちます"
            sleep 10
          done
          echo "受信タスクが受け取れる状態になりません"
          exit 1
      - name: 実行中であることを先に知らせる
        run: |
          curl -sS --fail-with-body -X POST "${{ secrets.QUESTETRA_RECEIVE_URL }}" \
            -d "key=${{ secrets.QUESTETRA_API_KEY }}" \
            -d "processInstanceId=${{ inputs.case_id }}" \
            -d "saveOnly=true" \
            -d "q_result=running" \
            -d "q_log_url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
      - uses: actions/checkout@v4
      - name: テストの実行
        run: echo "ここでビルドとテストを走らせます"
      - name: 結果を Questetra BPM Suite へ折り返す
        if: always()
        run: |
          curl -sS --fail-with-body -X POST "${{ secrets.QUESTETRA_RECEIVE_URL }}" \
            -d "key=${{ secrets.QUESTETRA_API_KEY }}" \
            -d "processInstanceId=${{ inputs.case_id }}" \
            -d "q_result=${{ job.status }}"
      - name: 問い合わせ先が受け取れる状態になるまで待つ
        run: |
          CALLBACK_URL="${{ secrets.QUESTETRA_WEBHOOK_URL }}/${{ inputs.case_id }}/${{ inputs.api_key }}/receive"
          echo "CALLBACK_URL=$CALLBACK_URL" >> "$GITHUB_ENV"
          for i in $(seq 1 30); do
            if curl -sf "$CALLBACK_URL/check"; then
              exit 0
            fi
            echo "まだ受け取れないので 10 秒待ちます"
            sleep 10
          done
          echo "受信タスクが受け取れる状態になりません"
          exit 1
      - name: リリース情報を問い合わせる
        run: |
          RESPONSE=$(curl -sS --fail-with-body -X POST "$CALLBACK_URL" \
            -H "Content-Type: application/json" \
            -d '{"action":"build_succeeded","repository":{"full_name":"${{ github.repository }}"},"workflow_run":{"id":${{ github.run_id }},"html_url":"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}}')
          echo "$RESPONSE" | jq .
          echo "$RESPONSE" | jq -r .tag > release_tag.txt
          echo "$RESPONSE" | jq -r .note > release_note.md
      - name: ドラフトのリリースを作る
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh release create "$(cat release_tag.txt)" --draft \
            --title "$(cat release_tag.txt)" \
            --notes-file release_note.md

ビルドが失敗すると、「結果を Questetra BPM Suite へ折り返す」だけが if: always() で実行され、後ろの 3 つは飛ばされます。ケースの側も[分岐 XOR ゲートウェイ]「成功か失敗か」で[終了イベント]へ分かれるので、問い合わせを待ち受ける工程には入りません。

組み立てた URL は $GITHUB_ENV に書き出して、後のステップから使います。ステップごとに別のシェルが起動するので、変数はそのままでは引き継がれません。

問い合わせは 1 回だけです。 この 1 回でトークンが工程を通過し、同時に応答が返ります。

応答から取り出した値は、ファイルに書いています。 リリースノートは複数行なので、$GITHUB_OUTPUT では扱いにくいためです。gh release create の --notes-file にそのまま渡せます。その前の jq . は、応答の JSON を GitHub ワークフローのログで確認できるようにするためのもので、処理には関係ありません。

gh release create には書き込みの権限が要ります。 ジョブに permissions: contents: write を足しています。

待つ側のループは curl -sf の成否だけを見ているので、受け取れないうちは上限の回数まで回ります。run: は bash -e で実行されるため、--fail-with-body を付けた問い合わせの方は、失敗するとその場でステップが終わります。応答を得られないままリリースを作ってしまうことはありません。

受け取れる状態かどうかを先に確かめるのは、折り返し編と同じ書き方です。折り返しの直後とはいえ、トークンが[受信タスク (Webhook)]に届くまでにはわずかに時間がかかります。

確認用の URL は、受信 URL の末尾に /check を足したものです。工程のプロパティ画面の[IP アドレス制限、自動処理ログ]を押すと開く画面に、折り返し編と同じく「受信可否確認 API」として表示されます。API キーとケース ID は受信 URL のパスに含まれているので、折り返し編のようにクエリ文字列で足す必要はありません。

上限まで待っても受け取れる状態にならなければ exit 1 でジョブを失敗させます。ジョブが失敗すると、問い合わせてくる相手はもういません。 ケースは[受信タスク (Webhook)]で待ち続けるので、この工程には締め切りを設定しておきます(締め切り・エラー対処編)。

受け取ったリクエストボディの解析

応答は受信した時点ですでに返っているので、届いた JSON の解析は受信の後で構いません。

トークンは、問い合わせが届いた時点で先へ進みます。 ドラフトのリリースができるのは、そのあとです。「リリースの承認」を開いたときに、まだできていないことがあります。できあがりを待ってから承認させたい場合は、リリースを作ったあとにもう一度折り返してもらう受信の工程を足すことになります。

保存された JSON から値を取り出すには[スクリプトタスク]が必要です。SpEL 式には JSON を解析する機能がありません。

const json = JSON.parse(engine.findDataByVarName('q_reply_json'));
engine.setDataByVarName('q_log_url', String(json.workflow_run.html_url));

折り返し編で作った「実行ログ URL」に、届いた JSON から取り出した実行ログの URL を入れています。この記事では同じ実行なので、折り返し編の saveOnly=true のステップが入れた値と同じになります。相手が外部サービスであれば、ここで取り出すのは向こうしか知らない値です。

折り返し編との使い分け

[受信タスク (HTTP)][受信タスク (Webhook)]
選ぶ場面送信する側を自分で用意できる送る形式を変えられない、または相手が応答を必要とする
受け取り方POST のパラメータリクエストボディをそのまま
成功時の応答HTTP 200(本文なし)業務データを差し込んだ応答を返せる
API キー全ケース共通の固定値ケースごとに変えられる
解析不要(データ項目に直接書き込まれる)[スクリプトタスク]が必要
受信 URL の渡し方あらかじめ登録できる処理を投げるときに API キーを渡し、相手に組み立てさせる

どちらでも作れる場合は[受信タスク (HTTP)]の方が手数が少なくて済みます。

連載・参考

連載「外部システムからの連絡でケースを進めよう」

参考

Questetra Supportをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む