Akatsuki Hackers Lab | 株式会社アカツキ(Akatsuki Inc.)

Akatsuki Hackers Labは株式会社アカツキが運営しています。

cocos2dx 3.17でASTCフォーマット使ってみた

こんにちは! Suです。
この記事は Akatsuki Advent Calendar 2022 24日目の記事です。
昨日はいたみんさんの Jenkins Log Parser Plugin の話でした。
色つけてログがみやすくなります。うちのプロジェクトにも導入したいですね〜
読みやすくていい記事でした。勉強になりました!

はじめに

cocos2d-x-3.17.2 はASTCという画像フォーマットをサポートしないので、サポートできるように色々頑張ったことをここに書き残します。
ソフトウェアデコードとハードウェアデコード両方とも実装してみました。

ASTC とは?

ASTCは綺麗で容量もそこそこ抑えられるテクスチャの圧縮フォーマットです。
iPhone6 以降、2015年以降に発売したAndroid端末などは大体使えます。

Block サイズは大きくなるど、綺麗に見えるがサイズも増えます。
ASTC のアルゴリズムはこの文章にて詳しく紹介しません 🙏 

cocos2dx は ASTC サポートしていますが、Axoml という cocos2dx-4.0 から派生したゲームエンジンはサポートしています。それを参考しつづ cocos2dx-3.17 で実装してみます!

ASTCソフトウェアデコード

まず Axoml エンジンから ASTC デコードコードを拝借いたします。

ASTCデコードライブラリ置く場所
ファイル/フォルダ 場所
axmol/astc.h cocos2d/cocos/base
axmol/astc.cpp cocos2d/cocos/base
axmol/thirdparty/astc cocos2d/external

xcodeprojに追加することもお忘れ無く!

 

ここでビルドして問題ないと確認しましょう。

続いて端末が ASTC に対応できるのかの実装です。
他のフォーマットを真似して、CCImage に ASTC の実装を追加します。

bool Image::isASTC(const unsigned char *data, ssize_t dataLen)
bool Image::initWithASTCData(const unsigned char * data, ssize_t dataLen)

を新規追加して、既存の

Image::initWithImageData(const unsigned char * data, ssize_t dataLen)
Image::detectFormat(const unsigned char * data, ssize_t dataLen)

に ASTC の分岐文を追加します。

▼クリックでコードを表示する

 

最後に HellowWorldScene に astc 画像を表示するボタンを作成します。
画像はみなさんご存知のレナさんにしました。
下記の ARM 社の astc-encoder を使って PNG から ASTC に変換しました。

▼クリックでコードを表示する


下記はソフトウェアデコードがかかった時間です。
ASTC 8x8のサイズ圧倒的に優秀ですが、
やはり遅いですね...ハードウェアでコード実装しないと使えないの気がします。

ASTCソフトウェアデコード
解像度 フォーマット サイズ 時間
512 png 386KB 約35ms
512 astc 4x4 226KB 約270ms
512 astc 8x8 70KB 約300ms
1024 png 1.4MB 約80ms
1024 astc 4x4 1.1MB 約892ms
1024 astc 8x8 266KB 約930ms
1960 png 4.1MB 約185ms
1960 astc 4x4 3.8MB 約3100ms
1960 astc 8x8 963KB 約3210ms

※時間は10回平均、cacheなし

動画はこちらです

ASTCハードウェアデコード

まず最初の問題は、GLES 3.0 から ASTC ldr が対応していますが、cocos2dx-3.17 が GLES 3 に対応しないです。昔一時 GLES 3.0 対応させたですが、どうやら一部 Android シミュレーターがクラッシュするとパフォーマンスの問題で revert されたみたいです。

実験として、ios を GLES 3.0 に対応させます。

▼クリックでコードを表示する

 

手元の端末 iPhone 8 で対応できる ASTC フォーマットを調べると、ldr しか対応できないので、今回はそれを対応させてみます。

GL_KHR_texture_compression_astc_ldr 

ちなみに、このサイトで対応しない端末が調べます。

