1
1

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 DevOps の PR 差分を CSV に落とすツール

Last updated at Posted at 2025-07-21

はじめに

  • 目的
    • Azure DevOps の Pull Request(PR)から 差分ファイル一覧 を取得
    • test を含むファイルを除外し、CSV にエクスポート
    • CI/CD やリリースノート作成の自動化に便利

フォルダ構成

PR-tool/
├─ get_PR_filename.py            # 差分取得スクリプト(本記事で解説)
└─ run_get_PR_filename.bat     # Windows 1クリック実行用

前提条件

項目 バージョン・備考
Python 3.8 以上
追加ライブラリ requests
Azure DevOps PAT (Personal Access Token) 発行済み
レポジトリの Read 権限 があれば OK

PAT の発行手順

1.Azure DevOps → User Settings → Personal access tokens
2.New Token を押下
3.Scopes で “Code (Read)” を有効
4.“Create” → トークンが表示されるのでコピー
5.絶対に Git 等へコミットしないこと!

get_PR_filename.py(サンプル)

import sys
import os
import csv
import requests
from datetime import datetime
from requests.auth import HTTPBasicAuth


def getLatestIterationId(org, project, repo, pr_id, headers, auth):
    url = (
        f"https://dev.azure.com/{org}/{project}"
        f"/_apis/git/repositories/{repo}/pullRequests/{pr_id}/iterations?api-version=7.0"
    )
    response = requests.get(url, headers=headers, auth=auth)
    response.raise_for_status()
    iterations = response.json().get("value", [])
    if not iterations:
        raise Exception(f"No iterations found for PR#{pr_id}")
    return max(i["id"] for i in iterations)


def getPullRequestChanges(org, project, repo, pr_id, iteration_id, headers, auth):
    url = (
        f"https://dev.azure.com/{org}/{project}"
        f"/_apis/git/repositories/{repo}/pullRequests/{pr_id}/iterations/{iteration_id}/changes"
        "?$top=1000&api-version=7.0"
    )
    response = requests.get(url, headers=headers, auth=auth)
    response.raise_for_status()
    return response.json().get("changes", [])  # 正しいキーは "changes"


def is_excluded(path):
    lower_path = path.lower()
    return (
        "test" in lower_path
        or "/stub" in lower_path
        or (path.endswith(".yml") and (
            path.count("/") == 1 or path.startswith("/azure-pipelines/")
        ))
    )


def write_csv(pr_id, changes, output_folder):
    filtered_changes = [
        c for c in changes if c.get("item", {}).get("path") and not is_excluded(c["item"]["path"])
    ]

    if not filtered_changes:
        print("[NO_CHANGES]")
        return None

    timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
    output_path = os.path.join(output_folder, f"PR_{pr_id}_{timestamp}.csv")
    try:
        with open(output_path, "w", newline="", encoding="utf-8") as f:
            writer = csv.writer(f)
            writer.writerow(["FolderPath", "FileName", "ChangeType"])
            for change in filtered_changes:
                path = change["item"]["path"]
                folder, filename = os.path.split(path)
                writer.writerow([folder, filename, change.get("changeType", "")])
    except IOError as e:
        print(f"File write error: {e}")
        return None

    print(f"CSV saved to: {output_path}")
    return output_path


def main():
    if len(sys.argv) < 6:
        print("Usage: script.py <ORG> <PROJECT> <REPO> <PR_ID> <PAT>")
        sys.exit(1)

    org = sys.argv[1]
    project = sys.argv[2]
    repo = sys.argv[3]
    pr_id = sys.argv[4]
    pat = sys.argv[5]

    script_path = sys.argv[0]
    base_dir = os.path.dirname(os.path.abspath(script_path))
    output_dir = os.path.join(base_dir, "csv")
    os.makedirs(output_dir, exist_ok=True)

    headers = {"Content-Type": "application/json"}
    auth = HTTPBasicAuth("", pat)

    try:
        iteration_id = getLatestIterationId(org, project, repo, pr_id, headers, auth)
        changes = getPullRequestChanges(org, project, repo, pr_id, iteration_id, headers, auth)
        csv_path = write_csv(pr_id, changes, output_dir)
        if csv_path:
            print(f"[CSV_PATH]{csv_path}")

    except requests.HTTPError as e:
        print(f"HTTP error: {e.response.status_code} {e.response.text}")
        sys.exit(1)
    except Exception as e:
        print(f"Unhandled exception: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()

run_get_PR_filename.bat

@echo off
REM === Azure DevOps PR 差分取得ツール ===

REM --- 環境変数 AZURE_PAT から PAT を取得 ---
set PAT=%AZURE_PAT%

REM --- その他引数 ---
set ORG=my-org
set PROJECT=my-project
set REPO=my-repo
set PR_ID=12345

REM --- 環境変数 PAT が設定されているか確認 ---
if "%PAT%"=="" (
    echo Error: AZURE_PAT environment variable is not set.
    pause
    exit /b 1
)

REM --- スクリプト実行(PATは環境変数から取得)---
python get_PR_filename.py %ORG% %PROJECT% %REPO% %PR_ID% %PAT%

pause

使い方

1.リポジトリ複製

git clone https://github.com/your-name/PR-tool.git
cd PR-tool

2.依存ライブラリのインストール

pip install requests
# または: pip install -r requirements.txt

3.スクリプト実行

Windows: run_get_PR_filename.bat をダブルクリック
CLI: python get_PR_filename.py

4.生成物確認

ルートに PR_files_YYYYMMDDhhmmss.csv が出力
カラム: FolderPath, FileName, ChangeType

😲 ハマりどころメモ

🔒 401 Unauthorized

  • 原因
    • PAT の権限不足
    • または有効期限切れ
  • 対策
    • Azure DevOps で新しい PAT を発行し、スクリプトの環境変数に設定し直す

📂 404 Not Found

  • 原因
    • project / repository / pull_request_id のいずれかが間違っている
  • 対策
    • Azure DevOps の URL を確認し、正しい値を設定

📦 取得件数が 0

  • 原因
    • PR の source と target ブランチが正しく指定されていない
  • 対策
    • sourceRefName と targetRefName を API レスポンスから確認

🚨 デフォルトで 100 件しか取得できない

  • 原因
    • Azure DevOps API の diffs/commits はデフォルトで最大 100 件までしか返さない
  • 対策
    • パラメータに ?$top=1000 を指定する
    • 1,000 件を超える場合は x-ms-continuationtoken を使い、ページネーション処理で繰り返し取得する(このスクリプトは対応済み)

おわりに

  • PAT は ハードコードしない / Git に上げない
  • 差分一覧を活用し、リリースノート生成や CI の品質チェックを自動化
  • 気づきや改善点があればコメントでぜひ共有してください 🙌
1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?