リリースブランチのライフサイクル管理を 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 で通知してくれます。

画像1

実際にはこんなかんじ。

リリースするバージョンを入力して

画像2

実行すると Zapier が Slack の発言を拾ってくれます。
release branch や 実行中の CircleCI Pipeline の URL も教えてくれます。

画像3

$ 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 を実行します。

画像4

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 を 💪

読んでいただきありがとうございますー よろしければシェアもおねがいします🙏