tutorial

引き出しの中の端末から、Pull Requestの緑のチェックまで。

所要時間はおよそ15分。そのほとんどはruntime bundleのダウンロードです。

必要なもの

非対応: 32bit ARM、x86 Android、Docker、KVM、ネスト仮想化。

端末選びは慎重に。 この端末はWorkflowに書かれたコードをそのまま実行し、 長時間にわたって発熱します。写真の入った普段使いの端末ではなく、 初期化してCI専用にした端末を使ってください。あわせて、その端末には ロック画面を設定しないでください。ロックされた端末は、人がアンロックするまで 再起動後もRunnerを開始できません。

セットアップ

  1. アプリをインストールする

    DroidRunnerはGoogle Playにはありません。ストア外から取得したコードを実行する という性質上、Device & Network Abuseポリシーに抵触するためです。 APKを直接インストールしてください。

    推奨: Obtainium。 無人で動くRunnerの更新を、人がリリースに気づくまで待たせるべきではないからです。 Add app からリポジトリURLを貼り付けます:

    https://github.com/m96-chan/DroidRunner

    APKフィルタに droidrunner-v.*\.apk を設定してください。各リリースには 対応ソースアーカイブ droidrunner-v<version>-source.tar.gz も入っている ため、フィルタが無いとObtainiumは2つのアセットから選ぶことになります。設定後は、 新しいタグが公開されるたびにObtainiumがアプリを上書き更新します。

    手動: 最新リリース から droidrunner-v<version>.apk をダウンロードして開きます。 ブラウザやファイルマネージャに対して「提供元不明のアプリ」の許可を一度求められます。

    上書き更新できるのは署名鍵が同じあいだだけです。別の署名のビルド(自前ビルドや 自前ビルドなど)へ移るにはアンインストールが必要で、その際 Runnerの登録情報と保存済みGitHub資格情報は失われます。 端末の再登録が必要になります。
  2. GitHub Appをインストールする

    DroidRunnerはトークンを貼り付けさせるのではなくGitHub Appとしてサインインします。 そのため、担当させるRepositoryまたはOrganizationにAppを入れておく必要があります。

    github.com/apps/droidgithubrunner を開き、Runnerに担当させるリポジトリへインストールします。 要求する権限は Administration: read and write で、 これはself-hosted runnerの登録に必要な権限です。 同時にリポジトリ設定を変更できる権限でもあるので、 所有する全リポジトリにではなく、対象を選んで付与してください。

    入れ忘れていても、サインイン時にアプリが検出して案内します。

  3. 端末からサインインする

    DroidRunnerを起動し、ヘッダの⚙からsetup画面を開いて Connect GitHubをタップします。

    8文字のコードが表示され、同時にクリップボードにコピーされます。 github.com/login/device で入力して承認してください。端末上でも、別のPCからでも構いません。

    これはOAuth Device Flowなので、APKの中にclient secretは入っておらず、 あなたがPATを発行したり更新したりする必要もありません。 得られたユーザートークンはAndroid Keystoreで暗号化されてAndroid側に留まり、 必要なときに短命のregistration tokenと交換されます。 ジョブが動くLinux環境にトークンが渡ることはありません。

  4. 登録先を選んで登録する

    Repository、またはAppをインストール済みのOrganizationを選び、 Registerをタップします。

    Organizationスコープは見た目より広いです。 Organization Runnerは、許可リスト付きの runner group に入れない限り組織内の全リポジトリからジョブを受け付けます。 Workflowのコードを実行する端末にとって、これは明確に広い信頼境界です。 まずは単一リポジトリから始めてください。

    登録はプログレスダイアログの中で進みます。registration tokenの取得、 最新のruntime-*リリースの探索、manifest署名の検証、 約200MBのbundleのダウンロード、SHA-256の照合、展開、 そして公式config.shの実行まで。 回線が細いと、ここがセットアップで一番長い工程になります。

    あとから登録先を変えることもできます。Runnerを停止して新しい対象を選ぶと、 ボタンがRe-registerに変わります。

  5. ジョブポリシーを決める

    引き続きsetup画面で、この端末がいつ仕事を受けてよいかを決めます。

    バッテリー残量thermal status空き容量の しきい値が、ジョブを保留する条件になります。高温・容量不足、あるいは バッテリー動作かつ残量がしきい値未満のあいだは新しいジョブを受け取らず、 条件が解消すれば自動で再開します。 例外はcritical thermalで、これは実行中のジョブも中断します。 サーマルスロットリング中の端末が出す数値は、そもそも信用に値しないからです。

    給電が消えただけでは保留しません。充電済みの端末は、残量が実際にしきい値を 割るまで停電をまたいで走り続けます。バッテリーを積んでいる意味はほぼそこにあります。 Run only on mains powerは別のトグルで、既定はオフです。 自分の充電を絶対に使わせたくない端末のために残してあります。

    ephemeralモードは、ジョブごとにRunnerを再登録し work directoryを消去します。ジョブの起動は遅くなりますが、 複数のWorkflow — あるいは複数の人 — がこの端末に届きうるなら、 こちらを既定にすべきです。

    起動時に開始を有効にすると、再起動後もRunnerが戻ります — ただし端末がアンロックされてからです。Androidは端末がcredential-lock状態のあいだ 起動ブロードキャストを保留し、最初のアンロックで配信します。それまでは runtimeも保存済みの認証情報も、そもそも読み出せません。 つまりセキュアなロック画面のある端末は、停電のあと誰かが手に取るまでCIに戻りません。 CI専用の端末にはロック画面を設定しないでください。再起動が本当に無人で完結するのは、 その構成だけです。それでも待ち時間が生じた場合は、 Runnerパネルが端末のオフラインだった時間を表示します。

    アプリが求めるバッテリー最適化の除外も許可しておいてください。 Runnerが夜のあいだに静かにオフラインになる原因は、 たいていベンダー製のタスクキラーです。

  6. 完了 — あとは自動で立ち上がります

    ダッシュボードに戻ります。数秒でRunnerパネルが listening for jobs になり、ヘッダにも同じ状態が出ます。

    登録済みの端末は、アプリを起動するたびにRunnerを自動で開始します。 手動のStart/StopはRunnerパネルにあります。GitHub側では Settings → Actions → Runners に、ラベル付きで端末が現れます。

