tanikoのぶろぐ

GitHubのPRレビュー依頼をSlackに通知する

この記事はDMMグループ Advent Calendar 2020 13日目の記事です。

初めに

プルリクエストのレビュー依頼をSlack上で行っていました。 PRを作成するたびに、Slackで「レビューをお願いします」と書くのが面倒だったので、レビュー依頼を半自動化させました。
BotによるDMもしくは、チャンネルへのメッセージによる通知を行います。

GitHubのPRまたはIssueにラベルをつけるとSlackにメッセージが飛びます。 個人、グループメンションに対応しています。Reviewersにも対応。 リポジトリのHooks設定、YAML設定するだけ。

1年以上前にコードを書きました。TypeScriptの知識も少なかったので、コードが汚いですが、ご了承ください。  

uzr

弊社では、GitHub Enterpriseのアカウント名とSlackのアカウント名が同じです。多くの人は、 aratani-yuya みたいに、姓-名 の形になっています。
そのため、 GitHubのアカウント名をそのまま使えば、Slackへの通知が可能になっています。もしも違うのであれば、変換の処理を入れる必要がありますが、今回はそれを省きました。
PRのラベルを指定のものにする、もしくはReviewersに誰かを追加すると、レビュー依頼が自動で出されるようになります。

レビュー依頼設定

1リポジトリに対して、複数のラベルの設定ができるようになっています。これは複数のチームが同じリポジトリを操作する可能性があるためです。

// setting/pr.yml
owner/repo:
  - label: Team-A
    channel: team-a-channel
    mentions:
      - team-a
    text: 'お手すきの際にレビューお願いします。'

Slack通知の流れ

  1. GitHubのWebhookをCloud Functionsに送る
  2. KMSからWebhookのSecret、SlackのTokenを取得
  3. Cloud Storageから設定を取得
  4. Slackにメッセージを送る
    • DM
    • チャンネルへメンション

以下のようなメッセージがチャンネル宛、もしくはDMで届きます。

@team-a
お手すきの際にレビューお願いします。

[PRタイトル]
by [User]
https://github.com/owner/repo/pull/1

ソースコード

exports.uzr = async function(req: Request, res: Response) {
  // Pingリクエストのチェック
  if (isPingRequest(req)) {
    return res.status(200).send('pong');
  }
  // GitHub WebhookのSecretの取得
  const secret = (await decrypt(env('GITHUB_SECRET', ''), KMS_SETTING)).plaintext.toString().trim();
  const github_host = env('GITHUB_HOST', '') as string;

  // GitHub Enterpriseからのアクセスかを確認
  if (!isValidIP(req.ip) || !validate(req, secret, github_host)) {
    return res.status(403).send('');
  }

  // labeled、review_requestedのWebhookかを確認
  if (!isAllowRequest(req)) {
    res.status(200).send('Not support request');
    return;
  }

  // Webhookのリクエストを判断
  const type = detectRequestType(req);
  const storage = new Storage();
  const bucket = storage.bucket(BUCKET_NAME);
  let message: Message | undefined;
  try {
    // Slackのメッセージを組み立て
    if (type === RequestType.REVIEW) {
      const data: PullRequest = req.body;
      const targets = parseJsonToReviewTargetsByLabel(data.repository.full_name, await getYamlFile(bucket.file(SETTING_FILE_PR)));
      message = createMessagesByLabel(targets, data, type);
    } else if (type === RequestType.REVIEW_REQUEST) {
      const data: ReviewerRequest = req.body;
      const users: User[] = await getUsers(bucket.file(USERS_FILE))
      const targets = getTargetsBySlackUsers(users, data.requested_reviewer.login);
      message = createMessageByReviewer(targets, data, type);
    } else if (type === RequestType.ISSUE) {
      const data: IssueRequest = req.body;
      const targets = parseJsonToReviewTargetsByLabel(data.repository.full_name, await getYamlFile(bucket.file(SETTING_FILE_ISSUE)));
      message = createMessagesByLabel(targets, data, type);
    } else {
      res.status(200).send('Not support request');
      return;
    }
  } catch (e) {
    console.log(e.toString());
    res.status(500).send('error');
    return;
  }

  if (message != undefined) {
    // Slackにメッセージを送信
    const groups = await getUserGroups(bucket.file(GROUPS_FILE));
    const slack_token = (await decrypt(env('SLACK_TOKEN', ''), KMS_SETTING)).plaintext.toString().trim();
    const client = new WebClient(slack_token);
    await send(client, message, groups);
  }
  res.status(200).send('ok');
};

