GitLab-CI/CDで複数環境でのテストを簡素に書く

GitLab.comにはGitLab-CI/CDが一緒になっているので、ライブラリ・パッケージ開発にも重宝してるのですが、悩ましいのが「バージョンのサポート」です。
それでも、変に新しすぎる機能を突っ込んでやや古いぐらいのバージョンに対応できるのは避けたいので、なるべく多くのバージョンでのテストを行うのが望ましいですね。

Travis-CIだと、テスト時の言語バージョンを割と簡単に複数指定できるのですが、GitLab-CI/CDだとどれだけ簡素に書けるかを少し調べてました。

# TL;DR

* YAMLのアンカーとエイリアスが普通に使えるので、使いましょう。
* 具体例は、 **「テストのみの処理」を共通化する（`before_script`は使わない）** へ

# 以降の例

* ちょっと個人的興味を兼ねて手を出している、「PHPのEOLでないバージョン全てで動くパッケージを作る」でサンプルコードを書いてます
  * 基本的にはどんな言語でもいけるはず
* テストコードとかも全く出す予定もなく、基本的にはYAMLが続きます

# (ここからが本文)

## まずは最新バージョンのテストジョブを定義する

```yaml:.gitlab-ci.yml
phpunit:
  image: php:7.2-cli-alpine
  script:
    - apk add git
    - curl https://getcomposer.org/download/composer.phar > composer.phar
    - php composer.phar install
    - vendor/bin/phpunit --bootstrap vendor/autoload.php
```

普通ですね。
GitLab-CI/CDは他のCIサービスなどと同じような感じで、`.gitlab-ci.yml`にジョブを定義すると、`git push`をトリガーにジョブが走ります。 [^1]

[^1]: 最近は別のファイルを指定できるらしいですが、今回は触れません

## 複数バージョンに雑に対応させる

一番最初のケースではPHP 7.2での動作を前提としていますが、PHP7.1での動作もちゃんと保証させたいとします。
とりあえず、雑にジョブを増やしてみましょう。

```yaml:.gitlab-ci.yml
phpunit-7.2:
  image: php:7.2-cli-alpine
  script:
    - apk add git
    - curl https://getcomposer.org/download/composer.phar > composer.phar
    - php composer.phar install
    - vendor/bin/phpunit --bootstrap vendor/autoload.php tests/

phpunit-7.1:
  image: php:7.1-cli-alpine
  script:
    - apk add git
    - curl https://getcomposer.org/download/composer.phar > composer.phar
    - php composer.phar install
    - vendor/bin/phpunit --bootstrap vendor/autoload.php tests/
```

コピペするとすごく手早く増やせます。
ジョブの名前を`phpunit-(PHPのバージョン)`にしておくことで、分かりやすいようにしておきましょう。
これぐらいだと、特にそんなに面倒な印象はありませんね。

さて、このままPHP 7.0, 5.6まで範囲を広げると、こうなります。

```yaml:.gitlab-ci.yml
phpunit-7.2:
  image: php:7.2-cli-alpine
  script:
    - apk add git
    - curl https://getcomposer.org/composer.phar > composer.phar
    - php composer.phar install
    - vendor/bin/phpunit --bootstrap vendor/autoload.php tests/

phpunit-7.1:
  image: php:7.1-cli-alpine
  script:
    - apk add git
    - curl https://getcomposer.org/composer.phar > composer.phar
    - php composer.phar install
    - vendor/bin/phpunit --bootstrap vendor/autoload.php tests/

phpunit-7.0:
  image: php:7.0-cli-alpine
  script:
    - apk add git
    - curl https://getcomposer.org/composer.phar > composer.phar
    - php composer.phar install
    - vendor/bin/phpunit --bootstrap vendor/autoload.php tests/

phpunit-5.6:
  image: php:5.6-cli-alpine
  script:
    - apk add git
    - curl https://getcomposer.org/composer.phar > composer.phar
    - php composer.phar install
    - vendor/bin/phpunit --bootstrap vendor/autoload.php tests/
```

うーん、かなり冗長ですね。
これ以上なにもしないなら問題はそこまで大きくはないです。
ただ、特殊なPHP拡張も必要なケースがあった場合は、毎回4箇所編集しないと駄目なのでよくありません。[^2]

[^2]: 超余談ですが、この記事を書いてるタイミングで、テストジョブのスクリプトの間違いに気づいて、今まさに全部編集して良くないことになりました。

## ジョブで共通する処理はまとめる

```yaml:.gitlab-ci.yml
before_script:
  - apk add git
  - curl https://getcomposer.org/composer.phar > composer.phar
  - php composer.phar install

phpunit-7.2:
  image: php:7.2-cli-alpine
  script:
    - vendor/bin/phpunit --bootstrap vendor/autoload.php tests/

phpunit-7.1:
  image: php:7.1-cli-alpine
  script:
    - vendor/bin/phpunit --bootstrap vendor/autoload.php tests/

phpunit-7.0:
  image: php:7.0-cli-alpine
  script:
    - vendor/bin/phpunit --bootstrap vendor/autoload.php tests/

phpunit-5.6:
  image: php:5.6-cli-alpine
  script:
    - vendor/bin/phpunit --bootstrap vendor/autoload.php tests/
```