最初のジョブ

.github/workflows/device.yml としてコミットします:

name: Device smoke

on:
  workflow_dispatch:

permissions:
  contents: read

jobs:
  smoke:
    runs-on: [self-hosted, android, arm64]
    steps:
      - uses: actions/checkout@v4
      - run: uname -a && nproc && free -h

Actionsタブから実行します。端末側のRunnerパネルが running jobに切り替わってログが流れ、 完了するとジョブカウンタが増えます。

ジョブを誰も拾わないときは。 runs-onのラベルが、 端末が実際に登録したラベルと一致しているか確認してください。 ラベルはアプリのタイトル下と、GitHubのRunner一覧に出ています。 runs-onのラベルは全て揃っている必要があり、 タイプミスがあるとジョブは失敗せずに待ち続けます。

NPUに聞いてみる

droidrunner-deviceはRunnerが起動するたびにAPKからゲストへ入るので、 対話するDevice Agentと常に同じ世代に保たれます。そのAgentとは、 実行中のジョブ向けに発行されたトークンで認証しながら、loopback経由で通信します。

runs-on: [self-hosted, android, nnapi-accelerator]
steps:
  - run: droidrunner-device capabilities
  - run: droidrunner-device devices
  - run: droidrunner-device test model my-model.tflite --device google-edgetpu --iterations 30
指定するのは android-npu ではなく nnapi-accelerator です。 android-npu は「そのSoCファミリーにNPUがあると分かっている」という ヒントにすぎず、約束ではありません。これを指定したジョブは、 全部CPUで走って何も言わない端末に着地することがあります。

