DeNA Testing Blog

Make Testing Fun, Smart, and Delighting End-Users

「Androidアプリのアーキテクチャにそってテストの書き方を学ぼう」というハンズオンを公開しました

こんにちは。SWETのAndroidチームに所属している外山(@sumio_tym)です。 SWET AndroidチームではAndroidのプロダクトに対する自動テストのサポートをしています。

はじめに

Android公式ドキュメント「アプリ アーキテクチャガイド」で推奨されているアーキテクチャの各レイヤごとに、テストの書き方を学ぶハンズオンを「Androidアプリのアーキテクチャにそってテストの書き方を学ぼう」というタイトルで公開しました。公開したハンズオンはDeNA Codelabsよりリンクされています。

また、来週開催されるDroidKaigi 2023のDeNAブースにて、SWET Androidチームのメンバーが本ハンズオンの簡単な解説を行います。 ブースに来て下さった皆さんが選んだトピックについて個別に解説しようと思いますので、興味がある方は是非ブースにお越しください。

ハンズオンの内容

本ハンズオンは、「Now in Android App」アプリを題材に、アプリアーキテクチャのレイヤにそってテストの書き方を学ぶ内容になっています。

Now in Android AppはAndroid開発者が参考にできるように作られた公式のサンプルアプリですが、それをハンズオンの演習内容に合うようにカスタマイズしています。

ハンズオンの目次は次のとおりです。

  • データレイヤをテストする
    • API通信をするコードのテストを実装しながらCoroutineのテストについて学ぶ
    • データソースに応じたテストの書き方を学ぶ
      • データベース(Room)のテストを書く
      • DataStoreのテストを書く
      • オンメモリキャッシュのテストを書く
  • UIレイヤをテストする
    • ViewModelをテストする
    • Jetpack Composeをテストする
      • Composeのユニットテストについて学ぶ
      • ViewModelを結合してComposeをテストする
      • ComposeのNavigationをテストする

なぜハンズオンを作ったのか

Androidアプリ開発で採用される技術要素やアーキテクチャは年々移り変わっています。 DeNAにおいても、Kotlin Coroutines/Flow、Dagger Hilt、Jetpack Composeなどが使われるようになり、 それらのテストの書き方についての質問がSWET Androidチームに寄せられたり、サポートの要望を受けるケースが増えていました。

そこで、現在使われている技術要素へのテストの書き方をDeNAのAndroid開発者が習得できるように、 公式サンプルアプリ「Now in Android App」を題材にしたテストハンズオンを企画しました。

Now in Androidアプリは、公式のアーキテクチャガイドや、テストのドキュメントに忠実にしたがって作られており、Android開発で主流となっている技術要素を採用しています。 そのため、本アプリを題材としたハンズオンを作れば、次のトピックをまとめて学ぶことができると考えました。

  • 推奨アーキテクチャの各レイヤに対応したテストの書き方
  • 各技術要素に対応したテストの書き方

作成したハンズオンは、毎週開催されているAndroidの社内勉強会「Android.Tuesday」の枠を借りてトピックごとに実施しました。 参加者へのアンケートでは、多くの方から「技術的な知見が得られた」という回答をいただいており、当初の目的は達成できたと考えています。 また、当日参加できなかった人のために、実施したときの動画を社内に公開しています。

今回公開したハンズオンは、それを社外に公開できるように体裁を整えたものになっています。 皆さんがテストを書くときの参考にしていただければ幸いです。

DroidKaigi 2023でお会いしましょう

「興味はあるけど読む時間がない」「手っ取り早くポイントを知りたい」という方のために、 DroidKaigi 2023のDeNAブースにて、個別に本ハンズオンの簡単な解説をしたいと思います。 気になるトピックがありましたら、是非DeNAブースにお立ち寄りいただき、SWETメンバーに声を掛けてみてください*1

皆さんとブースでお会いできるのを楽しみにしています!

*1:SWETメンバーが不在のときもあります。そのときは申し訳ありませんが時間を改めてお立ち寄りください

Lint Night #2を開催します!

こんにちは、SWETグループの稲垣( @get_me_power )です。 Lintに関する知識を共有することを目的として、2022/11/18に Lint Night #1を開催しました。

今回、約半年ぶりで復活、Lint Night #2をオフライン・オンライン同時開催します!

Lint Nightはプログラミング言語不問でLintに関するトピックを取り扱う勉強会です。ここでLintとはソースコードや文書を静的に解析して問題をみつけるツールのことです。ただ、どこまでをLintとするかには幅があるようです。(Lintの定義ついては次のスライドをご覧ください)

