本文へスキップ

swift-format の指摘を Xcode Cloud から PR コメントとして投稿する

📋目次

概要

Xcode Cloud のビルドで swift-format lint を走らせると、指摘はビルド警告として記録されます。 ただし GitHub 上で見えるのは Files changed の該当行に付く注釈だけです。 PR を開いてまず目に入る Conversation タブには何も出ないので、差分を一行ずつ追わないと指摘の存在に気付けません。

そこでビルド結果である xcresult から指摘を抜き出し、PR に 1 件のコメントとして投稿するようにしました。

PR に投稿された swift-format の指摘

仕組み

Run Script Build Phase で swift-format lint を実行すると、その出力を Xcode が警告として解釈して xcresult に記録します。 ビルド後に呼ばれる ci_post_xcodebuild.sh が xcresult を読み、指摘を Markdown に整形して GitHub API に投げます。

2 回目以降は既存のコメントを書き換えるので、コミットのたびに積み上がりません。

変更されたファイルだけを lint する

対象を絞らずに lint すると、既存コードの指摘まで全部出てきます。 手元のプロジェクトでは 6000 件を超えてしまい、PR コメントとしては成立しませんでした。

Pull Request ビルドなら CI_PULL_REQUEST_TARGET_COMMIT が入るので、これを基点にした差分だけを対象にします。

cd "${SRCROOT}" || exit 0

BASE="${CI_PULL_REQUEST_TARGET_COMMIT:-}"

git diff -z --name-only --diff-filter=d "${BASE}" @ -- '*.swift' \
  | xargs -0 xcrun swift-format lint --parallel

xcresult から指摘を取り出す

Xcode 16 以降の xcresulttool は構造化された JSON をそのまま返します。

xcrun xcresulttool get build-results --path "${CI_RESULT_BUNDLE_PATH}" --compact

warnings にはコンパイラの警告も混ざります。 swift-format の出力は [RuleName] メッセージ という形なので、この前置きの有無で仕分けできます。

jq -r '
  .warnings[]
  | select(.message | test("^\\[[A-Za-z0-9_]+\\] "))
  | (.message | capture("^\\[(?<rule>[A-Za-z0-9_]+)\\] (?<rest>.*)")) as $m
  | "| \(.sourceURL) | `\($m.rule)` | \($m.rest) |"
'

sourceURLfile:///path/To.swift#StartingLineNumber=41&... という形式です。 パスと行番号はここから取り出します。

PR コメントを 1 件だけ保つ

本文の先頭に HTML コメントのマーカーを埋め込んでおき、これを目印に既存コメントを探します。

COMMENT_ID=$(gh_api "${API}/repos/${REPO}/issues/${PR}/comments" \
  | jq -r --arg m '<!-- xcode-cloud-report -->' \
      'map(select(.body | contains($m))) | first | .id // empty')

見つかれば PATCH で本文を差し替え、なければ POST で新規投稿します。

jq -n --rawfile body "${BODY}" '{body: $body}' > "${PAYLOAD}"

本文の JSON 化は jq に任せます。 指摘のメッセージにはタブや改行が混ざるので、シェルで手書きのエスケープを組むと簡単に壊れます。

スクリプト全体

必要最低限まで削ったものを載せておきます。

ci_scripts/ci_post_xcodebuild.sh
#!/bin/bash
set -e

# レポート投稿に失敗してもビルド結果には影響させない
"$(dirname "$0")/xcresult_report.sh" || echo "レポート投稿に失敗しました"
ci_scripts/xcresult_report.sh
#!/bin/bash
set -euo pipefail

MARKER='<!-- xcode-cloud-report -->'
API="https://api.github.com"
REPO="${CI_PULL_REQUEST_TARGET_REPO:-}"
PR="${CI_PULL_REQUEST_NUMBER:-}"

# build 系以外の xcresult にはビルド情報が入らない
case "${CI_XCODEBUILD_ACTION:-}" in
  build | build-for-testing) ;;
  *) exit 0 ;;
esac

if [ -z "${GITHUB_TOKEN:-}" ] || [ -z "${PR}" ]; then
  exit 0
fi

WORK=$(mktemp -d)
trap 'rm -rf "${WORK}"' EXIT

xcrun xcresulttool get build-results \
  --path "${CI_RESULT_BUNDLE_PATH}" --compact > "${WORK}/build.json"

# StartingLineNumber は 0 始まりなので +1 する
ROWS=$(jq -r --arg root "${CI_PRIMARY_REPOSITORY_PATH}/" '
  .warnings[]
  | select(.message | test("^\\[[A-Za-z0-9_]+\\] "))
  | (.message | capture("^\\[(?<rule>[A-Za-z0-9_]+)\\] (?<rest>.*)")) as $m
  | (.sourceURL | sub("^file://"; "") | split("#")[0] | ltrimstr($root)) as $path
  | (.sourceURL | [scan("StartingLineNumber=([0-9]+)")] | flatten | first) as $n
  | "| `\($path)` | \(($n | tonumber) + 1) | `\($m.rule)` | \($m.rest) |"
' "${WORK}/build.json")

{
  echo "${MARKER}"
  echo "## Xcode Cloud ビルド結果"
  echo
  if [ -z "${ROWS}" ]; then
    echo "swift-format の指摘はありません。"
  else
    echo "| ファイル | 行 | ルール | 内容 |"
    echo "|---|---|---|---|"
    echo "${ROWS}"
  fi
} > "${WORK}/body.md"

gh_api() {
  curl -sS --fail-with-body \
    -H "Authorization: token ${GITHUB_TOKEN}" \
    -H "Accept: application/vnd.github+json" "$@"
}

ID=$(gh_api "${API}/repos/${REPO}/issues/${PR}/comments?per_page=100" \
  | jq -r --arg m "${MARKER}" \
      'map(select(.body | contains($m))) | first | .id // empty')

jq -n --rawfile body "${WORK}/body.md" '{body: $body}' > "${WORK}/payload.json"

if [ -n "${ID}" ]; then
  gh_api -X PATCH -d "@${WORK}/payload.json" \
    "${API}/repos/${REPO}/issues/comments/${ID}" > /dev/null
else
  gh_api -X POST -d "@${WORK}/payload.json" \
    "${API}/repos/${REPO}/issues/${PR}/comments" > /dev/null
fi

まとめ

Xcode Cloud のビルド結果は xcresulttool で素直に読めるので、PR に流し込むところまで短いスクリプトで済みました。 lint の指摘が Files changed に埋もれて困っているなら、試してみてください。

参考リンク

この記事をシェア