速さではなく、答えを確かめる

レイテンシは「グラフが受理された」ことしか言いません。正しく計算されたかは 別の問いで、モデルを出荷するなら重要なのはそちらです。ベンダーのシリコン上でだけ出る バグは、手元のPCで検査している限り正しいコードと見分けがつきません。

  - uses: m96-chan/DroidRunner/actions/run-model@main
    id: device
    with:
      model: build/model.tflite
      device: qnn-htp
      inputs: fixtures/input-0.bin
      output-dir: out
  - run: test "${{ steps.device.outputs.executed }}" = accelerator
  - run: cmp out/output-0.bin fixtures/golden-0.bin

結果には必ず executed(依頼した相手ではなく実際に実行した相手)が 付き、帰属できない実行は数値として報告せず拒否します。他リポジトリが依存できる形は RESULT-CONTRACT.md に書いてあります。

うまくいかないとき

画面を消すとRunnerがオフラインになる

ほぼ確実にベンダーの電池管理です。アプリ側は二段構えで、サービスはstickyにしてあり、 それでも落とされた場合は監視ジョブが15分以内に起こし直します(Xiaomiで両方が実際に 働くことを実測。片方が取りこぼしても、もう片方が拾いました)。あわせて、アプリが要求する バッテリー最適化の除外を許可し、Xiaomi/MIUI、Samsung、Huawei、OPPOでは最近のアプリ 一覧でロックし、自動起動も許可してください。

ただし、それらが一切通じないROMがあります。RedMagic 8では、stickyなサービスも、 アラームも、全制約を満たした監視ジョブも、30分待って一度も実行されませんでした (バッテリー最適化は除外済み、バックグラウンド制限もない状態で)。メーカーのアプリ別 電源設定画面は署名権限で保護されていてアプリからは開けないため、端末の設定から 手動でDroidRunnerを許可してください

「A session for this runner already exists」

アプリの再インストールや強制停止のあとに出ます。GitHub側に古いリスナーセッションが 残っている状態です。リスナーが再試行してRunner reconnectedを出せば解消します。 それまでキュー中のジョブは割り当てられないので、数分待つか、 ダッシュボードからRunnerを再起動してください。

登録に失敗する / すでに設定済みと言われる

再インストールではなく、setup画面から再登録してください。古いRunner状態は アプリ側で消します。トークンが期限切れの場合は、サインインし直してください。 ユーザートークンには有効期限を設定でき、期限切れはログイン要求ではなく 登録の失敗として現れます。

ジョブが保留のまま始まらない

理由はダッシュボードに出ています。バッテリー動作かつ残量がしきい値未満、 上がったthermal status、下限を割った空き容量のいずれかです。 Run only on mains powerをオンにしているなら、給電が切れただけでも保留になります。 充電する、冷ます、あるいはsetup画面でしきい値を下げてください。 これはバグではなく、ジョブ受付制御が仕事をしている状態です。

インストールがブロックされる

APKを開いたアプリに対して「不明なアプリのインストール」を許可してください。 MIUIでadbからサイドロードする場合は、開発者オプションの USBデバッグ(インストール)も必要です。

自分でビルドする

自前ビルドの場合、Device Flow用のGitHub App(無料・サーバ不要)、 APK署名鍵、そしてruntime bundleを配布するならmanifest署名鍵が必要です。 いずれも README に手順があります。最初のGradleビルドの前に一度だけ必要な prootのクロスコンパイルも同様です:

git clone https://github.com/m96-chan/DroidRunner
cd DroidRunner
ANDROID_NDK_ROOT=$ANDROID_HOME/ndk/<version> runtime/build-proot.sh
./gradlew assembleDebug

信頼する署名鍵を設定していないビルドはruntime bundleを検証できません。 その場合は、検証したふりをするのではなく、インストール時にその旨を表示します。