かまたま日記3

プログラミングメイン、たまに日常

Pull requestでPostされたBotのコメントを消すGitHub Actionを作った

皆さんCIで実行したテストやLint, カバレッジレポートの結果をbotがPull requestにpostするというのはやったことがあるのではないかと思います。 ただ、何回もPushしているとそのコメントが溜まってきて見づらくなったりすることは無いでしょうか?

たとえば弊社ではTerraformの設定ファイルが置いてあるレポジトリではCIで terraform plan をした結果をこんな感じでコメントとして貼り付けてます。

f:id:kamatama_41:20201118072616p:plain

(アプリケーションの環境ごとにポストしているので1 pushにつき4つのコメントが投稿されます)

これが毎pushごとに追加されるので、コミットが増えてくるとかなり見づらいです。今までは手動で消したり隠したりしてたのですが、自動で隠せるようにGitHub Actionを作りました。

github.com

使い方

こんな感じでActionを呼び出すだけです。絞り込み条件は2つあって、指定しない場合は過去に投稿されたコメント 全部を隠します ので、注意してください。

  • author: コメントしたアカウントのusername, たとえばbotのusername.
  • message_regex: 隠したいコメントの内容を正規表現でマッチさせる
on: pull_request

jobs:
  hide-pr-comments-action:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Hide PR comments
      uses: kamatama41/hide-pr-comments-action@v0
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        author: my-system-bot                 # OPTIONAL
        message_regex: "Test result: (OK|NG)" # OPTIONAL

実行するとこんな感じで過去のコメントが隠れます

f:id:kamatama_41:20201118073431p:plain

もし同じようなことで困っていたら、使ってみてください〜

Goでtesting.T#Helperを使う

Goのテストでヘルパーメソッドを作って共通のアサーションを定義することがあるかと思います。

package test

import "testing"

func Add(x, y int) int {
    return x + y
}

func TestAdd(t *testing.T) {
    AssertEquals(t, 2, Add(1, 1))
    AssertEquals(t, 3, Add(1, 2))
    AssertEquals(t, 4, Add(1, 3))
}

func AssertEquals(t *testing.T, expected, actual int) {
    if expected != actual {
        t.Errorf("Unexpected int\nexpected:%d, actual:%d", expected, actual)
    }
}

ここで言う AssertEquals が共通のアサーションですね。 これをこんな感じで、わざと失敗させてると以下のような出力になります。

func TestAdd(t *testing.T) {
    AssertEquals(t, 1, Add(1, 1))
    AssertEquals(t, 2, Add(1, 2))
    AssertEquals(t, 3, Add(1, 3))
}
=== RUN   TestAdd
    TestAdd: add_test.go:17: Unexpected int
        expected:1, actual:2
    TestAdd: add_test.go:17: Unexpected int
        expected:2, actual:3
    TestAdd: add_test.go:17: Unexpected int
        expected:3, actual:4
--- FAIL: TestAdd (0.00s)
FAIL

出力される行番号が全部ヘルパーメソッドの位置になってて実際のテストのどこで失敗したかが分かりづらいですね。 *1

こういうときは t.Helper() を呼ぶとそのヘルパーメソッドがcallerのスタックから除外されます。

AssertEquals関数を以下のように変更して実行します

func AssertEquals(t *testing.T, expected, actual int) {
    t.Helper()
    if expected != actual {
        t.Errorf("Unexpected int\nexpected:%d, actual:%d", expected, actual)
    }
}
=== RUN   TestAdd
    TestAdd: add_test.go:10: Unexpected int
        expected:1, actual:2
    TestAdd: add_test.go:11: Unexpected int
        expected:2, actual:3
    TestAdd: add_test.go:12: Unexpected int
        expected:3, actual:4
--- FAIL: TestAdd (0.00s)
FAIL

実際のアサーションを実行しているテストケースの行番号になりました。

まとめ

テストのヘルパーメソッドでは t.Helper() を呼び出すべし

*1:本来はこういうテストは一つのケースにまとめないでケースを分けるなどをした方が良いと思います

SEQSENSEの会社・仕事の紹介

2021/06/11 更新

はじめに

このエントリーは SEQSENSE株式会社 への採用 (主にクラウドチームのエンジニア) を目的としたエントリーです。もし興味を持った方は Findy or Wantedly から、もしくは @kamatama_41 まで直接連絡ください。

転職ドラフトにも参加しているので、よければラブコールをお願いします。