今回のLint Nightも次の方々をお招きしており、たっぷりとLintに関する知見の共有や議論が行えるイベントとなっております。

  • Emacsの構文チェック拡張機能flycheckの開発者でありPHPStanの有識者である@tadsanさん

https://twitter.com/cloud10designstwitter.com https://twitter.com/tadsantwitter.com https://twitter.com/kitasuketwitter.com

Lintを使ったことがある人、Lintを作ってみたい人、なんでもいいから問題解決の引き出しを増やしたい人、ぜひご参加ください!

オフライン限定になりますが、懇親会も予定しています。 2023/08/04のLint Night #2でみなさんにお会いできるのを楽しみにしています!

CI/CD Test Night #6を開催しました!

こんにちは。SWETのCI/CDチームに所属している井口(@hisa9chi)です。

2023/05/26にCI/CD Test Nightを約3年半ぶりに復活し、第6回目をオフライン/オンラインで開催しました!

本記事では、今回の発表のスライドを紹介していきます。本イベントは当日の登壇を動画でも公開していますのでそちらもあわせてご確認いただければ幸いです。

聴衆の反応

CI/CD Test Night #6 で盛り上がっている様子にtweetをまとめました。

当日の登壇動画

youtu.be

発表スライド紹介

@Kesin11: 「GitHub Actionsオタクによるセルフホストランナーのアーキテクチャ解説」

トップバッターは弊社SWET Grの@Kesin11による「GitHub Actionsオタクによるセルフホストランナーのアーキテクチャ解説」の発表でした。

規模の大きな組織でGitHub Actions Self-hosted runnersを運用する場合で起こりうる課題に関して取り上げ、どのような工夫をして解決しているかを紹介しています。以降に続く登壇者の方々が導入している仕組みも参考にしながら問題解決策の提案をしています。 www.docswell.com

@miyajan: 「philips-labs/terraform-aws-github-runner による GitHub Actions セルフホストランナーの大規模運用」

続いては、サイボウズ株式会社の@miyajan様による「philips-labs/terraform-aws-github-runnerによるGitHub Actionsセルフホストランナーの大規模運用」の発表でした。

GitHub Actions Self-hosted runnersとしてphilips-labs/terraform-aws-github-runnerを活用しており、その選定理由と社内でどのように運用をしているか紹介していただきました。社内の要望から複数のマシンタイプの利用を可能にするためmulti-runnerモジュールも活用されているようです。 www.docswell.com

@s4ichi: 「開発者体験を改善し続けるための Self-hosted runner 運用基盤」

3番目に、クックパッド株式会社の@s4ichi様による「開発者体験を改善し続けるためのSelf-hosted runner運用基盤」の発表でした。

CI環境としてGitHub Actionsを導入しようとした経緯や、導入するために要件からどのような仕組みとしたのかを紹介していただきました。GitHubのWebhookとlambdaやSQSを連携させることにより柔軟なオートスケールを実現されていることや、関連するメトリクスも可視化されているようです。

www.docswell.com

@whywaita: 「バリエーションで差をつける。myshoesの新たな挑戦」

最後に、株式会社サイバーエージェント@whywaita様による「バリエーションで差をつける。myshoesの新たな挑戦」の発表でした。

GitHub Actionsへ移行することとなった経緯を紹介した上で、自社開発のGitHub Actions Self-hosted runnerをオートスケールさせるmyshoesの紹介をしていただきました。発表の中ではSelf-hosted runnerの新たなるバリエーションを増やしたということでmacOSのrunnerも利用可能にしたという点は驚きでした。 speakerdeck.com

懇親会

発表終了後は、オフライン参加者限定で懇親会を開催しました。 参加していただいた皆様それぞれの違う立場で多くのノウハウを共有し会えたことと、時間内では語り尽くせないほど熱く語り尽くした懇親会になっていたと思います。

終わりに

今回登壇していただいた皆様、オフライン/オンラインで参加していただいた皆様のおかげで、無事CI/CD Test Night #6を終えることができました。大変感謝しております。 CI/CD Test Nightは、今後も定期的にイベントを開催する予定ですので、今回参加していただいた方々、参加できなかった方々も次回お会いしましょう!

また、SWETグループではCI/CDに限らずSWETの取り組みに興味があり、私達と一緒に働いてくれる仲間を募集しています。ご応募お待ちしています!

CI/CD Test Nightを復活開催します!

こんにちは、SWETでCI/CDチームの前田( @mad_p )です。

CI/CD関する知識を共有することを目的として、過去数回「CI/CD Test Night」を開催してきました。 今回、約2年半ぶりで復活、CI/CD Test Night #6をハイブリッド開催します!