端末が ASTC に対応できるのかをチェックするため、CCConfiguration に isSupportsASTC() を追加します。

▼クリックでコードを表示する

 

続いて ASTC 4x4、8x8 のPixelFormat を追加します。
それそれの internalformat は この仕様書 に書かれてます。

COMPRESSED_RGBA_ASTC_4x4_KHR 0x93B0 COMPRESSED_RGBA_ASTC_8x8_KHR 0x93B7

▼クリックでコードを表示する

 

bpp(BitsPrePixel) は Wikipedia で確認できます。

Block footprint Bit rate
4×4 8.00
8×8 2.00

主に下記 CCTexture2D.cpp の PixelFormat マップに ASTC の項目を追加することです。

static const PixelFormatInfoMapValue TexturePixelFormatInfoTablesValue[]

▼クリックでコードを表示する

 

最後にCCImage.cppにハードウェアデコードの処理を追加すればOK!

▼クリックでコードを表示する

 

では時間を測ってみましょう〜

ASTCデコード
解像度 フォーマット サイズ ソフトウェア時間 ハードウェア時間
512 png 386KB 約35ms 約35ms
512 astc 4x4 226KB 約270ms 約3ms
512 astc 8x8 70KB 約300ms 約2ms
1024 png 1.4MB 約80ms 約80ms
1024 astc 4x4 1.1MB 約892ms 約5ms
1024 astc 8x8 266KB 約930ms 約3ms
1960 png 4.1MB 約185ms 約185ms
1960 astc 4x4 3.8MB 約3100ms 約17ms
1960 astc 8x8 963KB 約3210ms 約6ms

※時間は10回平均、cacheなし

爆速じゃん!!!
Fooooooo 🎉🎉🎉

動画はこちらです

Github repo もありますー


最後に

ハードウェアデコードがすごかった...
Androidも試したいですね 🤔
もし今後他のフォーマットを追加したい時、もっとちゃんとしたコード書かないと思いますが、大体同じやり方でできると思います。
自分のメモもなりましたし、この記事読んだ君の役に立てればいいですね〜

明日は Sieben.L さんのドライブと関連した内容です。楽しみですね!

アカツキでは一緒に働くエンジニアを募集しています。
カジュアル面談もやっていますので、気軽にご応募ください。
https://hrmos.co/pages/aktsk/jobs?category=1220948640529788928

Fast DDLを使った高速なmigrationを実施してみた話

この記事は Akatsuki Games Advent Calendar 2022 の22日目の記事です。昨日はKazuma Sakamotoさんの「UE5でMMDを踊らせるぞ!」でした。こうした興味などでやってみたいことを実験してみてブログまで書くのは大事ですね!

 

はじめに

はじめまして、アカツキゲームスでサーバーエンジニアをやっています柴原です。今回は自分の担当しているモバイルゲームのプロジェクトにてAurora MySQL lab modeの機能の1つであるFast DDLを使った高速なmigrationを試してみました。

経緯

モバイルゲームの様な運用型のゲームでは、運用年数に比例してユーザデータの量が増えていきます。加えて、新機能追加に伴うテーブルへのカラム追加などが必要になる場面があります。そして、このような構造変更はデータ量に比例して時間がかかります。私の所属するプロジェクトでは、こうしたDBの構造変更の作業はメンテナンス時間を設けて、その時間内で行っています。メンテナンスの時間はゲームをプレイすることができないため、短ければ短いほど良いです。そのため、メンテナンス時間を短くできる可能性のあるFast DDLに着目しました。

Fast DDLとは

Fast DDL は、AWSが提供している機能の一つでALTER TABLE tbl_name ADD COLUMN col_name column_definitionの操作をほぼ瞬時に実行できる機能です。以下3つの制約を満たしている時にFast DDLを利用できます。

  • デフォルト値を持たないこと
  • nullableなカラムであること
  • 末尾挿入であること

しかし、ドキュメントにあるようにAWSは本番DBクラスターでの運用を推奨していません。本番環境で導入を検討する際には注意してください。

