github actions で ci が通ったら reviewers を自動でアサインできるようにしてみた

PR のレビュー依頼したいけど CI 通ってない。CIが通ったら依頼を出そうと思って忘れることがそこそこあります。
そのため github のラベルを付けといたら、CIが通ったタイミングでレビュー依頼を出してくれる君を作りました。

name: auto-assign

on:
  schedule:
    - cron: "*/10 1-10 * * 1-5"

jobs:
  init-echo:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: checkout2
        run: gh pr list --search "status:success label:auto-assign" --json number,reviewRequests |  jq '.[]|select(.reviewRequests|length == 0)|.number' | xargs -I@ gh pr edit @ --remove-label auto-assign --add-reviewer ここを書き換える
        env:
          GITHUB_TOKEN: ${{ secrets.XXX_GITHUB_TOKEN }}

もし Auto assignment を利用していた場合には add-reviewer に team を入れたらランダムにアサインされます.
Managing code review settings for your team - GitHub Docs

悩んだ点は、1つはCIが通ったタイミングで発火するちょうど良いトリガーが見つからなかったので cron で定期実行していることです。
これは何かいい方法があったら変えたいです。

もう一つはgithub cliの不具合でsecrets.GITHUB_TOKENでは権限が足りずに動かないこと。
既に issue になっていますが、不必要に強い権限を要求するようになっているようです。
github.com そのため、暫定的に personal access token を利用して回避しております。

対象のPRを絞る条件は何か甘いかもしれないんですが、今のところは動いていそうな感じです

github actions で少し複雑な workflow を組んでいると auto-merge を使うのが厳しい感じがする

これは 2021/12/12 時点の情報で github actions のアップデートが早いので近いうちに治っているかも。
また、軽く調査しただけなので、間違ったことを発信しているかも。

やりたいこと

github で PR の CI が全部通ったら自動的にマージしてほしい

前提条件

レポジトリの workflow は簡略化した具体例を書いておきます(動きません)

ポイントとしては以下のとおりです。

  • フロントとバックエンドで workflow が別れている
  • path で workflow の起動条件を絞っている
  • バックエンドは jobs を2つに分けており直列実行している
    • テスト分割のため martix を利用しており、job を1つにすることが難しい

サンプル

name: front

on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
    path:
      - "front/**"

jobs:
  test:
    runs-on: ubuntu-latest
    steps: 
      - name: test
        run: yarn test
name: backend

on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
    path:
      - "backend/**"

jobs:
  build:
    runs-on: ubuntu-latest
    steps: 
      - name: build
        run: docker build && docker push

  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        test_id: ["01", "02"]
    needs: build
    steps: 
      - name: docker run test${test_id}

auto-merge

やりたいことを実現するために、まず思いつくのが公式が用意している auto-merge です。

If you enable auto-merge for a pull request, the pull request will merge automatically when all required reviews are met and status checks have passed

Automatically merging a pull request - GitHub Docs

とまさにやりたいことができるような感じがします。

status checks で CI がすべて通ったときを選択できれば一件落着だったんですが、そう簡単には行きません。

Search for status checks, selecting the checks you want to require.

Managing a branch protection rule - GitHub Docs

branch protection rule で status checks を選び、それらが全て成功していれば auto-merge されるという仕組みでした。
status checks は github actions の job 名にあたるようです。
ここからは実際に検証した内容です。

検証

branch protection rule に指定されているが、path の対象外で workflow が起動しない場合

以下の指定で foo/xxx のファイルしか変更しなかった場合

name: echo1

on:
  pull_request:
    types:
      - xxx
    path:
      - "bar/**"

結果

required のまま動かなくなります(それはそうって感じ) f:id:k-murakami0609kun:20211212225404p:plain

branch protection rule に指定されており、その job 名が複数 workflow で指定されていた場合

branch protection rule が echo で workflow は以下のような感じです

name: echo1

jobs:
  echo:
    steps: 
name: echo2

jobs:
  echo:
    steps: 

結果

起動中の同名の job が全て終わった後に auto-merge されます。 f:id:k-murakami0609kun:20211212225717p:plain

ただし、同名でも起動していない場合はそれを待たずに auto-merge されます。
以下の例だと echo2.init-echo の sleep が長いため、echo2.echo が起動する前に echo1.echo が完了し、そのタイミングで auto-merge されてしまいます。

name: echo1

jobs:
  init-echo:
    steps: 
      run: sleep 20
  echo:
    steps: 
      run: sleep 10
name: echo2

jobs:
  init-echo:
    steps: 
      run: sleep 900
  echo:
    steps: 
      run: sleep 10