今回のテーマは「GitHub Actionsセルフホストランナーのインフラ運用」についてです。 去る3月のDeNA Techcon 2023では、Discord上のトークイベント SWET Online のテーマとして「GitHub Actionsのセルフホストランナー」をとりあげました。 当日の議論も大いにもり上がったのですが、話したりない部分も多かったと思います。 今回のCI/CD Test Nightは、このテーマについて、次の方々をお招きして、たっぷりと知見の共有や議論が行えるイベントとなっております。

twitter.com twitter.com twitter.com twitter.com (弊社SWETメンバー)

GitHub Actionsをどのように利用しているかや、セルフホストランナーの構成をどのようにしていてどう運用しているかなどそれぞれの現場での知見が多く知れると思います。 GitHub Actionsを利用している方々はもちろんのこと、セルフホストランナーの構成をどうしようか悩まれている方々、もしくはこれから利用しようとしている方々の参加をお待ちしております。

オフライン限定になりますが、懇親会も予定しています。 5/26のCI/CD Test Night #6でみなさんにお会いできるのを楽しみにしています!

Android Test Night #8を開催しました

こんにちは。SWETのAndroidチームに所属している外山(@sumio_tym)です。

2023/03/10にAndroid Test Night #8を開催しました。 約3年ぶりのオフライン開催で、懇親会も実施しました(同時にオンラインでも配信しました)!

本記事では、今回の発表のスライドを紹介していきます*1

聴衆の反応

Android Test Night #8 で盛り上がっている様子にtweetをまとめました。

発表スライド紹介

@STAR_ZERO「Coroutines Test 入門」

1つめは@STAR_ZEROさんによるKotlin Coroutineが関係するテストの書き方についての発表でした。

suspend関数のテスト、メインスレッドの差し替え方法、Flowのテスト例が簡潔にまとめられており、とても参考になりました。 ついつい忘れてしまいがちなStandardTestDispatcherUnconfinedTestDispatcherの違いや、テストスケジューラの時間制御の方法などが具体例と共に説明されているので、あとから見直すのもよいかもしれません。

@mr_mkeeda「Compose で手に入れた UI の Unit test」

2つめは@mr_mkeedaさんによるCmpose UIのユニットテストについての発表でした。

AndroidにおけるUIのテストは典型的にはInstrumented Testで行いますが、Compose UIのユニットテストであればRobolectricでも安定して動作するとのことでした。 ComposeのテストをRobolectricで動かす場合、RobolectricによってShadow(Robolectricが用意したFake実装)に置き換えられるのはCanvasにとどまるため、テストライブラリへの影響が少ないとの解説が目から鱗でした。

@swiz_ard「KMMのCI/CD」

3つめは@swiz_ardさんによるKMM (Kotlin Multiplatform Mobile)におけるCI/CD構築についての発表でした。

KMMでは、Android・iOSのネイティブコードとKMM共有コードの依存関係を解決する手段として、モノレポ・git submodule・パッケージマネージャーと、それぞれ利害得失が異なる3つの選択肢があるとのことです。 発表では、当初git submoduleが採用されていたプロジェクトを、パッケージマネージャーに移行した事例が紹介されていました。 自分も(KMMではありませんが)git submoduleの扱いには日々苦労させられているため、デメリットが大きくなって移行したという話にはとても共感しました。

懇親会

発表終了後、オフライン参加者で懇親会を開催しました。 参加者同士でたっぷり話せるように2時間確保していたのですが、話題に尽きることなくあっという間に2時間が終わってしまいました!

終わりに

登壇して下さった皆さん、参加して下さった皆さんのお陰で、無事Android Test Night #8を終えることができました。どうもありがとうございました。 Android Test Nightは、今後も定期的にイベントを開催する予定です。 また次回お会いしましょう!

また、SWETグループではAndroidに限らずテストの自動化やCI/CDに興味があり、私達と一緒に働いてくれる仲間を募集しています。ご応募お待ちしています!

*1:録画も公開したかったのですが、Zoomの設定ミスにより録画に失敗していました

Unityプロジェクト向けオートパイロットフレームワークの運用Tips

SWETグループの長谷川( @nowsprinting )です。

開発者自身の手によるUnityプロジェクトの品質向上アプローチのひとつに、ゲームプレイを自動化するオートパイロットによる検証があります。 このアプローチについて、DeNA内で開発・導入を進めているフレームワークAnjinを昨年紹介しました。

swet.dena.com

また、Anjin自体も先日オープンソース化しました。こちらのリポジトリからご利用いただけます。

github.com

本記事では、Anjinの、主にオートパイロットを安定運用するための機能を紹介します。