Fast DDLが使えるのは、Aurora MySQL version1とversion2(MySQL5.6もしくはMySQL5.7相当)です。Aurora MySQL version3ではFast DDLは廃止され、MySQL8系で導入されたinstant DDLが使用できるようになります。

設定

ゴールとしては、RDSクラスターのaurora_lab_modeの設定パラメータが1になっている状態です。設定手順を以下に示します。

  1. aurora_lab_modeを有効化したDBクラスターパラメータグループを用意します(これはすでに適用しているDBクラスターパラメータグループの値を変更して使用しても問題ありません)
  2. このパラメータグループを該当RDSに適用します(この段階ではまだ適用が終わってません)
  3. パラメータグループを完全に適用するためRDSの再起動を実施します
  4. 念のためにaurora_lab_modeが有効化されているか確認する(Optional: cli経由でも確認できますし、select @@aurora_lab_mode;でも確認できます)

検証してみた

本番のRDSをクローンした環境で実験を行いました。実際のメンテナンスにて行う予定のDB構造の変更に対して、Fast DDLの効果を比較検証しました。今回行うDBの構造変更は、通常のカラムとVirtual Generated カラムの追加です。この2点について検証を行いました。

通常のカラム追加の結果を以下に示します。

  • Fast DDLなし:2時間程かかる
  • Fast DDLあり:2秒 程かかる

結果は一目瞭然ですね。単純な比較ではありますが、3600倍高速になります。(恐らくどんなにデータ量が多くても数秒で終わるような仕組みだとは思います。)

続いてVirtual Generated カラム追加の結果を以下に示します。

  • Fast DDLなし:0.20秒
  • Fast DDLあり:2.80秒

結果としては、多少遅くなりました。この時間差であれば誤差なので、特に気にすることなくこの機能を有効化して作業ができそうなことが確認できました。

まとめ

今回は、Fast DDLを用いた高速なmigrationを検証しました。本番環境での利用にはリスクがあるものの、十分検討に足りうるものであると思います。本記事は、この機能を本番で使うことをお勧めしている訳ではないです。十分な事前検証を行った上で検討してみてください。

 

明日のAkatsuki Games Advent Calendar 2022 は、itmさんの「JenkinsのLog Parser触ってみた」です!お楽しみに!!

 

最後に、アカツキでは一緒に働くエンジニアを募集しています。
カジュアル面談もやっていますので、まずは雑談からいかがでしょうか。

hrmos.co

AWS Device Farm を使って Airtest を実行するときのフローとは

この記事は ソフトウェアテスト Advent Calendar 2022 14日目の記事です。

はじめに

はじめまして、アカツキゲームスでQAエンジニアをやっている山﨑@tomo_tk11です。私は現在、モバイルゲームアプリケーションの自動テストシステムの開発を担当しています。ゲームの自動テストといえば NetEase 社から提供されている Airtest が有名です。CEDEC でもいくつか事例が発表されており*1、私たちも Airtest を用いてテスト自動化を試みています。

実は、 Akatsuki Games Advent Calendar 2022 の5日目 に Airtest x AWS Device Farm を用いた自動テストの実行方法をまとめたのですが、そこでは書ききれなかったことがあるので別記事にしようと思い立ちました。

この記事では、自動テストの実行フローと、実行時に必要なパラメータ設定タイミングについてまとめています。

この記事を楽しんでいただくためのヒント

本記事は、以下の Airtest x AWS Device Farm を用いた自動テストの実行方法についてまとめた記事が前提となっています。この記事単体でも話が成立するように書いてはいますが、前提となる記事を読むことで理解の助けになると思います。

hackerslab.aktsk.jp

目次

ターゲット

  • Airtest(and Poco) を使用してテスト自動化を行っている方
  • AWS Device Farm を利用してテスト自動化を行っている方

この記事でわかること

  • AWS Device Farm からテストシナリオスクリプトを実行するまでのフロー
  • 各世界で必要なパラメータ
  • パラメータを設定するタイミング

AWS Device Farm x Airtest で動作保証すべき3つの環境

前提となっている記事の超要約です。

