【Unity】Cloud Build APIを使って最終ビルドの一覧を表示する

# 実現する事
Unity Cloud Buildの最終ビルドをWebページ公開

- ビルドされたすべてのプラットフォームの最終ビルドを表示
- WebなのでPC、スマホ問わず確認可能です

![スクリーンショット 2022-02-11 18.20.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/bb3c4724-ffda-18f7-5a3d-87db42ba4fca.png)

# 必要なもの
- PHPが使えるサーバー
- Cloud BuildのApi Key
- Organization ID
- Project ID

# 使用上の注意
**URLを知っていれば誰でも見れます。**
URLの流出に注意が必要です。
対策として、BASIC認証を設定したり、Webでは表示せずアプリ上からダウンロードリンクを取得してアプリからのみリンクを開く事も可能です。

# 各IDの取得
Unityのコンソールから各値を取得します。

#### Cloud BuildのApi Key取得
DevOps > Settings > API Settings
![スクリーンショット 2022-02-11 16.53.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/c2932da4-cad4-4a53-2e9f-0e11eccecbb5.png)

#### Organization IDの取得
Projects > Organaization Settings > Organization details
![スクリーンショット 2022-02-11 16.52.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/58d71107-3a1b-44bb-baa3-bc6f4336c468.png)

#### Project IDの取得
Projects > Project Settings > Project details
![スクリーンショット 2022-02-11 16.54.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/65a44c2d-a73b-903f-8478-24dc0ad9df9d.png)

# ソース
下記、2つのソースをサーバーに設置して`index.html`にアクセスするだけです。
例えば、サーバーの`app`ディレクトリに設置した場合は、`https://xxx.com/app/`がURLになります。2つのファイルは同じディレクトリに配置してください。

**`getLatest.php`の書き換える箇所**
取得したIDを設定してください。

| field | 説明 |
|-----------|------------|
| API_KEY | Cloud BuildのApi Keyを設定 |
| ORGANIZATION_ID | Organization IDを設定 |
| PROJECT_ID | Project IDを設定 |
| FAILURE_MESSAGE | 取得失敗時のエラーメッセージを設定 |

```getLatest.php
<?
const API_KEY = '';
const ORGANIZATION_ID = '';
const PROJECT_ID = '';
const FAILURE_MESSAGE = '取得に失敗しました。時間を空けてお試しください。';

// エラーを画面に表示しない
ini_set('display_errors', 0);
ini_set('display_startup_errors', 0);

try
{
  $headers =
  [
    'Content-Type: application/x-www-form-urlencoded',
    'Authorization: Basic ' . API_KEY
  ];

  $context = stream_context_create([
    'http' => [
      'method' => 'GET',
      'header' => implode("\r\n", $headers),
      'ignore_errors' => true
    ]
  ]);

  $response = new Responses();
  $response->infos = [];

  // 各プラットフォームの最終ビルド情報の取得
  $buids = file_get_contents('https://build-api.cloud.unity3d.com/api/v1/orgs/' . ORGANIZATION_ID . '/projects/' . PROJECT_ID . '/buildtargets?include_last_success=true', false, $context);

  if (strpos($http_response_header[0], '200') !== false)
  {
    // ビルド情報からシェア情報の取得
    $buidObjs = json_decode($buids, true, 512, JSON_THROW_ON_ERROR);
    for ($i=0; $i < count($buidObjs); $i++)
    {
      $buidObj = $buidObjs[$i];
      $resShare = file_get_contents('https://build-api.cloud.unity3d.com/api/v1/orgs/' . ORGANIZATION_ID . '/projects/' . PROJECT_ID . '/buildtargets/' . $buidObj['buildtargetid'] . '/builds/' . $buidObj["builds"][0]['build'] . '/share', false, $context);

      if (strpos($http_response_header[0], '200') !== false)
      {
        $resShareJson = json_decode($resShare, true, 512, JSON_THROW_ON_ERROR);
        $obj = new Response();
        $obj->platform = $buidObj['platform'];
        $obj->platformName = ConvertToPlatformName($buidObj['platform']);
        $obj->shareUrl = 'https://developer.cloud.unity3d.com/share/share.html?shareId=' . $resShareJson['shareid'];
        $obj->finished = $buidObj['builds'][0]['finished'];
        $obj->build = $buidObj['builds'][0]['build'];
        $response->infos[] = $obj;
      }
      else
      {
        throw new WebAPIException(FAILURE_MESSAGE);
      }
    }
  }
  else
  {
    throw new WebAPIException(FAILURE_MESSAGE);
  }

  // Jsonとして書き出し
  header('Content-Type: application/json; charset=utf-8');
  echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);

}
catch (Exception $e)
{
  echo $e->getMessage();
  exit();
}

function ConvertToPlatformName($current)
{
    switch ($current)
    {
      case 'ios':
        return 'iPhone';
      case 'android':
        return 'Android';
      default:
        return $current;
    }
}

class Responses
{
  public $infos;
}

class Response
{
  public $platform;
  public $platformName;
  public $shareUrl;
  public $build;
  public $finished;
}

class WebAPIException extends Exception
{
  public function __construct($msg){
    parent::__construct($msg, 800);
  }
}
```

