LiBz Tech Blog

LiBの開発者ブログ

GAS から Github に Pull Request を出してみた

f:id:libinc:20190906103500p:plain

イントロ

唐突ですが8月中頃ヘルニアになりました。

10日間立てなかったですし未だ歩くのが遅いです。どれくらいかというと80代ほどのおじいちゃんとどっちが早く歩けるか歩道で競い合ってます。

なので身の回りのお世話をお願いすることが多かったんですが、代わりに何かをしてもらうって結構大変でした。

  • 取って欲しい物と見られちゃマズい物が混在する棚から物を取ってもらう(ハラハラする)
  • 引き出しにあるはずなのに、開けて見たけどないと主張される(いや絶対にある)
  • アプリケーションで使うデータの Pull Request を代わりに出してもらいたいのにできない

じゃあ GAS からサクッとできるようにしよう!

モチベーション

アプリケーションで使用するデータ(マスターデータなど)をエンジニア以外の人間が更新できると便利な場面て結構ありますよね。

ソシャゲで言えば日々追加されるモンスターやアイテムなど、更新頻度が高いマスターデータが特に該当するかと思います。 そういったデータを企画職の人間が直接更新するようなイメージです。

私の関わっているサービスはマスターデータを Spreadsheet で管理しています。 なので、そこから企画職の人間が直接 Pull Request を出せるようになると便利だなーということで、GAS を使って Pull Request (以下 PR ) を出せるようにしてみました。

f:id:libinc:20190906135315p:plain
マスターデータのサンプル

実装

小話その1

PR 作成にあたり避けて通れないのは GithubAPI のどのバージョンを採用するかという問題です。 GitHubAPI の最新バージョンは v4 ですが、API v3 までは REST、API v4 からは GraphQL と大きな違いがあります。

今回は過去の偉人たちの資産をドキュメント的・コード的に活用できる算段で躊躇なく v3 を採用しました。

developer.github.com

小話その2

また実際やってみてわかったこととしてGit の仕組み上、管理する実態のファイルが「新規作成」or「更新」されたものかで、すべきことが変わります(!) これら2つに分けてやったことと実装を書いていきますが理由は省いています。

公式ガイドに Git の仕組みについて記載があるので読んだことない人はぜひ読んでみてください。

git-scm.com

小話その3

今回実装にあたり先をゆく偉人たちのコードを使用させていただいています。

使用したライブラリ

gasdump/GitHubAPI.gs at githubapi · matsubara0507/gasdump · GitHub

使用例(実装された方の記事)

GitHub API を GAS でいい感じで叩くためのライブラリを作った (未完)

ファイル新規作成

早速、実装していきます。

まずは GithubAPI を叩くためのライブラリを呼び出します。

var option = { name: prop.NAME, email: prop.EMAIL };
var github = new GitHubAPI.GitHubAPI(prop.GITHUB_USERNAME, prop.GITHUB_REPO, prop.GITHUB_TOKEN, option);

※ 脱線するので省きますが、アクセスキーやユーザネームなど重要なデータは Spreadsheet のプロパティ機能を使って管理しコード上で参照するようにしてます。

PR を出すブランチ名を決め、実装ブランチを基底ブランチから切ります。

var newBranchName = 'feature/hoge_' + Utilities.formatDate(date, option.tz, "yyyyMMddHHmm");
github.createBranch(prop.BASE_BRANCH_NAME, newBranchName);
var branch = github.getBranch(newBranchName);

ファイルを新規作成し、実装ブランチにコミットします。

今回マスターデータとして Spreadsheet のセルに記入されたデータから JSON を生成し、content の中に格納してます。treeblob などの概念は公式ガイドを読んでみてください。

createFile(github, content, type, id, newBranchName, branch);
function createFile(github, content, type, id, newBranchName, branch) {
  // 新しいブランチにコミットする
  var pTree = github.getTree(branch['commit']['commit']['tree']['sha']);
  var blob = github.createBlob(content);
  var data = {
    'tree': pTree['tree'].concat([{
      'path':'hoge.json',
      'mode': '100644',
      'type': 'blob',
      'sha': blob['sha']
    }])
  };
  var tree = github.createTree(data);
  var commit = github.createCommit('[update hoge_file] hogehoge', tree['sha'], branch['commit']['sha']);
  github.updateReference(newBranchName, commit['sha']);
}

実装ブランチから PR を作成します。

var title = 'hoge_' + Utilities.formatDate(date, option.tz, "yyyyMMddHHmm")
var body = makeBody(id, type, message);
github.createPullRequest(title, newBranchName, prop.BASE_BRANCH_NAME, body);
function makeBody(id, type, message) {
  var body = [];
  body.push( 'こんな感じでPRが作成されます');
  body.push('ちゃんと実装すれば、Description から Commit メッセージの変更、 Reviewers や Assignee のアサインも可能です。');
  return body.join('\r\n')
}

ここまで実装したら PR を作成する準備ができました。

