Azure DevOps Pipelineで PRの変更ファイルの依存関係をdependency-cruiserで可視化してコメントしたメモ

## 概要
[前回](https://qiita.com/hibohiboo/items/86f2881a0899f346863b)は[dependency-cruiser-report-actionでPRの変更ファイルの依存関係を可視化してコメントする](https://zenn.dev/mh4gf/articles/12fcdcba14e576)を参考に、GitHubActionsを使ってプルリクエストに含まれるファイルの依存性の可視化を行った。

Azure DevOpsのプルリクエストでも同じこと出来るだろ、と軽い気持ちで始めたら意外に大変だったのでメモを残す。

最終的には下記のような図をプルリクエスト時に作成できた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/86058dc1-7af8-d794-2ac6-dbe50a230e9e.png)

[ソースコード](https://github.com/hibohiboo/Yakumi/blob/95f5c2c93a18809c335558956c3e1a8bb430b728/azure-dev-ops/depcruise.yml)

## GitHubとの違い

GitHubと大きく異なった部分としては下記がある

- 既存のCustom Actionがない
  - GitHubでは[ユーザ作成のCustom action](https://github.com/MH4GF/dependency-cruiser-report-action)が公開されていた
  - Azure DevOpsでは同様のものは見つけることができなかった
- プルリクエストのコメントにmermaid記法が使えない
  - Azure DevOpsの場合、Wikiのみでmermaid記法を使用することができる。

この条件の違いから、Azure DevOpsでは、プルリク作成時にパイプラインを動かし、mermaidでのグラフ描画用のwikiを作成し、そのwikiへのリンクをプルリクエストのコメントにする方法をとろうと思う。

## 環境
- AzureDevOps
- リポジトリのブランチをcode as wikiとして使用している

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/40567841-c1bb-f07c-7d75-b12c8b7cee97.png)


## パイプラインのユーザに権限を付与する
* ビルドパイプライン用のユーザが自動作成されているので、下記２つの `Allow`権限を与える
  * Wikiの更新のための「 Contribute 」
  * プルリクエストにコメントするための「Contribute to pull requests」

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/c72624fd-6c63-8be4-3049-c32a69fd3dc8.png)


