walkingmask’s development log

IT系の情報などを適当に書いていきます

MENU

Google Apps ScriptでGitHubのProjectsの内容をSlackに流す

またGASネタです。GASの記事ばかり書いてて、GAS芸人になりつつあります。

下記の記事を見て、個人的に研究のTodoをProjectsで管理していて、それをSlackに流している話を書こうと思いました。

soudai.hatenablog.com

GASなのでGitHubにコードをあげていないのですが、雰囲気だけ記録したいと思います。

とりあえず見た目ですが、まずProjectsがあって、

f:id:walkingmask:20180410204554p:plain

変更があったりすると、Slack botがこんな感じでreportしてくれます。

f:id:walkingmask:20180410204643p:plain

この子(bot)がやってるのは、

  • Projectsで「カード追加」「カード移動」があった時にその内容をSlackに流す
  • GASのトリガーを使って毎朝Projectsの内容を(長かった場合は省略して)流す

です。

ProjectsのColumnやCardsをGitHub APIで取ってきてGAS経由でSlackに流しています。

かなり個人利用を目的にしたものなので、チーム用途には向いてない感じですね。

何かの参考になれば幸いです。

作り方

各種セットアップ

GitHubリポジトリ作成、Projectsの作成、APIトークン取得は済んでるものとします。

Permissionはrepoだけチェックしておけばok。

トークンをメモったら、cURLでプロジェクトのURLとidを取得します。

OWENER="Your GitHub Account Name"
REPOSITORY="Repository Name"
TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
curl \
-H "Accept: application/vnd.github.inertia-preview+json" \
-H "Authorization: token ${TOKEN}" \
"https://api.github.com/repos/${OWENER}/${REPOSITORY}/projects"

レスポンスのhtml_urlとidをメモっておきます。次に、このidを使ってcolumn idを取得します。

TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
PROJECT_ID="xxxxxxx"
curl \
-H "Accept: application/vnd.github.inertia-preview+json" \
-H "Authorization: token ${TOKEN}" \
"https://api.github.com/projects/${PROJECT_ID}/columns"

レスポンスのidとnameのペアをメモしておきます。

次に、この辺を参考にSlackのWebhookを用意します。

これで、GitHub側とSlack側の準備は完了です。

GAS

GASを作っていきます。

まずは、プロパティを埋めていきます。PropertiesServiceとか使っても良いと思いますが、ここでは簡単に変数に格納します。channel、botname、iconemoji、title、colorは任意です。

var properties = {
    "github_api": "https://api.github.com",
    "github_api_token": "xxxxx",
    "github_secret_key": "xxxxxx",
    "proj_name": "hoge",
    "proj_url": "https://github.com/walkingmask/hoge/projects/1?fullscreen=true",
    "proj_columns": [
        {
            "title": "To do😡",
            "id": "xxx",
            "color": "danger"
        },
        {
            "title": "In progress😨",
            "id": "xxx",
            "color": "warning"
        },
        {
            "title": "Done😇",
            "id": "xxx",
            "color": "good"
        }
    ],
    "slack_web_hook": "https://hooks.slack.com/services/xxx",
    "slack_channel": "#_hoge",
    "slack_bot_name": "Progress of walkingmask",
    "slack_icon_emoji": ":walkingmask:",
};

Projectsから色々取ってきて整形する感じのものを定義します。

// Projectsのカラムに含まれるカードを取得するやつ
function getCards(api, column_id, token) {
    var cards = [];
    var url = api + "/projects/columns/" + column_id + "/cards";
    var options = {
            "headers": {
                "Accept": "application/vnd.github.inertia-preview+json", // 試験段階だから特殊なヘッダーが必要
                "Authorization": "token " + token,
            }
        }
    var response = UrlFetchApp.fetch(url, options);
    response = JSON.parse(response.getContentText());
    for (var i in response) {
        cards.push(response[i].note);
    }
    if (1 > cards.length) {
        // 何もなかった
        cards.push("( ᐛ👐) パァ");
    }
    return cards;
}

