Google Apps Scriptの本番環境へのデプロイ方法

# はじめに
一旦筆が止まるとダメですね。。。およそ1年ぶりの投稿です。

[毎週行っている作業](https://qiita.com/h-yuki/items/4ba27e415a740d10b870)を自動化する Google Apps Script(GAS) の、[基本設計](https://qiita.com/h-yuki/items/e00b8743968606582f99)に示した7ステップのうち、2, 3, 4について説明したところで止まっています。6は4と同じなので省くとして、1, 5, 7が残ってますが、今回はその話ではありません。5が少し関係するのですが、1年近く問題なく動いていたスプレッドシートを作るための GAS スクリプトが、突然動かなくなりました。

問題は Google側にあるようですが、毎週の作業を止めるわけにはいきませんので、回避策を講じて、本番環境にデプロイしました。今回はこのデプロイ方法について書きます。当然、スクリプトなどで自動化したいのですが、そこまでは手がつけられておらず、手動です(^^;)

# 発生した問題
このシステムでは、毎週の作業分担を行うためのスプレッドシートを GAS で自動生成しています。そのシートだけ見ればわかるように、簡単な使い方を書いていたり、表の部分には当然見出しがあります。これらを誤って書き換えてしまわないように、セルの保護をしています。GoogleApps の Webアプリから操作する場合は、下図のようにセルの範囲を選択して、右クリックのメニューから「範囲を保護」を選択して設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354068/baf5c8bf-ef2d-dbf7-3460-7c076485dd6f.png)