検証からわかったこと

auto-merge がうまくいくケース

paths を使っていない

素朴に branch protection rule に全ての job を入れればうまくいくはずです(未検証)

pathsは使っているが、全ての workflow で job が1個

全て job 名を同じにして branch protection rule にその job 名を入れればうまくいくはずです

pathsは使っており、全ての workflow で job が複数あるが、workflow 内で直列実行してない

ちょっとトリッキーですが、以下の方法がとれます。

I created another workflow with dummy jobs that have the same names with those required jobs that are not always triggered.

https://github.community/t/github-actions-and-required-checks-in-a-monorepo-using-paths-to-limit-execution/16586/2

auto-merge がうまくいかないケース

上に当てはめることができないケースで、私のレポジトリでは適用が難しそうでした。
paths の指定を無しにするもの、テスト分割実行をやめるわけにもいかないので...。

で、どうするかというと、外からポーリングしてPRの状態を見るようにするのが地道だけどうまくいきそうだと思っています。
すでに marketplace に何かあるかもしれません。

actions で良い機能ないか調べてみたんですが workflow_run で待ち合わせするのはしんどそうで、check_suite っていうそれっぽい trigger もあるんですけど使い方が悪いのかなんだか動いてません。

まとめ

私の用途では使えなさそうでした。
またこれに限らず、workflow は個々で動いているのでCIが全部終わったらみたいな制御はしんどいなと思っています(通知とかも難しい)

ファミコンエミュレーターを作ってた

今後仕事でgolangを使う可能性が少しありそうだなーと思い、素振りの題材としてファミコンエミュレータ作っていまして、最初の関門であるhello worldが表示されるようになりました。

github.com

既に色々な方々が挑戦し資料が残っているので、他の人の記事で見かけなかったものだけ書いてみます。

ネット対戦(ぽい何か)のベースを作った

せっかくならファミコンにない機能を加えたいという思いから、ネット対戦に挑戦してみることにしました。

実装の方向性としては、(クライアント側の入力に応じて)サーバー側でゲーム結果を生成し、クライアント側はそれを表示するだけにしました。 これにした理由としてはサーバー側の実装が増えたほうが課題として良いというのと、クラウドゲーミングっぽくてかっこよさそうという個人的な理由です。

細かい実装の話では、通信は速度の観点からwebrtcを、クライアント側には動画を送るようにしています(画像+音声を別々に送ると音ズレしそうなので)。

進捗としては、入力に応じて画面表示が切り替わるようにはできたので、エミュレーターの実装が進めばそれっぽく動くのではと想像してます。パフォーマンスという大きすぎる課題は残りますが...

https://j.gifs.com/jZxx2B.gif

オペコードの情報をjsonに落とせるようにした

6502のオペコードは150個程度あります。cpuを実装するには、それぞれのオペコードでCycle数やプログラムカウンタをいくつ動かすかなどの情報を保持する必要があります。

(私の観測範囲では)その情報をプログラマが扱いやすい状態に保存されているものを見つけられず、いちいちコピペする必要があったので、スクレイピングしてjsonで保存できるようにしました。
https://github.com/k-murakami0609/nesc/blob/main/tool/get_instraction.py

私はこのjsonをテンプレートエンジンに渡してgolangの構造体の形で出力してます
https://github.com/k-murakami0609/nesc/blob/main/nes/opcodes.go#L99

github actionsで自前のコンテナを使えるようにした

エミュレーターとは関係ないのですが、github actionsでGitHub Container Registryに登録されたコンテナを利用する方法にハマったので書いておきます。

最初は以下の例のようにstepの中にdocker runを書いていました

 # 略
 jobs:
   build:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
 
       - name: Login to GitHub Container Registry
         uses: docker/login-action@v1
         with:
           registry: ghcr.io
           username: ${{ github.repository_owner }}
           password: ${{ secrets.CR_PAT }}
 
       - uses: actions/cache@v2
         with:
           path: ~/go/pkg/mod
           key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
           restore-keys: |
             ${{ runner.os }}-go-
 
       - name: Pull
         run: docker pull ghcr.io/k-murakami0609/nesc/gstreamer
         
       - name: Lint
         run: docker run -v $PWD:/opt -v ~/go/pkg/mod:/root/go/pkg/mod ghcr.io/k-murakami0609/nesc/gstreamer golangci-lint run
 
       - name: Test
         run: docker run -v $PWD:/opt -v ~/go/pkg/mod:/root/go/pkg/mod  ghcr.io/k-murakami0609/nesc/gstreamer go test -v ./...
 
       - name: Fix permissions for cache (workaround)
         run: sudo chmod -R a+rwx ~/go/pkg/mod

