iOSチームの荒巻です。SmartNewsアプリは日米でユーザーが急成長し、チームも急激に拡大しています。そのような状況の中で昨年、SmartNewsアプリのWeeklyリリースを支える技術というイベントを開催しました。 イベントでは主に組織体制やコミュニケーションについて説明しましたが、ここでは技術的な側面を紹介したいと思います。 主にiOSチームの取り組みを紹介しますが、Androidチームもおおむね同じような仕組みで回っています。

背景と全体のプロセスの理解のため、まず開発サイクルとフェーズについて、次に各フェーズで起点となるタイミングについて説明します。 そのあと毎週のリリースを実現するための開発フローやGitHubワークフローなどについて紹介します。

リリースサイクルの変遷

開発サイクルの過去と現状について説明します。

リリースサイクル:2週間

2017年時点では、毎週iOS版とAndroid版を交互にリリースしていました。つまりiOS版だけを見ると二週間に一度リリースする状況でした。

バージョン 227-35 36-312 313-319 320-326
iOS 4.1.5 QA リリース
Android 4.1.5 開発 QA リリース
iOS 4.1.6 開発 開発 QA リリース
Android 4.1.6 開発 開発 QA

この体制はおおむねうまく稼働していましたが、リリースのためには毎回様々な手作業や確認を行う必要があり、それなりに時間を取られていました。 一方PdM(Product Manager)の視点では、

  • 同じ機能を両方のプラットフォームにリリースするのに余分に一週間かかる
  • 機能をリリースするタイミングを逃すと追加で二週間待つ必要がある

という課題がありました。二週間待ちたくないため、スケジュールを数日延長することもしばしばありました。

リリースサイクル短縮プロジェクト

これらを解決するため、リリースサイクルの短縮をプロジェクト化し、以下のような方針で少しずつ改善していきました。

  • 全ての手順をドキュメント化・定型化・チェックリスト化
  • 自動化できる単純作業はできるだけ自動化し、結果をSlackに通知
  • 全体のプロセスを開発者だけでなくPdMやQAチームへも周知

その結果、以下のような工夫を取り入れることになりました。

  • Google Sheetsをスケジュールのマスターとし、Google CalendarとNotionに連携
  • 機能追加するときはNotionの表に機能を追加し、対象バージョンを指定する
  • ブランチ戦略の変更、自動ブランチ生成、自動マージ支援
  • Xcode Serverでビルド、テスト、TestFlightおよびFirebaseへアップロード
  • GitHub Actionsでリリース
  • それぞれのチェックポイントでSlackに通知

それぞれについて、後半で説明していきます。

リリースサイクル:1週間

2021年5月現在、毎週リリースを行なっています。 継続的にリリースサイクルを短縮する取り組みを続け、2020年にこの体制に到達しました。

大まかなフェーズは プランニング -> 開発 -> QA -> リリース の4つで、毎週それぞれのフェーズがパイプライン状に処理されていきます。例えば2/26の週にはバージョン8.30.0をリリースしますが、同じ週にQAチームはiOS版とAndroid版の8.31.0に対してQAを行い、開発チームは8.32.0の開発に取り組みます。8.33.0のプランニング確認も行います。

バージョン 226-34 35-311 312-318 319-325
8.30.0 リリース
8.31.0 QA リリース
8.32.0 開発 QA リリース
8.33.0 プランニング 開発 QA リリース
8.34.0 プランニング 開発 QA

タイミング

各フェーズで起点となるタイミングについて説明します。

プランニング確認

上の表の「プランニング」の週に行います。 リリースの3週間前にプランニング確認のミーティングを行います。開発の進捗や、一週間でQAが行える分量なのかなどを確認して、そのバージョンに含める機能を最終調整して確定します。そのタイミングまでは、PdMやエンジニアがNotionに機能を追記していきます。

QA準備

上の表の「開発」の週に行います。 リリースの2週間前にQA準備のミーティングを行い、そのバージョンのテストシナリオやQAリソースが足りているかどうかを確認します。休日などの都合に応じて工程調整を行ったりもします。

