GitLab CI/CD でマージリクエストをトリガーにしてビルドしたらハマった

GitLab CI/CD を使用してマージリクエストをトリガーにしてビルドしたら
最新の develop ブランチを取り込んでいない feature ブランチからのマージリクエストだと
成果物がデグレってしまうようなビルドジョブだったので自戒を込めて対策を書きます

# TL;DR
- マージリクエストをもとにビルドする際は、マージ後の状態でビルドする（言葉にしたら当たり前のことだけども。。。）
- feature/xxxx から develop へのマージリクエストでは、develop が `git merge feature/xxxx` した状態でビルドする（feature/xxxx が `git merge develop` した状態ではなく）

# 前提条件
- SaaS 型の GitLab CI/CD を使用
- マージリクエストをトリガーにして develop 環境にマージされる前にビルド
- ブランチ構造は後述の通りです

# ブランチ構造とパイプラインの設計
### リモートブランチの構造
標準的な 3 ブランチの分け方をしています。

- master ブランチ → production 環境に対応
- develop ブランチ → develop 環境に対応
- feature/{feature_name} ブランチ → 作業用で対応する稼働環境はなし

チームで行っていた git を用いた基本的なブランチ操作は以下のようでした

1. ローカルの develop で `git pull` して最新化する
2. develop から作業ブランチ作成 `git checkout -b feature/{feature_name}`
3. feature/{feature_name} で作業完了して `git add {target_files} && git commit -m "your message"`
4. feature/{feature_name} からリモートにむけて `git push -u origin feature/{feature_name}`
5. リモートで feature/{feature_name} から develop にマージリクエスト
6. リモートで feature/{feature_name} から develop にマージ承認
7. リモートの feature/{feature_name} を削除する


### GitLab CI/CD のパイプライン
GitLab CI/CD ではパイプラインで行う設定を `.gitlab-ci.yml` というファイルに記述していきます
主な構成は以下の通りです。ちょっと物足りないくらいのシンプルな設計です
マージリクエスト をトリガーにしてテスト, ビルドすることで develop ブランチに feature ブランチが取り込まれる前にちゃんとマージしてもいいかどうかのチェックができます。

#### ステージ
基本的には test / build / deploy の３つです
今回は build ステージの build_job というジョブについて書いています。

#### ブランチの操作とジョブの関係
ブランチに対する操作が マージリクエスト or マージ、対象となるブランチが develop or master で動作が異なるようなパイプラインを想定していました。

| ブランチの操作 | 実行されるジョブ |
|:-:|:-:|
|  feature/{feature_name} → develop にマージリクエスト |  test ジョブ と build ジョブ が実行され、ビルド成果物 A が作成される |
| develop ブランチにマージ  |  deploy ジョブが実行され、ビルド成果物 A が develop 環境にデプロイされる |
|  develop → master にマージリクエスト | なにも動作しない  |
| master にマージ  | deploy ジョブが実行され、ビルド成果物 A が production 環境にデプロイされる  |


以上のようなパイプラインを想定していたのですが、build ジョブ の書き方に誤りがあり、想定した動作をしていませんでした。
今回の「成果物のデグレ」の原因にもなった `.gitlab-ci.yml` で対応するビルドジョブはおおよそ以下のようでした（抜粋）

```code:.gitlab-ci.yml
build_job:
  stage: build
  script:
    - set -eux
    - ./gradlew war # gradleラッパーを使用して warファイル を build してます
  only:
    - merge_requests
  except:
    - master
    - /^feature/.*$/
```


# 起きた問題と対策
期待していた動きは feature/aaaa → develop にマージリクエストされたら
**feature/aaaa で追加した機能を加えた最新の develop のビルド成果物** 
ができることだったのに
実際には
**feature/aaaa のビルド成果物** 
ができていました。。。

これではもし `feature/aaaa` が最新の develop を取り込んでいなかったらできる成果物は
あくまでも最新の develop を反映していないことになります。
この最新ではないビルド成果物が develop 環境にデプロイされていたため、デグレが発生していました
もちろん、最新の develop ブランチを取り込んだ作業ブランチがほとんどで、こまめに develop ブランチにマージしていたので大きなデグレなどは発生していなかったのですが、皆のコードが環境に反映できてないスーパー申し訳ミスでした

## ビルドスクリプトの修正
`.gitlab-ci.yml` を以下のように修正して解決しました。

- git を install
- `git fetch` で remote の他のブランチの最新の状態を取得（ここは使うブランチだけに制限したほうがいいです）
- `git checkout -b develop origin/develop` で origin/develop を develop ブランチとして作成して移動
- `git merge origin/${CI_COMMIT_REF_NAME}` で feature/aaaa ブランチを develop にマージする 
- この状態でビルドする

> ※ amazon linux2 ベースのイメージを使用していたので git を入れています
※ マージが失敗する場合はエラーを検知してジョブを中断します
※ ${CI_COMMIT_REF_NAME} は GitLab CI が提供する環境変数です。詳しくは[こちら](https://qiita.com/ynott/items/4c5085b4cd6221bb71c5)に記載されています
※ 実際には build.sh などのスクリプトに切り出しています(build_job 内での動作が直感的ではないし、長くなってきたので)

```code:.gitlab-ci.yml
build_job:
  stage: build
  script:
    - set -eux
+   - git fetch 
+   - git checkout -b develop origin/develop
+   - git merge origin/${CI_COMMIT_REF_NAME}
    - ./gradlew war # gradleラッパーを使用して warファイル を build してます
  only:
    - merge_requests
  except:
    - master
    - /^feature/.*$/
```


# まとめ
- Before
  - マージリクエスト時に develop にマージする前の feature/{feature_name} の状態で成果物をビルド
  - マージ時に feature/{feature_name} のビルド成果物をデプロイ
- After
  - マージリクエスト時に feature/{feature_name} を develop にマージした後の状態で成果物をビルド
  - マージ時に最新の develop にのビルド成果物をデプロイ

# 参考
- [GitLab CI/CD Pipeline Configuration Reference](https://docs.gitlab.com/ee/ci/yaml/README.html)
- [GitLab CI/CDパイプライン設定リファレンス(日本語訳:GitLab CI/CD Pipeline Configuration Reference)](https://qiita.com/ynott/items/1ff698868ef85e50f5a1)
- [GitLab CI/CD Variables を翻訳しました。](https://qiita.com/ynott/items/4c5085b4cd6221bb71c5)