これでも動いてはいます。が、golangci-lintの代わりにreviewdog/action-golangci-lintを使いたいと思ったときに、自前のコンテナでreviewdog動かすの???となりました。

調べたところjobs.<job_id>.containerを設定することでステップが実行される環境を指定できるそうです。
https://github.blog/changelog/2020-09-24-github-actions-private-registry-support-for-job-and-service-containers/ (余談ですが、GitHub Container Registryとgithub actionsで検索かけると、github actionsでRegistryに登録する記事ばかり出てこの情報が見つけづらかったです...)

その結果、こんな感じにシンプルになりました

 # 略
 jobs:
   ci:
     runs-on: ubuntu-latest
     container:
       image: ghcr.io/k-murakami0609/nesc/gstreamer
       credentials:
         username: ${{ github.repository_owner }}
         password: ${{ secrets.CR_PAT }}
     steps:
       - uses: actions/checkout@v2
 
       - uses: actions/cache@v2
         with:
           path: ~/go/pkg/mod
           key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
           restore-keys: |
             ${{ runner.os }}-go-
             
       - name: Get
         run: go get
 
       - name: Lint
         uses: reviewdog/action-golangci-lint@v1
         with:
           reporter: github-pr-review
 
       - name: Test
         run: go test -v ./...

もう少し描画部分を実装すればマリオくらいは動かせそうなんですが、後は作るだけなのであまりモチベーションが湧いてないです。 パフォーマンスチューニングも、webrtcや動画周りが闇が深そうで気後れしている感じです。 次何やろうかなー

よく参考にしてたサイト

https://qiita.com/bokuweb/items/1575337bef44ae82f4d3
https://github.com/fogleman/nes
http://nesdev.com/NESDoc.pdf

xv6-riscv 第7 ~ 8章のまとめ

引き続き7と8章をやってます

7章 Multithreading

github.com

課題

switching between threads (moderate)

スレッドの切り替え処理を書いてみようという課題でした。 ほぼ処理は実装済みで、スレッド作成時context->raに関数のアドレス渡したり、切替時にはレジスタの中身を切り替えたりするだけで終わりでした。

Using threads (moderate)

xv6から離れてロックの使い方を学ぼうという課題でした。

元々のコードは、CPU分だけLinkedHashMapが存在し10000個の値を並列でLinkedHashMapに追加していきますが、追加時にロックをしてないため、タイミングによっては追加されない(きれいに列にならない)ので、ロックをかけようというものです。

課題の後半では、CPUごとに別のロックを用意すると高速化できるよねという課題でした。

あと、実装では読み取りの時にもロックかけているけど、全く意味ないなとブログ書いてて気づきました。

Barrier (moderate)

xv6から離れてロックの使い方を学ぼうというものでBarrier (computer science) - Wikipediaを作ろうという課題。

複数スレッドでの待ち合わせをどう実装するかというものと言う理解です。 pthread_cond_waitpthread_cond_broadcastの使い方がわかればすぐ終わりました。

8章 locks

github.com

Memory allocator (moderate)

kalloc, kfreeを高速化しようという課題。

元々のコードでは全CPU共通で空きメモリの管理を行っており、メモリ取得/開放時のロックも全CPU共通のため、待ち時間が多いです。 今回の課題は、CPUごとに空きメモリとロックを管理することで待ち時間を減らすという修正を行っていきました。

特殊な実装としては、自分の空きメモリはなくなった場合でも他のCPU用の空きメモリが残っている可能性がありますので、 その時は他からメモリを使わせてもらうといった実装が必要でした。

Buffer cache (hard)

難しくて手を出せてないです。 ファイルシステムのBuffer cacheで、メモリ上にデータを持っていたらいちいちディスクまでデータを読みにいかないとかその類だと思います。 面白そうなので、先に課題が終わったら戻ってやる予定です。


次はファイルシステムなんですが、全体的に難しいので時間をかけて進めていきたいと思っています。

xv6-riscv 第5 ~ 6章のまとめ

xv6の課題の続きで、今回は5章と6章をまとめます

第5章 xv6 lazy page allocation

github.com

課題

  • Eliminate allocation from sbrk() (easy)
  • Lazy allocation (moderate)
  • Lazytests and Usertests (moderate)