QA開始

上の表の「QA」の週です。 QA開始前までに全てのpull requestをマージしておく必要があります。このタイミングに間に合わなかった場合は次のバージョンに延期されます。 QA開始の前日の夜にtrunkからリリースブランチを生成します。現在のQA開始は金曜日であり、木曜日の夜にリリースブランチを生成しています。

ちなみに以前は月曜日から金曜日までQAを行なっていましたが、最終確認が延びると週をまたいでしまうので、木曜日に変更しました。(実は上の表も一週間が金曜日からはじまっています)

QA完了

通常、上の表の「QA」の週の最終営業日です。 QAが完了すると、QAチームのマネージャーがSlackに完了報告します。それをもってリリース担当者がリリース版の準備に入ります。

自動化

ここからは開発支援のスクリプトや自動化について紹介します。

チェックリスト

リリース担当者を持ち回りで決め、チェックリストをNotionのテンプレートにより作成します。

Notionのテンプレート

リリースのためのチェックリストは以下のような内容で20項目ほどあります。リリースしたあと、次の担当者に引き継ぐところまでが一連の手順となります。(※ なお実際に運用しているものは英語です)

リリースチェックリスト

マスターデータ

リリースの日付に関するマスターデータはGoogle Sheetsで管理するようにしました。QA開始やリリースの日付を入れておき、GitHub Actionsワークフローのcronタスクにより、Notionなどに同期します。(例えばNotionにios 8.30.0 (2021-03-01 リリース予定)のような値が追加されます)

バージョンに対応するmilestoneの生成

GitHub Actionsワークフローのcronタスクです。 リリースのマスターデータを拾ってきて、各バージョンに対応するmilestone(たとえば1.0.0)を作成します。 pull requestにはこのmilestoneを設定するので、ある程度先のバージョンまで作成します。

日付をスクリプトで前処理し、作成する必要があれば、GitHub APIでmilestoneを登録・更新します。(以下は新規登録の一部)

- name: create milestones by calling github API
      env:
        CREATES: ${{ steps.filter.outputs.creates }}
      run: |
        echo $CREATES | jq -c '.[]' | while read release; do
            echo $release
            curl --request POST \
            --url https://api.github.com/repos/${{ github.repository }}/milestones \
            --header 'Authorization: Bearer ${{ secrets.GITHUB_ACCESS_TOKEN }}' \
            --header 'Content-Type: application/json' \
            --data "$release"
        done

リリースブランチの生成

GitHub Actionsワークフローのcronタスクです。 QA開始の前日の夜にtrunkからリリースブランチを生成してpushします。また、GitHub上に、そのバージョンに対応したリリース用のissue(たとえばRelease 1.0.0)を作ります。QAが完了したら、リリース担当者がそのissueを閉じることで、リリース用のワークフローが発動します。

また、.xcconfigにバージョン番号を定義してあり、バージョン番号を進めるpull requestを生成します。(以下に一部抜粋)

- name: bump version
      env:
        GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_ACCESS_TOKEN }}
        VERSION: ${{ steps.next_version.outputs.version }}
      run: |
        file_name=App/SmartNewsApp.xcconfig
        git checkout develop
        sed -i -e "s/APP_VERSION=.*/APP_VERSION=$VERSION/g" $file_name
    - name: create pull request
      uses: peter-evans/create-pull-request@v3
      with:
        token: ${{ secrets.GITHUB_ACCESS_TOKEN }}
        title: Bump version to ${{ steps.next_version.outputs.version }}
        commit-message: Bump version to ${{ steps.next_version.outputs.version }}
        branch: feature/bump-version-to-${{ steps.next_version.outputs.version }}
        base: develop

マージもれの警告

GitHub Actionsワークフローのcronタスクです。 リリースする機能は全てQA開始の前日までにマージする必要があるため、その前日(つまりQA開始の前々日)に、該当するバージョンのマージされていないpull requestをSlackに通知するようにしました。(以下に一部抜粋)