Airtest を Local で実行し、自動テストする場合はシンプルな実行環境になります。しかし、IDE, CLI の実行をそれぞれサポートしたり、Device Farm をテスト実行環境として採用したりすると、テスト実行手段が増えて自動化システムが複雑になります。結果として、すべての環境で動作保証がされなくなり、開発効率の低下に繋がります。

なので、テスト実行環境を整理し、最低限動作保証すべき環境を3つに絞りました。

環境を絞るまでの過程は Airtest で CLI, IDE や Device Farm, Local など様々なテスト実行手段、環境があったとしても楽に自動テストを実行する方法 に書いているので気になる方はご確認ください。

実行命令環境 実行環境 デバイスホストマシンOS デバイス 実行方法 ファイル拡張子 Python Runtime ランチャー
Local Local Windows/Mac Unity Editor/Android/iOS Airtest IDE .air pipenv(3.9.14) custom_launcher.py
Local Local Windows/Mac/Linux Unity Editor/Android/iOS invoke → pytest .py pipenv(3.9.14) test_main.py
Local/Jenkins AWS Device Farm Linux Android/iOS invoke → AWS CLI → pytest .py セルフビルド(3.9.14) test_main.py

自動テストの実行フローとパラメータ設定タイミング

実行方法はシンプルになったので、ここでは実行フローに焦点を当てて解説していきます。各実行方法でテスト実行し、どういうフローで共通のテストシナリオスクリプトにたどり着くのかを見ていきましょう。先に結論である全体図を貼ります。

自動テストシステムに存在する世界

  • Device Farm: 実行環境
    • Jenkins: Device Farm を楽に実行するための手段
  • Python: テストスクリプトの言語
    • Airtest: 自動テストフレームワーク及びテスト実行手段(IDE)
      • pytest: テスト実行手段(CLI)
      • ゲームアプリ: テスト実行対象

多くの世界が存在しており、それぞれに役割があります。そして、それぞれの世界で取り扱うパラメータが存在します。そのパラメータが設定されるタイミングや、管理方法を整理しないままシステムを構築すると複雑化の原因になります。

各世界のパラメータについて

今後増えるかもしれませんが、現時点でのパラメータを列挙します。一つ一つ解説していくと長くなるのでここではやりませんが、Device Farm や Airtest 世界のパラメータは独自ドメインのものが少ないので参考になるかと思います。PYTHONPATH や PROJECT_ROOT については 前提記事で解説しています。

  • Jenkins 世界のパラメータ
    • テスト管理リポジトリのブランチ名
  • Device Farm 世界のパラメータ
    • テスト実行対象のアプリバージョン
    • 起動前に配置するアセットバンドルサイズ
    • 実行時の Run 名
    • 実行するデバイスプール
    • タイムアウト時間(分)
  • Python 世界のパラメータ
    • モジュールパス(PYTHONPATH)
  • pytest 世界のパラメータ
    • 実行するテストシナリオ
  • Airtest 世界のパラメータ
    • プロジェクトルートパス(PROJECT_ROOT)
    • 操作ログの prefix
    • 操作ログ出力先のパス
    • レポート出力先のパス
  • ゲームアプリ 世界のパラメータ
    • アプリケーションパッケージ名
    • 接続するゲームサーバ名

Device Farm 世界のフローとパラメータ設定タイミング

Device Farm 実行から始まるフローは、動作保証すべき環境表の3行目に該当します。エンジニアだけが実行するのであれば Local でコマンドを叩くだけでも良いのですが、自動テストは誰でも実行できたり、特定の時間に実行できたりする必要があるので、 Jenkins ジョブを用意すると便利です。ここでは多くのパラメータを設定しますが、Device Farm を実行する作業はフローの末端に位置するので致し方ありません。特に Device Farm に関するパラメータが多数を占めています。Device Farm のパラメータはAWS CLI のコマンドや testspec.yaml ファイル内に反映されます。

invoke について