```index.html
<!doctype html>
<html lang="jp">
<head>
  <meta charset="utf-8">
  <title>Download App</title>
  <meta name="description" content="ダウンロード">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="robots" content="noindex"><!-- クローラ-の巡回拒否 -->
  <style type="text/css">
    body, input, textarea
    {
      font-family: "游ゴシック Medium", "Yu Gothic Medium", "游ゴシック体", "YuGothic", "ヒラギノ角ゴ ProN W3", "Hiragino Kaku Gothic ProN", "メイリオ", "Meiryo", "verdana", sans-serif;
    }
    h1
    {
      text-align: center;
      font-size: 25px;
      margin-bottom: 35px;
    }
    a
    {
      color: #1669a6;
      text-decoration: none;
    }
    ul, ol
    {
      background: #fcfcfc;
      padding: 0.5em 0.5em 0.5em 2em;
      border: solid 3px gray;
    }
    ul li, ol li
    {
      line-height: 1.5;
      padding: 0.5em 0;
    }
    #wrap
    {
      max-width: 350px;
      margin-left: auto;
      margin-right: auto;
      padding: 2em;
    }
    #wrap p
    {
      font-size: 13px;
    }
    #loading
    {
      position: absolute;
      left: 0; top: 0;
      width: 100%; height: 100%;
      background: white;
      z-index: 2147483647;
    }
    #loading p
    {
      text-align: center;
      margin: 0;
      position: absolute;
      top: 50%;
      left: 50%;
      margin-right: -50%;
      transform: translate(-50%, -50%)
    }
    #loading span
    {
      font-size: 3em;
      display: inline-block;
      background-color: #dcdcdc;
      margin: 0;
      animation: loading .7s infinite alternate;
    }
    #loading span:nth-child(2) { animation-delay: .1s; }
    #loading span:nth-child(3) { animation-delay: .2s; }
    #loading span:nth-child(4) { animation-delay: .3s; }
    #loading span:nth-child(5) { animation-delay: .4s; }
    #loading span:nth-child(6) { animation-delay: .5s; }
    #loading span:nth-child(7) { animation-delay: .6s; }
    #loading span:nth-child(8) { animation-delay: .7s; }
    #loading span:nth-child(9) { animation-delay: .8s; }
    #loading span:nth-child(10) { animation-delay: .9s; }
    @keyframes loading
    {
      0% { transform: scale(1.2); }
      100% { transform: scale(0.8); }
    }
  </style>
</head>

<body>
  <div id="loading"><p><span>N</span><span>O</span><span>W</span><br><span>L</span><span>O</span><span>A</span><span>D</span><span>I</span><span>N</span><span>G</span></p></div>
  <div id="wrap">
    <h1>ダウンロード</h1>
    <ul id="results"></ul>
    <p id="mobileMsg">※iPhoneはSafari、AndroidはChromeで開いてください。</p>
  </div>
</body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script>
$(function()
{
    $.ajax('getLatest.php',
      {
        type: 'get',
        dataType: 'json'
      }
    )
    // [検索成功]
    .done(function(data)
    {
      let $results = $("#results");

      if(data == null
        || data.infos == null
        || data.infos.length <= 0)
      {
        $results.append($("<li>").text("ビルドがありません。"));
        $("#mobileMsg").css("display", "none");
        return;
      }

      let isMobile = false;
      for (let i = 0; i < data.infos.length; i++)
      {
        let info = data.infos[i];
        let date = new Date(info.finished);
        let li = $("<li>");
        let a = $("<a>").attr("href", info.shareUrl).text(info.platformName + " #" + info.build + " (" + date.toLocaleDateString() + "更新)");

        $results.append(li.append(a));
        if(info.platform == "ios" || "android")
        {
          isMobile = true;
        }
      }

      if(!isMobile)
      {
        $("#mobileMsg").css("display", "none");
      }

      $("#loading").fadeOut(200);
    })
    // [検索失敗]
    .fail(function(XMLHttpRequest, textStatus, errorThrown)
    {
      $("#loading > p").html("取得の失敗<br>[" + XMLHttpRequest.responseText + "]<br>" + textStatus + "<br>" + errorThrown.message);
    });
});
</script>
</html>
```

