Google Apps Scriptのマニフェストを外部から更新する

先日投稿させていただきました「[Google Apps Scriptに追加されたマニフェストについて](https://qiita.com/tanaike/items/b3538e7550555ebb51f5)」のその後として、マニフェストをスクリプトエディタ上で更新するだけでなく、外部からも更新することができればより用途が広がるのではないかと考え、実験しました。先日投稿した記事の追記2へ外部からの更新ができるようになっていることを確認したことだけ記載させていただきました。本記事では、実際にマニフェストを更新する方法についてデモを使用して紹介させていただきます。外部からマニフェストを更新できることが分かりましたので、今後のアプリケーションへの応用が期待できます。

# プロジェクトの構造
マニフェストが加わった後のプロジェクトの構造は次のようになっています。typeに対してこれまでのserver_jsとhtmlに加えてjsonが新たに加わっています。マニフェストを更新する場合は、typeがjsonのsourceを変更することになります。

~~~json
{
    "files": [
        {
            "id": "#####",
            "name": "Code",
            "type": "server_js",
            "source": "function myFunction() {\n  \n}"
        },
        {
            "id": "#####",
            "name": "Index",
            "type": "html",
            "source": "<!DOCTYPE html>\n<html>\n  <head>\n    <base target=\"_top\">\n  </head>\n  <body>\n    \n  </body>\n</html>\n"
        },
        {
            "id": "#####",
            "name": "appsscript",
            "type": "json",
            "source": "{\n  \"timeZone\": \"Asia/Tokyo\",\n  \"dependencies\": {\n  },\n  \"exceptionLogging\": \"STACKDRIVER\"\n}"
        }
    ]
}
~~~

また、既存プロジェクトをアップデートする場合、そのプロジェクト内に無いファイル名のスクリプトをアップデートとして実行すると既存のスクリプトが全て削除されています。マニフェスト更新時にもこれを心配しましたが、マニフェストファイルは独立しているようでプロジェクト内に無いファイル名でスクリプトを上書きしてもマニフェストファイル自体は変化しませんでした。

# マニフェスト更新の流れ
**ここで紹介させていただく方法は、スタンドアロンスクリプト（プロジェクト）に対して使用することができます。スプレッドシートなどのバウンドスクリプトはプロジェクトを更新したり新規作成することができないため使用することができません。**

## 新規プロジェクトの場合
新規でプロジェクトを作成する際にマニフェストをデフォルトから変更したい場合は、特に気にすることなく、プロジェクト内に登録するJSONを用意し、そこへマニフェストを挿入するだけで新規作成時からユーザが意図したマニフェストがプロジェクトに反映されます。例えば、プロジェクトの新規作成時からライブラリをインストールしておいたり、拡張サービスを初めから使用できる状態にしておくことも可能です。新規作成した後にアップデートでマニフェストを更新することも可能です。

使用するAPIは、新規作成時の1回です。使用するAPIの詳細は下記のドキュメントで見ることができます。

- [プロジェクトの新規作成について](https://developers.google.com/apps-script/guides/import-export#import_projects_to_drive)

## 既存プロジェクトの場合
既存のプロジェクトのマニフェストを更新するには次のような流れで行います。

1. プロジェクトをダウンロードします。
1. ダウンロードするとプロジェクト内のすべてのスクリプトがJSONとして返されます。JSONデータの中でtype、nameがそれぞれjson、appsscriptの要素について、そのsource部分を変更することでマニフェストの更新ができます。
    - スクリプトのIDを変更したり削除すると"Bad Request"が返されます。
    - 他のスクリプトを変更すると、アップデート時に反映されてしまうので注意が必要です。
1. 変更したJSONをプロジェクトにアップデートとしてアップロードします。これで更新内容が反映されます。

使用するAPIは、ダウンロード時とアップデート時の2回です。使用するAPIの詳細は下記のドキュメントで見ることができます。

- [プロジェクトのダウンロードについて](https://developers.google.com/apps-script/guides/import-export#export_projects_from_drive)
- [プロジェクトのアップデートについて](https://developers.google.com/drive/v3/reference/files/update)

## スコープ
上記を実行するためにはアクセストークンが必要です。実行に必要なスコープは次の2つです。

- ``https://www.googleapis.com/auth/drive``
- ``https://www.googleapis.com/auth/drive.scripts``

### GASを使用する場合
ここで、もしもGASで上記の流れを実行しようと考える場合は、2つのスコープに加えてUrlFetchAppで使用するためのスコープも必要になります。ただし、GASでは、``ScriptApp.getOAuthToken()``を使用してアクセストークンを取得する場合、自動でスコープを反映する機能があるようです。これについては以前[こちら](https://qiita.com/tanaike/items/1d49f73ab05403d0fe4c)の記事で少し紹介させていただきました。

自動で反映されることを考慮したときのスコープは次の通りです。

- ``https://www.googleapis.com/auth/drive``
    - Drive API v3によるプロジェクトの新規作成（アップロード）、ダウンロード、アップデートで使用されます。
    - このスコープはDriveAppのコマンドがスクリプト上に書かれていると自動で``ScriptApp.getOAuthToken()``に反映されます。
- ``https://www.googleapis.com/auth/drive.scripts``
    - Drive API v3によるプロジェクトのアップデートで使用されます。
    - **このスコープは自動で``ScriptApp.getOAuthToken()``には反映されません。** これがボトルネックでした。
- ``https://www.googleapis.com/auth/script.external_request``
    -  Drive API v3を使用するために用いるUrlFetchAppで使用されます。
    - このスコープはUrlFetchAppのコマンドがスクリプト上に書かれていると自動で``ScriptApp.getOAuthToken()``に反映されます。

これら3つのスコープは自動で反映されるものと、手動でないと反映されないものに分かれますが、マニフェストを使用してスコープを設定する場合は、必要なスコープを自動手動含めてそれら全てを登録する必要があります。そこで、3つのスコープを事前にマニフェストへ手動で登録することで、``ScriptApp.getOAuthToken()``を使用して上記の流れを実行できるアクセストークンを取得することができるようになります。これまではOAuth2プロセスを実行するためのスクリプトを用意する必要がありましたが、それが不要になったことは大変な進化です。

#### 補足
Google拡張サービスを使用してもデフォルトでスコープが足りないようで、"The app does not have the required scope to update Apps Scripts." のようなエラーが発生してプロジェクトのアップデートはできません。マニフェストを更新して不足と思われるスコープを追加するとそのエラーはなくなりましたが、新たなエラー"Bad request"が発生してそこで詰まってしまいました。（``Drive.Files.update()``のパラメータのBlobに対してプロジェクトの構造を``Utilities.newBlob()``を使って変換してもだめでした。）このエラーの変化から、``Drive.Files.update()``もまた``ScriptApp.getOAuthToken()``を使用しているのかもしれないと思いました。

# マニフェスト更新のデモンストレーション
マニフェスト更新について、上記の流れを実際に実行するため、今回は少し前に[こちら](https://qiita.com/tanaike/items/f5249ddb6bdedb9b85ae)で紹介させていただきましたCLIツールへ実装してみました。CLIツールのリポジトリは[こちら](https://github.com/tanaikech/ggsrun)です。このツールは、既にスコープを選択してアクセストークンを取得できるようになっていますので簡単に実装することができます。
デモンストレーションではマニフェストを更新してGoogle拡張サービスを全てONにしています。

![demo_manifects.gif](https://qiita-image-store.s3.amazonaws.com/0/133383/36bce0d6-8499-d375-1188-54fe8f9f23e0.gif)


この情報がお役に立ちましたら幸いです。