タスクランナーとして invoke を導入しています。invoke は Python でタスクを記述できるので、細かく実行しないといけないコマンドをまとめて1つのタスクで定義できます。以下の例では、 testspec.yaml の生成、AWS CLI の実行を行う run_device_farm.sh の処理を invoke タスクで行っています。シェルスクリプトに渡す環境変数をオプションとしてサクッと渡せるのは魅力的です。また、default 値も設定できる点も良いです。

また、 run_device_farm.sh の処理を Pythonとして記述してしまえば invoke 内に書くこともできるでしょう。

@task(
    help={
        "scenario": "Scenario to run",
        "environment": "Test environment",
        "df-run-name": "DF run session name",
        "df-device-type": "DF device type",
        "df-timeout": "DF session timeout min",
        "app-package": "Bundle ID of app under test",
    }
)
def df(
    c,
    scenario="smoke",
    environment="server01",
    df_run_name=None,
    df_device_type="Pixel5",
    df_timeout=5,
    app_package="app.package.name",
):
    """
    Run test in AWS Device Farm.
      Args:
        scenario: Scenario to run
        environment: Test environment
        df_run_name: DF run session name
        df_device_type: DF device type
        df_timeout: DF session timeout min
        app_package: Bundle ID of app under test
    """
    if not df_run_name:
        df_run_name = f"{os.getenv('USER')}-{scenario}"
    mp.invoke("Start running autotest in DF...")
    c.run(
        f"TEST_SCENARIO={scenario} \
        TEST_ENVIRONMENT={environment} \
        DF_RUN_NAME={df_run_name} \
        DF_DEVICE_TYPE={df_device_type} \
        DF_TIMEOUT={df_timeout} \
        APP_PACKAGE={app_package} \
        ./run_device_farm.sh",
        hide=False,
    )
    mp.invoke("Running autotest in DF: Completed!")

Airtest 世界のフローとパラメータ設定タイミング

Airtest IDE を使ってテスト実行するフローは動作保証すべき環境表の1行目、invoke → pytest を使って CLI でテスト実行するフローは2行目に該当します。

pytest addoption について

まず pytest から説明します。pytest はテストフレームワークなのでテストを効率よく行うための機能がたくさん備わっています。その中で今回は addoption に着目します。

def pytest_addoption(parser):
    """pytest実行時オプション"""
    parser.addoption("-S", "--scenario", action="store", help="実行するシナリオ")
    parser.addoption("-E", "--environment", action="store", help="接続先の環境")
    parser.addoption("-A", "--app-package", action="store", help="テスト対象アプリのBundle ID")

pytest_addoption でオプションを定義することで、pytest 実行時に値を渡せるようになります。その値は Python で書かれたテストスクリプト内で参照可能です。値参照が手軽にできるのはテストスクリプトと同じ言語のテストフレームワークを使う強みでもあります。

class TestMain:
        def test_main(self, main_context):
            """Test Main
          Args:
              main_context: Test run context
          """
            test_scenario = main_context.scenario
            test_environment = main_context.environment
            app_package = main_context.app_package

値は main_context から取得することができます。

pytest -s tests/test_main.air/test_main.py \
    --scenario smoke \
    --environment server01 \
    --app-package app.package.name

このように、pytest 実行時にオプションを渡します。本システムでは pytest は直接実行しておらず、 invoke を経由して pytest を実行しています。

Airtest IDE の Settings について

次に、Airtest IDE の実行フローについてです。IDE はフロー図から分かる通り、本流から外れた位置からの実行方法です。しかしながら、この IDE 実行においても CLI 実行と同様のテストシナリオスクリプトを動作させたいため、頑張って工夫します。

本流のフローでは、 pipenv で作られた仮想環境空間や Device Farm 内の Python 空間でテスト実行されるため、 PYTHONPATH を認識させるのは難しくありません。しかし、 IDE はその空間外でアプリケーションが起動しているため、普通にアプリケーションを開くと PYTHONPATH が定義されていない状態になります。もちろん global に環境変数を定義すれば認識可能ですが、それでは他の Python Project に影響を与えてしまいます。従って、以下の対応を行います。

  • IDE を pipenv の仮想環境空間からコマンドラインで開く(mac OS だと open コマンド)
  • IDE の Options > Settings の Custom Python Path に仮想環境空間内の Python Path を設定する(pipenv なので .pyenv のパス)