# 表示する内容をカスタマイズしたい！
[Unity Cloud Build API](https://build-api.cloud.unity3d.com/docs/1.0.0/index.html)
サイトで簡単に取得テストできます。ここでテストしてカスタマイズ内容が決まったら`getLatest.php`を修正してください。

### Unity Cloud Build APIの概要
SwaggerベースのREST APIです。
端的に説明すると、特定の決まったURLにアクセスする事で情報を取得したり、ビルドを走らせたりする事ができます。

### 現在の取得の流れ

![getLatestSequence.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/3b25a2a9-e866-6569-bda2-d654278f3b7f.png)

### 適応方法
テストで取得したい内容が確定するとURLが確定します。そのURLにアクセスする事で情報が取得できます。
`getLatest.php`のURLを置き換えてください。

>'https://build-api.cloud.unity3d.com/api/v1/orgs/' . ORGANIZATION_ID . '/projects/' . PROJECT_ID . '/buildtargets?include_last_success=true

### テスト手順
#### 1. サイトトップにAPI Keyを入力
![スクリーンショット 2022-02-11 19.19.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/42158e9c-03f7-86e5-f982-b50d7d6670e0.png)

#### 2. 左のリストから実行したい内容を選択
※スクロールでも可
![スクリーンショット 2022-02-11 19.24.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/98d78190-7781-8d71-d1b1-10ba28d3348b.png)

#### 3. 項目を入力してリクエスト

下記は、読み替えてください。

| サイト上 | 実際の名称 |
|-----------|------------|
| orgid | Organization ID |
| projectid | Project ID |

![スクリーンショット 2022-02-11 19.26.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/9474d55a-9264-39ae-120e-ec364dd94cdc.png)

# Webでは表示したくない
`index.html`を削除

# アプリ内(Unity上)から取得

下記コードをAssets配下に保存

```CloudBuildHelper.cs
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public static class CloudBuildHelper
{
    [Serializable]
    public class BuildInfos
    {
        public BuildInfo[] infos;

        public override string ToString()
        {
            var msg = $"{GetType().Name} -\n";
            if (infos.Length > 0)
            {
                foreach (var info in infos)
                {
                    msg += $"{info}\n";
                }
            }
            else
            {
                msg += "ビルドがありません。";
            }
            return msg;
        }
    }
    
    [Serializable]
    public class BuildInfo
    {
        public string platform;
        public string platformName;
        public string shareUrl;
        public int build;
        public string finished;
        
        public override string ToString() => JsonUtility.ToJson(this, true);
    }
    
    public static IEnumerator GetLatestBuildInfos(string uri, Action<BuildInfos> successAction, Action<string> failedAction)
    {
        using (UnityWebRequest webRequest = UnityWebRequest.Get(uri))
        {
            yield return webRequest.SendWebRequest();

            if (webRequest.result == UnityWebRequest.Result.Success
                && !string.IsNullOrEmpty(webRequest.downloadHandler.text))
            {
                var obj = JsonUtility.FromJson<BuildInfos>(webRequest.downloadHandler.text);
                if (obj != default
                    && obj.infos.Length > 0)
                {
                    foreach (var info in obj.infos)
                    {
                        if (DateTime.TryParse(info.finished, out DateTime dateTime))
                        {
                            info.finished = dateTime.ToString("yyyy/MM/dd");
                        }
                    }
                    successAction?.Invoke(obj);
                }
                else
                {
                    failedAction?.Invoke($"Error: ダウンロードした内容を展開できませんでした。管理者にお問合せください。\n{webRequest.downloadHandler.text}");
                }
            }
            else
            {
                failedAction?.Invoke($"Error: {webRequest.error}");
            }
        }
    }
}
```

下記の形で利用できます。
第一引数に`getLatest.php`のURLを設定してください。

```C#
StartCoroutine(CloudBuildHelper.GetLatestBuildInfos(
    "https://xxxxx.com/getLatest.php",
    response =>
    {
        // 成功した場合
        // 下記の形で取得できます。
        // response.infos[0].platform;
        // response.infos[0].platformName;
        // response.infos[0].shareUrl;
        // response.infos[0].build;
        // response.infos[0].finished;
        
        // [確認用] Json形式で一覧書き出し
        Debug.Log(response);
    },
    error =>
    {
        // 失敗した場合
    }));
```

Console: [確認用] Json形式で一覧書き出し

```json
{
    "platform": "ios",
    "platformName": "iPhone",
    "shareUrl": "https://developer.cloud.unity3d.com/share/share.html?shareId=xxxxx",
    "build": 44,
    "finished": "2022/02/10"
}
{
    "platform": "android",
    "platformName": "Android",
    "shareUrl": "https://developer.cloud.unity3d.com/share/share.html?shareId=xxxxx",
    "build": 39,
    "finished": "2022/02/10"
}
```