つばくろぐ @takamii228

知は力なり

flutterのバージョンをGitLab CI Runner上で動的に切り替える

この記事はFlutter Advent Calendar 2019 (#2)の記事です。

※12/21 8時時点で18日分に空きがあったので埋めさせていただきました(参考

はじめに

GitLab CIを使ってFlutterのCI/CDをやっているのですが、CI環境において複数のFlutterのバージョンを管理する必要があり、少し悩んだので以下にまとめます。

GitLab CI Runnerは以下のような感じで、shellモードでMac mini上で動かしています。

f:id:takamii228:20191220225237p:plain

今回、Flutterのバージョンアップの検証が必要になったのですが、すでに配信中のアプリで利用しているFlutterのバージョンに対してはバージョンアップが完了するまでの間に緊急パッチを宛てる可能性があります。そのため、緊急パッチのビルド時につかうFlutterのバージョンと、バージョンアップ検証で使うFlutterのバージョンをそれぞれのビルドで使い分ける必要があります。

検討した解決策

CI環境上で複数のflutterのバージョンを動作させる方法として、以下の2つ案を考えました。

  1. 配信版と追加開発をブランチを分けて、ブランチごとに固定のGitLab CI Runnerを割り当てる
  2. GitLab CIの実行時に使うバージョンを動的に切り替えられるようにする

メリット・デメリットは以下の通りです。

メリット
デメリット
案1
  • 要件を満たすことができる
  • 各バージョンを設定したCI Runnerに対応するブランチの命名規則を決める必要がある
  • .gitlab-ci.ymlの設定が煩雑になる。
  • 各CI Runnerに対して別々にバージョンの設定する必要がある
案2
  • 要件を満たすことができる
  • CI Runnerとブランチの命名規則を対応させる必要がない
  • .gitlab-ci.ymlの設定がシンプルになる
  • 各CI Runnerの設定を分ける必要がない
    • バージョンが変わる場合、CIの実行の度に都度バージョン切り替えの処理が走るためCIの実行時間が長くなる

    GitLab CIではCI Runnerにタグをつけることができ、tagonlyexcept等を記述することで特定のブランチのジョブを特定のCI Runnerで実行させる、という設定ができます。

    https://docs.gitlab.com/ce/ci/yaml/

    しかしデメリットで書いた通り、普段の開発においてはfetuareブランチやhotfixブランチを切ったりリリースタグを切ったりとブランチの命名規則を複数考慮してgitlab-ci.ymlを設定するのはなかなか骨が折れます。

    今回は設定が煩雑になることに加え、バージョンアップ版のリリースまでの移行期間のみ並行運用が走ること、また都度実行で増える実行時間が2-3分程度で待てるレベルであることを踏まえて2を選択することにしました。

    実装

    各ビルドで使うflutterのバージョンはpubspec.yamlに記載できるため、それを使ってflutterのバージョンを切り替えてからCIを実行するように.gitlab-ci.ymlを設定します。

    ...
    environment:
      sdk: ">=2.1.0 <3.0.0"
      flutter: 1.9.1+hotfix.6
    ...
    

    yamlから特定のキーの値を取得するのにはyqが使えます。

    yq.readthedocs.io

    pubspec.yamlに記載されたバージョンでflutterのversionを切り替えるシェルは以下のように記載できます。

    #!/bin/sh
    
    set -x
    set -e
    
    # get tagert flutter version
    TARGET_FLUTTER_VERSION=`cat pubspec.yaml | yq -r .environment.flutter`
    
    TARGET_FLUTTER_VERSION_TAG_NAME=v${TARGET_FLUTTER_VERSION}
    
    CURRENT_DIR=`pwd`
    FLUTTER_DIR=${HOME}/development/flutter
    
    cd ${FLUTTER_DIR}
    
    # current version
    flutter --version
    
    # flutter checkout
    git checkout -f master
    git pull
    git checkout -f ${TARGET_FLUTTER_VERSION_TAG_NAME}
    
    flutter precache
    
    flutter doctor
    
    # updated version
    flutter --version
    
    cd ${CURRENT_DIR}
    

    ポイントとしてはflutterのバージョンはpubspec.yamlにはvなしで記載されているのに対し、flutterのタグではvがついているためgit checkoutするときに付与する必要があります。

    flutter.dev

    flutterのバージョンの切り替えはflutter versionコマンドを使うこともできますが、これを実行するには対象のバージョンがあるchannelをflutter channelで事前に指定し、かつupgradeを実行して最新化しておく必要があります。

    最新化がされていなかったり、切り替えたいバージョンのchannelが異なっている場合はflutter versionコマンドは失敗してしまいます。

    $ flutter version -f v1.13.3
    There is no version: 1.13.3
    Unable to checkout version branch for version 1.13.3.
    

    なので、今回はflutterコマンドがインストールされている場所~/development/flutterに移動し、master上でpullして都度タグをcheckoutする方式を取るようにしました。

    チェックアウトした後はflutter precacheすることで依存するライブラリが落ちてきます。

    macOS install - Flutter

    最後に↑のシェルを.gitlab-ci.ymlbefore_scriptの中で実行するように設定すれば完成です。

    ...
    before_script:
      - ./set-flutter-version.sh
    ...
    

    似たような感じでxcode-selectすればXCodeのバージョンも指定できそうですね。

    gradle wrapperやmaven wrapperでのバージョン指定、Docker buildなどと同様にビルドさせる環境のツールのバージョンはgitレポジトリに定義含めてしまって、CIの実行の度に動的に変えられるようにしておくと管理が便利になりますね。

    あとはflutterのバージョン切り替えもDocker Buildみたいにキャッシュできたらいいのになー。

    FlutterでID/Password入力画面を作ってみる

    FlutterでID/Pass入力画面を作るときに設定するのパラメータをちょっと調べたのてまとめてみます。(※入力のバリデーションやオートフォーカス等のアクションは含みません)

    よくあるID/Pass入力画面の要件

    よくあるID/Passの入力画面の要件といえばこんな感じでしょうか。

    • ID入力

      • 半角英数が入力できる
    • Password

      • 半角英数が入力できる
      • 入力内容はマスクしたい
      • 入力内容を確認したいので、マスクの表示非表示を切り替えられるようにしたい
      • 予測変換はさせたくないしヒストリーに記録なんてさせたくない
      • 入力欄のコピペはブロックしたい

    入力フォームはTextInputFiledを使えば実現できます。中でTextFieldをラップしているようです。

    TextFormField class - material library - Dart API

    TextField constructor - TextField class - material library - Dart API

    上記の要件に絡みそうなパラメータは以下の通りです。

    • obscureText
      • 入力内容のマスク表示の切り替え
    • autocorrect
      • オートコレクト・予測変換の有効・無効化
    • enableInteractiveSelection
      • 入力エリアのセレクトアクション(コピペ、選択、削除など)の有効・無効化
    • TextInputType

    今回の要件では、以下のような設定にするのがよさそうです。

    ID入力欄

    パラメータ 理由
    obscureText false IDは常に見せておくため
    autocrrect false/true どちらでもよさそう。メールアドレスとかだとサジェストがあると嬉しいかもしれない。
    enableInteractiveSelection true コピペしたいケースがありそうなので。
    TextInputType emailAddress IDがメールアドレスのケースも多く、とりあえず半角英数で立ち上がってほしいので。

    Password入力欄

    パラメータ 理由
    obscureText true/false 右端に目のアイコンを用意し、クリックで切り替えられるようにする。
    autocrrect false パスワードは守りたいので。
    enableInteractiveSelection false コピペはブロックしたいので。
    TextInputType visiblePassword パスワードを見せる専用のキーボードタイプ。v1.9.1+hotfix-6から使える模様。

    Widgetに上記の設定をしてiOS/Androidそれぞれエミュレータで動かしてみます。

    今回試したコードサンプルはこちら。

    github.com

    なおPassword入力画面のパーツについては本家のflutter garallyにもあり、今回の実装の参考にしました。

    github.com

    実行環境は以下の通りです。

    $ flutter doctor
    Doctor summary (to see all details, run flutter doctor -v):
    [] Flutter (Channel unknown, v1.9.1+hotfix.6, on Mac OS X 10.14.6 18G87, locale en-JP)
     
    [] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
    [] Xcode - develop for iOS and macOS (Xcode 11.3)
    [] Android Studio (version 3.5)
    [!] IntelliJ IDEA Ultimate Edition (version 2019.3)
        ✗ Flutter plugin not installed; this adds Flutter specific functionality.
        ✗ Dart plugin not installed; this adds Dart specific functionality.
    [!] VS Code (version 1.41.0)
        ✗ Flutter extension not installed; install from
          https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter
    [] Connected device (2 available)
    
    ! Doctor found issues in 2 categories.
    
    $ flutter devices
    2 connected devices:
    
    AOSP on IA Emulator • emulator-5554                        • android-x86 • Android 9 (API 28) (emulator)
    iPhone 11 Pro Max   • 424952E6-D634-4B19-AA9A-8552B62DE670 • ios         •com.apple.CoreSimulator.SimRuntime.iOS-13-3 (simulator)
    

    挙動確認

    iOS

    f:id:takamii228:20191214225141p:plainf:id:takamii228:20191214225400p:plainf:id:takamii228:20191214225410p:plain

    想定通り動いてそうです。

    Android

    f:id:takamii228:20191214225437p:plainf:id:takamii228:20191214225447p:plainf:id:takamii228:20191214225459p:plain

    Androidも大丈夫そうです。

    余談:TextInputTypeがvisiblePasswordじゃないとAndroidでautocorrectが効かないらしい。

    そもそもFlutterにはautocorrectのバグがあったようです。

    github.com

    このバグの対処のために、最新のstableであるv1.12.13+hotfix.5ではenableSuggestionsというパラメータが追加されたようです。 このパラメータ追加で予測変換を一律制御できるのかな〜と思ったのですがそもそもAndroidのみとのこと。またIssueのコメントにあるようにTextInputType.textだと予測変換の無効化は効かない模様。まじかー。

    入力のI/O周りはAndroidiOSの両OSのモジュールをうまくラップして制御するのが難しいのでしょうかね。そういえばWebやDesktopもあるんだった・・・。

    あと今回他のサービスはどうなってるのかな〜と思っていろいろ調べたのですが、ほとんどがWebViewでの入力画面が立ち上がるパターンでした。。。

    GitLab APIを使ってtag名とtagのタイムスタンプをcsv形式で出力する

    リリース頻度のデータを分析するために、GitLabのtag名とtagを作成した時のタイムスタンプの一覧が欲しくなったのでGitLab API経由で取得するシェルを書きました。

    #!/bin/sh
    
    BASE_URL="xxxx"
    API_TOKEN="xxxx"
    PROJECT_ID="xxxxx"
    
    # 全tag一覧を取得するための最大ページ数を取得する
    TOTAL_PAGES=`curl -D - -s -o /dev/null -X GET -H "PRIVATE-TOKEN: ${API_TOKEN}" "${BASE_URL}/api/v4/projects/${PROJECT_ID}/repository/tags?per_page=100" | grep "X-Total-Pages" | tr -d "X-Total-Pages: " | tr -d "\r"` 
    
    # タグとタグのタイムスタンプをカンマ区切りで取得する
    RESULT=""
    
    for i in `seq 1 ${TOTAL_PAGES}`
    do
      RESULT=${RESULT}`curl -X GET -H "PRIVATE-TOKEN: ${API_TOKEN}" "${BASE_URL}/api/v4/projects/${PROJECT_ID}/repository/tags?per_page=100&page=${i}" | jq -r '.[] | (.name|tostring) + "," + (.commit.authored_date|tostring)'`"\n"
    done
    
    # UTCで返却されるので、JSTに変換し出力する
    for line in ${RESULT}
    do
      TAG_NAME=`echo ${line} | cut -d ',' -f1`
      TIMESTAMP_UTC=`echo ${line} | cut -d ',' -f2`
      TIMESTAMP_JST=`date -v+9H -j -f "%Y-%m-%dT%T.000Z" ${TIMESTAMP_UTC} +%Y-%m-%dT%T.000Z`
      echo ${TAG_NAME},${TIMESTAMP_JST}
    done
    

    tag一覧の取得は/api/v4/projects/${PROJECT_ID}/repository/tagsのエンドポイントを叩くと取れます。

    https://docs.gitlab.com/ce/api/

    tag一覧の取得はページングのハンドリングが必要なので、レスポンスヘッダーのX-Total-Pagesからページ数を取得し、最終的にはfor文で回してjqで加工したものを連結します。

    タイムゾーンの設定によってはUTCで結果が帰ってくるので、最後にdateコマンドでJSTに変換しています。

    上記のシェルを実行すると以下のようなcsvが取得できます。

    1.5.0,2019-09-30T10:59:49.000Z
    1.4.0,2019-09-26T15:35:21.000Z
    1.3.2,2019-09-20T19:00:11.000Z
    1.3.1,2019-09-17T18:21:30.000Z
    1.3.0,2019-09-11T21:01:37.000Z
    1.2.0,2019-09-03T22:08:34.000Z
    1.1.0,2019-08-27T22:41:38.000Z
    1.0.2,2019-08-20T22:37:28.000Z
    1.0.1,2019-08-13T22:13:56.000Z
    1.0.0,2019-08-06T21:38:05.000Z
    0.3.0,2019-07-30T21:16:30.000Z
    0.2.0,2019-07-23T22:38:51.000Z
    0.1.0,2019-07-17T15:30:01.000Z

    あとはこれをSpreadSheetなりExcelなりPythonなりで集計すればリリース頻度の分析の集計ができます。