GASで設定する場合は、[Range#protect()](https://developers.google.com/apps-script/reference/spreadsheet/range#protect()) を使います。本投稿を作成している時点で、この関数がエラーになります。Issue Tracker にも登録されているのですが、Google の担当者の動きは非常に遅く、多くの方が被害を受けている状況です。有料版では動いているという情報もありますが、まあ待つしかないです。
https://issuetracker.google.com/issues/148894990

暫定対策としては、この関数を使っている部分を全てコメントアウトしました。(^^;)
使って頂いているメンバーには、書き換えないように気を付けて下さいね、とご案内しました。

# デプロイに必要な処理
では本題に入りますが、まず開発環境と本番環境の違いについて説明し、デプロイに必要な処理を明確にします。

## 開発環境と本番環境
Web(クラウド)ベースのシステム開発・運用をされている方には当たり前の話ですが、開発を行うための環境と、本番運用の環境は分けています。他に、本番とほぼ同じ条件のステージング環境を用意して、デモ、障害解析、リハーサルなどに使うのが一般的です。お仕事ではありませんので、このプロジェクトでは開発環境と本番環境だけを使い分けています。

### 開発環境
開発環境では、使い方のわからない関数を実行して結果を確かめたり、ログをたくさん出力してデバッグしたり、単体テストを実施したり、といった開発のための作業をします。このプロジェクトでは、開発は私一人で行っていますので、自分のGoogleアカウントを使って開発しています。

[前回の記事](https://qiita.com/h-yuki/items/7b676dd86f97460ed0a5)で、Slackに通知する説明をしましたが、Slackのワークスペースをたくさん作るのは面倒なので、このプロジェクトでは同じワークスペースの中でチャネルを分けて、開発ではテスト用チャネルを使っています。(お仕事であればワークスペースを分けた方がよいと思います。)

### 本番環境
本番環境には、テストが終わったスクリプトだけをデプロイします。本番運用に自分のアカウントを使うと、何かの間違いでスクリプトを削除してしまうなど、本番運用に対するリスクがあります。逆に、必要以上に自分の情報がメンバーからアクセスできるようになってしまうことも考えられます。こうした理由から、このプロジェクトでは、本番運用のための専用 Google アカウントを作成しました。

Slack の通知先は、関係者が登録されている本番用のチャネルを使います。

## デプロイするファイル
[こちらの投稿](https://qiita.com/h-yuki/items/e00b8743968606582f99#%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%82%BF%E3%82%A4%E3%83%97%E3%81%AE%E9%81%B8%E6%8A%9E)に書いた通り、このプロジェクトではコンテナーバウンドスクリプトを使っていますので、コンテナであるスプレッドシートをコピーすれば OK です。コンテナーに関連付けられた GAS スクリプトも一緒にコピーされます。但し、スクリプトのプロパティとトリガーはコピーされない点に注意が必要です。

## 環境により異なるパラメータ
[こちらの投稿](https://qiita.com/h-yuki/items/7b676dd86f97460ed0a5#%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3-slack_webhook)に書きましたが、環境により異なるパラメータである、Slackの通知先は、スクリプトのプロパティに登録しています。プロパティはコピーされませんので、本番環境へのデプロイ時にプロパティを登録する必要があります。

## 必要な処理のまとめ
以上をまとめると、本番環境へのデプロイに必要な処理は以下となります。

- スプレッドシート(コンテナ)を自分のアカウントのドライブから本番アカウントのドライブにコピーする
- スクリプトのプロパティを設定(登録)する
- 毎週同じ日時にスクリプトを自動実行するためのトリガーを設定する

完全自動化とはいかないまでも、少し仕組みを考えないと大変ですし、ミスが発生する可能性が高いですね。

# デプロイ方法
デプロイに必要な処理について順に説明します。

## スプレッドシートをコピーする
ここは極めて原始的な方法で手動コピーしています。

- 自分のアカウントで開発しているスプレッドシート(コンテナ)を、本番アカウントに共有する
- 本番アカウントで Googleドライブにログインして、共有されたスプレッドシートをコピーする
    - 共有されているファイルは、左サイドバーの「共有アイテム」から探すと簡単に見つけられます
    - コンテナのシートを直接メンバーが触ることはありませんので、名前は何でも構いません(このプロジェクトでは「XXXのコピー」のままです^^;)
- 古いバージョンのスプレッドシートを削除する

古いバージョンはいつ削除すればよいのかが少し難しいですね。このプロジェクトでは、スクリプトは週1回決まった時間に実行するだけですから、それ以外の時間であればいつでも削除できます。常に動いている必要のあるプロジェクトですとそうはいきませんので、工夫が必要です。少なくとも、後で述べる設定が完了してから削除した方がよいでしょう。

削除に関してもうひとつ注意点があります。コンテナファイルを削除すると、コンテナファイルはゴミ箱に入ります。関連づけられているスクリプトもゴミ箱に入ります。実はこの状態では、スクリプトのトリガーは生きていて、週1回の自動実行が走ってしまうのです。これを避けるために、Googleドライブのゴミ箱を開いて「完全に削除」します。コンテナファイルを完全に削除すれば、スクリプトもトリガーも削除されます。

## プロパティを設定する
[こちらの投稿](https://qiita.com/h-yuki/items/7b676dd86f97460ed0a5#%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3-slack_webhook)に書いたことを手作業で行えばよいのですが、これを少し自動化してみます。

まず、ここまで中身は空のままであったコンテナファイルに、プロパティを記載できるようにします。また、設定用の関数を起動するためのボタンを作成します。ボタンは、メニューの「挿入」→「図形描画」で作成できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354068/088e136c-19f3-58d6-2d2d-f662fe5c8161.png)

スクリプトには、初期化ボタンから起動するための```init()```関数を定義します。セルの値を取得してプロパティに設定するだけの簡単なコードです。

```javascript:main.gs
function init() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const s = ss.getActiveSheet();
  const url = s.getRange(2, 2).getValue();
  PropertiesService.getScriptProperties().setProperty('SLACK_WEBHOOK', url);
}
```

ボタンと関連づけるためには、描画オブジェクト(ボタン)を右クリックして、メニュー(右端の点々)から「スクリプト割り当て」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354068/02525b58-74c4-c886-80db-8d4015ec07b9.png)

ダイアログに関数名```init```を入力して、「OK」ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354068/609f4987-2ce0-44ab-9ee8-a8e165fb3ca8.png)

これで関数の関連付けは完了です。スプレッドシート上で B2セルに Webhook URL を入力して、「初期化実行」ボタンを(左)クリックして下さい。権限の承認を求められた場合は、許可して下さい。実行が完了するとプロパティの設定ができているはずです。スクリプトエディタのメニューから、「ファイル」→「プロジェクトのプロパティ」を選択し、「スクリプトのプロパティ」タブで確認してみて下さい。

## 自動実行のトリガーを設定する
同様に、[こちらの投稿](https://qiita.com/h-yuki/items/e00b8743968606582f99#%E3%83%88%E3%83%AA%E3%82%AC%E3%83%BC%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B)に記載したことを、同じボタンで初期化できるようにしてみます。

先ほどの```init()```関数に以下のコードを追加します。やや複雑に見えますが、何となく雰囲気は分かりますね。詳しくは API仕様書を参照して下さい。([TriggerBuilder#timeBased()](https://developers.google.com/apps-script/reference/script/trigger-builder#timeBased()), [ClockTriggerBuilder](https://developers.google.com/apps-script/reference/script/clock-trigger-builder.html))

```javascript:main.gs
  ScriptApp.newTrigger('weeklyAction')
  .timeBased()
  .onWeekDay(ScriptApp.WeekDay.MONDAY)
  .atHour(6)
  .create();
```

再度「初期化実行」ボタンをクリックして、スクリプトエディタの時計のアイコン、またはメニューの「編集」→「現在のプロジェクトのトリガー」で確認してみて下さい。鉛筆マークのアイコンでトリガーの編集画面に入ってみると、期待した設定になっていることが確認できます。但し、[こちらの投稿](https://qiita.com/h-yuki/items/e00b8743968606582f99#%E3%83%88%E3%83%AA%E3%82%AC%E3%83%BC%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B)と1点だけ異なっているところがあって、「エラー通知設定」が「毎日通知を受け取る」になっています。実は、この設定をプログラムで変更することはできないようです。Issue Trackerでは 8年も放置されています。これは諦めるしかなさそうですね。
https://issuetracker.google.com/issues/36755104

実は、これだけでは問題があります。更にもう一度「初期化実行」ボタンをクリックすると、全く同じトリガーがもうひとつ登録されてしまうのです。つまり、```weeklyAction()```関数を起動するトリガーが登録されている場合は、一度削除してから登録する、または再登録しない、などの対応が必要です。後者を実装して、実行結果をダイアログ表示するコードも追加した、```init()```関数の定義を以下に示します。

```javascript:main.gs
function init() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const s = ss.getActiveSheet();
  const url = s.getRange(2, 2).getValue();
  PropertiesService.getScriptProperties().setProperty('SLACK_WEBHOOK', url);
  
  const triggers = ScriptApp.getProjectTriggers();
  var registered = false;
  for(var i=0; i<triggers.length; i++) {
    if(triggers[i].getHandlerFunction() == 'weeklyAction') {
      registered = true;
      break;
    }
  }
  
  var msg = 'スクリプトプロパティを設定しました\\n';
  if(registered) {
    msg += 'トリガーは初期化済みです';
  } else {
    ScriptApp.newTrigger('weeklyAction')
    .timeBased()
    .onWeekDay(ScriptApp.WeekDay.MONDAY)
    .atHour(6)
    .create();
    msg += 'トリガーの初期化が完了しました';
  }
  Browser.msgBox(msg);
}
```

# おわりに
今回は、GASで発生した不具合を回避するためのコードをデプロイするために、久しぶりに環境を触ったので、デプロイ方法について説明してみました。

それにしても、GASのバグ対応の遅さはひどいですね。早く修正されることを願っています。