GitLab-CI/CDでは、全ジョブの実行時に各ジョブの`script`より前に実行したい共通処理を`before_script`上に書くことができます。
これによって、「テスト時に同じ処理を共通化して編集コストを減らす」ことが実現できます [^3]

[^3]: ただし、各ジョブごとに`script`を空にすることはできないので、「全く同じコマンド」しかなくても、何かしら`script`を書く必要があります。

## テスト以外があったら？

GitLab-CI/CDはCIだけでなくCDも視野に入れています。
だからというわけではないのですが、

* パッケージのリリースもGitLab-CI/CDを止めたい
* ユニットテストの前に文法チェック(`php -l`や`phan`などなど)をやってNGならパイプラインを止めたい

などなど、単純なテスト以外のジョブも混ざってきます。
となってくると、`before_script`にいろいろ詰め込みすぎると「リリースには全く必要がないのに、`composer install`をしないといけない」などという事態になって、よろしくありません。

## 「テストのみの処理」を共通化する（`before_script`は使わない）

```yaml:.gitlab-ci.yml
phpunit-7.2: &phpunit-latest
  image: php:7.2-cli-alpine
  script:
    - apk add git
    - curl https://getcomposer.org/composer.phar > composer.phar
    - php composer.phar install
    - vendor/bin/phpunit --bootstrap vendor/autoload.php tests/

phpunit-7.1:
  <<: *phpunit-latest
  image: php:7.1-cli-alpine

phpunit-7.0:
  <<: *phpunit-latest
  image: php:7.0-cli-alpine

phpunit-5.6:
  <<: *phpunit-latest
  image: php:5.6-cli-alpine
```

YAMLには「アンカー」と「エイリアス」という仕様があり、「`&`でアンカーを定義」「`*`でエイリアスとしてアンカーのデータ構造をまるっと参照」が可能です。
これによって、`<<: *(名前)`によるアンカー先のデータをそのまま使いまわせるため、**「`script`を記述せずにジョブを増やす」**ことが可能になります。

こんなことも可能です。（PHPのバージョンは同じだけど、　composerのバージョンを変える例）

```yaml:.gitlab-ci.yml
phpunit-7.2: &phpunit-latest
  image: php:7.2-cli-alpine
  variables:
    COMPOSER_VERSION: 1.7.2
  script:
    - apk add git
    - curl https://getcomposer.org/download/${COMPOSER_VERSION}/composer.phar > composer.phar
    - php composer.phar install
    - vendor/bin/phpunit --bootstrap vendor/autoload.php tests/

phpunit-7.2-composer-1.6.5:
  <<: *phpunit-latest
  variables:
    COMPOSER_VERSION: 1.6.5
```

これによって、「Laravelの複数バージョンにも対応している何か」も比較的テストしやすくなります。[^4]

[^4]: さらに、ジョブ内で`before_script`の定義もできるため、「バージョン間で事前準備がほんの少しだけ違う」にも対応可能なはず

# まとめ

ライブラリ開発のテストをCIなどでやると、記述内容の重複が起こってしまうケースがあります。
特に「Webアプリケーションフレームワークの拡張」を目的としたライブラリなどを開発していて、
可能な限り守備範囲を広げようとすると、言語バージョンxFWバージョンの掛け算でパターン数が爆発します。
YAMLの機能をきちんと使うことで、DRYでいけそうなシーンで適切にDRYな設定が書けるので、よりスリムなYAMLを目指したいと思います。


# 余談

![phpunit-multiple-versions-in-gitlab-ci.png](https://qiita-image-store.s3.amazonaws.com/0/6805/9a2b75ac-5fc5-3f24-3d66-c3425a5af7d3.png)

前段で作ってるパッケージについて、ローカルで最低限のテストをパスしたプロジェクトを作って、↑の`.gitlab-ci.yml`でCIを流した結果がこちら。
(テストコードの側で)PHP7.2から使えるメソッド使ってたりしてて、いきなり複数バージョン対応の大変さを思い知らされました。 [^5]

[^5]: その他、PHPUnitのバージョン対応問題とか、関数定義時の型宣言の対応量とか、敵が多いです


# 外部リンク/参考情報など

* [Travis-CIにおける、言語指定のドキュメント(代表としてPHP)](https://docs.travis-ci.com/user/languages/php/)
* [YAMLのエイリアスでAnsibleファイルの重複を減らす
(`YAML アンカー エイリアス`でググってたまたまトップにあった記事)](https://dev.classmethod.jp/server-side/ansible/refactoring_ansible_yaml_using_alias_and_anchor/)
* [YAMLのAnchorとAliasを使ってconfigをDRYに書く
(`YAML アンカー エイリアス`でググってたまたま2番目にあった記事)](https://ohbarye.hatenablog.jp/entry/2018/01/22/232921)
* [YAMLのアンカーについての仕様](http://yaml.org/spec/1.2/spec.html#id2765878)