AppStoreConnectAPIを使ってProvisioningProfileのダウンロードを自動化する

# はじめに
開発者体験:DXをめちゃくちゃ改善した話 Advent Calendar 2021の22日目です！
弊社で作ってめっちゃ効率上がったツールの話です


# 経緯
### ビルドについて
会社でUnity製モバイルアプリを開発しているのですが、ビルドを全てJenkinsマシンで自動化していました。
最近チーム規模の拡大等の理由により、ビルドマシン１台では待ちが発生してきたためビルドマシンを２台増やし、MacMini3台構成にしました。

### マシンが増えたことで起きてしまう問題

マシンが増えて高速でビルドができるようになり解決！と思っていたのですが、思っていなかったところでめんどくさい作業が発生してしまいました。
**ProvisioningProfileの更新**です。

弊社ではメンバーの新規ジョイン・業務委託メンバーの追加、既存メンバーの機種変更などのタイミングで新しい端末をProvisioningProfileに追加し、DLし、Xcodeに追加する　という作業を　__温かみのある手作業で行っていました__

![prv1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159111/f17bdfd6-6a5c-4dad-2bbb-f4e8e736c4c2.png)
端末の追加とProvisioningProfile自体の端末リストの更新はまだ１回だけの作業なので良いのですが、弊社の開発するアプリは __ProvisioningProfileの数が１０数個あり、これらを３台のMacMiniでAppleDeveloperを開き、ProvisioningProfileを１個づつDLし、クリックして開いてXcodeへ登録という地味に面倒な上に妙に神経使う作業が発生してしまいました__

![prv2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159111/cd40f9c7-d0d5-209d-715a-2a8ad6f9ebd6.png)

正直エンジニア以外でも出来る作業やろ！とは思いつつ、__AppleDeveloperをエンジニア以外が弄るのはインシデントにつながる恐れがあるためなかなか他の人にお願いすることもできません__

 **そもそも頼まれた他の人が苦しむだけなので負の連鎖です。**

# 自動化や！
調べてみた所AppStoreConnectでの操作を行えるAPI、**AppStoreConnectAPI**というものがありました（そのまんまな命名。。）
https://developer.apple.com/jp/app-store-connect/api/
リファレンスを見てみるとProvisioningProfileがDLできそうな雰囲気です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159111/d20d39f2-4dd9-c2d5-38dc-7238e58205e0.png)

これを使い、ProvisioningProfileを全件DLし、自動で開くシェルスクリプトを作れば業務効率化待ったなし！

# 早速やってみる（準備編）
AppStoreConnectAPIを使うにはAPIを叩くためのKeyが３種類必要です
- Issuer ID
- キーID
- 秘密鍵ファイル(p8ファイル）

AppStoreConnectのユーザーとアクセス→キー→AppStoreConnectAPIから発行できるので発行しておきましょう。
もし表示されない場合はアカウントの権限が足りない可能性があるので別途上司などに依頼しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159111/d3ff9184-34ec-c6f0-414a-1a9912307d24.png)

# 早速やってみる（Python編）

今回は実際にAPI叩き、ファイルを保存する処理をPythonで書いてみました
AppStoreConnectAPIを叩くサンプルとしてはこちらを参考にしました
https://gist.github.com/corrieriluca/3c9f062401582dad83013d7363741b0c