課題は3つに分かれていますが、全てLazy allocationを実装しようというものになっています。
課題前の実装ではsbrkを呼び出すと、即座にプロセスに割り当てられるメモリを増やします。 ただ、sbrkを呼び出したからと言って、プロセスはすぐに割り当てられたメモリを使うとは限りません。 そこで、この課題ではsbrkを呼び出したときにはメモリの割当を行わず、実際に使用するときにメモリを割り当てる実装を行います。

考え方は至ってシンプルですが、影響箇所が割と多い課題でした。

  • 考慮が必要なケース

    • モリーのコピー
      • まだ割当がされていない領域はコピーせずスキップする
    • モリーの削除
      • まだ割当がされていない領域は削除せずスキップする
    • モリーの読み込み/書き込み
      • まだ割当がされていない領域に読み込み/書き込みを行う場合には、そのタイミングでメモリ割り当ても一緒に行う
  • バリデーション

    • sbrkで確保した以上のメモリーを要求された時
    • kallocが失敗した時
    • user stackを侵食しようとした時

第6章 Copy-on-Write Fork for xv6

github.com (cowtestは通るようになったが、usertestsはまだうまく行っておらず成功してない)

課題

Implement copy-on write(hard)

COW forkを実装しようという課題です。

通常のforkでは、forkが行われた時に親から子へメモリの内容をコピーします。 ただ、内容に変更があるまでは親子共に同じものであるため、 forkの時ではなく書き込みがあったときにメモリの内容をコピーするように遅延しようというものになっています。

第一感は複雑そうな課題がでたなぁという印象でしたが、仕組みは意外とシンプルでした。

1 . fork時にページエントリを作成するが、親子共に同じ物理アドレスを参照するようにする
2. 1で作成したページエントリは、読み込み専用かつCOWで対象というフラグも立てておく
3. もし書き込みがあった場合にはページフォルトのトラップが発生する
4. トラップハンドラでページフォルトをキャッチして、もしCOWの対象であればコピーする

これに加えて、メモリ解放時のロジックも変更する必要がありました。
今までは一つの物理アドレスは一つのページエントリから参照されていたので、何も考えずにメモリ開放するだけでOKでした。
しかし、今回から複数のページエントリから一つの物理アドレスを参照される可能性がでるようになりました。
そのため、物理アドレスがいくつのページエントリから参照をされているかを管理しておき、メモリ解放が要求された時に参照が0だった場合のみ、実際にメモリ解放を行うというロジックに変更しました。

「reference count」と名前がついてましたが、GCの仕組みでもこんな感じを見たことがあったので理解はすんなりできました。


長い道と思いましたが、残り課題が5個になりました。 この調子で続けていきたいですね!

xv6-riscv 第2, 4章のまとめ

xv6の課題の続きで、今回は2章と4章をまとめます (3章の課題難しくてスキップしている)  

第2章 system calls

github.com

課題

System call tracing (moderate)

システムコールを呼ばれるたびにログに吐き出すシステムコールを作る課題

課題の意図は、システムコールの追加方法を学ぼうというものっぽいので、誘導にそってやれば特に難しいことはなかったです。

解くのにかかった時間: 1時間

Sysinfo (moderate)

残メモリと使用中のプロセス数を取得するシステムコールを作る課題

こちらも誘導が丁寧なので上からやっていけば簡単に終わりました。 強いて言えばkernelからuserにsysinfoの情報を渡すときに、copyoutを使わなければ行けないのが少々トリッキーなくらいでした。

解くのにかかった時間: 1時間

第4章 Traps and system calls

この3章、4章あたりから課題が急に難しくなってきた感じです。

github.com

課題

RISC-V assembly (easy)

RISC-Vのアセンブラを学ぼうという課題で、読めば終わるものでした

解くのにかかった時間: 20分

Backtrace (moderate)

backtrace関数を作ろうという課題。

stack frameとtrap frameを混同しておりめっちゃ時間がかかりました。 この課題で必要なのはstack frameでそれっぽいことは別紙に書いてあります。 https://pdos.csail.mit.edu/6.828/2020/lec/l-riscv-slides.pdf

stack frameの構造さえわかってしまえば、空になるまでframeを戻っていく簡単な課題でした。

上記の誤解を気づけたのは、gdbのbacktraceコマンドで今回の正解が表示させてみたり、 frame infoコマンドと私が書いたコードの差分をとってみたり、結構泥臭いことやってました。

解くのにかかった時間: 6時間

Alarm (hard)

この課題では、テストコードで割り込み上限回数とコールバック関数が与えられます。
そして、テストコードが断続的にタイマー割り込みを発生させるので、割り込みの回数を記憶しておき割り込み上限回数まで達したらコールバックを呼び出すという課題です。

