GitLab CIでMR時にマージ後のブランチのテストを実施する

# TL;DR

GitLab EEや premiumユーザでなくても、こんなかんじのymlで出来ます。

```yml
.only_mr_create_branch:
  before_script:
    - git config --local user.name "ci_user"
    - git config --local user.email "ci_user@hogehoge.xyz"
    - git fetch
    - git checkout -b testing "origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}"
    - git merge origin/"${CI_COMMIT_REF_NAME}"

.exec_only_mr:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH'
      when: always
    - when: never


unit_test_merged_branch:
  extends: [ .exec_only_mr, .only_mr_create_branch]
  stage: test
  script:
    # マージ後ブランチで依存関係を引き込まないとエラーになってしまう。
    - npm install
    - npm run test
```


# はじめに

GitLab CIでは、`rules` キーワードを用いることでマージリクエスト(MR)作成時・更新時に、MR元ブランチでのCI パイプラインの実行ができるようになっています。
- [マージリクエストパイプライン | GitLab](https://gitlab-docs.creationline.com/ee/ci/merge_request_pipelines/)

しかし、このCIパイプラインの実行は、そのままではMR元ブランチを元にした実行となるため「MRがターゲットブランチにマージされた後」の状態でテスト結果を確認することが出来ません。
また、あまりケースは多くないかもしれませんが、masterマージ前はテストが通っていたが、マージ後にテストが通らなくなってしまう場合が考えられます。


MR後、masterでテストがFailすると、それなりの時間・コストが committer、レビュワーにかかってしまい、負荷になっている人も多いのではないでしょうか。

- committerに確認依頼、修正依頼 -> 再レビュー、再マージ、テスト確認...のループ
- そもそもレビュワーが手元にブランチを引き込んでローカルでmasterにマージしてみてテスト実行してよ
  - ここまでやるのが理想かもしれませんが、レビュワーの時間・コストを考えると全件で実施するのは大変ですね...



このような問題の解決策として、GitLabではマージトレイン等の機能が提供されており、これを用いることでマージ後ブランチでのテスト結果をマージ前に把握することが可能となっています。
- [Pipelines for Merged Results | GitLab](https://docs.gitlab.com/ee/ci/merge_request_pipelines/pipelines_for_merged_results/)
- [Merge Trains | GitLab](https://docs.gitlab.com/ee/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.html)

ただし、このマージ後ブランチでのテストは、GitLab Premium以上のライセンスでのみ設定でき、通常のCommunity Edition や Freeライセンスのユーザは使用することが出来ません。




今回は、このマージ後ブランチでのテストを頑張って自分で設定してみます。


# 設定方法

## 実現したいこと

まず、実現したいことの整理です

- MRの作成・更新時にテストが自動で行われてほしい
- テストの実施は、マージ後ブランチを仮想的に作成して実施されてほしい
- MRでのテストは、master(デフォルトブランチ)へ向けてのMRの場合にみ実施されてほしい

## .gitlab-ci.ymlの設定

早速設定していきます。

### 1. MR時のみ実行されるように設定

[公式の例](https://gitlab-docs.creationline.com/ee/ci/merge_request_pipelines/) では、`only/except` を使った例が書かれていますが、今回は`rules`を使って設定していきます

```yml
.exec_only_mr:
  rules:
    # PipeLineの動作がMR契機の場合　かつ、　MRのターゲットがデフォルトブランチになっている場合のみ
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH'
      when: always
    # それ以外では実施してほしくない。
    - when: never
```

また、[公式のサンプル](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Workflows/MergeRequest-Pipelines.gitlab-ci.yml) を参考にすると以下のようになります。

```yml
.exec_only_mr:
  rules:
    - if: '$CI_MERGE_REQUEST_IID && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH'
      when: always
    - when: never
```


あとは、MRのときにのみ実施されてほしいJobで、`.exec_only_mr`を`extends`指定してやれば実現が可能です。以下のようになります。

```yml
unit_test_exec_only_mr:
  extends: [.exec_only_mr]
  stage: test
  script:
    # すきなテストを実施。
    - npm run test
```

これで、MR時のみに実行されてほしいJobが定義できました


### 2. MR時に、マージ後のブランチを作成してテストする

つぎに、MR後ブランチを作成してテストされるようにしていきます。

GitLab CI のpredefined variables でMR元、MRターゲットのブランチ名は取得できるので以下のようなスクリプトを実行することで実現ができそうです。

```sh
# remoteブランチを取得
$ git fetch
# MRターゲットブランチを testing という名前でチェックアウト
$ git checkout -b testing "origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}"
# testingブランチに、MR元のコミットをマージしてくる
$ git merge origin/"${CI_COMMIT_REF_NAME}"
```

あとは、このスクリプトをテスト実施前に実行することで、マージ後ブランチでのテストが実施されそうです。ただし、ここで注意すべきなのが下記です

- リポジトリのCI/CD設定で `Git strategy` に`git clone` が指定されているか
- jobを実施するDocker Imageでgitが使用できるか (Docker Executorの場合)
- jobを実施するExecutorで gitのユーザ名、メールアドレスが登録されているか


`git clone`ではなく `git fetch` を使用していると、毎回のJobが完全にクリーンな状態で行われるわけでなく、過去のjobで使用したブランチが残っている状態になってしまうため、スクリプトでのbranch作成時に、`A branch named 'testing' already exists.` と怒られてしまいます。(2敗)

`git clone`を使用するようにしていると、毎回クリーンな状態になるのでこちらが推奨されます。(ただ、リポジトリ自体が重たい、等の場合は毎回 git cloneするのも難しい場合があるかもしれません。この場合は要工夫です。)


alpineなどの軽量イメージを使用している場合、gitコマンドが使えないことが多々あります。
この場合は、gitがインストールされているイメージを使用してください。 Circle CIが公式で公開しているイメージ類は基本的にgitなどがインストールされてるのでこちらを使うのも手です。

- [cimg/node - Docker Image | Docker Hub](https://hub.docker.com/r/cimg/node)
  - ※追記 : circle-ciのimage類はcimgでリプレースが進行中なのでこっちをつかう。
  - 最初からDocker Imageで使えるコマンド類はこちらも参考に。
  - [CircleCI-Public/cimg-base: The CircleCI Base (Ubuntu) Docker Convenience Image.](https://github.com/CircleCI-Public/cimg-base)




gitのユーザ名、メールアドレスの設定がされていない場合には、ブランチのマージ時に `Please tell me who you are.` と注意が出ます。
今回のテスト実行ブランチは、remote側にCI PipeLineからPushしたりするわけではないので、適当なユーザ名、メールアドレスを設定すれば問題有りません。
Docker Executor以外で実行する場合、git configを globalで設定していると他のJobに影響があるかもしれませんので注意してください。

```sh
$ git config --local user.name "ci_user"
$ git config --local user.email "ci_user@hogehoge.xyz"
```

これらの注意点を踏まえると、スクリプトは下記のようになります。before scriptとして準備を定義しているので、このjobをextendsするか ymlアンカーで入れてやればJob実施前にブランチのセットアップが実行されます。

npm installのような依存関係準備を他のhidden job内のbefore scriptで実施している場合には競合して上書きされる可能性があるので注意です。

```yml
.only_mr_create_branch:
  before_script:
    - git config --local user.name "ci_user"
    - git config --local user.email "ci_user@hogehoge.xyz"
    # 確認のためにbranchのリストを出してみる
    - git branch --list --all
    - git fetch
    - git checkout -b testing "origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}"
    - git merge origin/"${CI_COMMIT_REF_NAME}"
```



### 3. 1,2,の設定をあわせる

1, 2, の内容を踏まえてると以下のような設定になります。

```yml
.only_mr_create_branch:
  before_script:
    - git config --local user.name "ci_user"
    - git config --local user.email "ci_user@hogehoge.xyz"
    - git fetch
    - git checkout -b testing "origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}"
    - git merge origin/"${CI_COMMIT_REF_NAME}"

.exec_only_mr:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH'
      when: always
    - when: never


unit_test_merged_branch:
  extends: [ .exec_only_mr, .only_mr_create_branch]
  stage: test
  script:
    # マージ後ブランチで依存関係を引き込まないとエラーになってしまう。
    - npm install
    - npm run test
```

# まとめ

上記では、最低限の設定だけを行いました。他に、 「`wip` や`Draft` のMRのはテスト実施しない」、「masterマージ後でのみ実施するjobも定義する」、「テスト後、ビルドを実施してどこかに仮デプロイ」など必要な設定を追加することが必要になりそうです。

ここまで設定していくと長々としたymlになってしまうのである程度テンプレート化したほうが、使い回しやすいかもしれません。

premiumユーザに変更して公式提供の機能を使うのがやはり便利そうでですね

ご指摘、コメントなどあれば宜しくおねがいします。

# 参考

- [Gitlab CI でマージリクエストのマージ結果でパイプラインを実行する - ngyukiの日記](https://ngyuki.hatenablog.com/entry/2020/02/15/205425)

こちらの記事のほうがymlアンカーを使ってscriptをうまく書かれています。スマートだ、、、

- [Pipelines for Merged Results | GitLab](https://docs.gitlab.com/ee/ci/merge_request_pipelines/pipelines_for_merged_results/)
- [Merge Trains | GitLab](https://docs.gitlab.com/ee/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.html)

GitLabの公式ドキュメントです。