GASでGitHubにある放置されたままのブランチを通知する

## この記事の内容
GASを利用して、GitHubから放置されたままのブランチを探してSlackへ通知を行う実装を行いました
その過程と知見とかをまとめてます
完成版のコードは後半にまとめてますのでそちらを参照してください

## はじめに
最近になってGitHubの[branches](https://github.com/vuejs/vue/branches)というURLがあることに気がつきました
開発をしていると何となく実装してpushはしたけどPRできずに腐っているブランチというのはいくつかあると思います
以下はVuejsのリポジトリのbrachesですが、このようにactiveなものslateなものなどが可視化されていてとてもわかりやすいです

- Active: 最後のコミットが最初にあるブランチから順に、過去3か月以内にコミットしたすべてのブランチが表示される
- Slate: 過去3カ月以内に誰もコミットしていないすべてのブランチが表示される
![2018-12-07-050704_1025x735_scrot.png](https://qiita-image-store.s3.amazonaws.com/0/125774/0a57b855-34a3-860d-d362-ff8029cc8257.png)

see: [Viewing branches in your repository - User Documentation](https://help.github.com/articles/viewing-branches-in-your-repository/)

腐っているブランチがあるかどうかはここを見にいけばよいのですが、どうせならSlackに通知してほしい
ぱっと探した感じ、GitHubではそれを通知するような機能は提供していなかったようなので、どうせならGASの入門ついでに通知Botを作ってみようということになりました

## やったこと
### GASでSlackにメッセージを送信する
[【初心者向け】GASを使ってSlackへ自動通知 - Qiita](https://qiita.com/chihiro/items/c7b11abc78f5d806c3a8) を参考に実装しました
詳しいことはリンク先にあるので参照してください

### GitHubからブランチの情報を取得してみる
github の grachqlを使ってデータを取得します

今回取得したいデータは、リポジトリにあるすべてのブランチとそれに紐づいた最後のコミット、さらに誰がコミットしたのか分かれば十分です

[GraphQL API Explorer | GitHub Developer Guide](https://developer.github.com/v4/explorer/) でqueryを試すことができます!便利!!
ドキュメントはこちら [GitHub GraphQL API v4 | GitHub Developer Guide](https://developer.github.com/v4/)

queryは以下のような感じになりました

```
query { 
  repository(owner: "Teamup-inc", name: "teamup360") { 
    name,
    refs(first: 10, refPrefix:"refs/heads/") {
      nodes {
        name
        target {
          ... on Commit {
            history(first: 1) {
              nodes {
                messageHeadline
                committedDate
                url
                author {
                  name
                }
              }
            }
          }
        }
      }
    }
  }
}
```
branchesがrefsになってますね、GitObjectに寄った名前になってるんですね
branchのすぐ下にcommitsがあるのかな?と思って試行錯誤していたので結構つまりました...
targetがあって `... on Commit` でアクセスして、そこのhistoryを参照するという結構面倒な感じでした
(`... on Commit` って最初 ...なので何か省略しているのかな?と思い込んでスルーしてエラーになったのはないしょ)


その他は以下を参考に実装
[Google Apps ScriptでGithub GraphQL API v4を叩く - Qiita](https://qiita.com/abeyuya/items/f30e7d63568b23092671) 
[Branches on Repository - GraphQL API - GitHub Platform Forum](https://platform.github.community/t/branches-on-repository/1766)
[GitHub GraphQL repo commit history query](https://gist.github.com/jonathansick/8bbe88a85addaeeea4e7fe9ef15b016b)



### ブランチの情報からSlateなものを絞り込む
取得したデータのCommitDateを参照して3ヶ月以上経過したブランチを判別してみます

```データ構造.js
{
  "name": "branch_name",
  "target": {
    "history": {
      "nodes": [
        {
          "messageHeadline": "コミットメッセージ",
          "committedDate": "2018-04-03T08:18:10Z",
          "url": "コミットのURL",
          "author": {
            "name": "Author Name"
          }
        }
      ]
    }
  }
}
```
3ヶ月以上経過したかどうかはcommitedDateを参照すれば判別できそうです
ただ、jsで日付の比較を行うのは面倒です...
[Moment.js](https://momentjs.com/docs/#/displaying/unix-timestamp/)使えないのかな?と思って調べたところ普通に外部ライブラリとして存在していたので導入してみました
[日付＆時刻の便利ライブラリ「Moment.js」をGoogle Apps Scriptで使う方法](https://tonari-it.com/gas-moment-js-moment/)

ざっくりとこんな感じのコードで判定はできそうです

```判別.js
  this.formatBranch = function(branch){
    const commits = branch.target.history.nodes
    return {
      name: branch.name,
      lastCommit: commits[0]
    }
  };
  this.slateFilter = function(branch) {
    // 最後のコミットから3ヶ月以上経過しているもの
    // を取得する
    const moment = Moment.moment
    const today = moment()
    const commitDate = moment(branch.lastCommit.committedDate)
    const elapsedDate = today.diff(commitDate, 'days')
    return elapsedDate > 30 * 3
  }
```


### Slackのメッセージを加工する
最後に取得したSlateなブランチをSlackへいい感じに通知しましょう
Slackへはattachmentsを利用することで結構リッチな通知を作成することが可能です
詳しくは以下を参照
[SlackのIncoming Webhooksを使い倒す - Qiita](https://qiita.com/ik-fib/items/b4a502d173a22b3947a0)
[Message Formatting | Slack](https://api.slack.com/docs/messages/builder?msg=%7B%22text%22%3A%22This%20%26amp%3B%20that.%20%26lt%3B%20and%20%26gt%3B%22%7D)

### 環境変数
この手のコードを書くときに困るのがToken情報をハードコーディングしたくない問題です
一般的には環境変数が設定できるので、問題ないのですが、GASには環境変数ってあるの?ってなりました
適当にポチポチ探していたらスクリプトプロパティという項目を発見したので、調べてみるとまさに環境変数でした

[【初心者向けGAS】スクリプトプロパティを操作してそのデータを取り出す方法](https://tonari-it.com/gas-properties-script-property/#toc2) こちらを参考にToken情報を環境変数に設定してほぼ完了です

### 完成したもの
こんな感じで通知されます
![2018-12-07-050704_1025x735_scrot.png](https://qiita-image-store.s3.amazonaws.com/0/125774/8d3d041a-e31d-a851-4526-d09818927cbd.png)

このスクリプトの実行頻度は週に1度くらいでよさそうなので、トリガーを設定します
[GoogleAppsScriptのトリガーを使って自動化をマスターしよう | えびせん](https://ebijun.work/google-apps-script-trigger/)

おしまい


## 完成版のコード
シンタックスハイライトの問題で拡張子をjsにしているので書き写すときは .gsへ変更してください

```main.js
function main() {
  const slateBranches = new SlateBranches(PropertiesService.getScriptProperties().getProperty('GitHubAccessToken')).list()
  const slackNotify = new SlackNotify(PropertiesService.getScriptProperties().getProperty('SlackWebhookUrl'))
  const attachments = slateBranches.map(buildAttachment)
  slackNotify.post(attachments)
}

function buildAttachment(branch) {
  const commit = branch.lastCommit
  const author = commit.author
  return {
    "color": "#cc3344",
    "author_name": author.name,
    "title": branch.name,
    "title_link": 'https://github.com/:owner/:repo/compare/' + branch.name,
    "text": commit.messageHeadline,
    "footer": "GitHub last commit:",
    "footer_icon": "https://assets-cdn.github.com/favicon.ico",
    "ts": Moment.moment(commit.committedDate).unix()
  }
}
```

```SlackNotify.js
function SlackNotify(postUrl) {
  this.postUrl = postUrl
  this.username = 'GASBot'
  this.icon = ':robot_face:'
  this._buildOptions = function(payload) {
    const options = {
      method: "post",
      contentType: "application/json",
      payload: payload
    }
    return options
  }
  this.post = function(message) {
    const isAttachmens = Array.isArray(message)
    const payload = JSON.stringify({
      username: this.username,
      icon_emoji: this.icon,
      text: isAttachmens ? '' : message,
      attachments: isAttachmens ? message : []
    })
    const options = this._buildOptions(payload)
    UrlFetchApp.fetch(this.postUrl, options)
  }
}
```
```SlateBranches.js
function SlateBranches(token) {
  this.token = token
  this.endpoint = "https://api.github.com/graphql"
  this.query = 'query { \
    repository(owner: ":owner", name: ":repo") { \
      name, \
      refs(first: 10, refPrefix:"refs/heads/") { \
        nodes { \
          name \
          target { \
            ... on Commit { \
              history(first: 1) { \
                nodes { \
                  oid \
                  messageHeadline \
                  committedDate \
                  url \
                  author { \
                    name \
                  } \
                } \
              } \
            } \
          } \
        } \
      } \
    } \
  }'
  this.list = function() {
    const slateBanches = this._getBranches().
      map(this._formatBranch).
      filter(this._slateFilter)
    return slateBanches
  }
  this._buildRequestOption = function() {
    return {
      method: "post",
      contentType: "application/json",
      headers: {
        Authorization: 'bearer ' + this.token,
      },
      payload: JSON.stringify({ query: this.query }),
    }
  }
  this._getBranches = function(){
    const option = this._buildRequestOption();
    const res = UrlFetchApp.fetch(this.endpoint, option)
    const json = JSON.parse(res.getContentText())
    return json.data.repository.refs.nodes
  };
  this._formatBranch = function(branch){
    const commits = branch.target.history.nodes
    return {
      name: branch.name,
      lastCommit: commits[0]
    }
  };
  this._slateFilter = function(branch) {
    // 最後のコミットから3ヶ月以上経過しているもの
    // を取得する
    const moment = Moment.moment
    const today = moment()
    const commitDate = moment(branch.lastCommit.committedDate)
    const elapsedDate = today.diff(commitDate, 'days')
    return elapsedDate > 30 * 3
  }
}
```

その他の参考リンク:

- [GAS ビギナーが GAS を使いこなすために知るべきこと 10 選 - Qiita](https://qiita.com/tanabee/items/2c51681396fe12b6a0e4)
- [Google Apps Script Advent Calendar 2018 - Qiita](https://qiita.com/advent-calendar/2018/gas)