その他のパラメータ設定タイミング

フロー図には記載していませんが、Python Project のコンフィグ/設定ファイル(pyproject.toml)でもパラメータを設定できるようにしています。コンフィグファイルでは比較的静的な情報を定義すべきです。一応各パラメータをコンフィグファイルで設定できるようにしていますが、ここで全てを設定するのは非推奨です。

[autotest]
project_root = "/Users/YOUR_NAME/repos/name"
python_path = ["/Users/YOUR_NAME/repos/name", "/Users/YOUR_NAME/repos/name/.venv/lib/python3.9/site-packages"]
log_dir = "logs"
report_export_dir = "logs"
log_prefix = "autotest"
app_package = "app.package.name"
app_version = "1.0.0"
test_environment = "server01"
test_scenario = ["smoke"]

パラメータの基本方針

各世界で取り扱うパラメータは種類が多いのに加えて、上記のコンフィグファイルや環境変数、pytest のオプションなど管理方法も様々です。ここで、それぞれの管理方法の基本方針を定義します。

  1. プロジェクトレベルの比較的静的な情報はコンフィグ/設定ファイルで(pyproject.toml)で指定可能
  2. コンフィグより更新の頻度や可能性の高い設定値は環境変数でも指定可能(コンフィグの設定を上書きする)
  3. コンフィグ/環境変数よりも粒度の小さい個別実行単位などの指定にはオプション引数で指定(上記1, 2を上書き)

Setting クラスによるパラメータの集中管理

テストシナリオスクリプト内でそれらをバラバラに参照するわけにはいかないので、集中管理するための Setting クラスを作成します。そして、上記の基本方針も仕様に組み込みます。

class Setting:
    """
    テスト実行に必要な全設定をConfig/Environment/オプション引数などの全てのソースから集約して保持するデータクラス

    処理順や優先度は以下の通り。左から順に処理され以降に同じ設定があれば後の処理で上書きされていき、
    テスト実行前に必要な準備処理が終わった段階では全ての設定がSettingに保持されている。
    """

    def __init__(
        self,
        project_root=Config.project_root,
        python_path=Config.python_path,
        log_dir=Config.log_dir,
        log_prefix=Config.log_prefix,
        report_export_dir=Config.report_export_dir,
        app_package=Config.app_package,
        app_version=Config.app_version,
        test_environment=Config.test_environment,
        test_scenario=Config.test_scenario,
    ):
        self.project_root = Environment.PROJECT_ROOT or project_root
        self.python_path = Environment.PYTHONPATH if all(Environment.PYTHONPATH) else python_path
        self.log_dir = Environment.LOG_DIR or log_dir
        self.report_export_dir = Environment.REPORT_EXPORT_DIR or report_export_dir
        self.log_prefix = Environment.LOG_PREFIX or log_prefix
        self.app_package = Environment.APP_PACKAGE or app_package
        self.app_version = Environment.APP_VERSION or app_version
        self.test_environment = Environment.TEST_ENVIRONMENT or test_environment
        self.test_scenario = (
            Environment.TEST_SCENARIO if all(Environment.TEST_SCENARIO) else test_scenario
        )

Config クラスは toml ファイルから、Envrionment クラスは環境変数から値の読み込みを行っています。中身は単純なのでここでは説明は割愛します。ここで重要なのは、以下の2点です。

  • Setting クラスで値を集中管理できている
  • 管理方法ごとに優先度が付けられている
    • Config < Environment < Command line Option < Setting

これでテストシナリオスクリプトで値が欲しいときには、Setting クラスを参照するだけでよしなに値を取得することができるようになりました。

まとめ

この記事では、自動テストの実行フローとパラメータの設定タイミングについてまとめました。

