mcpField
E
enforcing-stability-in-ci

enforcing-stability-in-ci

github.com/skydovesテスト・品質520122 回インストール19 週間前に更新
リポジトリ

enforcing-stability-in-ci について

skydoves/compose-stability-analyzer Gradle プラグインで、CI に Compose の安定性回帰チェックを追加するスキルです。Android チームはベースラインをコミットしてプルリクエストの回帰を検出でき、マルチプラットフォーム向けに ComposeGuard も紹介します。

主な機能

  1. 01モジュール別の安定性ベースライン生成
  2. 02現在の結果とコミット済みベースラインの比較
  3. 03安定性回帰時に CI を失敗させる設定
  4. 04パッケージとクラスの除外設定
  5. 05マルチプラットフォームで ComposeGuard を利用

活用シーン

  1. 01スキップ可能性を下げるプルリクエストをブロック
  2. 02モノレポの UI モジュール全体の安定性を追跡
  3. 03意図的なベースライン更新をレビューして理由を記録

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/skydoves/compose-performance-skills --skill enforcing-stability-in-ci

インストール後、Claude Code で/enforcing-stability-in-ciを入力して読み込まれたか確認します。

詳細

CI で安定性を強制する — 安定性の後退時にビルドを失敗させる

安定性は、気づかないうちに後退する性質があります。共有データクラスに新しい var を追加すると、数十の画面でスキップが無効になることがありますが、ビルド出力には何も表示されません。安定性をバイナリ互換性と同じように扱い、ベースラインをコミットして、すべての PR で差分を確認し、差分が悪化した場合はビルドを失敗させましょう。このスキルでは、skydoves/compose-stability-analyzer Gradle プラグインを使ってこのゲートを設定し、Android 以外のマルチプラットフォームプロジェクト向けに j-roskopf/ComposeGuard を紹介します。

このスキルを使う場面

  • チームが、手動チェックではなく、CI で強制される安定性のサービスレベル目標を求めている。
  • PR で不安定なパラメーターが導入されたものの、フレーム落ちの報告が届くまで誰も気づかなかった。
  • 大規模なアプリを強いスキップに移行しており、リファクタリング中に後退を検知する仕組みが必要である。
  • 多数の機能モジュールを含むモノレポ全体で、安定性の実践方法を標準化したい。
  • ユーザーが stabilityCheck、stabilityDump、ベースラインのずれ、ComposeGuard、または「安定性が後退したらビルドを失敗させる」と言及している。

このスキルを使わない場面

  • 個人開発者による使い捨てのプロトタイプ。ベースラインのメンテナンスコストに見合いません。
  • 安定性をまだ一度も診断していない。まず ../diagnosing-compose-stability/SKILL.md を実行し、現在の状態が許容できることを確認してください。問題のある状態をベースライン化すると、その問題を固定してしまいます。
  • 概念的な質問が「コンパイラーはなぜ X を Y と分類したのか」である。../understanding-stability-inference/SKILL.md を使ってください。

前提条件

  • Compose コンパイラー Gradle プラグイン(org.jetbrains.kotlin.plugin.compose)を使用する Kotlin 2.0 以降。強いスキップはデフォルトで有効です。
  • Gradle 8 以降。
  • ./gradlew assemble または同等の処理をすでに実行する、動作中の CI パイプライン。
  • Compose Compiler のレポートについて理解していること(まず ../diagnosing-compose-stability/SKILL.md を読んでください)。
  • 生成ファイルをリポジトリにコミットできること(.stability ベースラインをチェックインする必要があります)。

ワークフロー

1. プラグインを適用する

バージョンカタログにプラグインを追加し、UI コンポーザブルを含むすべてのモジュールに適用します。

# gradle/libs.versions.toml
[versions]
compose-stability-analyzer = "0.7.3"

[plugins]
compose-stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version.ref = "compose-stability-analyzer" }
// app/build.gradle.kts (and every UI feature module)
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
    alias(libs.plugins.compose.stability.analyzer)
}