SEQSENSEについて

2016年10月に創業したロボットのスタートアップです。惑星探査機はやぶさに搭載されたローバーのミネルバの開発にも携わった明治大学黒田洋司教授の持つ自律移動ロボットの技術を広く一般に使える形で商用化したいという思いから、CEOの中村と組んで創業しました。

そういった経緯もあり、コアとなる技術は高度な自律移動性能を持ったロボットの製作技術なります。現在は深刻な人手不足に悩む警備業界にフォーカスを当てて、ロボット警備のサービスを提供しています。

2019年8月に商用化を開始して、2021年6月時点で都心のオフィスビルを中心に10の施設で警備ロボットが稼働しています。*1

2021年2月には東京都庁で実証実験も行い、SEQSENSEのロボット (SQ-2) が警備業務を1ヶ月間東京都庁で行いました。

www.metro.tokyo.lg.jp

  • 『世界を変えない』
  • 『ヒト型も、ネコ型も、目指さない。』

というちょっと変わったミッション・開発理念を持っていますが、これは「本当に実用性のあるロボットを世に送りだして、人口減少に向かう社会を、テクノロジーの力で悪い方向に変えない様にしよう」という意味を持っています。詳しくは以下のインタビューが参考になるかと思います。

xtech.mec.co.jp

弊社のYouTubeチャンネル *2

www.youtube.com

チームについて

人数はSlackの#randomにいる人が40人で、数えてみるとエンジニアとそれ以外の人が2:1くらいの割合でした。

エンジニアリングチームは大きく4つに分かれていて

  • ハードウェア (ロボットのハードウェアの設計・製造)
  • ロボソフト (ロボ上で動く自律移動ソフトウェアの開発)
  • クラウド (ロボットを管理するWebアプリなどの開発)
  • AI (画像認識モジュールの開発)

というチームで仕事を行っています。全然使っている技術が違うので、普段はそれぞれのチームで開発を行っていますが、当然チームをまたいだ機能開発なんかもあるので、その場合は連携して仕事を行います。

クラウドチームの仕事

ここからは私の所属しているクラウドチームの仕事の紹介になります。

クラウドチームが担当しているのは主にクラウド上で動くアプリケーションの開発・運用です。具体的には以下のようなモジュールがあります *3

  • 顧客 (警備員) が利用するWebアプリケーション
  • 管理者 (我々) が利用するWebアプリケーション
  • ロボットから上がってくる情報を処理するWorker
  • ロボット上で動くクラウドと情報のやり取りを行うagentプログラム
  • ロボットからの動画・音声を配信するサーバ

技術スタックとしては、顧客用のWebアプリケーションがRuby (on Rails)で書かれている以外は、バックエンドの処理はほぼGo言語で書かれています。フロントエンドはReact (Next.js)、内部サービス間の通信ではgRPC, GraphQLなんかが使われています。あとロボットから送られてくる動画をWebで配信する際にWebRTC (pion) を使っていたりもします。クラウドAWSを使っていて、前述のアプリケーションすべてがECS上のDockerコンテナとして動いています。

その他監視はDatadog, CIはGitHub Actions, インフラ管理はTerraformを使っています。

仕事の進め方としては、すべてのチケットはGitHubのissueとして上げるようにしていて、2週間に1回のスプリント計画MTGで各人がissue一覧から何をやるかを決めて進めていきます。細かい見積もりとかスプリントバックログを作ったりといった厳密さはないゆるいスクラム*4でやっています。今の所は人数が多くないのとそれぞれのメンバーの自律性が高いので大きな問題は出ずにやれています。

システム的な詳細は2020年6月にAWS IoT Loftで発表したときの資料がパブリックに上がってるものでは一番詳しいです

kamatama41.hatenablog.com

クラウドチームの技術的課題

割と大きめなのを上げるとこんなのがあります。当然この他にもサービスの機能追加・改修などは随時発生しています。

管理アプリケーションのリプレース

社内用の管理アプリケーションは歴史的経緯から、現在Django + jQueryで構築されているのですが、メンテナンス性に課題があるため *5 現在React (Next.js) + GraphQLの構成の新アプリケーションに移行中です。

OTA (over-the-air) アップデートの仕組み構築

ロボットのソフトウェアをクラウド経由で自動でアップデートする仕組みを構築する。

  • Docker imageの更新
  • systemdサービスの更新
  • その他細かいプロビジョニング
  • など