Automated QAが保存するスクリーンショットの退避

uGUIコンポーネントのシナリオテストを行なう UGUIPlaybackAgent の実装には、 Automated QAパッケージ のRecorded Playback機能を使用しています。 Playback機能は再生操作の前後にスクリーンショットを撮影して Application.persistentDataPath 下に書き出してくれるのですが、新しい操作シナリオの実行がはじまる契機で削除されてしまいます。

Anjinでは複数のシナリオを連続して実行しますので、スクリーンショットを残すのであればすべてのシナリオ分を残したくなります。 そこでAnjinでは、シナリオ実行が終わるたびにAutomated QAの出力先パスを求め*1、退避させる手段を取りました。

詳しくは、UGUIPlaybackAgent クラスの StoreScreenshots メソッドを参照してください。

Unityエディター終了時のクラッシュ対策

Anjinを活用している開発チームでは、Anjinを夜間に定期実行されるように設定しています。 GitHub ActionsワークフローやJenkinsから起動されたAnjinは一定時間動作したらUnityエディターを終了しますが、このとき終了コード0以外で終わってしまうことがあります*2

これを起動元がエラーと判断してアラートを上げてしまうことを回避するため、Anjinは終了時にJUnit Report形式*3のXMLファイルを出力しています。 これにより起動元は、Unityエディターの終了コードではなく、JUnit Reportファイル内の errors の数で合否を判断できます。

JUnit Reportファイル出力の実装は、JUnitReporter クラスを参照してください。

サーバ通信エラーなどによるタイトル戻し対策

「タイトル戻し」とは弊社内での通称ですが、通信エラーなどで進行不能となったときに「タイトル画面に戻ります」といったボタンを表示してタイトル画面からやり直させる振る舞いのことを指します。 もしエラー発生時に動作していたのがモンキーAgentであれば、Agentはそのままタイトル画面に戻るボタンをクリックし、そのままAnjinの実行は継続されます。

しかし、プレイバックAgentなど操作シナリオを再現するAgentでは、本来クリックしたかったボタンの代わりに別のボタンが出現したことでシナリオ継続不可能となってしまい、アラートが上がってしまいます。 エラーの内容が報告すべきものであればこの振る舞いでよいのですが、通信エラーなど無視したいもの(むしろ継続して動作してほしいもの)もあります。 *4

この問題は、次のビルトインAgentを組み合わせることで回避できます。

EmergencyExitAgent

このAgentは、EmergencyExit コンポーネントがアタッチされたボタンの出現を監視し、Scene内に出現したら即時クリックする機能を持っています。 つまり、エラー発生時に表示される「タイトル画面に戻ります」ボタンにこれをアタッチしておけば、プレイバックAgentがシナリオ継続不能と判断する前にタイトルSceneへと遷移できるのです。 Sceneが切り替わった時点でプレイバックAgentは終了してタイトル画面用のAgentに切り替わります。

EmergencyExit は、Anjinが動作している間は常駐して監視を続ける必要があります。それには、AutopilotSettingsにある Observer Agent として登録して使用します *5

OneTimeAgent

たとえばタイトルSceneの操作は、起動後1回目の表示時と2回目以降で異なるシナリオを要求されることがほとんどではないでしょうか。 初回起動では年齢確認やチュートリアルの操作が必要になるためです。

このAgentは1つの子Agentを設定でき、それをオートパイロット実行期間を通じて1回だけ実行します。2回目以降の実行はスキップされます。 これを利用して次のようにAgentを組合わせることで、1回目と2回目以降で異なるシナリオを実行できます。

タイトルScene向け SerialCompositeAgent
    ├── 子1: OneTimeAgent
    │   └── 子: 1回目のシナリオの PlaybackAgent
    └── 子2: 2回目以降シナリオの PlaybackAgent

まとめ

このように、できるだけ単機能・最小限のAgentだけをビルトインAgentとして提供し、Unityエディター上で組み合わせることにより(ノーコードで)ゲームタイトルに合わせたオートパイロット動作を実現できるようにしています。

とはいえ、あまりに複雑な組み合わせを強いるのは本末転倒です。 複雑な要件に対しては、ゲームタイトル固有のカスタムAgent*6をエンジニア(プログラマー)が作成することを検討してください。

We are hiring

SWETグループでは、このようなUnityプロジェクトに対する品質向上アプローチも行っています。 興味を持たれた方は、ぜひ採用情報を確認ください!

*1:パスにはタイムスタンプが含まれるため

*2:プロジェクトによっては高頻度で発生していました

*3:JUnit形式を選択したのは、これを扱えるライブラリがGitHub ActionsやJenkinsプラグインに存在するためです