結果として、それぞれの方法でテスト実行し、どういうフローで同一のテストシナリオスクリプトに到達するかを可視化することができました。また、自動テストの実行方法によって設定するパラメータに違いはありますが、invoke(タスクランナー) や pytest の addoption、 IDE の Settings、pyproject.toml(コンフィグファイル) を用いて解決することができました。更に Setting クラスを作り、パラメータの集中管理 + 優先度付けを行うことができました。

この記事によって、ゲームアプリケーションのテスト自動化に取り組まれる方が増えると私は嬉しいです。それでは、良いクリスマスを!

*1:CEDEC Airtest でググると何件かヒットします。サムザップさんの取り組みが有名です。

Engineering Managerを廃止して1年経ちました

こんにちは、ゆのん(id:yunon_phys)です。このエントリーはAkatsuki Games Advent Calendar 2022の14日目の記事です。昨日はMaxBaconPowerさんの「巨大数でわかる Elixir の魅力」でした。Elixirが再帰が得意とはいえ、良くこんな題材を思いついたなと感心しました。早くふぃっしゅ数を見てみたいものです。

さて本題に入るわけですが、昨年、Engineering Manager(EM)を廃止して3つに分割したという話を書きました。そこから1年経ち、どのような状態になったのか、ふりかえりも含めて書いていきます。本記事は前回の記事を読まなくても読めるようにしていますが、更に背景理解したい方は前回の記事も読んでみてください。

hackerslab.aktsk.jp

ずばりEMを無くして良かったのか

これはマクロに見ると明確に良かったと思っています。ただし、ミクロに見ていくと、良かったところ、改善すべきところは見えてきています。EMをTech評価とEngineering Office(エンジニアの人事部門)とプロジェクトマネジメントに分けたのですが、それぞれ自分なりにふりかえっていきます。

続きを読む

GitHub Actions でコミットの status checks を取る

この記事は Akatsuki Advent Calendar 2022 の7日目の記事です。

adventar.org

昨日は g_garden さんの GAS で 「slack に雑談お題投稿君」を作る でした。リモートワーク下で雑談を自然に作り出すことの難しさは日々感じます。技術で解決できるといいですね!

zenn.dev

こんばんは。4月から長期インターンをしている otofune です。

先日担当するプロジェクトで、運用されている git のブランチを複数から1つに、リリースのトリガーを環境ごとに用意されたブランチが更新された時からタグが打たれた時に変更しました。

当然リリースするためには自動テストが通過していることを確認したいですが、再度実行すると効率がよくありません。
そこで、GitHub からコミットの status checks を取得することで代用しました。

TL;DR

サンプルはこちら https://github.com/otofune/actions-graphql-status-checks-example

github.com

  • コミットの status checks は REST API ではまとめて取得できず、GraphQL API を使うほうが適切
  • GraphQL API の StatusCheckRollup を取得するとよい。ただし Commit から取得する必要があり、癖がある
  • GitHub Actions から GraphQL API を使うために actions/github-script が使える
  • 例えば Actions などの status checks を変更する環境から参照するときは、自分自身をうまく除外してやらないといけない

status checks とはなにか、取得する方法

GitHub 上のコミットに保留、合格、不合格の状態をつける機能を総称して status checks と呼ぶようです。 コミットの横にあるチェックマークのことだと思えばよさそうです。

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks

歴史的経緯によりこれを実現する機能には Statuses と、それより新しい Checks (2018/5/7 リリース) の2種類があります。

GitHub の UI 上ではうまくまとめて表示されていますが、調べた限りでは REST API ではまとめて取ることができず、Statuses API と Checks API を個別に取る必要があります。

もし片方のみが使われているのであればどちらかを REST API で取得すればよいですが、該当プロジェクトでは CircleCI と GitHub Actions を併用していました。
GitHub Actions は Checks API を使い、CircleCI はデフォルトで Statuses API を使います。

また Statuses はいまいちよくわからない応答を返します。List commit statuses for a reference では編集履歴のようなものが返され、Get the combined status for a specific reference を使用しないと求めたような結果になりません。

実際のレスポンス例 (List)