// Projectsの内容を取得するやつ
function getKanban() {
    var columns = properties.proj_columns;
    var api = properties.github_api;
    var token = properties.github_api_token;
    var kanban = [];
    for (var i in columns) {
        var cards = getCards(api, columns[i].id, token);
        kanban.push({"title": columns[i].title, "cards": cards, "color": columns[i].color});
    }
    return kanban;
}

// カードを整形するやつ
function formatCards(cards) {
    var num_cards = cards.length;
    var formated_cards = "";
    for (var i=0; i < (num_cards>3?3:num_cards); i++) {
        formated_cards += "• " + cards[i] + "\n";
    }
    if (num_cards > 3) {
        var rest = num_cards - 3;
        formated_cards += "and "+rest+" items👍\n";
    }
    return formated_cards;
}

// Projectsを整形するやつ
function formatKanban(kanban) {
    var attachments = [];
    for (var i in kanban) {
        var formated_cards = formatCards(kanban[i].cards);
        attachments.push({
            "fallback": kanban[i].title,
            "color": kanban[i].color,
            "fields":[
                {
                    "title": kanban[i].title,
                    "value": formatCards(kanban[i].cards),
                    "short": false
                }
            ]
        });
    }
    return attachments;
}

Slackに投げる関数を定義します。

function postSlack(title, attachments) {
    var payload = {
        "channel": properties.slack_channel,
        "username": properties.slack_bot_name,
        "icon_emoji": properties.slack_icon_emoji,
        "text": title + " <" + properties.proj_url + "|[URL]>",
        "attachments": attachments,
    };
    var options = {
        "method": "post",
        "contentType": "application/json",
        "payload": JSON.stringify(payload)
    };
    UrlFetchApp.fetch(properties.slack_web_hook, options);
}

あとは、Projectsの変更に応じて動いたり、日報するための関数を定義します。

// GitHubのHook(Projectsの変更)からPOST受けたら動くやつ
function doPost(request){
    eventReport(request.parameter.payload);
}

// Projectsの変更をSlackに流すやつ
function eventReport(payload) {
    var payload_json = JSON.parse(payload);
    var action = payload_json.action;
    var column = id2Title(payload_json.project_card.column_id, properties.proj_columns);
    var value = "• " + payload_json.project_card.note;
    var field_title;
    if (action == "created") {
        field_title = "Created new task in " + column;
    }
    else if (action == "edited") {
        field_title = "Edited task in " + column;
    }
    else if (action == "moved") {
        var changes = payload_json.changes;
        if (!changes) {
            return;
        }
        var from_column = id2Title(changes.column_id.from, properties.proj_columns);
        value += "\n" + from_column + " :arrow_right: " + column;
        field_title = "Moved task";
    }
    else {
        return;
    }
    var attachments = [{
        "fallback": "Event report",
        "color": "#439FE0",
        "fields": [
            {
                "title": field_title,
                "value": value,
                "short": false
            }
        ]
    }];
    postSlack("Event report", attachments);
}

// 日報するやつ(トリガーで動かす)
function dailyReport() {
    var kanban = getKanban();
    var formated_kanban = formatKanban(kanban);
    postSlack("Daily report", formated_kanban);
}

dailyReportは朝とかに動くようにトリガーを設定してあげてください。

可愛い便利関数はこちら。

function id2Title(column_id, columns) {
    for (var i in columns) {
        if (column_id == columns[i].id) {
            return columns[i].title;
        }
    }
}

まとめ

書いてる間に何で作ったんだろとか思ってたんですが、自分の研究の進捗を研究室メンバーにシェアするために作ったことを思い出しました。

(最近研究に進捗がなくて毎日同じレポートをするだけbotになっている...)

このbot自体は汎用性低いと思うのですが、GitHubでProjectsのAPIを叩いたり、columnやcardsを整形したりといった部分が参考になれば良いなと思います。

GitHubのProjects自体はTodo管理としてはシンプルでありながらも使いやすいなと感じてます。

あとは、issuesと連携するような動きを追加するともっと面白そうですね。

APIがプレビュー段階なので、今後の動向にも注目です。