App Store Connectの審査情報をキャッチして通知する仕組み

こんにちは。[CYBIRD Advent Calendar 2022](https://qiita.com/advent-calendar/2022/cybird)、20日目担当の@yuki_utsumiです。
普段は品質管理部エンジニアとして業務の自動化などをしています。
アドベントカレンダー19日目の記事は@namikoroさんの[「現場エンジニアが担当するカジュアル面談のいろは」](https://qiita.com/namikoro/items/786bf0f65cf8494c81aa)でした。私もカジュアル面談の際はかな〜り参考にさせてもらいました。ぜひ読んでみてください。


今回は、私が社内で運用しているApp Store Connectの情報を取得してくる仕組みについて紹介します。

# どんな仕組み？
現在、社内ではAppleアプリの審査内容に変更があると、その内容がslackに通知されるbotを運用しています。
![スクリーンショット 2022-12-12 2.34.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2318588/abb4a90b-6995-d0ec-d1a0-6ad36e55fe1d.png)

審査状況を人が監視する必要がなく、何か異常があった場合にはすぐに対応できるので負担削減に一役買っています。
ここでは[App Store Connect API](https://developer.apple.com/jp/app-store-connect/api/)を利用して審査状況を自動で取得するようにしています。

APIを利用する方法として、外部ツール（例えば[fastlane](https://github.com/fastlane/fastlane)）を利用する手もありますが、今回はAPIを直接叩いています。（以前は利用していましたが、APIの仕様変更で一時使えなくなり、そのまま直接利用するようになりました）

以下で、プログラムのロジックを実行順に説明していきます。

# 大まかな流れ
1. 公開しているアプリを全て取得する
1. アプリごとに以下を繰り返す
    1. アプリのステータスを取得
    1. 記録しておいた前回のステータスと比較する
    1. 比較に変更があったらslackに通知を行う
    1. アプリのステータスを記録する
1. 終了


# 準備
- App Store Connect APIを利用できるようにする
詳しい説明は[ドキュメント](https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api)に譲りますが、APIを使えるようにする準備が必要です。
情報を取得したいアプリにアクセスできるアカウントを作成して、管理者アカウントでキーを発行します。
今回は情報を取得するのみに使いますが、かなり大きな権限を持つキーを使うため、管理には十分な注意が必要です。

- rubyが実行できるようにする
以下ではrubyでプログラムを実行しています。


# APIトークンの取得
まずはAPIを利用するためのトークンを取得します。
以下は関数などの一部抜粋を見やすくしたもので、実際に利用しているものとは違うため、参考程度にどうぞ。（今後出てくるものは全て同様）

```ruby
#App store connect APIのトークンを取得する
def get_api_token(issue_id, key_id, private_key_path)
  private_key = OpenSSL::PKey.read(File.read(private_key_path))

  token = JWT.encode(
     {
      iss: issue_id,
      exp: Time.now.to_i + 20 * 60,  # トークンの有効期限は20分以内とAppleが規定している
      aud: "appstoreconnect-v1"      # audは appstoreconnect-v1 固定
     },
     private_key,
     "ES256",                        # 署名方式は ES256 固定
     header_fields={
       kid: key_id
     }
  )
  return token
end
```
この部分のコードは[こちら](https://qiita.com/kurikazu/items/135a791ba09121d7a534)を参考にさせていただきました。
引数の値は全て前項の「App Store Connect APIを利用できるようになっている」の際に表示され取得しているはずです。

# アプリ一覧の取得
閲覧できる全てのアプリを取得しています。
tokenは前項で取得したAPIトークンです。

```ruby:
def get_app_list(token)
  uri = URI.parse("https://api.appstoreconnect.apple.com/v1/apps")
  request = Net::HTTP::Get.new(uri)
  request["Authorization"] = "Bearer #{token}"
  req_options = {
    use_ssl: uri.scheme == "https",
  }

  response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
    http.request(request)
  end

  return response.body
end
```

これで結果が返ってくるので
```ruby
# アプリ一覧を取得
app_list = JSON.parse(get_app_list(token))["data"]
```
といった要領でリストを取得します。
JSONの中身の構造については適宜出力して中身を確認してみると、他用途にも活用できるかと思います。


# アプリのバージョン情報の取得
前項で取得したリストに対してfor文を回すことでアプリごとの処理ができます。

```ruby
    for app in app_list do
```
「アプリ一覧の取得」と同じ要領でAPIを叩いてバージョン（1.1とか1.2とかの更新のこと）の一覧を取得します。

```ruby:
def get_version_list(token, id)
  uri = URI.parse("https://api.appstoreconnect.apple.com/v1/apps/#{id}/appStoreVersions")
  request = Net::HTTP::Get.new(uri)
  request["Authorization"] = "Bearer #{token}"
  req_options = {
    use_ssl: uri.scheme == "https",
  }

  response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
    http.request(request)
  end

  return response.body
end
```
違いはuri部分のみですね。


```ruby
versions = JSON.parse(appstoreconnect_curl($API_token, "versions", id))
```
同様にJSONにして取得します。



# アプリのステータスの取得

ここまででバージョン一覧が取得したので、その中からIOS向けにリリースされた最新のものを抜き出し、そこから「appStoreState」≒AppStoreにおける審査状況を取得してきます。

```ruby
def get_latest_version(versions)
  #アプリのバージョン情報はプラットフォームごとに新しい順で並んでいる前提
  for version in versions["data"] do
    if version["attributes"]["platform"] == "IOS" then
        latest_state = version["attributes"]["appStoreState"]
        return latest_state
    end
  end
end
```
アプリの名前（id）とそのステータスのペアはファイルで保存しておき、保存しておいた前回のステータスと取得した今回のステータスを比較します。比較し終わったら上書きし、次回の比較に備えます。
ステータスに変更がなかった場合は動かず、変更があった場合はその内容によって動作を変えます。


### アプリのステータスについて
アプリがとりうるステータスのうち、受け取った場合特別に動作させるものを紹介します。APIからは"IN_REVIEW"といった形で英語で返ってきます。

| 英語 | 日本語 | 補足 |
|:-----------|------------|:------------|
| "DEVELOPER_REJECTED"      | デベロッパにより却下済み    | 正常：こちらで申請を取り下げた      |
| "IN_REVIEW"   |審査中      | 正常：審査中      |
| "INVALID_BINARY"      | バイナリが無効      | 異常：提出データに異常        |
| "METADATA_REJECTED"        | メタデータ却下済み          | 異常：審査に落ちた         |
| "PENDING_DEVELOPER_RELEASE"       | デベロッパによるリリース待ち       | 正常：審査を通過した       |
| "PREORDER_READY_FOR_SALE"| 予約注文の配信準備完了| 正常：リリース| 
| "READY_FOR_REVIEW"| 提出準備中| 正常：審査待ち| 
| "READY_FOR_SALE"| 配信準備完了| 正常：公開している状態| 
| "REJECTED"| 却下済み| 異常：審査に落ちた| 
|"WAITING_FOR_REVIEW" | 審査待ち|正常：審査待ち | 



返ってくる値の完全なリストは以下から確認してください。主だったものに変更は入りませんが、新たなステータスが追加されていることはままあります。（「輸出コンプライアンス待ち」とか前見た時はなかった）
参考：[App Store Connect ヘルプ](https://help.apple.com/app-store-connect/#/dev18557d60e)　と[英語版の方のヘルプ](https://help.apple.com/app-store-connect/en.lproj/static.html#dev4953b6f61)の「App and submission statuses」の項

# 必要な場合はslackへ通知
本筋からずれるため、rubyでslackに通知する詳しい説明は省略します。
ここまでで取得した情報を利用して、たとえばこんな感じで文面を作成します。

```ruby
appname = app["attributes"]["name"]
url = "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/ng/app/#{app["id"]}"

msg = "<!here> \n:tada:`アプリ情報変更　Pending Developer Release`:tada:\n「#{appname}」がデベロッパによるリリース待ちになりました。\n\n以下で確認ができます。\n#{url}"
```
Slackのメッセージで確認を促します。

```ruby
def send_slack(sl_msg, sl_token, sl_roomname)

  uri = URI.parse("https://slack.com/api/chat.postMessage")
  request = Net::HTTP::Post.new(uri)
  request.set_form_data(
    "channel" => "#{sl_roomname}",
    "text" => "#{sl_msg}",
    "token" => "#{sl_token}",
  )

  req_options = {
    use_ssl: uri.scheme == "https",
  }

  response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
    http.request(request)
  end
end
```

通知が終わったら終了。これまでの流れを全てのアプリに対して繰り返します。


# 定期実行
以上のプログラムを定期実行するようにします。私はAmazon EC2のサーバー上でJenkinsを動かし、5分ごとに実行するようにしています。

# 終わり
という感じで以上となります。参考になれば幸いです。
CYBIRD Advent Calendar 2022 の21日目は、@kappysanさんの「AR空間にプレゼントを贈ってみた」です。お楽しみに！