```python
import os
import base64
import requests,time
from authlib.jose import jwt

KEY_ID = ""
ISSUER_ID = ""
#通信有効期限は20分程度なのです。
EXPAIR_TIME = int(round(time.time() + (20.0 * 60.0)))

P8_FILE = ".p8 filepath."
KEY_ID = "hogehoge"
ISSUER_ID = "fugafugafuga"


#認証ファイル
with open(P8_FILE, "r") as file:
    private_key = file.read()

header = {
    "alg" : "ES256",
    "kid" : KEY_ID,
    "typ" : "JWT"
}

payload = {
    "iss" : ISSUER_ID,
    "exp" : EXPAIR_TIME,
    "aud" : "appstoreconnect-v1"
}

token = jwt.encode(header,payload,private_key)

JWT = "Bearer " + token.decode()
URL = "https://api.appstoreconnect.apple.com/v1/profiles"
HEAD = {"Authorization": JWT}

#API叩いてProvisioningを取得
#もう使ってない（必要ない）Profile落とさないように検索をかける
res = requests.get(URL,params={"filter[name]": "検索したいProvisioningProfileの名前","limit": 50},headers=HEAD)

os.makedirs("provisioning", exist_ok=True)

json = res.json()
#jsonにしてdataの配列を回す
data_array = json["data"]
for data in data_array:
    attr = data["attributes"]
    #無効はSkip
    if attr["profileState"] == "INVALID":
        continue

    name = attr["name"]
    #profileContentがファイルの中身なのでこれをDecodeしたものをファイルとして保存してやる
    content = attr["profileContent"]
    write_file = open("provisioning/" + name + ".mobileprovision","bw")
    write_file.write(base64.b64decode(content))
    write_file.close()

```

大事なところとしては

```python
res = requests.get(URL,params={"filter[name]": "検索したいProvisioningProfileの名前","limit": 50},headers=HEAD)
```
ここでAPIを叩いています。filter[name]でProvisioningProfile取得する際に名前でフィルターをかけています。
弊社では開発中のプロダクトのProvisioningProfileのみ落としたいので利用しています

```python
for data in data_array:
    attr = data["attributes"]
    #無効はSkip
    if attr["profileState"] == "INVALID":
        continue

```
ここで取得したProvisioningをforで回しながら、チェックをしていきます
何らかの事情により（例：証明書期限切れ）ProvisioningProfileが無効になっていた場合、落としても意味が無いのでスキップしています
["profileState"]の中にVALID,INVALIDで値が入っているのでチェックして弾くようにします。

実際に保存しているのがこの辺の処理です

```python
name = attr["name"]
#profileContentがファイルの中身なのでこれをDecodeしたものをファイルとして保存してやる
content = attr["profileContent"]
write_file = open("provisioning/" + name + ".mobileprovision","bw")
write_file.write(base64.b64decode(content))
write_file.close()
```

バイナリファイルとして保存するのでopenを呼ぶときのモードはbwにしておきましょう（１敗）


このpythonファイルを実行するとこのような結果になります

```console
$ python main.py
```

![スクリーンショット 2021-12-22 16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159111/2402ed35-aae1-f7bb-afc7-06d1872a85f4.png)
フォルダが生成されProvisioningProfileが大量にDLされました🎉🎉🎉



# 早速やってみる（シェルスクリプト編）
最後にこのpythonファイルを実行し、ProvisioningProfileをひたすら開くシェルを作成します
といっても超簡単です

```shell　run.sh
python3 main.py
for file in provisioning/*; do
echo $file
open $file
done
```
forでファイル名を回してopenコマンドで開いているだけのシンプルな物です。

あとはこの　shellを必要なタイミングで叩くだけでローカルのProvisioningProfileを最新の状態にすることが出来ます！

# 作ってみて

前々からやりたかった自動化だったのですが実際にやってみてその便利さに泣きました（マジ）
端末を買い替えた人が出たときや検証端末を増やす際も、AppleDeveloperでProvisioningProfileの更新をしてしまえば各ビルドマシンにSSHとかで入ってshell叩くだけになったので数時間単位で業務時間の削減になりました🎉

あと、結構属人化しがちだった作業だったのですがシェルスクリプト叩ける人なら誰でも出来る（つまりエンジニアなら全員出来る）ようになったので属人化防止にも繋がりました（流石にAppleDeveloper周りは人を選ぶけど。。）

# おまけ
Slackで喜ぶ人
![よろこび.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159111/a00fbec8-61e7-10cb-4998-1f7d7951aa78.png)