プラグインは、同じコンパイラー出力を読み取れるよう、Compose コンパイラープラグインの後に適用する必要があります。

2. DSL ブロックを設定する
// app/build.gradle.kts
composeStabilityAnalyzer {
    enabled.set(true)

    stabilityValidation {
        enabled.set(true)
        outputDir.set(layout.projectDirectory.dir("stability"))
        failOnStabilityChange.set(true)
        ignoredPackages.set(
            listOf(
                "com.example.preview",     // @Preview-only composables
                "com.example.benchmark",   // microbenchmark fixtures
            )
        )
        ignoredClasses.set(
            listOf(
                "com.example.testing.FakeRepository",
            )
        )
        stabilityConfigurationFiles.add(
            rootProject.layout.projectDirectory.file("stability_config.conf")
        )
    }
}

主な設定項目:

  • outputDir — .stability ベースラインファイルの出力先です。ソースと自然に同じ場所に配置されるよう、モジュール内のパスを選んでください。build/ には出力しないでください。このディレクトリは gitignore 対象であり、ベースラインをコミットする必要があるためです。
  • failOnStabilityChange — CI では true に設定します。開発者は作業速度を優先してローカルでは false に切り替えても構いませんが、コミットする値は true である必要があります。
  • ignoredPackages / ignoredClasses — @Preview コンポーザブル、ベンチマーク用フィクスチャ、意図的に安定性ルールに違反させているテスト用ダブルを除外します。
  • stabilityConfigurationFiles — Compose コンパイラーと同じ stability_config.conf を指定し、プラグインの判定をコンパイラーの判定に一致させます。
3. ローカルでベースラインを生成する
./gradlew :app:stabilityDump
# or, for a specific variant:
./gradlew :app:debugStabilityDump

プラグインは、outputDir の下に、モジュールごと、バリアントごとに 1 つの .stability ファイルを書き込みます。これらをコミットしてください。

git add app/stability/
git commit -m "chore(stability): seed baseline"

1 つの .stability エントリーは、次のようになります。

@Composable
public fun com.example.CounterDisplay(count: com.example.MainViewModel): kotlin.Unit
  skippable: false
  restartable: true
  params:
    - count: RUNTIME (requires runtime check)

各エントリーには、コンポーザブルのシグネチャ、skippable / restartable フラグ、各パラメーターの安定性分類が記録されます。差分形式は実行ごとに安定しているため、PR レビュアーは意味のある変更内容を確認できます。

4. stabilityCheck を CI に組み込む

コンパイルを行うジョブと同じジョブに、Gradle タスクの呼び出しを追加します。別のステージに分けるより、早くフィードバックを得られます。

# .github/workflows/ci.yml
name: CI
on:
  pull_request:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17
      - uses: gradle/actions/setup-gradle@v4
      - name: Assemble and check stability
        run: ./gradlew assemble :app:stabilityCheck
      - name: Upload stability diff on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: stability-diff
          path: app/build/reports/stability/

stabilityCheck は、コミット済みのベースラインを読み取り、build/ に現在のスナップショットを再生成して、両者を比較します。コンポーザブルのスキップ可能性が低下した場合、またはクラスの安定性が低下した場合は、タスクを失敗させます。PR チェックが赤くなり、開発者はどのコンポーザブルが、なぜ後退したのかを正確に確認できます。

5. 回帰に対処する

stabilityCheck が失敗すると、タスクの出力に、回帰したコンポーザブルと、新たに不安定になったパラメーターまたはクラスが示されます。開発者が取れる対応は 2 つあります。

  • 回帰を修正する。 問題の型を安定化します(var → val、List → ImmutableList、Flow パラメーター → 上流での collectAsStateWithLifecycle)。ローカルで :stabilityDump を再実行し、ベースラインに変更がないことを確認してから、プッシュします。
  • 理由を明記してベースラインを更新する。 一部の回帰は許容できます(例:Java POJO を受け入れる必要がある相互運用境界)。:stabilityDump を実行してベースラインを更新し、コミットしたうえで、その回帰が意図的である理由を PR の説明に記載します。コードレビューでは理由が求められます。差分はプレーンテキストで、レビュー可能です。

