経緯
Kotlin 言語で作られた大規模なプロジェクトがあり、その中に不適切な実装(該当 Issue)が1種類ですが多くの箇所にありました。それらを修正したいですが、すべてを修正する工数は取れなそうです。よって新機能開発や機能改修のついでに、そこも修正できるチーム開発体制を作ろうと思います。Kotlin の静的解析ツール detekt に該当 Issue を検出させて、reviewdog や Danger でプルリク差分にそれがあればコメントで指摘する GitHub Actions の設定を行いました。しかしコメントも見落とされる可能性があるので、プルリク差分の中に該当 Issue があれば CI を失敗にして、強制力を持たせてみようと思います。
注意点
- 言うまでも無く、このような設定を行うときは、チームの合意を取りましょう。
- この記事の内容は個人の見解です。私の所属先でこのような設定を行っているわけではないです。
detekt について
今回、プルリク差分の中にあれば指摘したい該当 Issue の検出は detekt で行います。detekt の設定やカスタムルールの開発方法の説明は、こちらの記事に譲ります。
実際に検出したい該当 Issue は違いますが、上記の記事でカスタムルールのサンプルとして紹介されていた ContextOrder という Issue がプルリク差分にあれば、CI を失敗にしてみようと思います。
// これは context: Context が第1引数なので良し
fun okFunc(context: Context, bar: String) {
// 何かの実装
}
// これは context: Context が第2引数なので detekt の Issue になる。
// これがプルリク差分の中にあれば CI が失敗になるように設定することが、この記事の本題。
fun ngFunc(bar: String, context: Context) {
// 何かの実装
}
関連して Konsist の紹介
今回はプルリク差分の中に該当 Issue があれば CI を失敗にしますが、プロジェクト全体に1件でも該当 Issue があれば CI を失敗にしたい場合は、Konsist が使えます。Konsist は JVM 環境の単体テストで実行時型情報をチェックするツールです。Issue があれば単体テストを失敗にすることで CI を失敗に出来ます。
Konsist の使用例
表題の設定方法
detekt の特定の Issue がプルリク差分から検出されたら、CI を失敗にするためには、レポートを SARIF 形式で出力したあとに、いくつかの UNIX コマンドを組み合わせます。それらをひとつずつ紹介します。
レポートの出力
Gradle でレポートを出力します。
./gradlew detekt
find コマンド
マルチモジュール構成かつレポートを Gradle で統合していない場合は、レポートファイルがモジュールごとに出力されるので、find コマンドでそれらを列挙します。
find . -type f -name detekt.sarif
./kgsios/build/reports/detekt/detekt.sarif
./common/build/reports/detekt/detekt.sarif
./detekt-extensions/build/reports/detekt/detekt.sarif
./feature/home/build/reports/detekt/detekt.sarif
./androidApp/build/reports/detekt/detekt.sarif
./data/remote/build/reports/detekt/detekt.sarif
今回は -exec
引数も使っています。次の jq コマンドの {} +
に列挙したファイルを渡しています。
(コメントで教えて頂きました。ありがとうございます。)
関連記事
find の -exec optionの末尾につく ; と + の違い。
jq コマンド
SARIF 形式は JSON ファイルです。detekt が出力した SARIF ファイルはこのような形式です。(大幅に省略しています)
{
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"results": [
{
"level": "warning",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "file:///Users/takada/work/KmmGithubSearch/androidApp/src/main/java/com/tfandkusu/kgs/MainActivity.kt"
},
"region": {
"endColumn": 6,
"endLine": 24,
"startColumn": 5,
"startLine": 22
}
}
}
],
"message": {
"text": "Context must be the first parameter"
},
"ruleId": "detekt.extra-rules.ContextOrder"
}
]
}
]
}
よって jq コマンドで該当 Issue が含まれている URI 一覧を得ます。
jq -r ".runs[].results[] | select(.ruleId == \"detekt.extra-rules.ContextOrder\") | .locations[].physicalLocation.artifactLocation.uri" {} + # {} + は入力 SARIF ファイル名
-r
は --raw-output
です。出力に ""
が含まれなくなります。
.runs[].results[]
の中から ruleId
が detekt.extra-rules.ContextOrder
の要素を探して、その .locations[].physicalLocation.artifactLocation.uri
を得ています。
URI 形式で出力されます。
file:///Users/takada/work/KmmGithubSearch/feature/home/src/commonMain/kotlin/com/tfandkusu/kgs/feature/home/HomeActionCreator.kt
file:///Users/takada/work/KmmGithubSearch/androidApp/src/main/java/com/tfandkusu/kgs/MainActivity.kt
sed コマンド
sed コマンドによる文字列置き換えで、該当 Issue を含むファイルの URI をリポジトリルートからの相対パスに変換します。
sed "s|file://$PWD/||"
$PWD
環境変数でカレントディレクトリを取得しています。
file:///Users/takada/takada/KmmGithubSearch/androidApp/src/main/java/com/tfandkusu/kgs/MainActivity.kt
↓
androidApp/src/main/java/com/tfandkusu/kgs/MainActivity.kt
sort コマンド
並び替えコマンドです。あとで join コマンドを使うたためにファイル一覧を昇順にします。
これまでの処理をパイプでつないで、テキストファイルに保存する
これまでの処理をパイプでつないで、テキストファイルに保存します。
find . -type f -name detekt.sarif -exec \
jq -r ".runs[].results[] | select(.ruleId == \"detekt.extra-rules.ContextOrder\") | .locations[].physicalLocation.artifactLocation.uri" {} + | \
sed "s|file://$PWD/||" | \
sort > error_files.txt
git コマンド、sort コマンド
main ブランチと差分があるファイル一覧を得て、昇順にしてからテキストファイルに保存します。
git diff --name-only origin/main | sort > change_files.txt
(ここで「プルリク差分」とは main ブランチとの差分と定義しています)
join コマンド
これまでのステップで作成された、error_files.txt
と change_files.txt
の共通する行を取得します。
join error_files.txt change_files.txt
この結果が空ならばプルリク差分には該当 Issue 無し。そうで無ければ該当 Issue 有りとなります。
grep コマンド
grep コマンドを使い、結果が空か否かを判定します。
! join error_files.txt change_files.txt | grep '.'
grep '.'
ですべての文字列にマッチします。grep コマンドはマッチしたら成功、マッチしなければ失敗ですが、今回は結果を反転させたいです。そのため先頭に !
を付けています。
GitHub Actions のワークフローで、これまでのコマンドを実行する
GitHub Actions のワークフローでこれまでのコマンドを実行する設定はこのようになります。
name: check
on:
push:
branches:
- main
pull_request:
types:
- opened
- synchronize
- reopened
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '17'
- run: ./gradlew deteKt
- run: |
find . -type f -name detekt.sarif -exec \
jq -r ".runs[].results[] | select(.ruleId == \"detekt.extra-rules.ContextOrder\") | .locations[].physicalLocation.artifactLocation.uri" {} + | \
sed "s|file://$PWD/||" | \
sort > error_files.txt
- run: git diff --name-only origin/main | sort > change_files.txt
- name: ContextOrder 違反チェック
run: "! join error_files.txt change_files.txt | grep '.'"
最後のコマンドについては name:
フィールドを設定することで、CI 失敗理由を分かりやすくしています。また !
は YAML ファイルにおいてタグを表すので、ダブルクオテーションで囲んでいます。
まとめ
今回は新機能開発や機能改修のついでに、プルリク差分にある既存の問題点を修正する強制力を CI に持たせる設定方法(所謂シェル芸)を紹介しました。問題点は detekt で Issue として検出できるようにしておき、SARIF 形式のレポートファイルから jq, sed コマンドで該当 Issue を含むファイル一覧を得ます。git コマンドで main ブランチとの差分ファイル一覧を得たら、共通の行があるかを join コマンドで確認して、あれば CI を失敗にすることで、表題の設定ができることを解説しました。