現在はメンテナンス時間を取って手動で作業を行ってるのですが、稼働するロボットが100台を超えるスケールになると回らなくなるので、この仕組の構築は急務で、現在はAWS IoT Jobsとスケジューラ (今の所Rundeckを検討中) で行う予定です。

データ分析の基盤構築

商用化を開始して2年ほど経ち、ユーザーやロボットのデータも蓄積してきたので、データ分析をして素早い意思決定やサービス改善に活かしていきたいと考えています。サービス上にある各種統計情報・ユーザーのアクセス情報などデータを集めるところからBIダッシュボードツールを入れてレポーティングするところまで、やること盛りだくさんです。

エレベーター連携

現在は三菱電機さんとクラウド側で連携してロボットをエレベーターに乗せているのですが *6 オフィスビルには他にもたくさんのベンダーのエレベーターがあり、それらも使えるようにすることが喫緊の課題になっています。こちらは様々な方向性を検討中です。

SEQSENSEのクラウドチームで働いて面白いところ

いわゆる普通のWebサービスと一番違うところは、ロボットという実際のモノの開発に携われるところになります。自分の開発した機能でロボットが動いたりするところは非常にユニークな体験かなと思います。

技術的な観点でいうと、Webサービス以外でも、IoT的なロボットとクラウドの双方向のデータのやり取りだったり、動画配信、エレベーター連携など様々なモジュールがあり、それらを非同期で組み合わせて一つのサービスを実現しているところが、難しいながらもやりがいのあるポイントです。

また、SEQSENSEはハードウェアの設計からクラウドサービスの提供まですべて社内で内製しているので、多種多様なバックグラウンドの人と関われるところだったり、まだ未開拓の ロボット警備 という市場を1から作っていけるというのも、大変ですが面白いところだと思います。

*1:詳しくは https://www.seqsense.com/product/ を参照

*2:動画1つしか無いですが

*3:この他にも色々あります

*4:といって良いかも微妙なレベル

*5:DjangojQueryが悪いと言うよりは社内の技術スタック的な問題で

*6:https://www.risktaisaku.com/articles/-/22215

AWS IoT@Loft #11 で登壇してきた

iot-loft.connpass.com

スマートビルディングにおけるIoT活用の取り組み
というお題でスマートビルディングを警備するSEQSENSEのロボットのサービスについてお話してきました。

初めてのオンライン登壇、自分が話するときは200人近くの接続者がいたようで結構緊張しましたが、無難にこなせて(?) 良かったです。

スライドを見て興味を持っていただいたら是非ともお話しましょう!

www.wantedly.com

(追記) AWSさんの記事も投稿されました aws.amazon.com

RenovateでGoのプロジェクトの依存性を更新する

弊社でRenovateを使ってるので、知見のメモです

Privateリポジトリを含む場合

Renovateを適用したいGoのリポジトリのgo.modに同じorganizationの別のprivate repositoryを含む場合*1、その参照されているリポジトリにもRenovateを適用すれば、Renovateが持つGitHubトークンでそのprivate repositoryもgo mod download出来ます。

Support private go modules on github.com · Issue #3202 · renovatebot/renovate · GitHub

you don't need to check in a github token - Renovate will reuse the existing token for github.com. If you are running via the app, that means it uses the same token as Renovate uses to access the current repository, so you need to make sure Renovate is installed into the source repo too (Renovate can be disabled, but it must be installed in order to have permissions to read the contents).

一部のライブラリ更新頻度高すぎ問題

弊社の場合、主に golang.org/x/aws-sdk-goなんですが、頻繁に更新が来すぎて鬱陶しいです。他にも適宜更新にしていると結構業務時間取られるので、弊社では以下のようなポリシーにしています

  • 外部パッケージは週末にアップデート (して週明けにまとめてマージ)
  • 内部モジュールは更新され次第

renovate.json的にはこんな感じです (抜粋)

{
    "extends": [
        "config:base",
        "schedule:weekends",
    ],
    "postUpdateOptions": [
        "gomodTidy"
    ],
    "packageRules": [
        {
            "packagePatterns": [
                "^github.com/seqsense/"
            ],
            "schedule": [
                "at any time"
            ]
        }
    ],
    "timezone": "Asia/Tokyo"
}

go.sumのpackage sumが抜ける問題

RenovateでpostUpdateOptionsgomodTidy を指定している場合 *2 go mod tidy するときにリソースの都合で依存パッケージを全てはダウンロードしていないために、 go.sum からパッケージのsumが抜けることがあります。

