CircleCIからGitHub Actionsへの移行
By kkoudev
CircleCIから移行しようと思った理由
今まではCircleCIのPerformance Planを使い、iOSビルドはmachineタイプをlargeにして少しでもビルド速度を速めると行った手段をとっていたのですが、
それが急に年間契約でなければmacOSのlargeが使えなくなってしまったというのが一番の理由です。
さすがに年間契約をするほどの覚悟はなかったため、これを機に移行することを決意しました。
GitHub Actionsへの移行
最近GitHub Actionsの利用事例も多く聞くようになったので気になって調べてみると、
ベータ版のときとは違って使い勝手はほぼCircleCI同等か条件によってはそれ以上になっていました。
またホストランナーのスペックもCircleCIやBitriseの通常プラン以上に良く、
料金についてもmacOSビルドはLinuxビルドの10倍の分数を消費するものの、
利用頻度から計算するとほぼCircleCIのPerformance Planと変わらないかむしろ安いくらいだったため、
十分利用可能なレベルに達していると判断して移行することにしました。
CircleCIからGitHub Actionsへの移行方法
設定ファイルの書き方については公式でCircleCIからの移行の方法を紹介しています。
ただ、これを見るだけでそのまま移行ができるのかというとそういうわけではありません。
CircleCIに出来て、GitHub Actionsに出来ないこともあれば、その逆もあります。
そのため、CircleCIから移行する際に引っかかるポイントを元に移行方法を紹介します。
(今回Linuxプロジェクトも移行したのですが、世の中的には Linux ホストの例が多いと思うので、macOSホストを例として紹介します)
1. workflowのトリガーの指定
トリガーの指定方法は以下のように設定ファイルの先頭に記述します。
on:
push:
branches:
- develop
# 本番用のタグPushではアプリケーションのバージョン(x.x.x)と、ビルド番号をドットで合わせたものにしています
tags:
- "[0-9]+.[0-9]+.[0-9]+.[0-9]+"
# workflowの手動実行許可
workflow_dispatch:
これはCircleCIとほぼ同じなのですが、利用可能なパターンがCircleCIよりは限られています。
具体的には、CircleCIのときだと正規表現が使えたのですが、GitHub Actionsでは正規表現を使うことはできません。
とはいえ、ある程度のパターンは利用可能なので大きく困ることはないかと思います。
利用可能なフィルタのパターンは以下にまとめられています。
2. CircleCIのContextに相当するものがない
最初に引っかかったポイントはここです。
CircleCIだとworkflowごとに環境変数をグループ化したcontextというものをビルド環境別に適用することが出来たのですが、
GitHub Actionsではこれに相当する機能がありません。
代替手段としては以下のような手段があります。
- workflowファイルを環境ごとに分ける
- pushされたブランチ、タグ種別ごとに動的にビルド設定を分ける
おおよそこの2つの手段が考えられます。
ただ1についてはビルド設定(YAMLファイル)を重複記述しなければならないため、メンテ効率を考えると 2 の手段が個人的にはおすすめです。
私の例だと、CircleCIを使っていたときは CIRCLE_TAG
という環境変数からタグPushされたかどうかで本番ビルドとステージングビルドを区別していました。(タグPushは本番ビルド、developブランチへのPushがステージングビルド)
これに相当する環境変数はGitHub Actionsにはないのですが、gitのrefを取得できる GITHUB_REF
という環境変数があるので、ここからタグPushされたかどうかが取得できます。
環境変数は steps の最初の方で秘匿情報以外のものは動的生成することを心がけます。
具体的には、GITHUB_ENV に書き込んでいく方法 で実現します。
■ 1-1. CircleCIの CIRCLE_TAG に相当する GITHUB_TAG を作るスクリプト
#--------------------------------------------------
# Create GITHUB_TAG
#--------------------------------------------------
GITHUB_TAG=$(echo $GITHUB_REF | sed -E 's#refs/tags/##g' | tr -d '\n')
if [[ $GITHUB_TAG = $GITHUB_REF ]]; then
# ブランチPushの場合は GITHUB_TAG を意図的に空の値として宣言する必要があります
echo "GITHUB_TAG=" >> $GITHUB_ENV
GITHUB_TAG=""
else
echo "GITHUB_TAG=$GITHUB_TAG" >> $GITHUB_ENV
fi
■ 1-2. CircleCIの CIRCLE_TAG の有無でステージング or 本番かを判断する環境変数を作成
#--------------------------------------------------
# Create BUILD_ENV
#--------------------------------------------------
if [[ -z $GITHUB_TAG ]]; then
echo 'BUILD_ENV=stg' >> $GITHUB_ENV
BUILD_ENV=stg
else
echo 'BUILD_ENV=prod' >> $GITHUB_ENV
BUILD_ENV=prod
fi
この 1-1 と 1-2 は1つのシェルスクリプトファイルに記述することを意識しています。
GITHUB_ENV に書き込んだ環境変数はその step が完了しないと有効になりません。
つまり、書き込んだあとに即利用したいケースはサポートしていないので、 echo のあとに変数宣言も行っています。
これを workflow_env.sh
というスクリプトにまとめて記述しておき、以下のように呼び出します。
jobs:
build:
runs-on: macos-10.15
steps:
# ... 省略 ...
- name: Create environment variables
run: |
.github/workflows/workflow_env.sh
これで必要な環境変数の作成ができました。
秘匿情報は別途Organizationまたは対象リポジトリのSecretsから設定して参照するようにします。
そのときに環境別に参照変数や処理を分けたい場合は、この workflow_env.sh で作成した BUILD_ENV
の値別に環境変数や処理を決定するように分岐処理を記述します。
3. submodule を取得する方法
CircleCIの場合は管理ツールからCheckout対象キーを指定しつつ、 add_ssh_keys で fingerprints を設定することで submodule の取得ができましたが、
GitHub Actions では submodule リポジトリと対象ディレクトリのパスを以下のように記述する必要があります。
jobs:
build:
runs-on: macos-10.15
steps:
# ... 省略 ...
- name: Clone submodule
uses: actions/checkout@v2
with:
repository: kkoudev/protobuf
path: proto
ssh-key: ${{ secrets.PROTOBUF_DEPLOY_KEY }}
persist-credentials: true
- name: Checkout submodule
run: |
git submodule update -i
ssh-keyには submodule リポジトリで発行しておいたDeploy Keyの内容を指定します。
Organizationまたは対象リポジトリのSecretsに設定しておきましょう。
4. fastlane match を利用する際にProvisioning Profileや秘密鍵(p12)を保存したリポジトリへアクセスする方法
これもCircleCIのときは submodule と同じく、fingerprintsを設定ファイルに記述すれば clone できるようになるのですが、
GitHub Actionsではそのような機能は提供されておらず、自前で構築する必要があります。
やることは難しくなく、リポジトリのDeploy Keyを発行してそれを .ssh に配置し、かつ .ssh/config に github.com に接続するときにその秘密鍵を使うように記述するのみです。
とはいえ、いちいちそれを毎回記述するのは面倒です。
GitHub Actionsでは誰かが定義したアクションをMarketplaceに公開できるという仕組みがあり、
Marketplaceを探すとよく使われそうなアクションが公開されています。
そこで上記の処理を行うアクションがないかと探したところ kielabokkie/ssh-key-and-known-hosts-action というアクションがあったので、
これを利用することにしました。
jobs:
build:
runs-on: macos-10.15
steps:
# ... 省略 ...
- name: Prepare fastlane
uses: kielabokkie/ssh-key-and-known-hosts-action@v1
with:
ssh-private-key: ${{ secrets.FASTLANE_DEPLOY_KEY }}
ssh-host: github.com
FASTLANE_DEPLOY_KEY
はOrganizationまたは対象リポジトリのSecretsとして、fastlane matchの秘匿情報を保存したプライベートリポジトリのDeploy Keyの内容を定義します。
これで fastlane match が利用できるようになりました。
5. Xcodeのバージョンの設定方法
XcodeのバージョンはCircleCIの場合はYAMLの設定ファイルにバージョンを記述する箇所があり、そこのバージョンを変えるだけでした。
GitHub Actionsでは標準ではそのような設定は提供されておらず、ホストランナーに予め用意された利用したいバージョンのXcodeのディレクトリを xcode-select で指定するという方法を取る必要があります。
jobs:
build:
runs-on: macos-10.15
steps:
# ... 省略 ...
- name: Setup Xcode version
run: |
sudo xcode-select -s '/Applications/Xcode_12.2.app/Contents/Developer'
利用可能なXcodeの一覧は以下のリンク先に公開されています。
6. Rubyのバージョンの設定方法
これもホストランナーにいくつかのRubyが既に利用可能になっているのですが、
デフォルトでは最新バージョンが選択されています。
バージョンを切り替えるためのアクションが公式から提供されているので、これを利用します。
jobs:
build:
runs-on: macos-10.15
steps:
# ... 省略 ...
- name: Setup Ruby version
uses: actions/setup-ruby@v1
with:
ruby-version: 2.6.6
- name: Install bundler
run: |
gem i bundler
デフォルト以外のバージョンを選択した場合、bundlerが入っていないので自前でインストールする必要があります。 利用可能なRubyのバージョンは以下のリンク先に公開されています。
7. キャッシュの利用方法
私が元々iOSビルドのCIにBitriseを使わずにCircleCIを使っていた理由の1つとして、
どのファイルをキャッシュするかを明示的に指定可能であったためです。
(Bitriseもキャッシュは可能なものの、どのファイルをキャッシュしているのかがわかりづらい仕様となっていました)
GitHub ActionsでもCircleCIと同じように、キャッシュ対象のファイルを明示的に指定することができます。
以下のようにキャッシュ対象ファイルを指定します。
jobs:
build:
runs-on: macos-10.15
steps:
# ... 省略 ...
- name: Creates swift version
run: |
swift --version > swift-version
- name: Cache gem
uses: actions/cache@v2
id: cache_gem
with:
path: |
ios/vendor
key: ${{ runner.os }}-gem-${{ hashFiles('ios/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
continue-on-error: true
- name: Cache CocoaPods
uses: actions/cache@v2
id: cache_pods
with:
path: |
ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('swift-version') }}-${{ hashFiles('ios/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-${{ hashFiles('swift-version') }}-
continue-on-error: true
- name: Cache Carthage
uses: actions/cache@v2
id: cache_carthage
with:
path: |
ios/Carthage
key: ${{ runner.os }}-carthage-${{ hashFiles('swift-version') }}-${{ hashFiles('ios/Cartfile.resolved') }}
restore-keys: |
${{ runner.os }}-carthage-${{ hashFiles('swift-version') }}-
continue-on-error: true
# React Nativeアプリケーションの場合はこちらも追加する
- name: Cache yarn
uses: actions/cache@v2
id: cache_yarn
with:
path: |
node_modules
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
continue-on-error: true
iOSビルドバイナリはSwiftのバージョンごとに形式が変わるため、最初にswiftコマンドからバージョン情報を取得してテキストファイルに保存しておき、
そのハッシュ情報も込みでキャッシュキーを生成するようにします。
CarthageのキャッシュについてはRomeを利用する手もあるので、その辺はプロジェクトに応じて使い分けてください。
8. fastlaneによるXcodeプロジェクトのビルド
あとはCircleCIのときと同じです。
fastlaneでkeychainを作成する setup_circle_ci という関数があるのですが、
GitHub Actionsでもこれを利用して keychain 情報を作成できます。
そのため、あとはCircleCIのときから特に手を加えずに fastlane コマンドでそのままビルドが可能です。
私の場合はReact Nativeプロジェクトであったため、 package.json に scripts で定義された fastlane コマンドを、
環境種別(BUILD_ENV)ごとに実行するようにしています。
(fastlaneの具体的な設定については本筋とは少し話がずれてしまうため省略します)
jobs:
build:
runs-on: macos-10.15
steps:
# ... 省略 ...
# React Nativeプロジェクトなので yarn で依存関係をインストール
# また、fastlaneを使うため bundle install で Gemfile に定義された依存関係もインストールする
- name: "Install dependencies"
run: |
yarn
cd ios && bundle install
- name: "Build and deploy"
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
run: |
yarn dist:${BUILD_ENV}:ios
9. Slackへの成功・失敗の通知方法
最後に、GitHub Actionsが成功・失敗したかをSlackチャンネルへ通知するための方法を紹介します。
これも公式には機能提供されていませんが、Marketplaceにいくつかアクションが公開されています。
その中でも、ホストランナーのOSを問わず使えて、かつ詳細情報を通知可能な 8398a7/action-slack のアクションがおすすめです。
以下のように記述します。
jobs:
build:
runs-on: macos-10.15
steps:
# ... 省略 ...
- name: Slack Notification
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: all
username: ${{ secrets.SLACK_USERNAME }}
icon_emoji: ${{ secrets.SLACK_ICON_EMOJI }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
MATRIX_CONTEXT: ${{ toJson(matrix) }}
if: always()
withで設定可能なパラメーターはこちらに紹介されています。
必須パラメーターとなるのは SLACK_WEBHOOK_URL
と、fieldsによっては MATRIC_CONTEXT
が必要になります。
とりあえずどちらも指定しておくのが良いかと思います。
設定ファイルまとめ
以上の設定をすべてまとめると以下のようになります。
name: kkoudev/react-native:ios
on:
push:
branches:
- develop
# 本番用のタグPushではアプリケーションのバージョン(x.x.x)と、ビルド番号をドットで合わせたもににしています
tags:
- "[0-9]+.[0-9]+.[0-9]+.[0-9]+"
# workflowの手動実行許可
workflow_dispatch:
jobs:
build:
runs-on: macos-10.15
steps:
- name: Clone repository
uses: actions/checkout@v2
- name: Clone submodule
uses: actions/checkout@v2
with:
repository: kkoudev/protobuf
path: proto
ssh-key: ${{ secrets.PROTOBUF_DEPLOY_KEY }}
persist-credentials: true
- name: Checkout submodule
run: |
git submodule update -i
- name: Create environment variables
run: |
.github/workflows/workflow_env.sh
- name: Prepare fastlane
uses: kielabokkie/ssh-key-and-known-hosts-action@v1
with:
ssh-private-key: ${{ secrets.FASTLANE_DEPLOY_KEY }}
ssh-host: github.com
- name: Setup Xcode version
run: |
sudo xcode-select -s '/Applications/Xcode_12.2.app/Contents/Developer'
- name: Setup Ruby version
uses: actions/setup-ruby@v1
with:
ruby-version: 2.6.6
- name: Install bundler
run: |
gem i bundler
- name: Create swift version
run: |
swift --version > swift-version
- name: Cache gem
uses: actions/cache@v2
id: cache_gem
with:
path: |
ios/vendor
key: ${{ runner.os }}-gem-${{ hashFiles('ios/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
continue-on-error: true
- name: Cache CocoaPods
uses: actions/cache@v2
id: cache_pods
with:
path: |
ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('swift-version') }}-${{ hashFiles('ios/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-${{ hashFiles('swift-version') }}-
continue-on-error: true
- name: Cache Carthage
uses: actions/cache@v2
id: cache_carthage
with:
path: |
ios/Carthage
key: ${{ runner.os }}-carthage-${{ hashFiles('swift-version') }}-${{ hashFiles('ios/Cartfile.resolved') }}
restore-keys: |
${{ runner.os }}-carthage-${{ hashFiles('swift-version') }}-
continue-on-error: true
- name: Cache yarn
uses: actions/cache@v2
id: cache_yarn
with:
path: |
node_modules
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
continue-on-error: true
- name: "Install dependencies"
run: |
yarn
cd ios && bundle install
- name: "Build and deploy"
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
run: |
yarn dist:${BUILD_ENV}:ios
- name: Slack Notification
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: all
username: ${{ secrets.SLACK_USERNAME }}
icon_emoji: ${{ secrets.SLACK_ICON_EMOJI }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
MATRIX_CONTEXT: ${{ toJson(matrix) }}
if: always()
実効速度について
速度については通常プランであればCircleCIと同等かそれ以上に速いため、全然問題ない感じです。
今回はmacOSの例で紹介しましたが、Linuxプロジェクトの場合も本当に速いので
macOSに限らず全てのプロジェクトをGitHub Actionsへ移行し、CI環境を統一することで更に使い勝手が良くなるかと思います。
まとめ
今回はCircleCIからGitHub Actionsへの移行方法を紹介させていただきました。
CircleCIから移行を検討している方や、また新規に導入しようとしている方の参考になれば幸いです。