GitHub ActionsでArduinoプロジェクトに自動テスト環境を構築する

最近またArduinoを再開したので、開発環境アップデートの第1弾として、CI環境の見直しを行いました。
昔々、まだGitHub Actionsとかいう超便利な代物が無かった時代、自作のArduinoライブラリはTravisCIに突っ込んで自動テストを回していました。
今はGitHub Actionsもあることですし、手始めに移行してみることにしました。

# 公式ワークフロー
- [arduino/compile-sketches](https://github.com/marketplace/actions/compile-arduino-sketches)

GitHub Actionsは、再利用可能なワークフローをマーケットプレイス経由で簡単に組み込めるのが最高に便利ですよね。
Arduinoも [公式](https://github.com/arduino) がワークフローを公開してくれています。

公式ワークフローは2種類あります。

- 実行環境インストール、ボードやライブラリの追加、コンパイルまで全自動 (上記)
- 実行環境インストールのみ ([arduino/setup-arduino-cli](https://github.com/marketplace/actions/setup-arduino-cli))

複雑な処理を行う場合は単体版の方が便利かと思いますが、今回はテストを回したいだけなので、全自動版を使用します。

# リポジトリへ組み込む
早速、自分のリポジトリへワークフローを追加していきます。

- [dojyorin/arduino_base64](https://github.com/dojyorin/arduino_base64)

まずディレクトリ構成ですが、他の一般的なワークフローと同じく、以下のような構成をとります。

- `.github/`
    - `workflows/`
        - `test.yaml`
        - `release.yaml`
        - ...

そして僕はYAMLが ~~*何で存在するのか理解できないレベルで生理的に無理*~~ 苦手なので、基本的にワークフローはJSONで書いてから [yq](https://github.com/mikefarah/yq) というフォーマット変換ツールへ通してYAMLを ~~*仕方無く*~~ 生み出しています。

- `.github/`
    - `workflows_json/`
        - `to-yaml.sh`
        - `test.json`
        - `release.json`
        - ...

```sh:to-yaml.sh
set -uC
cd ${0%/*}
yq -P -I 4 ./${1}.json | head -c -1 | tee ../workflows/${1}.yaml &> /dev/null
```

```sh
# test.json -> test.yaml
./to-yaml.sh test
```

# ワークフローの記述
まずは見ていただければ、やってることは何となく分かるかと思います。

<details>
<summary>test.json</summary>

```json:test.json
{
    "name": "Test",
    "on": {
        "push": {
            "branches": [
                "dev"
            ],
            "paths-ignore": [
                ".git*",
                "**.md",
                "*.properties"
            ]
        },
        "pull_request": {
            "branches": [
                "master",
                "dev"
            ],
            "paths-ignore": [
                ".git*",
                "**.md",
                "*.properties"
            ]
        }
    },
    "jobs": {
        "test": {
            "name": "Test: ${{matrix.board.name}}",
            "runs-on": "ubuntu-latest",
            "strategy": {
                "fail-fast": true,
                "matrix": {
                    "board": [{
                        "vendor": "arduino",
                        "arch": "samd",
                        "name": "arduino_zero_native"
                    }, {
                        "vendor": "adafruit",
                        "arch": "samd",
                        "name": "adafruit_trinket_m0"
                    }, {
                        "vendor": "teensy",
                        "arch": "avr",
                        "name": "teensy41"
                    }],
                    "include": [{
                        "index": "https://downloads.arduino.cc/packages/package_index.json",
                        "board": {
                            "vendor": "arduino"
                        }
                    }, {
                        "index": "https://adafruit.github.io/arduino-board-index/package_adafruit_index.json",
                        "board": {
                            "vendor": "adafruit"
                        }
                    }, {
                        "index": "https://www.pjrc.com/teensy/package_teensy_index.json",
                        "board": {
                            "vendor": "teensy"
                        }
                    }]
                }
            },
            "steps": [{
                "name": "clone repository",
                "uses": "actions/checkout@v3"
            }, {
                "name": "install arduino and run test",
                "uses": "arduino/compile-sketches@v1",
                "with": {
                    "fqbn": "${{matrix.board.vendor}}:${{matrix.board.arch}}:${{matrix.board.name}}",
                    "platforms": "- name: ${{matrix.board.vendor}}:${{matrix.board.arch}}\n  source-url: ${{matrix.index}}"
                }
            }]
        }
    }
}
```

</details>

要点だけ掻い摘んで解説します。

## `matrix.board`
GitHub Actionsの並列実行機能で、大量のボードを同時並行でテストできます。

テストしたいボードの [FQBN]() を分解して記述します。
FQBNとは `製造者:アーキテクチャ:ボード名` で構成された、Arduinoにおけるボード識別子のことです。
それら3要素を、それぞれ `matrix.board` 配列のオブジェクトプロパティとして以下のように記述します。

- `vendor` ... 製造者
- `arch` ... アーキテクチャ
- `name` ... ボード名

FQBNは製造者によって予め定義されているため、事前に確認しておく必要があります。

## `matrix.include`
上記の並列実行において、特定のコンテキストにのみプロパティを追加したい場合、ここでパターンマッチと追加するプロパティを記述できます。

Arduinoに慣れ親しんだ方ならご存知かと思いますが、サードパーティ製のボードを使用する場合は、製造者が提供しているボード定義を追加する必要があります。
全コンテキストにおいて全ボード定義を追加するのも不可能ではないですが、不必要なボード定義までダウンロードすることになり、ネットワーク負荷増大に繋がるため、コンテキストに必要なボード定義のみを追加するために、この機能を使います。

例えば、FQBNが `adafruit:samd:adafruit_trinket_m0`　の場合、必要なボード定義はAdafruit社の `package_adafruit_index.json` です。
そこで、以下のように `matrix.include` を指定することで、特定の `vendor` の時のみその製造者が提供しているボード定義の `index` プロパティを追加できます。

```jsonc:test.json(抜粋)
{
    "index": "https://adafruit.github.io/arduino-board-index/package_adafruit_index.json",
    // ↓ で "matrix.board.vendor" が "adafruit" の時のみ "matrix.index"　に ↑ を追加する
    "board": {
        "vendor": "arduino"
    }
}
```

## `arduino/compile-sketches@v1`
今回の目玉です。
全自動で全てよろしくやってくれる本体です。

デフォルトのテスト対象は `examples/` の中身全部となります。

他のテスト対象を指定したい場合や、依存関係ライブラリが存在する場合は、別途 `with` プロパティ内でオプション設定できます。

ここでは2個のオプションを指定します。

### `with.fqbn`
ボードのFQBN、つまり `matrix.board` の3要素を全てコロンで繋げた値となります。

### `with.platforms`
少し厄介です。

この中身はGitHub Actionsワークフローそのものではなく、実行中のワークフロー内で別に解析されるため、YAML文字列で書く必要があります。つらい。
ここで指定するのは、インストールしたいボードのプラットフォームと、そのボードが定義されているインデックスのURLです。

プラットフォームとは、具体的に言うとコンパイラのことで、これは製造者がアーキテクチャ毎に用意しているため、識別子は `製造者:アーキテクチャ` となります。

例えば、FQBNが `adafruit:samd:adafruit_trinket_m0` の場合、プラットフォームは `adafruit:samd` となります。
そしてボード定義は `board.include` で拡張された `matrix.index` に格納されています。

## 実行してみる
今回のワークフローでは `dev` ブランチにpushしたとき、テストが走るようになっています。
`master` ブランチへは直接プッシュできないよう保護しているので、基本的に細かい修正は　`dev` で、全く新しい機能を追加するときは `dev` から新たに `feat-xxx` ブランチを切る感じで運用してます。

- [dojyorin/arduino_base64 actions](https://github.com/dojyorin/arduino_base64/actions)

お恥ずかしながら...ここで実際のワークフローの挙動を確認できます。

# まとめ
自動テストが出来るようになることで、開発効率が圧倒的に良くなります。
皆さんも是非GitHub Actionsを活用してArduino開発を加速させてみてください。