ベースラインの更新は、バイナリ互換性ベースラインの更新と同様に扱ってください。コードレビューで確認できる状態にし、説明を必須とします。

6. 除外設定は慎重に調整する

ignoredPackages と ignoredClasses は、プレビュー、ベンチマーク、テスト用の代替実装、生成コードなど、正当な例外のためにあります。本番コードの回帰を見えなくするために使用しないでください。除外項目には必ず理由を説明するコメントを付けてください。理由のない除外項目の追加は、レビュアーが却下するべきです。

// RIGHT
ignoredPackages.set(
    listOf(
        "com.example.preview",  // @Preview composables intentionally use mutable state for tooling
        "com.example.benchmark" // microbenchmark fixtures need fresh allocations per iteration
    )
)
// WRONG
ignoredPackages.set(listOf("com.example.feature.cart"))
// WRONG because: silencing an entire production package hides real regressions in cart.
// The team will rediscover the lost skipping as field jank weeks later.
7. 別の方法:マルチプラットフォーム/非 Android では ComposeGuard を使用する

プロジェクトが Compose Multiplatform(デスクトップ、KMP 経由の iOS)である場合、または Android Gradle plugin の対象外である場合は、j-roskopf/ComposeGuard を使用してください。これは同じ考え方(ベースラインをコミットし、CI で差分を確認し、回帰時に失敗させる)を実装しており、<variant>ComposeCompilerGenerate および <variant>ComposeCompilerCheck タスクを提供します。上記の stabilityDump/stabilityCheck と同じ方法で組み込んでください。Android のみを対象とするプロジェクトでは、実行時の相関分析に @TraceRecomposition と統合されるため、skydoves プラグインを第一の推奨とします。

パターン

パターン:ベースラインをコミットし、PR で差分を確認する
# RIGHT
app/stability/
  app-debug.stability   <-- committed
  app-release.stability <-- committed

# CI runs:
./gradlew :app:stabilityCheck   # diffs current vs committed baseline; fails on regression
# WRONG
app/build/stability/    <-- in build/, gitignored, never committed
# WRONG because: every CI run starts from a fresh build/, the baseline does not exist,
# stabilityCheck either no-ops or always passes. The gate is theater.
パターン:CI では failOnStabilityChange を有効にし、ローカルでは任意にする
// RIGHT — committed default
composeStabilityAnalyzer {
    stabilityValidation {
        failOnStabilityChange.set(true)
    }
}
// WRONG — committed default
composeStabilityAnalyzer {
    stabilityValidation {
        failOnStabilityChange.set(false)
    }
}
// WRONG because: turns the gate into an informational warning. Developers learn to ignore
// the warning. Stability regresses anyway. Set true and let CI block the merge.

ローカルでは、開発者が渡せる Gradle プロパティを使って上書きできます。

failOnStabilityChange.set(
    providers.gradleProperty("composeStabilityStrict").orNull?.toBoolean() ?: true
)
# Local fast iteration — opt out explicitly per invocation
./gradlew :app:stabilityCheck -PcomposeStabilityStrict=false
パターン:別のジョブではなく、assemble ジョブで stabilityCheck を実行する
# RIGHT — earlier signal, single workflow run
- run: ./gradlew assemble :app:stabilityCheck
# WRONG — separate jobs split signal
jobs:
  assemble:
    steps: [./gradlew assemble]
  stability:
    needs: assemble
    steps: [./gradlew :app:stabilityCheck]
# WRONG because: the stability job re-resolves dependencies and re-runs Compose compilation
# from scratch, doubling CI time and delaying the failure signal. Same job, single Gradle daemon.
パターン:CI ゲートと実行時の @TraceRecomposition を組み合わせる