割り込み時、どうやってコールバック関数を呼び出すかが難問でした。
正直わからなかったので先人たちの知恵を借りたところ、epcを直接書き換えれば良いということがわかり、目からウロコでした。
同時に、このままwebエンジニアとして生きていたら、一生仕事でこのコード書くことないんだろうなと思いました。

解くのにかかった時間: 3時間

xv6-riscv 第1章のまとめ

MITで行われているxv6について授業の資料が公開されていたので、先々週くらいからチャンレンジしています。 4章くらいまで終わったので、まずは1章を軽くまとめてみます。

はじめに

xv6とは

近代UNIX系OSの元となったUnix v6を移植したものです。

Unix v6とは

  • エッセンスは現在のOSにも通づるものがある
  • 約1万行程度とOSにしては少ない分量で書かれているのでOSの全体像を学習するのにとても役立つ
  • ただし、古いC言語PDP-11という古いマシン(CPU? プロセッサ?)用に記述されているので読みづらい部分がある

という特徴があります

xv6は

  • C言語はC99以降のものなり、またriscvで動作するようになっているので、2020年度の我々にも読みやすいものとなっている
  • 教育用なのでコードや注釈もわかりやすいものになっている
  • 講義の資料や課題とかまでついている

となっており、OSの全体像を把握したい人にはうってつけのものになっています。

ドキュメントなど

このページの上にあるxv6というタブからソースコードと資料(pdf)をダウンロードできます。 また、各授業ごとに課題もついております。 https://pdos.csail.mit.edu/6.S081/2020/schedule.html

私は英語が得意ではないので、この日本語にしてくれた方の資料を読みつつ、気になるところがあれば英語の資料にあたる方法をとっています。 https://www.sugawara-lab.jp/lecture.html

進め方としては

  1. その章の資料を読む
  2. 課題を読んで、実際のソースコードを改良して機能追加をする
  3. 付属してあるテストコードを実行してテストが通るまで改良を続ける
  4. 次の章を読む

といったサイクルを繰り返します

課題は半日以上かかる歯ごたえがあり、点数とかも表示されるのでゲーム感覚で楽しめてます。 脱線になりますが、私は勉強するときは手を動かしたいタイプで、また独学だと課題に正解しているかわからないことが多いので、今回のようなテストコードが付属してある形式はとても好みです。

本章

本文のまとめとかはよくあると思うので、課題のほうを中心にまとめてみます。 githubのコードもあげています。ただ、C言語が詳しくなくテストに通っているだけで模範解答では全く無いと思います。

各課題の難易度は、Easyだと1時間未満、Moderateは1 ~ 2時間、Hardはそれ以上かかる見込みだそうです

https://pdos.csail.mit.edu/6.S081/2020/labs/guidance.html

第1章 Xv6 and Unix utilities

github.com

Boot xv6 (easy)

xv6を起動する課題。

OSごとに手順がまとまっており、特につまるところはありませんでした。 https://pdos.csail.mit.edu/6.S081/2020/tools.html

sleep (easy)

実装済みのシステムコール sleep をコマンドラインから呼び出してみようという課題。

おそらく課題の意図としては、ディレクトリ構造やシステムコールの呼び出し方を把握するだけのもので特に難しいことはなかったです。 誘導もとても丁寧でした。

解くのにかかった時間: 20分

pingpong (easy)

forkとpipeを駆使して、子プロセスでping、親プロセスでpongと出力しようという課題。

割とよくある課題で本文中にもforkやpipeの解説があったため、割と簡単でした。

解くのにかかった時間: 30分

primes (moderate)/(hard)

ハードなのでスキップしてます。

find (moderate)

簡易版findコマンドを作ろうという課題。

これは仕様の勘違いで時間がかかりました。
find a b は、aディレクトリとbディレクトリから再帰的にファイルを検索するものかと思ってました。
実際には./a/b/*以下を再帰的に検索するもので、カレントディレクトリを起点に、bディレクトリから再帰的にファイルを検索するものだったそうです。

その他には、文字列にstrcmpを使わなければいけないなど、C言語っぽいところで詰まったことが多かったです。

解くのにかかった時間: 2時間

xargs (moderate)

簡易版xargsコマンドを作ろうという課題。

実装してみて、なるほどこんな簡単な仕組みで動いていたのかという印象を受けました。 xargsは結構すんなり作ることができましたが、前課題のfindに依存しておりそっちが壊れていたのに気づかず、時間がかかってしまいました。

解くのにかかった時間: 2時間