GraphQL API には StatusCheckRollup または Status という型があり、これらを使えばまとめて取得することができます。 ただし Union となっており、Statuses と Checks を意識してコードを書く必要はあります。

今回は GraphQL を使う方法を選ぶことにしました。 REST API の Statuses API の挙動がよくわからないこと、当初 Checks / Statuses をまとめた結論が取得できることも優位点であると考えて採用しました。実際には GitHub Actions から取得するために結論は常に pending となってしまうため自前での計算が必要でした (後述)。

GitHub Actions で status checks を取得する

GraphQL API を使う

GitHub API へのリクエストが JavaScript で書ける actions/github-script が GraphQL にも対応しています。

github A pre-authenticated octokit/rest.js client with pagination plugins https://github.com/actions/github-script

README の先頭にはこのように書いてありますが、graphql の example が載っているほか、ソースコード を見ると github object は @actions/github 由来です。 github.graphql から octokit/graphql.js が使えます。

どうやって status checks を取得するか - Commit を取得するには

StatusCheckRollup にしろ Status にしろ、Commit から辿るしかありません。

かといって Query の一覧を見ても直接 Commit を取る方法はありません。 実は Repository の object フィールドから取得する必要があります。

repository(name: $repo, owner: $owner) {
   object(expression: $expression) {
      __typename
      ... on Commit {
         (snip)
      }
   }
}

この object は nullable で、かつ Commit 専用 ではなく git の object を取得するためのものです。 必ずしも Commit が返ってくるとは限らない点に注意が必要です。Object として扱われるものはたくさんあります。(GitObject interface 参照)
しっかり null チェックをした上で __typename を見てエラーハンドリングしましょう。

また、権限が必要であることも忘れてはいけません。checks:readstatuses:read を permission に与えるとよいです。

自分自身の Job を除外し、結論を計算する

Checks の内、permalink を比べると除外できます。 permalink に入っているリンクは /:owner/:repo/actions/runs/:run_id/jobs/:unknown_id のような形式になっており run_id は context から取得できます。

なお一時期は /:owner/:repo/actions/runs/:job_id?check_suite_focus=true という URL が入っており、Job ID は与えられていないため別途 octokit.rest.actions.listJobsForWorkflowRunAttempt なりを使って取得する必要がありました。 いまでも API などから Job ID を取得してやると古い UI を表示することができるはずです。

除外したのち、うまく結論を計算するとよいです。

GitHub は次のような順番で判定しているっぽいと推測しています。

  1. 失敗している checks / statuses があれば結論を失敗にする
  2. 保留が1つでもあれば結論を保留にする
  3. すべて成功なら結論を成功にする

これで取得できました 🎉🎉🎉

詳しくは example の .github/workflows/graphql.js に書いてあります。

困った点

object の取得ミス

当初 git rev-parse ${{ github.ref }} を expression として渡していました。 タグを契機に起動するため、もし annotated tag だと tag の object id が取得され、Tag が取得されてしまっていました。

そもそも ${{ github.sha }} を使うか、git rev-parse ${{ github.ref }}^{commit} のように取得するオブジェクトを明記してやるとよいです。

nullable の検査漏れ・デバッグ

言わずもがなですが。

せっかく GraphQL API を使っているので型の自動生成ができればよかったのですが、actions/github-script で使うには大仰ではと思い、TypeScript ではなく JavaScript で書いたところかなり苦しめられました。

GitHub Actions 上でなにが起こったか追跡するのは困難です。とにかくログになんでも出力しておくと幸せになれます。

まとめ

GitHub Actions からある commit がテストに通過しているかを GitHub GraphQL API を使って取得し、活用できる状態にしました。振り返れば REST API で適当に作ってしまったほうがよかったのかもしれません。

最後まで読んで頂きありがとうございました。

明日の Akatsuki Games Advent Calendar 2022 は kazuma.ito さんの「UEでワールド構築のなにか」です!

最後に、アカツキでは一緒に働くエンジニアを募集しています。
カジュアル面談もやっていますので、気軽にご応募ください。
https://hrmos.co/pages/aktsk/jobs?category=1220948640529788928