message = """
<!subteam^S**********>
Hi, following filter shows the tickets of which fix versions are set to current version and are not closed yet.
Could you check the tickets assigned to you and let us know if it is OK to move to next version? Thanks.
https://jira/issues/?jql=%20status%20NOT%20IN%20(Closed%2C%20Resolved)%20and%20fixVersion%20%3D%20{version}
""".format(version=os.getenv("NEXT_RELEASE_VERSION"))
slack_client.chat_postMessage(channel=os.environ['SLACK_CHANNEL'], thread_ts=thread['ts'], text=message)

リリース

QAが完了したら、リリース担当者がリリース用のissueを閉じます。それによりリリース用のGitHub Actionsワークフローが発動し、masterブランチが更新されます。CIサーバ(Xcode Server)がそれを検出して、以下のような一連のバッチプロセスを実行します。

  • App Store用のバイナリをビルドする
  • GitHubにバージョンのタグを打つ
  • TestFlightにipaをアップロードする
  • TestFlightの処理が完了するまで待つ (ポーリング)
  • dSYMをダウンロードして、Firebase Crashlyticsにアップロードする
  • 各言語のWhat’s Newセクションの内容をApp Storeに転記する
  • App Store Connect上でSubmit
  • Slackに結果を報告する

以前はmasterブランチにマージを行うことでリリースを行っていましたが、現在は最終成果物をバックマージする必要がなくなったため、リリースブランチをmasterブランチとしてforce pushしています。ブランチポリシーは変わりましたが、masterブランチからストア版をビルドする仕組みは流用しています。

GitHub上のリリースですが、バージョンのタグを打ったとき、GitHub Actionsワークフローにより作成しています。CocoaPodsを導入しており、Pods/ディレクトリはリポジトリに含めていないのですが、リリースにはPods/以下のソースコードも含めるようにしました。(こちらについてはQiitaにて GitHub Actionsでリリースする という記事を書きました。)

また、App Store Connectの申請状況を監視しており、審査状態が更新されるとSlackに通知されます。

リリースブランチへの自動cherry-pick

以前はgit-flow風の開発フローを取り入れており、リリースブランチをmasterブランチにマージして最終成果物を生成していました。そしてリリースブランチをtrunk(developブランチ)にバックマージしていましたが、その際にコンフリクトが発生したり、trunkへのマージを少し待つなどの対応をしていました。

以前の開発フロー

毎週リリースに適応するため、TBD (Trunk Based Development) に乗り換えることにしました。TBDでは基本的にtrunkに対して開発を行い、不具合修正もtrunkに対して行います。リリースブランチへの修正が必要なときはリリースブランチに対してcherry-pickします。これにより最終成果物をtrunkへバックマージする必要がなくなりました。

現在の開発フロー

そして多くの場合、このcherry-pickは単純にリリースブランチにそのまま取り込むだけなので、GitHub Actionsワークフローで自動化することにしました。 pull requestに適切なマイルストーンを設定し、need cherry-pickのラベルを設定しておきます。このpull requestをマージすると、GitHub Actionsワークフローが発動し、リリースブランチにcherry-pickするpull requestを作成してマージします。 ただコンフリクトすることもあり、その場合はSlackに通知されるので、開発者が手動でコンフリクトを解決してpull requestを作成します。

auto cherry-pick

cherry-pickの処理はPythonで書かれています。(以下に一部抜粋)

for commit in pr.get_commits():
  print(f"Cherry-picking: {commit.sha}")
  try:
    print(gitrepo.git.cherry_pick("-x", commit.sha))
  except Exception as e:
    print(e)
    print(gitrepo.git.diff())
    slacksender.send(slack_webhook, repo_name, pr.user.login, pr_number, version, commit.sha)
    exit(0)
print("Pushing branch:", cherrypick_branch)
print(gitrepo.git.push("origin", cherrypick_branch))

レビュアーの自動割り当て