終わりに

主に弊チームで運用されているレビュー依頼半自動化のコードでした。リポジトリはこちら
レビュー依頼だけでなく、PRへのコメントの通知も、行えるようにできたらいいなと思います。

明日は yo_chan_yo_chan さんです。

文章生成をやってみる

文章作成機(?)を作る方法をなるべく簡単(?)に説明したいと思います. とても, 無能なものです.

はじめに

どんなものかと簡単に説明すると, 複数の文章を元に, 文章を作成するというものです.
PHPで雑に実装したので, それもどうぞ. https://github.com/taniko/udon

仕組みを雑に

まずはじめに, 元となるデータを用意します. 簡単にするために, 以下のような文章を用意します. 猫派の方はごめんなさい.

  1. 私は犬が好きです
  2. 私は猫が嫌いです

この2文を形態素解析をすると以下のようになったとします. MeCabとか使えばいいと思います.

  1. 私 は 犬 が 好き です
  2. 私 は 猫 が 嫌い です

これを1つの状態遷移図で表すと以下のようになります.

この状態遷移図から文章を生成してみます.

  1. 私は犬が好きです (本来の文章)
  2. 私は猫が嫌いです (本来の文章)
  3. 私は犬が嫌いです (新たな文章)
  4. 私は猫が好きです (新たな文章)

新たに2つの文章が得られました. 簡単ですね. そんなわけで, 以上, 文章作成機の仕組みでした.

もう少し面白く

仕組み自体は, あまり変わりませんが, 次の状態を現在の状態のみに依存させると面白くなるので, それについての話です. 実装したのはこっち.

以下のような3文があったとします.

  1. 私 は 犬 が 好き です
  2. 私 は 猫 が 嫌い です
  3. 私 が し ます

これを状態遷移図に書き起こします.

今までの通りにやると, 以下の5文が生成できます.

  1. 私は犬が好きです (本来の文章)
  2. 私は猫が嫌いです (本来の文章)
  3. 私がします (本来の文章)
  4. 私は犬が嫌いです (新たな文章)
  5. 私は猫が好きです (新たな文章)

もし, 次の状態を決めるのが, 現在の状態のみに依存するとなると以下のような表と図ができます.

現在の状態次の状態確率
2/3
1/3
1/2
1/2
1/2
1/2
1/3
好き1/3
嫌い1/3
ます1/1
好きです1/1
嫌いです1/1

こうすることで, 新たに以下のような文が生成できるようになります.

  • 私が好きです
  • 私が嫌いです

このように, 現在の状態のみに依存させると, 新たな文章が作成できるようになりました. 実装するなら, 下の表みたいに, 文の頭にBOS(Begin of Sentence), 文の終わりにEOS(End of Sentence)などを差し込んで, どの語から始めることができ, どの語で終わることができるのかを明確にした方がいいと思います.

現在の状態次の状態確率
BOS3/3
ますEOS1/1
ですEOS1/1

ちなみに, 私のツイートから生成された文章はこんな漢字です.

おしまい.

調べると幸せになる用語

  • MeCab
  • mecab-ipadic-neologd
  • マルコフ連鎖

おわりに

PassCodeの"Tonight/Taking you out"を買いました. ライブDVDがほしかったので. (下の映像は入っていないです.)

PassCode - ONE STEP BEYON

PassCode - Tonight

Laravelが 201 Createdを返すってさ

Laravel 5.6での話です. 5.6から, 新たにモデルを作成した際に200 OKではなく, 201 Createdを返すようになったってだけの話です. (今更?)

どういうことか

LaravelをAPIとして利用していた時に, Eloquentモデルを返した際に, 200だったのが, 201に変更になりました.

// app/Http/Controllers/PostController.php
<?php

namespace App\Http\Controllers;

use App\Post;
use App\Http\Requests\Post\CreateRequest;

class PostController extends Controller
{
    public function create(CreateRequest $request)
    {
        return $request
            ->user()
            ->posts()
            ->save(Post::make($request->only(['text'])));
    }
}

こんな感じのコードがあった場合, 5.5までだと200 OKで新規に作成されたPostのデータがレスポンスとして, 返ってきていました. しかし, 5.6からは201 CreatedとPostのデータが返ってくるようになりました.
特に副作用がなさそうですが, テストやライブラリで200かを使っていれば思わぬ動作をしてしまいます. 皆さんは, axiosとかでasync/awaitとかthen/catchとか使っているので大丈夫ですよね. 私はPHPUnitでassertStatus(200)とかしていたのでテストが転けた.