スクリプトエディタのメニューから、実行>関数を実行>メソッド名(任意です)、を押してテストしましょう。

f:id:libinc:20190906122043p:plain

PRが作成されました。

ファイル更新

ファイルを新規作成する代わりに既存ファイルを呼び出して上書きする必要があります。

コード上の表現でいえば baseContent を取得し createFile の代わりに updateFile を呼び出すだけです。それ以外は全て同じ流れになります。

// 既存ファイルを呼び出す
baseContent = github.get('/contents/hoge.json', { 'ref': newBranchName })

// 既存ファイルの上書き
updateFile(github, content, type, id, newBranchName, baseContent);
function updateFile(github, content, type, id, newBranchName, baseContent) {
  // 新しいブランチにコミットする
  github.updateContents(
   'hoge.json',
   '[update hoge_file] hogehoge',
    Utilities.base64Encode(content, Utilities.Charset.UTF_8),
    baseContent['sha'],
    newBranchName
  );
}

細かいネタ

関数を実行すれば PR が作成されるようになりましたが、都度実行するのはメンドウです。メニュー化しちゃいましょう。 これでエンジニア以外も楽に PR を出せるようになりました。

function onOpen() {
  var ui = SpreadsheetApp.getUi();
  var menu = ui.createMenu('Github');
  menu.addItem('PRを作成', 'メソッド名(任意です)');
  menu.addToUi();
}

このようになります。 f:id:libinc:20190906124825p:plain

結論

GAS から PR を出すことができました。

Git が CUI から操作する裏で内部的にどういったことを行っているか理解するよいきっかけになりました。

GAS に関してもセキュアにアクセスキーなど機密情報を使うにはどうすべきか学べたし、思った以上に多機能だったことに驚きました。

これで安心して腰を労った生活が送れそうです。

コード全文

このコードではファイルが存在しなければ新規作成、ファイルが既に存在すれば更新となるよう実装してます。 使用者(エンジニア以外)はファイルの存在を意識せずに PR が出せます。

// Github API v3
// see `using library` https://github.com/matsubara0507/gasdump/blob/githubapi/GitHubAPI/GitHubAPI.gs
// see `github api v3 reference` https://developer.github.com/v3

function createPullRequest(type, content, id, message) {
  const prop = PropertiesService.getScriptProperties().getProperties();
  const date = new Date();
  
  var option = { name: prop.NAME, email: prop.EMAIL };
  var github = new GitHubAPI.GitHubAPI(prop.GITHUB_USERNAME, prop.GITHUB_REPO, prop.GITHUB_TOKEN, option);
  
  // developから新しいブランチ切る
  var newBranchName = 'feature/hoge_' + Utilities.formatDate(date, option.tz, "yyyyMMddHHmm");
  github.createBranch(prop.BASE_BRANCH_NAME, newBranchName);
  var branch = github.getBranch(newBranchName);
  
  var baseContent = null;
  var isNewMaster = false;
  
  // 指定ファイルが存在しなければ404エラー
  try {
    // エラーが起きない場合、指定ファイルは既存
    baseContent = github.get('/contents/hoge.json', { 'ref': newBranchName })
  } catch(e) {
    // エラーの場合、指定ファイルは新規
    isNewMaster = true;
  }
  
  if (isNewMaster) {
    createFile(github, content, type, id, newBranchName, branch);
  } else {
    updateFile(github, content, type, id, newBranchName, baseContent);
  }
  
  // 新しいブランチからdevelopへPR出す
  var title = 'hoge_' + Utilities.formatDate(date, option.tz, "yyyyMMddHHmm")
  var body = makeBody(id, type, message);
  github.createPullRequest(title, newBranchName, prop.BASE_BRANCH_NAME, body);
}

function createFile(github, content, type, id, newBranchName, branch) {
  // 新しいブランチにコミットする
  var pTree = github.getTree(branch['commit']['commit']['tree']['sha']);
  var blob = github.createBlob(content);
  var data = {
    'tree': pTree['tree'].concat([{
      'path': 'hoge.json',
      'mode': '100644',
      'type': 'blob',
      'sha': blob['sha']
    }])
  };
  var tree = github.createTree(data);
  var commit = github.createCommit('[update hoge_file] hogehoge', tree['sha'], branch['commit']['sha']);
  github.updateReference(newBranchName, commit['sha']);
}

function updateFile(github, content, type, id, newBranchName, baseContent) {
  // 新しいブランチにコミットする
  github.updateContents(
   'hoge.json',
    '[update hoge_file] hogehoge',
    Utilities.base64Encode(content, Utilities.Charset.UTF_8),
    baseContent['sha'],
    newBranchName
  );
}

function makeBody(id, type, message) {
  var body = [];
  body.push( 'こんな感じでPRが作成されます');
  body.push('ちゃんと実装すれば、Description から Commit メッセージの変更、 Reviewers や Assignee のアサインも可能です。');
  return body.join('\r\n')
}