pull requestのレビューには以下の二つの課題がありました。

  • 総数が多く、すぐにレビューされないことがある
    • 情報共有やワークロードの観点で全員にレビューの機会を持ってほしいが、レビュアーが偏ったり、誰もレビューしないまま数日が過ぎてしまうことがあった。
  • 特定のコンポーネントはドメイン知識が必要
    • 現在、開発部門はマトリックス組織となっており、複数のグループにわかれて開発を行っている。リポジトリは一つで、共通部分はありつつも、特定のコンポーネントはドメイン知識が必要な状態となっている。

これを解決するため、Auto Assign Actionを導入し、優先的にレビューする人をランダムに二人割り当てるようにしました。(もちろん割り当てられなかった人もレビューしてよい) また、コンポーネントをラベルとして用意しておき、コンポーネントのラベルを指定したときは、一人はコンポーネントのオーナーのチームから選ぶように拡張しました。例えばAdsというラベルを指定すると、一人は広告チームから選ばれます。

プロジェクトファイルのコンフリクト

Xcode環境ではプロジェクトファイル(.pbxproj)がしばしばコンフリクトするので、mergepbx(objectVersion-47ブランチ)を導入しました。ただmergepbxでマージすると、ファイルの順番が変わってしまうことがあります。 これはマージしたあとにXcodeで開いて閉じると修正でき、Rubyのgemのxcodeprojでも同じことができます。

#!/usr/bin/env ruby
require 'xcodeproj'
Xcodeproj::Project.open('MyProject.xcodeproj').save

このスクリプトをGitHub Actionsワークフローやgitのpost-mergeフックで実行し、差分があるときは警告するようにしました。

#!/bin/sh
PROJECT_FILE=MyProject.xcodeproj
bundle exec ./scripts/clean_pbxproj.rb
DIFF=$(git diff $PROJECT_FILE)
if [ ! "$DIFF" = '' ]; then
  echo "Project file is not clean; Please check-in $PROJECT_FILE"
  echo "$ git add $PROJECT_FILE; git commit -m \"chore: Merge\""
fi

lint

ビルド時にSwiftFormatおよびSwiftLintをかけるようにしました。また、差分に対してだけlintをかけるGitHub Actionsワークフローを導入しました。

マーケティングチームとの連携

What’s Newセクションのテキストは、日米のマーケティングチームに毎回準備してもらう必要があります。こらちはGitHubに専用のリポジトリを用意して、次のバージョンに対応するファイルを自動生成するようにしました。 リリース日の数日前のタイミングで、そのリンクをマーケティングチームに対してSlackで通知するようになっています。できあがったテキストはFastlane経由で登録します。

以下のような形式で、GitHub上で編集してもらいます。

What's new

まとめ

これらの取り組みにより、チームが拡大しつつも開発の勢いを落とすことなく数多くの機能をリリースすることができました。

Team activity

よかったこと

改めて、取り組んでみてよかったことを挙げてみます。

  • リリースサイクル
    • 機能をリリースして検証するまでの期間が短縮された
    • 同じ機能がiOSとAndroidで同時にリリースできるようになった (片方を待たなくてよくなった)
  • リリース作業
    • 誰でもリリース作業ができるようになった
    • オペレーションミスが減り、また、ミスがあっても気づきやすくなった
  • 開発フロー
    • 手作業のマージが減った
    • pull requestがスムーズにレビューおよびマージされるようになった

なおリリースサイクル短縮の取り組みはトップダウンでしたが、自動化スクリプトなどボトムアップの提案がいくつもあり、チームワークという点でも良かったと思います。

将来に向けて

現在、開発やリリースのオペレーションは円滑に回っている状況ですが、コードベースの成長に伴い、新しい課題も増えつつあります。SmartNewsアプリはリリースしてからすでに8年が経過し、レガシーコードもそれなりに蓄積されてきました。目の前の開発だけでなく、長期的な課題にも対処していく必要に迫られています。その一環として例えばシナリオベースの自動テストの導入なども行なっており、そのような取り組みに対しても紹介できればと思っています。

PR

スマートニュースでは「世界中の良質な情報を必要な人に送り届ける」というミッションのもと、よい体験をユーザーに届けるための開発を行なっています。iOSだけでなく幅広い職種の開発者を募集しております。ぜひ SmartNews Careers をご覧ください。