このプラグインで証明できるのは、クラスが安定として分類されていることです。実行中のアプリで実際に再コンポーズが減ることまでは証明できません。CI ゲートと実行時トレース(../../measurement/tracing-recompositions-at-runtime/SKILL.md を参照)を組み合わせることで、コンパイル時の安定性分類と実行時の再コンポーズ回数という、両方の指標をチームで確認できます。CI ゲートが成功しているのに再コンポーズ回数が増加している場合は、分類では検出できない実行時のみの無効化要因(例:誤った位置での mutableStateOf の読み取り)があることを意味します。

必須ルール

  • 必須: .stability ベースラインファイルをバージョン管理にコミットしてください。build/ にあるものはベースラインではありません。
  • 必須: コミットする Gradle 設定で failOnStabilityChange.set(true) を設定し、CI でリグレッションがあればマージをブロックしてください。Gradle プロパティを使ったローカルでの無効化は許容されますが、false をコミットしてはいけません。
  • 禁止: 理由を説明する1行コメントを付けずに、パッケージやクラスを ignoredPackages / ignoredClasses に追加してはいけません。根拠のない除外は、安定性が気付かないうちに再び損なわれる原因になります。
  • 禁止: 既知の問題がある状態をベースライン化して、作業完了と見なしてはいけません。まず ../diagnosing-compose-stability/SKILL.md と ../stabilizing-compose-types/SKILL.md を実行し、チームが責任を持って維持できるものだけをベースライン化してください。
  • 必須: :app だけでなく、すべての UI モジュールにプラグインを適用してください。機能モジュールのリグレッションは、その機能がメイングラフに組み込まれるまで、:app のみを対象とするチェックでは検出できません。
  • 推奨: :stabilityCheck は、コンパイルと同じ CI ジョブで assemble の後に実行してください。より早く問題を検知でき、Gradle デーモンは1つで済み、実時間も半分になります。
  • 推奨: ComposeGuard のほうが適している場合(KMP/デスクトップ/iOS)は、ComposeGuard を使用してください。ただし、ワークフローの形は正確に合わせてください。ベースラインをコミットし、差分があればビルドを失敗させ、コードレビューで除外の理由を明確にします。
  • 推奨: stabilityConfigurationFiles には、Compose コンパイラーが読み込むものと同じ stability_config.conf を指定し、プラグインとコンパイラーの分類を一致させてください。

安定性は契約であり、魔法の呪文ではありません。CI のゲートは、その契約を守らせる仕組みです。スキップ可能性は診断指標であって、KPI ではありません。目標は「チームが合意したベースラインからリグレッションがないこと」であり、「100% スキップ可能にすること」ではありません。

確認項目

  • すべての UI モジュールの build.gradle.kts に composeStabilityAnalyzer プラグインブロックがある。
  • app/stability/(または設定した outputDir)がリポジトリにコミットされ、リリースするバリアントごとに少なくとも1つの .stability ファイルが含まれている。
  • failOnStabilityChange.set(true) がコミット済みのデフォルト設定になっている。
  • CI ワークフローで、すべてのプルリクエストに対して ./gradlew :app:stabilityCheck(または同等のモジュールパス)が実行される。
  • ゲートをテストする。一時的なブランチで共有データクラスの val を var に変更してプッシュし、回帰したコンポーザブルの名前を含む失敗出力とともに、プルリクエストのチェックが失敗することを確認する。その後、テスト用の変更を元に戻す。
  • ignoredPackages と ignoredClasses のすべてのエントリに、除外する理由を説明するコメントが付いている。
  • 開発者がローカルで ./gradlew :app:stabilityDump を実行したとき、変更がある場合でも .stability ファイルだけに限定され、プレーンテキストとしてレビューできる。

参考資料

CI で安定性チェックに合格した結果と実際の再コンポジション回数との相関を実行時に確認するには、../../measurement/tracing-recompositions-at-runtime/SKILL.md を参照してください。stabilityCheck が何をチェックするかを決定する分類モデルの概念については、../understanding-stability-inference/SKILL.md を参照してください。ゲートで検出されたリグレッションを解決する上流の型修正については、../stabilizing-compose-types/SKILL.md を参照してください。

コメント