## パイプラインを作成する
- 利用しているリポジトリがモノレポ構成でnpmをモノレポ管理に使っていたため、パイプラインで使用するNodeを更新する必要があった。( デフォルトのnpmが古くてモノレポ管理できないバージョン）
  今回は下記の設定としてパイプラインに明記
  - Node: `20.14.0`
  - npm: `10.8.1`

```yml:azure-devops/depcruise.yml
# ブランチポリシーにより起動させる

trigger: none
pr: none
jobs:
- job: check
  pool:
    vmImage: ubuntu-latest
  steps:
    - task: UseNode@1
      inputs:
        version: '20.14.0'
      displayName: 'Install Node.js'
    - task: Npm@1
      inputs:
        command: custom
        customCommand: 'install -g npm@10.8.1'
      displayName: 'Upgrade npm to latest version'
    - task: Npm@1
      name: npm_install
      inputs:
        command: custom
        customCommand: 'install --ignore-scripts'
        workingDir: $(Build.SourcesDirectory)
    - task: Bash@3
      displayName: 'POST PR Comment'
      inputs:
        workingDirectory: $(Build.SourcesDirectory)/app
        targetType: 'inline'
        script: |
          # 変数準備
          PROJECT_NAME_ENCODED=$(echo -n "$(System.TeamProject)" | perl -MURI::Escape -ne 'print uri_escape($_)')
          ADO_API_BASE=$(echo "$(System.CollectionUri)${PROJECT_NAME_ENCODED}/_apis/git/repositories/$(Build.Repository.Name)")
          ADO_API_VERSION=$(echo "api-version=7.2-preview.1")

          ######################################################################
          # PRに含まれるファイルを取得
          BRANCH1="$(System.PullRequest.TargetBranch)"
          BRANCH2="$(System.PullRequest.SourceBranch)"         
          BASE_BRANCH=${BRANCH1#refs/heads/}
          TARGET_BRANCH=${BRANCH2#refs/heads/}
          BASE_BRANCH_ENCODED=${BASE_BRANCH//\//%2F}
          TARGET_BRANCH_ENCODED=${TARGET_BRANCH//\//%2F}
          ADO_DIFF_API=$(echo "${ADO_API_BASE}/diffs/commits?baseVersion=${BASE_BRANCH_ENCODED}&targetVersion=${TARGET_BRANCH_ENCODED}&${ADO_API_VERSION}")
          CHANGED_FILES=$(curl "$ADO_DIFF_API" \
            --header "Accept: application/json" \
            --header "Authorization: Bearer $SYSTEM_ACCESSTOKEN")
          CHANGED_FILES_CSV=$(echo $CHANGED_FILES | jq -r '.changes[] | select(.item.isFolder != true and (.item.path | type == "string" and startswith("/app/src") )   ) | .item.path' | sed 's/\/app\///g' | tr '\n' ',')
          IFS=',' read -ra FILES <<< "$CHANGED_FILES_CSV"

          ######################################################################
          # dependency-cruiserで解析した結果をmermaid形式で出力
          npm install
          MERMAID_OUTUPUT=$(npm run depcruise -- "${FILES[@]}")
          MERMAID_OUTUPUT=$(echo "${MERMAID_OUTUPUT}" | tail -n +4)
          MERMAID_CONTENT=$(echo -e ":::mermaid \n\n ${MERMAID_OUTUPUT} \n\n :::")        
          WIKI_CONTENT_JSON=$(jq --arg comment "$MERMAID_CONTENT" '.content = $comment' <<< '{"content": ""}')

          ######################################################################
          # meraidグラフ用のwikiが作成済かどうかをチェック
          bid=$(echo  "$TARGET_BRANCH_ENCODED" | awk -F'%2F' '{print $2}')
          WIKI_PATH="pullrequests%2Fbid%2F${bid}"
          AZURE_URL=$(echo "$(System.CollectionUri)${PROJECT_NAME_ENCODED}")
          WIKI_API_PAGE_URL=$(echo "${AZURE_URL}/_apis/wiki/wikis/%E8%96%AC%E5%91%B3/pages?path=${WIKI_PATH}&api-version=7.1-preview.1")
          auth="Authorization: Bearer $SYSTEM_ACCESSTOKEN"
          ct="Content-Type: application/json"
          EXISTS_CHEKCK_RESPONSE=$(curl -XGET -s -H "$auth" -i "$WIKI_API_PAGE_URL")
          { read -r status_line; read -r etag_line; } < <( echo "$EXISTS_CHEKCK_RESPONSE" | grep -i -P '^(HTTP/|ETag:)' )

          ######################################################################
          if [[ $status_line =~ ^HTTP/[0-9.]+\ 200 ]]; then
              ######################################################################
              # ページが存在する場合は更新
              etag_value=$(echo "$etag_line" | awk '{print $2}' | tr -d '\r')
              ifmatch="If-Match: ${etag_value}"
              response_wiki_page=$(curl -XPUT -s -H "$auth" -H "$ct" -H "$ifmatch" "${WIKI_API_PAGE_URL}&versionDescriptor.version=${BASE_BRANCH_ENCODED}" --data "${WIKI_CONTENT_JSON}")
          else
              ######################################################################
              # ページが存在しない場合は新規作成
              response_wiki_page=$(curl -XPUT -s -H "$auth" -H "$ct" "${WIKI_API_PAGE_URL}&versionDescriptor.version=${BASE_BRANCH_ENCODED}" --data "${WIKI_CONTENT_JSON}")

              ######################################################################
              # 新規作成時のみ、プルリクエストコメントを投稿する
              WIKI_ID=$(echo "$response_wiki_page" | jq -r '.id')
              COMMENT=$(echo -e "[mermaid](${AZURE_URL}/_wiki/wikis/%E8%96%AC%E5%91%B3/${WIKI_ID})")
              ADO_API=$(echo "${ADO_API_BASE}/pullRequests/$(System.PullRequest.PullRequestId)/threads?${ADO_API_VERSION}")
              PR_COMMENT=$(jq --arg comment "$COMMENT" '.comments[0].content = $comment' <<< '{"comments": [{"parentCommentId": 0,"content": "","commentType": 1}],"status": 1}')

              curl "$ADO_API" \
              --header "Content-Type: application/json" \
              --header "Accept: application/json" \
              --header "Authorization: Bearer $SYSTEM_ACCESSTOKEN" \
              --data "$PR_COMMENT" \
              --verbose
          fi
      env:
        SYSTEM_ACCESSTOKEN: $(System.AccessToken)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/b790b580-2752-bb92-438e-95385ed04427.png)


## ブランチポリシーの設定

パイプラインで使用する`System.PullRequest.PullRequestId`が、これはブランチポリシーによるパイプラインでのみ値を取得できるため。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/ef8d73df-4a89-84c8-0181-d236ab73ca67.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/ae55687b-dc40-efdf-23a8-9c6ca4648889.png)

## 実行結果

プルリクが書き込まれた。リンクをクリックすると、最初に示した mermaid でのファイルの依存関係を表した図が確認できる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/dc100d90-79e8-fe60-6709-adf5fd3138c9.png)


## メモ

### Wiki作成時のレスポンス例

```json
{
  "path": "/pullrequests/bid/474",
  "order": 0,
  "gitItemPath": "/wiki/pullrequests/bid/474.md",
  "subPages": [],
  "url": "https://dev.azure.com/...?pagePath=%2Fpullrequests%2Fbid%2F474",
  "remoteUrl": "https://dev.azure.com/...?pagePath=%2Fpullrequests%2Fbid%2F474",
  "id": 973,
  "content": ":::mermaid \n\n \nflowchart LR\n\nsubgraph 0[\"src\"]\nsubgraph 1[\"pages\"]\n2[\"HollowFluxPage.tsx\"]\nend\nsubgraph 3[\"domain\"]\nsubgraph 4[\"hollow\"]\n5[\"constants.ts\"]\n6[\"cards.json\"]\n7[\"createUdonariumZip.ts\"]\nend\nsubgraph 8[\"udonarium\"]\n9[\"common.ts\"]\nA[\"fileArchiver.ts\"]\nB[\"canvas.ts\"]\nC[\"FileReaderUtil.ts\"]\nD[\"mimeType.ts\"]\nE[\"udonariumZip.ts\"]\nend\nend\nend\n2-->5\n2-->7\n5-->6\n7-->9\n7-->A\n7-->E\n9-->A\nA-->B\nB-->C\nB-->D\nE-->9\nE-->A \n\n :::"
}
```

## 参考
[dependency-cruiser-report-actionでPRの変更ファイルの依存関係を可視化してコメントする](https://zenn.dev/mh4gf/articles/12fcdcba14e576)
[しばやん雑記 - Azure Pipelines のディレクトリを指す変数はどれを使うべきなのか調べた](https://blog.shibayan.jp/entry/20190806/1565085492)
[Azure DevOps Service REST APIでWikiの更新を試してみたメモ](https://qiita.com/hibohiboo/items/0a0123c60bebf51c6149)
[Azure PipelinesのタスクでPullRequestに対してコメントをつけたい](https://qiita.com/fukasawah/items/7bebc8cf172983434b6f)

[UseNode@1 - エコシステム v1 タスク Node.js 使用する](https://learn.microsoft.com/ja-jp/azure/devops/pipelines/tasks/reference/use-node-v1?view=azure-pipelines)
[既定のリポジトリのアクセス許可](https://learn.microsoft.com/ja-jp/azure/devops/repos/git/set-git-repository-permissions?view=azure-devops)
[テンプレート使用方法のリファレンス](https://learn.microsoft.com/ja-jp/azure/devops/pipelines/process/templates?view=azure-devops&pivots=templates-includes)
[Diffs - Get](https://learn.microsoft.com/en-us/rest/api/azure/devops/git/diffs/get?view=azure-devops-rest-7.1&tabs=HTTP)
[システム変数 (DevOps Services)](https://learn.microsoft.com/ja-jp/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#system-variables-devops-services)

[【curl】超入門(GET/POST/PUT/DELETEでリクエスト)[LINUX]](https://qiita.com/takuyanin/items/949201e3eb100d4384e1)