*4:Anjinはログを監視してエラーを報告しますが、指定した文字列を含むエラーログを無視する設定ができます。前回のブログ記事を参照してください

*5:Observer Agent に設定されたAgentは、DontDestroyOnLoad ではなく、新しいSceneがロードされる度に破棄・生成される点に留意してください

*6:AbstractAgent を継承して実装できます

GitHub Actionsのセルフホストランナーでジョブごとにディレクトリを分離する方法

CI/CDマニアの加瀬(@Kesin11)です。先日、DeNA/setup-job-workspace-action をOSSとして公開しました。これは、GitHub Actionsでは本来1つのリポジトリに対して作業用のディレクトリは1つだけになるところを、ジョブごとにディレクトリを分離できるActionです。大規模なリポジトリをセルフホストランナーで扱うというニッチ環境用ですが、当てはまるユースケースではとても便利です。

本記事ではDeNA/setup-job-workspace-actionを作成した経緯や仕組みについて紹介しますが、これに関連する内容も含む発表を2023/03/02開催のDeNA TechCon 2023においてSWETの加瀬とゲーム事業部SREチームの白柳が次の2つをテーマに登壇する予定です。

  • SWETがこれまで注力してきたゲーム開発向けのJenkinsの整備
  • ゲーム事業部にSREチームが誕生するまで

ぜひこちらもご参加頂けると嬉しいです!

techcon2023.dena.dev

JenkinsからGitHub Actionsへ移行する際の課題

DeNAのゲーム開発ではCI/CD基盤として主にJenkinsを利用しており、SWETではゲーム開発にてJenkinsを効果的に活用してもらうための仕組みづくりや運用コストの省力化に取り組んできました。一方でここ数年知名度が急上昇しているGitHub Actionsにも注目しており、JenkinsからGitHub Actionsに乗り換えることを視野に入れて調査・検討をしています。

検討する中で大きな懸念であったのは、ゲーム開発のような大規模なリポジトリの場合、ビルドキャッシュを次のビルドにおいて利用できないことでビルド時間が多くかかってしまうという問題でした。

一般的なGitHub Actionsの使い方ではGitHubがホストしているランナーのVMを毎回のビルドのタイミングで立ち上げるため、ビルドキャッシュなどを次のビルドに持ち越すためにはactions/cacheなどでキャッシュするというのが基本的な戦略です。
一方で、DeNAではほとんどのチームがGitHub Enterprise Server(GHES)を利用しており、GitHubがホストしているランナーを利用できないためセルフホストランナーを利用することになります。セルフホストランナーでは前回ビルドしたときのディレクトリの状態がそのまま維持されてしまうのですが、actions/checkoutがデフォルトの挙動として git clean -ffdx も実行してくれるためセルフホストランナーであっても基本的には毎回のビルドはほぼクリーンな環境で実行されます1

逆にこの挙動があるため、次回ビルドの効率化のためにビルドキャッシュを残したい場合にはactions/checkoutのオプションにある clean: false を指定することで自動的に git clean -ffdx が実行されないようにする必要があります。JenkinsでUnityのゲームをビルドするパイプラインを作成したときには、Unityが利用しているUPMパッケージやプラットフォーム向けに最適化されたアセットのキャッシュが含まれる Library/ ディレクトリを.gitignoreに追加した上で明示的に git clean -ffd(-x無し)を実行することでこのディレクトリが削除されないように工夫して上手くビルドキャッシュを効かせることを実現していました。

当初はGitHub Actionsでもこの戦略を実行していたのですが、ある日気がついたら Library/ ディレクトリが意図せずに削除されてしまう現象が発生しました。原因を調べたところ、Unityのビルド別のWorkflowが実行された際に Library/ ディレクトリが削除されていたことが分かりました。

