リリースブランチのライフサイクル管理を Zapier で自動化する
これは Zapier Advent Calendar 2019 13日目の記事です。
現在 note の iOS アプリ(以下 note-ios)をひとりで開発しています。
ひとりですが未来の同僚が歴史を追跡しやすいようにリリース作業時は専用 branch と Release Tag を作成して作業しています。
なかでもリリース前に温かみのある手作業で毎度繰り返しているのが以下の作業。
QA 前
- リリース時はリリースバージョン用の branch を作成(release/x.y.z)
- x.y.z にバージョンアップして commit & push(手動実行)
- QA 用 adhoc app を配布(CircleCI 経由で自動配布)
- Magic Pod にテスト用 app ファイルをアップロードしてバッチ実行
リリース後
- App Store 公開後 branch から release tag 作成
- release branch から main branch(master)に向けて PR を作成して merge
今回はこの一連の流れを Zapier で自動化してみました。
QA 前
今回リリースするバージョンを Slack Workflow 経由で入力すると release branch 作成から一通りの作業を実行して Slack で通知してくれます。
実際にはこんなかんじ。
リリースするバージョンを入力して
実行すると Zapier が Slack の発言を拾ってくれます。
release branch や 実行中の CircleCI Pipeline の URL も教えてくれます。
$ release branch の作成
input data の定義はこんな感じで
github_token="your_github_token_here"
branch={branch}
api_root="https://api.github.com/repos/{repoName}"
あとは GitHub API で branch 作成。
import base64
import json
headers = {
"Content-type": "application/json",
"Authorization": f"token {input_data['github_token']}"
}
master_ref_response = requests.get(
f"{input_data['api_root']}/git/ref/heads/master",
headers=headers)
master_ref_response.raise_for_status()
master_ref_json = master_ref_response.json()
master_sha = master_ref_json['object']['sha']
release_branch_response = requests.post(
f"{input_data['api_root']}/git/refs",
data=json.dumps({
"ref": f"refs/heads/{input_data['branch']}",
"sha": master_sha }),
headers=headers)
release_branch_response.raise_for_status()
output = release_branch_response.json()
$ CircleCI Pipeline の実行
input data を定義して
circleci_token="your_circleci_token_here"
branch={branch}
api_root="https://circleci.com/api/v2"
CircleCI API v2 で Pipeline を実行。
import json
headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
auth = (input_data['circleci_token'], '')
# release/x.y.z 想定
version = input_data['branch'].replace('release/', '')
response = requests.post(
f"{input_data['api_root']}/project/gh/{repoName}/pipeline",
data=json.dumps({
"branch": input_data['branch'],
"parameters": {
"run_auto_update_commit": True,
"next_version": version,
"run_integration_tests": True }}),
headers=headers,
auth=auth)
response.raise_for_status()
# Pipeline number を Slack 投稿時に利用
output = response.json()
ちなみに POST parameter に
"parameters": {
"run_auto_update_commit": True,
"next_version": version,
"run_integration_tests": True }}),
とありますが、これは API 経由でしか実行されない Workflow を指定するために config.yml に設定している変数です。
parameters:
run_auto_update_commit:
type: boolean
default: false
next_version:
type: string
default: ""
# false のままにして E2E Test を skip させることも可能
run_integration_tests:
type: boolean
default: false
workflows:
version: 2.1
auto_update_commit:
when: << pipeline.parameters.run_auto_update_commit >>
jobs:
- "Commit and Push version update":
filters:
branches:
only: /^release\/.+$/
$ Magic Pod API で E2E Test
バージョンアップ commit は先の GitHub API、UnitTest 実行や adhoc app 配信は fastlane を実行してるだけなので割愛。
note のモバイルアプリ開発では Magic Pod を E2E Testing Framework として採用しています。
大きな理由として XCUITest / InstrumentationRegistry (Android) に精通していないひとでも、アプリ操作のシナリオをシミュレーター上の UI 要素をマウス操作である程度簡単に組み立てられる点にあります。
(導入までのサービス調査で Katalon も検証したのですがとにかく Desktop App の使い方がわかりにくい & CircleCI 上で実行するための公式解が Linux コンテナ案しかなかったので断念しました。)
導入当時は Desktop App しかなかったので CI コンテナ上に Desktop App をインストールしてローカルビルドの .app を対象にテストするしかありませんでした。
MAGIC_POD_VERSION="version here"
if [ ! -e MagicPodDesktop${MAGIC_POD_VERSION}.zip ]; then
curl -L -O https://github.com/Magic-Pod/japanese-issue-and-doc/releases/download/${MAGIC_POD_VERSION}/MagicPodDesktop${MAGIC_POD_VERSION}.zip
unzip -q MagicPodDesktop${MAGIC_POD_VERSION}.zip
mv "Magic Pod Desktop.app" /Applications
fi
echo -n ${magic_pod_token} > ~/.magic_pod_token
"/Applications/Magic Pod Desktop.app/Contents/MacOS/Magic Pod Desktop" run --magic_pod_config=$(pwd)/.magicpod/magic_pod_config.json
しかも Cloud Test 用に .app ファイルをアップロードする方法は手動しかなく流石に API でなんとかできないかなーと中の人に相談したところ CLI ツールが爆誕してました ✨
現在は Cloud Test 用のアップロードと Batch Test 実行に CLI ツールを使ってます。
RELEASE_VERSION="version here"
curl -L -O https://github.com/Magic-Pod/magic-pod-api-client/releases/download/${RELEASE_VERSION}/mac64_magic-pod-api-client.zip
unzip -q mac64_magic-pod-api-client.zip
# upload myApp-{version}.app
FILE_NO=$(xcrun agvtool what-marketing-version -terse1 | \
xargs -I{} sh -c 'cp -R path/to/myApp.app myApp-{}.app; \
./magic-pod-api-client upload-app -a myApp-{}.app')
# run test async
# https://magic-pod.com/api/v1.0/doc/
./magic-pod-api-client batch-run -s "simulator setting json here"
リリース後
Ready for Sale のメールをトリガーにして GitHub API で Release Tag & PR 作成と PR merge を実行します。
merge 実行結果を Pull Request のリンクとともに Slack で教えてくれます。
$ Release Tag 作成
実は QA 前のバージョンアップ commit 時に github-changelog-generator を使って前回の Release Tag から HEAD までの diff を ReleaseNote.md として Markdown ファイルにエクスポートしています。
この ReleaseNote.md の内容を Release Tag の説明文に使ってます。
#input data
release_note_file="ReleaseNote.md"
github_token="your_github_token_here"
version={version}
api_root="https://api.github.com/repos/{repoName}"
# release note 本文
release_note_url = f"{input_data['api_root']}/contents/{input_data['release_note_file']}?ref={branch}"
body_response = requests.get(release_note_url, headers=headers)
body_response.raise_for_status()
body_json = body_response.json()
decoded_body = base64.b64decode(body_json['content']).decode('utf-8')
released_response = requests.post(
f"{input_data['api_root']}/releases",
data=json.dumps({
'tag_name': input_data['version'],
'target_commitish': branch,
'name': input_data['version'],
'body': decoded_body,
'draft': False,
'prereleases': False }),
headers=headers)
released_response.raise_for_status()
released_json = released_response.json()
$ PR 作成 & merge
自動生成 & merge された PR を後で探すために Label をつけておくとよいかなーと思い付与してます。
# PR 作成
pr_response = requests.post(
f"{input_data['api_root']}/pulls",
data=json.dumps({
'title': f"[Auto Create & Merge] {input_data['version']}",
'head': branch,
'base': 'master',
'body': 'description here' }),
headers=headers)
pr_response.raise_for_status()
pr_json = pr_response.json()
pr_number = str(pr_json['number'])
pr_link = f"https://github.com/{repoName}/pull/{pr_number}"
# 専用 Label を付与 (お好みで)
issue_response = requests.post(
f"{input_data['api_root']}/issues/{pr_number}/labels",
data=json.dumps({ 'labels': ['auto merge', 'zapier', ...] }),
headers=headers)
issue_response.raise_for_status()
# merge
merge_response = requests.put(
f"{input_data['api_root']}/pulls/{pr_number}/merge",
headers=headers)
result = merge_response.json()
# Slack 投稿用 Step に渡すレスポンスを作成
if 'merged' in result and result['merged'] == True:
output = {
'merged': True,
'message': f"{input_data['version']} リリースされたので master merge したよ {pr_link}"
}
else:
output = {
'merged': False,
'message': f"{input_data['version']} リリースされたけど auto master merge できなかったよ {pr_link}"
}
ハマリポイント
Zapier 連携 App がなくても API 実行でどうにかなる便利な Code Step ですが制限を理解しておかないとハマることがあります。
こちらのリンクを見ると Code Step は AWS Lambda で動いているようです。
The environment in which your Code steps run (AWS Lambda) has an I/O limit of 6MB.
またこちらのヘルプを見ると Code Step の実行環境について言及されています。
1 step につき無料プランユーザーは RAM 128mb/最大実行時間1秒
有料プランユーザーは RAM 256mb/最大実行時間10秒
有料プランユーザーとはいえ10秒を超える処理はエラー扱いなので適宜 Step 分割しておくと良いかもしれません。
ではではよい Zapier Life を 💪
読んでいただきありがとうございますー よろしければシェアもおねがいします🙏