ci-pipeline-and-gates について
Dart と Flutter アプリ向けに、監査可能な CI パイプラインを設計するスキルです。ツールチェーンの固定、契約に基づくゲート、ローカルでの再現、CI で検証できない範囲の明示を重視します。
主な機能
- 01各ゲートを名前付き契約に対応
- 02CI ランナーとツールチェーンを固定
- 03コード生成結果の鮮度を確認
- 04ランダム順テストと静的ゲートを実行
- 05閾値なしでカバレッジを公開
- 06CI チェックによるリポジトリ変更を防止
活用シーン
- 01単一パッケージの Flutter CI 構築
- 02古い生成コードやスキーマ出力の検出
- 03ソースレベルの不変条件チェック追加
- 04CI で検証できないリリース項目の明示
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ zakariaf/ flutter-skills --skill ci-pipeline-and-gatesインストール後、Claude Code で/ci-pipeline-and-gatesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
examples/policy_grep_test.dartreferences/policy-grep-gate.mdworkflow-skeleton.mdscripts/banned-strings.shci-gates.sh詳細
CI パイプラインとゲート
CI ゲートは飾りの緑色のチェックではありません。リリースを阻止する契約を、人間に頼ることなく、ビルドが合格または不合格になるチェックに変えるものです。パイプラインはシンプルに保ちましょう。ほとんどの単一パッケージアプリではワークフローファイルを 1 つにし、すべてのジョブをバージョン固定し、各ゲートを名前の付いた契約にひも付け、CI では本当に重要なことを証明できない場合は、その旨を正直に記載します。
作業内容に応じて、次のリファレンスを確認してください。
references/workflow-skeleton.md— コピーして使える単一ファイルのci.yml。バージョン固定されたランナーとツールチェーン、鮮度ゲート、フォーマットと解析、ランダム化テスト、host-sqlite、coverage-strip、およびリリースビルドのジョブが含まれます。references/policy-grep-gate.md— grep ベースの不変条件ゲート。3 つの基準、コメントの先行削除、構造を基準にしたアンカー付け、結果を蓄積して最後に一度だけ失敗させること、見知らぬ人にも理由がわかるように記述することが要点です。
プルリクエストの前に scripts/ci-gates.sh と scripts/banned-strings.sh を実行し、静的ゲートをローカルで再現してください。
妥協できないルール
- 各ゲートは、必ず1つの名前付きコントラクトに対応させ、その名前を明記してください。 明示されたコントラクトに対応しないチェックはゲートではありません。アドバイザリーとして、そう明記するか、追加しないでください。根拠のないマージブロッカーを決して作らないでください。これにより、パイプラインの監査可能性が保たれ、ゲートの乱立を防げます。
- すべてのジョブで、ランナーとツールチェーンを固定してください。
runs-on: ubuntu-24.04(決して-latestを使わないでください。イメージの変更により、レビュー可能な差分がないまま lcov やツールチェーンのバージョンが変わります)と、flutter-versionまたはflutter-version-fileを明示し、channel: stableを指定したsubosito/flutter-action@v2を使ってください。SDK を固定しないと、ゴールデンが気付かないうちに再生成され、決定性が損なわれます。バージョンを更新するのは、専用の PR のみです。 - 確認していないアクションのバージョンはインラインで明示し、メジャーバージョンを決して推測しないでください。 誤ったメジャーバージョンを指定すると、気付かないうちに失敗したり、動作が変わったりします。確認していないバージョンには、推測で
@v7と書かず、隣に# VERIFY:と記してください。 - フォーマットと解析は必須ゲートです。
dart format --output=none --set-exit-if-changed .とflutter analyze --fatal-infosを実行してください。--fatal-infosは重要です。修正されずに残った info は、次回から無視される警告になります。lint-and-style-configを参照してください。 - コード生成とスキーマの鮮度確認は、ビルド手順ではなく鮮度ゲートです。 ジェネレーターを実行してから、出力に対して
git diff --exit-codeを実行してください。差分がある場合は失敗させ、再生成してコミットするよう作成者に伝えるメッセージを表示します。これは生成コードのコミットに対する唯一の対策であり、「スキーマを変更したのにマイグレーションを忘れた」ケースを検出する唯一のゲートです。run-codegenとrun-migrationを参照してください。 - テストの実行順をランダム化し、テストホストに必要なネイティブ依存関係を用意してください。
flutter test --test-randomize-ordering-seed randomを使えば、テスト間の状態漏れを手軽に検出できます。Linux では、flutter testは通常の Dart VM で実行されるため、sqlite3_flutter_libsは機能しません。実際の DB を開くテストスイートの前にホストのsqlite3をインストールしてください。そうしないと、リポジトリの不具合に見える別の理由で失敗します。 - 静的な grep はランタイムで検出できない問題を捉えますが、3つの基準を満たす場合に限ります。 grep ベースのゲートが正当化されるのは、不変条件が「テキストで判定可能」で、「破られても何も起きず」、「1行で破れる」場合に限ります(
references/policy-grep-gate.md)。合格したテストでは証明できない「ソースグラフ」の特性(ランタイムで到達しない import や、マニフェスト属性など)を証明します。3つの基準のうち2つしか満たさない場合は、grep ではなくコードレビューで対応してください。 - カバレッジは公開するレポートであり、決してゲートではありません。 パーセンテージのしきい値を設けず、有料サービスも使わないでください。何も検証しないカバレッジ対象行には価値がありません。まず、数値が実態より高く見える問題を修正してください。
flutter test --coverageは、テストから import されないファイルを含めません。そのため、テストされていないファイルは分母の行数がゼロとなり、カバレッジは0%ではありません。テストされたファイルが1つだけでも、約100%と報告される可能性があります。テストされていないファイルを含めてから、lcov.infoから生成コードを取り除いてください。testing-strategyを参照してください。 - ゲートは検証するものであり、リポジトリに合格のお墨付きを与えたり、変更したりするものではありません。
--update-goldensやdart format --fixを使わず、CI から再生成したコードをコミットしないでください。チェック対象を修正するゲートは、何も検証していません。再生成はローカルで人がレビューして行います。ゴールデンの実行には固定されたランナーを使い、CI では比較のみを行います。widget-golden-and-a11y-testingを参照してください。 - ゲートが赤ならブロックしてください。限界についても正直に伝えてください。 ゲートに
continue-on-error: trueを指定せず、ゲートがスキップされた状態でマージせず、静的解析の指摘を// ignore:で無視しないでください。また、CI では証明できないこと(音声出力、実フォントでの描画、実機での実際の動作など)を明確に記してください。実機での手動確認は面倒な作業ではなく、リリースに不可欠な成果物として扱ってください。
単一のワークフロー(単一パッケージのデフォルト)
ほとんどのアプリでは、.github/workflows/ci.yml の1ファイルで十分です。すべてを固定し、高速なフィードバックを1つのジョブにまとめてください。
name: ci
on:
push: { branches: [main] }
pull_request:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-24.04 # pinned, not -latest — rule 2
timeout-minutes: 20
steps:
- uses: actions/checkout@v4 # VERIFY: confirm current major
- uses: subosito/flutter-action@v2 # v2 is current; there is no v3
with:
channel: stable
flutter-version: '3.29.0' # pin, or flutter-version-file: .fvmrc
cache: true
- run: flutter pub get
- run: dart format --output=none --set-exit-if-changed .
- run: flutter analyze --fatal-infos
- run: flutter test --test-randomize-ordering-seed random --reporter expanded
鮮度ゲート、ホストの sqlite、カバレッジの除外、リリースビルドのジョブを含む完全なスケルトンは、references/workflow-skeleton.md にあります。
鮮度ゲート:ジェネレーターを実行し、出力との差分を確認する
*.g.dart/*.freezed.dart/*.drift.dart をコミットしても安全な理由と、マイグレーションのないスキーマ変更を検出する方法を説明します。
- name: Generated code is up to date
run: |
dart run build_runner build --delete-conflicting-outputs
if ! git diff --exit-code -- '*.g.dart' '*.freezed.dart' '*.drift.dart'; then
echo "::error::Generated code is stale. Run build_runner and commit the result."
exit 1
fi
- name: Schema dumps are up to date
run: |
dart run drift_dev schema dump lib/data/app_database.dart drift_schemas/
if ! git diff --exit-code -- drift_schemas/; then
echo "::error::drift_schemas/ is stale — you changed the schema without"
echo "::error::bumping schemaVersion and dumping. Shipping this means NO"
echo "::error::MIGRATION RUNS and existing rows fail to load."
exit 1
fi
パターンを編集する前に、実際にどのサフィックスがコード生成で出力されるか(モダンな .drift.dart か、レガシーなパートファイルの .g.dart か)を確認してください。推測してはいけません。鮮度ゲートを警告に弱めたり、タグのときだけ実行したりしてはいけません。
静的ゲート:ソースグラフを検索し、まとめて失敗させる
実行時に到達しない禁止インポートや、設定属性など、どのテストでも検出できない契約があります。それらは grep で検索してください。まずコメントを取り除き(ルールの説明自体に検索対象の文字列が含まれるためです)、構造(インポート URI や XML 要素など)に照合し、違反をすべて集めて、第三者にも理解できる理由とともに一覧を表示して、一度だけ失敗させます。
// test/policy/banned_imports_test.dart — match the import URI, not the file.
import 'dart:io';
import 'package:test/test.dart';
void main() {
test('lib/ imports nothing that can reach the network or phone home', () {
const banned = <String>[
'package:http/', 'package:dio/',
'package:firebase_core/', 'package:sentry_flutter/',
];
final importUri =
RegExp(r'''^\s*import\s+['"]([^'"]+)['"]''', multiLine: true);
final lineComment = RegExp(r'//[^\n]*');
final offenders = <String>[];
for (final f in Directory('lib').listSync(recursive: true).whereType<File>()) {
if (!f.path.endsWith('.dart') || f.path.endsWith('.g.dart')) continue;
final code = f.readAsStringSync().replaceAll(lineComment, '');
for (final m in importUri.allMatches(code)) {
final uri = m.group(1)!;
if (banned.any(uri.startsWith)) offenders.add('${f.path}: $uri');
}
}
expect(offenders, isEmpty,
reason: 'These imports break the no-network promise.\n${offenders.join('\n')}');
});
}
これらを作成する際の基準と注意点は references/policy-grep-gate.md に、完全なファイルは examples/policy_grep_test.dart にあります。シェルゲートで同じ手法を使う場合は、Dart のテストスイート外で PR 前チェックを行うための scripts/banned-strings.sh を参照してください。
カバレッジ:測定して公開するが、ゲートにはしない
数値を引用する前に、上振れの嘘を正し、その後で生成コードを除外します。
# REQUIRED first: include untested files so the denominator is honest —
# a generated test that imports every lib/ file (see workflow-skeleton.md),
# otherwise one tested file can report ~100%. Collect coverage in the same
# randomized run; do not run the suite twice.
- run: flutter test --coverage --test-randomize-ordering-seed random
- name: Strip generated code from coverage
run: |
lcov --remove coverage/lcov.info \
'lib/**/*.g.dart' 'lib/**/*.freezed.dart' 'lib/**/*.drift.dart' \
-o coverage/lcov.info --ignore-errors unused # lcov 2.x errors on unused patterns
# Publish for transparency. Do NOT add a min-coverage threshold.
全体にしきい値を設定する代わりに、回復不能なバグにつながる少数の名前付きファイル(マイグレーション、金額/単位を扱うコア、リポジトリの書き込み経路)について、すでに嘘だとわかっている指標のために行数を数えるのではなく、差分を読んでカバレッジの下限を維持します。ファイル単位で下限を設ける理由については testing-strategy を参照してください。
CI でのゴールデンテスト:検証のみ行い、更新しない
CI でゴールデンテストを実行する場合は、固定したランナーで実行し、比較のみ行ってください。CI で --update-goldens を実行してはいけません。そうすると、ゲートは何も検証しなくなります。OS やフォントによる差異は実際に発生します。あるホストで作成したゴールデンが別のホストでは失敗することがあるため、ランナーを固定してそれを基準として受け入れるか、ゴールデンテストを CI から外し、レイアウトに依存しないウィジェットテストと実機での確認に頼ってください。2 つの経路を使うゴールデンテスト戦略については widget-golden-and-a11y-testing を参照してください。
マルチパッケージの場合(ワークスペース)
リポジトリが実際に Dart の pub ワークスペース/Melos モノレポである場合に限ります。単一パッケージのアプリには該当しません。
- ワークスペースのルートで一度だけ依存関係を解決します(ルートで
dart pub getを実行すると、すべてのメンバーの依存関係が取得されます)。解析の前にルートでコード生成を実行してください(run-codegenを参照)。 - 禁止インポートのグラフチェックはパッケージ境界ごとに適用します(純粋な Dart のコアでは
package:flutterをインポートしません)。これは、静的ゲートがソーステキストではなく、解決済みの依存関係グラフを読み取る唯一のケースです。 - 固定した
flutter-versionは 1 つのenv:エントリーに記述し、すべてのジョブから参照します。そうすれば、バージョン更新は 1 か所の編集で済みます。
これらを小規模なアプリでも必須であるかのように説明してはいけません。単一パッケージには不要なオーバーヘッドです。
アンチパターン
ubuntu-latest/変動する SDK。 イメージやツールチェーンの変化が、レビュー用の差分なしにワークフローへ入り込み、成功したビルドを嘘にしてしまいます。- アクションのメジャーバージョンを勝手に決めること。 確認していない
@v7は、ひそかに失敗したり動作を変えたりします。代わりに# VERIFY:を付けて確認してください。 - CI で自動的に承認すること。
--update-goldens、修正を書き込むdart format、生成コードのコミットなどを行うと、ゲートは何も検証しなくなります。 - カバレッジ率によるゲート。 何も検証しないテストを増やす動機になり、未テストのファイルを含めるまでは実際の数値より高く表示されます。
- ファイル全体への
containsgrep。 ルール自体のコメントや文字列リテラルにも一致します。コメントを除去して構造に絞り込むか、誤検知を繰り返して削除されることになります。 - 問題のある項目を探すループ内での
expect。 1 件目を報告して残りを隠してしまいます。結果を蓄積し、最後に一度だけ失敗させてください。 - ゲートへの
continue-on-error: trueの指定、ゲートをスキップしたままのマージ、静的検出結果に対する// ignore:の指定。 抑制されたゲートは、まさに検出するために存在する抜け道です。 - 鮮度ゲートなしでのスキーマ変更。 移行処理なしでリリースされ、既存データの読み込みがひそかに失敗します。
- CI の成功がオーディオ/実フォントの描画/デバイス上の動作を証明すると主張すること。 CI では証明できません。限界を明示し、手動確認を残してください。
- 好みに基づく grep(sealed 型での
default:やオーバーフローモディファイアを禁止するなど)。誤検知がディレクトリの削除につながります。これはコードレビューであり、ゲートではありません。
完了の定義
- すべてのジョブで
runs-on: ubuntu-24.04(または明示的なイメージ)と、明示的なflutter-version/flutter-version-fileおよびchannel: stableを指定したsubosito/flutter-action@v2を固定します。未確認のアクションバージョンには# VERIFY:を付けます。 -
dart format --set-exit-if-changedとflutter analyze --fatal-infosを実行し、問題があればブロックします。 - コード生成と(使用している場合は)スキーマの鮮度確認では、ジェネレーターを実行してから
git diff --exit-codeを実行し、対応方法が分かるメッセージを表示して失敗させます。 - テストは
--test-randomize-ordering-seed randomを指定して実行します。ネイティブホスト依存関係(例:sqlite3)がある場合は、必要なテストスイートの実行前にインストールします。 - 各ゲートを名前付きの契約に対応付けます。名前のないマージブロッカーを設けず、アドバイザリーチェックはその旨を明示します。
- 静的 grep(使用する場合)は、コメントを除去し、構造に絞り込み、結果を蓄積して一度だけ失敗させます。また、初めて読む人にも分かる理由を示します。
- カバレッジは、過大表示を防ぐ修正と生成ファイルの除外を適用してレポートとして公開します。パーセンテージによるゲートは設けません。
-
continue-on-errorを使うゲート、--update-goldensを使うゲート、リポジトリを変更するゲートは設けません。 - ワークフローでは CI で証明できないことを正直に記載し、リリース成果物として手動のデバイス上での確認を明示します。
関連スキル
run-codegen— 鮮度ゲートで差分を確認する、決定的なbuild_runnerの実行手順です。run-migration— スキーマ鮮度ゲートが保護する、スキーマスナップショットの手順です。lint-and-style-config— analyze ゲートで実行するanalysis_options.yamlと、--fatal-infosの昇格設定です。testing-strategy— カバレッジルールの根拠となる、ファイル単位のカバレッジ基準と、モックよりフェイクを優先する方針です。widget-golden-and-a11y-testing— 2 段階のゴールデン戦略と、CI は検証のみを行い、承認は決して行わない理由です。codegen-and-toolchain— ゲートで参照する SDK の固定、パッケージごとのbuild.yaml、アナライザー/カバレッジの除外設定です。dependency-hygiene— ゲートが前提とする、コミット済みのロックファイルとバージョンを固定した lint です。design-review-workflow— CI で証明できないことを確認する、アプリごとに一度行う手動 QA 一式です。run-goldens-rebaseline— CI では決して実行してはならない、ローカルで人がレビューして承認する手順です。release-and-store-shipping— ゲートがすべて成功した後に行うことです。署名済み成果物、アーカイブ済みシンボル、ストアへの申告、段階的なロールアウトが含まれます。
参考資料
- subosito/flutter-action — GitHub Actions 向けの Flutter 環境。https://github.com/subosito/flutter-action
- Flutter — Android アプリのビルドとリリース。https://docs.flutter.dev/deployment/android
- Flutter API —
matchesGoldenFile(ゴールデンテストは OS/フォント/バージョンの影響を受けます。--update-goldens)。https://api.flutter.dev/flutter/flutter_test/matchesGoldenFile.html - Dart —
dart testランナーのフラグ(--test-randomize-ordering-seed)。https://dart.dev/tools/dart-test - Code With Andrea — Flutter のテストカバレッジ(
lcov.info、生成ファイルの除外、未テストファイルに関する誤解)。https://codewithandrea.com/articles/flutter-test-coverage/ - Dart — pub ワークスペース(モノレポ)。https://dart.dev/tools/pub/workspaces
- GitHub Actions — concurrency、permissions、固定したランナーの使用。https://docs.github.com/actions