GitHub Actionsは1つのリポジトリに対してWorkflow(リポジトリ中の .github/workflows/*.yml)を複数登録可能で、さらに1つのWorkflowの中に複数のJobを登録できるという構造です。しかし実はセルフホストランナー内では1つのリポジトリに対して1つのディレクトリ(ワークスペース)しか持てないディレクトリ構造になっています。この制約により、たとえば次のような状況で意図せずにキャッシュが削除されるということが発生していました。

  1. 手動トリガーによる build.yml でUnityビルドが行われ、ビルドキャッシュが Library/ に残る
  2. 2回目以降の build.yml でUnityのビルドが行われたときは Library/ が存在するためキャッシュが効く
  3. 別のpull-reqが作られたタイミングでlint.yml によるLintのWorkflowが実行される
    • このときにactions/checkoutのデフォルトオプションにより、意図せずに Library/ が削除されてしまう
  4. 3以降のタイミングで初回の手動トリガーによる build.yml のUnityビルドでは Library/ が存在しないためキャッシュが効かない

問題は3の lint.yml の中でactions/checkoutに clean: false のオプションを渡していないことなのですが、GitHub Actionsに慣れた人ほどこのオプションに馴染みがないでしょうからWorkflowを編集する際に忘れないことを徹底してもらうのは無理があります。また今回の例のように異なるWorkflowではなく、同じWorkflow内でも新たなJobを追加した際のactions/checkoutにオプションを忘れてしまえば同じ問題が発生します。そこまで意識を徹底するのは自分でも難しいと思いました。

別の方法として clean: false を諦め、セルフホストランナーでもGHES v3.5から利用可能となったactions/cacheで Library/ をキャッシュさせるという戦略も考えたのですが、Library/ は軽くGB単位の大きさであったことからキャッシュの上限や転送時間を考えると現実的ではないと諦めざるを得ませんでした。ここまでの事情はQualiArtsさんの 【Unity】ゲーム開発の現場でなぜJenkinsが利用され続けるのか で紹介されていたGitHub ActionsよりJenkinsの方がゲーム開発には向いている理由とほぼ同じとなります。

QualiArtsさんの記事にあるように "ジョブごとにワークスペースが維持される" というJenkinsの挙動をGitHub Actionsのセルフホストランナーで再現させる、あるいは別の代替案がないことにはJenkinsから本格的にGitHub Actionsに移行することは難しいと考えました。

シンボリックリンクを用いたワークスペースの分離

そもそも根本的な問題はセルフホストランナーのディレクトリ構造が1リポジトリに対して1ワークスペースしか持てないことです。Jenkinsでは同様の問題が起きなかった理由をディレクトリ構造を比較して説明します。

Jenkinsではまずジョブというリポジトリに紐付かない概念を作成し、ジョブ1つに対して基本は1つのディレクトリ(ワークスペース)が作成されます。したがって次の図のようにjob_AAAjob_BBBという2つのジョブは別々のワークスペースで実行されますので、同じリポジトリrepo_Aを利用していたとしてもGitHub Actionsのように前ビルドで生成されビルドキャッシュとして残しておきたいファイルなどがうっかり消されてしまうという問題は発生しません。さらにいえば、プラグインを利用したりジョブの中でcheckout処理を工夫することでジョブとワークスペースの関係も1:Nにさらに拡張できる柔軟性がありました。

# JenkinsのAgentマシンのディレクトリ構造

$HOME/jenkins-agent/workspace/
├── job_AAA
│   └── repo_Aをcheckoutした中身
├── job_BBB
│   └── repo_Aをcheckoutした中身
└── job_CCC
    └── repo_Bをcheckoutした中身

一方、GitHub Actionsのセルフホストランナー内のディレクトリ構造はこのようになっています。

# GitHub Actionsセルフホストランナーのマシンのディレクトリ構造

$HOME/actions-runner/_work/
├── repo_A
│   └── repo_A
│       └── repo_Aをcheckoutした中身
└── repo_B
    └── repo_B
        └── repo_Bをcheckoutした中身

repo_A はリポジトリの名前の文字列で、なぜか2回同じディレクトリ名でネストされていました2

$HOME/actions-runner/_work/repo_A/repo_A までのパスはGitHub Actionsの実行中に環境変数の$GITHUB_WORKSPACEとして参照可能であり、すべてのactionはこのパスを起点に動作します。たとえばactions/checkoutを実行するとこのディレクトリの中に.gitやその他のファイルが生成されます。色々なドキュメントやランナーのオプションなども調べたのですが $GITHUB_WORKSPACE 自体のパスを変更する方法は発見できませんでした。

しかし逆にいえば $GITHUB_WORKSPACE は必ずこのパス構造になっていると思われるので、シンボリックリンクという古典的な方法で擬似的にディレクトリを変更するという方法をひらめきました。コマンドとしては次のようになります。

# 以下2つの環境変数はジョブ実行中にランナーに自動的にセットされているもの
# GITHUB_WORKSPACE = _work/repo_A/repo_A までの絶対パス
# RUNNER_WORKSPACE = _work/repo_A までの絶対パス

# デフォルトのワークスペースを.bakという名前で一時的に退避
mv ${GITHUB_WORKSPACE} ${GITHUB_WORKSPACE}.bak

# 差し替える予定のディレクトリを WorkflowとJob のペアでユニークなるような名前で作成
TMP_DIR="${RUNNER_WORKSPACE}/${WorkflowのYAMLのファイル名}-${Job}"
mkdir -p ${TMP_DIR}

# 作成したディレクトリをGitHub Actionsの元々のディレクトリの位置にシンボリックリンクを作成
ln -s "${TMP_DIR}" ${GITHUB_WORKSPACE}

この時点でのディレクトリ構造はこのようになります。本来ワークスペースとして使用されるはずの repo_A ディレクトリのパスを新たに作成した new-workspace ディレクトリへのシンボリックリンクで置き換えます。

# リポジトリ名 = repo_A
# {WorkflowのYAMLのファイル名}-{Job名} = new-workspaceとした場合

$HOME/actions-runner/_work/
└── repo_A
    ├── new-workspace
    ├── repo_A -> $HOME/actions-runner/_work/repo_A/new-workspace へのシンボリックリンク
    └── repo_A.bak

このままではシンボリックリンクで置き換えた repo_AGITHUB_WORKSPACE) が次のJobでも使われてしまうので、Jobが完了したタイミングで次のコマンドによって元に戻す必要もあります。

# シンボリックリンクを削除
unlink ${GITHUB_WORKSPACE}

# .bakに退避させていたデフォルトのワークスペースを元々のパスに戻す
mv ${GITHUB_WORKSPACE}.bak ${GITHUB_WORKSPACE}

この一連のコマンドをJobの開始時と終了時に実行できれば、若干無理やりではありますが元々のGitHub Actionsのリポジトリとワークスペースが1:1である制約を突破して1:Nに拡張できると考えました。

actionとして実装(DeNA/setup-job-workspace-action

bashコマンドはすでに用意できているのでcomposite actionsで実装しようと考えたのですが、Job終了時に.bakを元に戻す一連のコマンドが確実に実行される必要があるため、Jobの終了時に確実に実行されるpost処理を利用できるJavaScript actionとして実装しました。JavaScriptで実装したといってもNode.jsには元々ファイル操作系のAPIが用意されているため、ほとんど先述の一連のコマンドを愚直に置き換えただけのシンプルな実装になっています。

このシンボリックリンクを用いた方法で実装したactionの特に優れている点は、このactionを使わない場合は元々のセルフホストランナーのワークスペースの構造が維持されるのでこのactionを利用してディレクトリを分けたいJobとそうでないJobが共存できる点です。実は別の手段として最近セルフホストランナーに追加されたjob hook scriptも検討していたのですが、こちらで実現しようとした場合はすべてのJobで一律にディレクトリを分けることを強制させる実装になったと思います。actionとして提供することで、ワークスペースを分けたい場合のみ明示的にYAMLで呼び出すようにユーザー側でコントロールできる方がシンプルで分かりやすいと今では思っています。

DeNA/setup-job-workspace-actionの使い方

基本(オプション無し)

setup-job-workspace-actionは先述の仕組みでディレクトリを作成してシンボリックリンクなどを追加するため、actions/checkoutよりも前に実行されないと意味がありません。必ず先に呼ばれるように順番に気をつけてください。

jobs:
  default:
    runs-on: [self-hosted]
    steps:
      # 必ずactions/checkoutより前にsetup-job-workspace-actionを追加してください
      - uses: DeNA/setup-job-workspace-action@v2

      - uses: actions/checkout@v3

      # ... 通常のビルド処理のステップを記述

いずれのオプションも指定しないデフォルトの挙動では {WorkflowのYAMLのファイル名}-{Job名} という名前のディレクトリが作成されてここが新たなワークスペースとなります。WorkflowとJobのペアでユニークなディレクトリ名ですので、Jobごとに独立したワークスペースが与えられていたJenkinsに近いディレクトリ構造となります。

GHESのバージョンが多少古い場合のデフォルト挙動の違い

具体的にはセルフホストランナーのactions/runner@2.300.0未満の場合なのですが、作成されるディレクトリの名前が {Workflow名}-{Job名} と若干変わります。これはYAMLのファイル名を特定するための GITHUB_WORKFLOW_REF という環境変数が用意されるようになったのがactions/runner@2.300.0からであるためです。セルフホストランナーのバージョンによってディレクトリ名が変化することを回避したい場合は、後述の workspace-name オプションで任意のディレクトリ名で固定してください。

prefix, suffix

jobs:
  with_prefix_and_suffix:
    runs-on: [self-hosted]
    steps:
      - uses: DeNA/setup-job-workspace-action@v2
        with:
          prefix: "foo-"
          suffix: "-bar"
      - uses: actions/checkout@v3

prefix, suffixのオプションを指定するとディレクトリ名の前後に特定の文字列を追加可能です。仮にこのYAMLのファイル名がbuild.ymlの場合は先述のデフォルト名の挙動と合わせて、foo-build-with_prefix_and_suffix-bar というディレクトリ名になります。
このオプションが想定している用途はWorkspace名やJob名に加えて追加のパラメーターによってワークスペースの使い分けをさらに細分化したいケースです。たとえば workflow_dispatch で手動トリガーした際の何らかのパラメーターの値を context.inputs で取得してそれを prefix に与えることで、パラメーターに応じてワークスペースを細かく切り替えることが可能になります。

workspace-name

jobs:
  given_dir_name:
    runs-on: [self-hosted]
    steps:
      - uses: DeNA/setup-job-workspace-action@v2
        with:
          workspace-name: foo_bar_workspace
      - uses: actions/checkout@v3

workspace-name を指定すると作成されるディレクトリの名前を完全にコントロール可能です。この例では foo_bar_workspace というディレクトリ名になります。
このオプションが想定している用途はデフォルトよりも荒い粒度としてWorkspace名だけでワークスペースを分けたい、あるいは逆にさまざまな変数などを駆使してフルカスタムしたい場合です。たとえばactions/github-scriptなどと組み合わせることで、jsを駆使して動的に組み立てた文字列を渡すといったことも可能です。次の例では手動トリガーの場合のみブランチ名が含まれるディレクトリ名を動的に生成しています。

jobs:
  given_dir_name_dynamically:
    runs-on: [self-hosted]
    steps:
      - uses: actions/github-script@v6
        id: set-workspace-name
        with:
          result-encoding: string
          script: |
            const branch = context.ref.split('/').slice(2).join('/')
            return (context.eventName === "workflow_dispatch")
              ? `manual_trigger_${branch}`
              : "default"
      - uses: DeNA/setup-job-workspace-action@v2
        with:
          workspace-name: ${{ steps.set-workspace-name.outputs.result }}
      - uses: actions/checkout@v3

DeNA/setup-job-workspace-actionを利用するメリット・デメリット

最後に自分が考えているDeNA/setup-job-workspace-actionのメリットと、デメリットについて紹介しておきます。

メリット

  • Jenkins時代と同様にジョブ単位でディレクトリが分かれるため、ビルドキャッシュを活かしやすい
    • Unityのビルドキャッシュとして重要な Library/ ディレクトリを確実に維持できる
  • リリース候補となるビルドの作成時など、完全にクリーンな環境からビルドする場合に開発用ビルドのためのキャッシュを削除してしまうことがなくなる
    • 特殊な事情があるジョブは workspace-name でディレクトリ名を固定するなどの工夫で他のジョブのワークスペースとは完全に隔離が可能
  • このactionsを使わないジョブでは元々の挙動が維持されるため、GitHub Actionsに詳しくない人にも負担を強いることがない

デメリット

まとめ

JenkinsからGitHub Actionsに移行するにあたって障壁のひとつであったワークスペースの分離をGitHub Actionsでも可能にするDeNA/setup-job-workspace-actionを紹介しました。

特にゲーム開発のような大規模なリポジトリでJenkinsからGitHub Actions + セルフホストランナーの構成に移行する場合には便利なはずです。実際にDeNA内で導入してくれたチームから嬉しいコメントをもらいました!

  • キャッシュ管理がかなり楽になりました
  • UnityのLibraryディレクトリが毎回消されなくなったので、Unityのテストがめっちゃ早く終わるようになりました

最後に宣伝ですが、2023/03/02のDeNA TechCon 2023で「ゲーム開発のJenkins整備から横断SREチームが誕生するまで」という発表をいたします。自分からはSWETグループによるこれまでのJenkinsについての取り組みを、ゲーム事業部SREチームの白柳からSREチーム誕生までの経緯とその取り組み、そして最後にゲーム開発においてGitHub Actionsを利用し始めたという内容を紹介いたします。

techcon2023.dena.dev

本記事の内容に興味を持った方には面白い発表だと思いますのでぜひ当日DeNA TechCon 2023にお越しください!


  1. -ffd のオプションによりgit管理されているファイル以外は削除され、さらに通常は削除されない.gitignoreで定義されているファイルも-xのオプションにより削除されます。
  2. 推測としては、今回の動作確認で使用したOrganizationレベルのセルフホストランナーであればリポジトリ名だけでユニークであることが保証できるからかもしれません。Enterpriseレベルのセルフホストランナーではもしかしたら {ORG}/repo_A のような構造かもしれませんが、試していないので完全な推測です。
  3. Jenkinsでは一定期間以上ビルドされていないジョブのディレクトリは自動で削除されるなど、ディスク容量の消費を抑える機構が存在しています