地味な変更点ではあるかと思いますが, せっかく201 Createdという, ちゃんと意味があるものがあるので, 嬉しいですね.

ちなみに

この話自体はLaravel 5.5の時に出ていて, PRも出されていましたが, バグフィックスとかではなく, 破壊的変更であったため, 見送られ, 5.6でPRがマージされました. 仕組み自体はRouterでレスポンス(コントローラでreturnしたもの)がIlluminate\Database\Eloquent\Modelで,wasRecentlyCreatedtrueだったら201 Createdで返すって感じです. 詳しくはコードを.

PHPの面白いところ

注意 - 皮肉的な記事です. いくつか使っていて, 辛いところを記述するので, ネタみたいな記事です.

配列周り

PHPには, 配列がありまして, $foo = [1, 2, 3]とかです. 前までは$foo = array(1, 2, 3)でした.
そんな配列なのですが, JavaScriptとかRubyとかと違って, arrayであって, array objectではないわけです. そんなわけで, 配列の操作がしたければ, array_*()を使うわけです. array_push()に関しては, $array[] = $valでできます.
そんなPHPの配列で面白いところは, array_map()です. array_push($array, $val_1), array_filter($array, $callback), array_reduce($array, $callback)など, 第1引数に配列を持ちます. しかし, array_map()array_map($callback, $array)と順番が逆なのです. これが辛い. 統一していて欲しい.
PHPの配列が辛い人は, Illuminate\Support\Collectionを使ってオブジェクトにすればいいと思うので, composer install illuminate/supportしましょう.

オブジェクトのコピー

PHPのオブジェクトを別の変数に渡す時, cloneを利用します.

<?php
class Sample {
    public $count = 0;
}

$foo = new Sample();
$bar = clone $foo;

$bar->count++;

var_dump($foo->count); // int(0)
var_dump($bar->count); // int(1)

もしもcloneを使用しないと, 思わぬ副作用を生み出します.

<?php
class Sample {
    public $count = 0;
}

$foo = new Sample();
$bar = $foo;

$bar->count++;

var_dump($foo->count); // int(1)
var_dump($bar->count); // int(1)

$foo->countも書き換わってしまうんですね. そんなcloneですが, クローンが作成される際に呼ばれるマジックメソッド__clone()というものがあります. これは作成された新たなオブジェクトで呼ばれます.

<?php
class Sample {
    public $count = 0;

    public function __clone() {
        $this->count++;
    }
}

$foo = new Sample();
$bar = clone $foo;

var_dump($foo->count); // int(0)
var_dump($bar->count); // int(1)

これにより, 遺伝子操作されたクローンを作ることができます. 使う機会があるかはわかりませんが.

ついでに

homebrewでのPHPのインストール方法が変わりました. いままではbrew install php72だったのですが, これからは, brew install php@7.2という形になります. あと, xdebugがインストールされていないようなので, 調べたらpeclを使ってとあったので, pecl install xdebugをしましょう. 詳しくは GitHubのIssueで https://github.com/Homebrew/homebrew-php/issues/4721

Clipasってサイトを作りました

ただ単に, SNSのアカウントなどのURLをまとめるサービスであるClipasを作りました.

clipasについて

どんなサービスかは見てもらう方が早いと思うので私のページのURLを載せておきます.

https://clipas.me/taniko

自分に関するURLまとめておくだけのサービスなので, SNS, ホームページ等のURLを載せて, このサービスのURLを他のサービスのURL欄に載せておくだけです. 自分のプロフィールへのリンク, アカウント情報を1箇所にまとめられるので便利.

作った理由

Twitter, GitHub, Instagramなどでプロファイル欄にURLを載せられる場所ありますよね? そこにが何のURLを載せていますか? どのURLを載せるか悩んでいませんか?
私は TwitterのはGitHubのリンク. GitHub にはホームページのリンクを載せていました. 他にもFlickrやInstagramなどでも様々でした. 1箇所で自分のアカウント一覧を表示してくれるサービスを作りました.

今後

clipasに説明欄が足りないなと思ったので, あとで加えようと思います. また, 公式(?)サービス一覧追加のリクエストフォームがあればいいなと. あと, できればユーザアイコンも.