これはかなり困る問題なんですが、Renovate側では今の所対策する予定は無いそうです。ということで、同僚がgo.sumを修正する用のGitHub Actionを作ってくれました。

github.com

このActionを以下のような感じで仕込めば、RenovateのPRに対して、go mod tidyを行って変更があったらコミットしてpushしてくれます.

name: go-mod-fix
on:
  push:
    branches:
      - renovate/*

jobs:
  go-mod-fix:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 2
      - name: fix
        uses: at-wat/go-sum-fix-action@v0
        with:
          git_user: @@MAINTAINER_NAME@@
          git_email: @@MAINTAINER_EMAIL_ADDRESS@@
          github_token: ${{ secrets.GITHUB_TOKEN }}
          commit_style: squash
          push: force

patchバージョンのみをAutomergeしたい場合

デフォルトではminorとpatchは同じブランチで管理されるので、patchのみをアップデートする場合 separateMinorPatch: true が必要。 ということでaws-sdk-goのpatch versionのみをautomergeしたい場合は以下のように書きます。

{
  "packageRules": [
    {
      "packagePatterns": [
        "^github.com/aws/aws-sdk-go"
      ],
      "separateMinorPatch": true,
      "patch": {
        "automerge": true
      }
    }
  ]
}

最後に

色々問題もありましたが、Renovateのおかげで弊社のGoプロジェクトはかなり健全な状態にできてるかと思います。もしRenovateを使おうと検討している場合、参考になれば幸いです。

*1:よくあるのは複数のプロジェクトから参照される共通コアライブラリ的なの

*2:というかこれをしたくない場合ってあるのかな?

Herokuで特定のGoのバージョンを使う

デフォルトはGo buildpackのデフォルトバージョンが使われます (2020/05/31現在1.12.17)

これを変えたい場合以下の方法があります

  • go.mod// +heroku goVersion go1.14 というディレクティブを追加する
  • アプリケーションの環境変数GOVERSION を追加する (例: GOVERSION=go1.14)

go.modgo ディレクティブのバージョンを使うという提案もあるようで、もう少ししたら go.mod のバージョンを使うようになるかもしれません

参考

elements.heroku.com

GitHub Actionsでテストを分割する

やりたいこと

GitHub Actionsでテストケースを分割して、複数ノードで実行する

CircleCIだとプラットフォーム側で対応してくれていますがGH Actionsだと見当たらなかったので同等のことをやるための設定のメモです。

設定例

  rspec:
    runs-on: ubuntu-latest
    strategy:
      max-parallel: 2
      matrix:
        ci_node_total: [2]
        ci_node_index: [0, 1]
    steps:
      - uses: actions/checkout@v2
      - uses: ruby/setup-ruby@v1
      - name: Bundle install
        run: bundle install
      - name: DB setup
        run: bundle exec rake db:setup
      - name: Rspec
        run: |
          ruby -e 'Dir.glob("spec/**/*_spec.rb").each{|f| puts f}' | awk "NR%${CI_NODE_TOTAL}==${CI_NODE_INDEX} {print}" > /tmp/tests-to-run
          cat /tmp/tests-to-run
          bundle exec rspec $(cat /tmp/tests-to-run)
        env:
          CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
          CI_NODE_INDEX: ${{ matrix.ci_node_index }}

GitHub ActionsにはBuild Matrix という機能があって、本来はOSのバージョンと言語のバージョンの複数のマトリクステストを実施するための機能なのですが、これを使って ci_node_totalci_node_index という2つのパラメタを登録します。

  • ci_node_total: ノードの合計, max-parallel と同じ数にする必要がある
  • ci_node_index: ノードのインデックス, 0始まりで ci_node_total-1 で終わる配列

これで、 rspec のJobは (2, 0) と (2, 1) という2つの設定で実施されることになります

f:id:kamatama_41:20200518161853p:plain

あとはindex毎にspec配下のファイルを分割してrspecコマンドに渡すだけです。CircleCIだとsizeの大きさとか各exampleのかかった時間とかでいい感じに平準化してくれますが、GitHub Actionsにはそういう機能は無いので、単にspecファイルを2分割して渡すのみです。

ちなみに、Go言語の場合はこんな感じでテストを分割実行できます

$ go test $(go list ./... | awk "NR%${CI_NODE_TOTAL}==${CI_NODE_INDEX} {print}")