master branchにMRが作成された時に他のbranchに同一のMRを作成するCI構築

# 目的
アプリケーションの管理の都合上、masterブランチと並行して存在するbranch(今回は`production`とする)があります。
これら２つbranchはOSSのバージョン等の違いがありますが、基本的には同じ機能を備えたアプリケーションである必要があるため、masterに入れた改修(MR)についてはproductionにも取り込む必要があります。
しかし、このような運用をしていると

**masterブランチにはあげたMRを、productionブランチにはあげ忘れた！**


となってしまう可能性もあり得ます。

そこで以下の流れで、特定のbranch(`master`)に上がったMRをそのまま他のbranch(`production`)に**自動で**あげる方法を検討しました。
以下の流れで説明していきます。


- 完成した全体の構成
- 特定のbranchにMRが上がったことを検知する
- 対象のMRと同一のcommit情報を持ったbranchを作成する
- コマンドライン上でMRを作成する

## 完成した全体の構成
構成としてはシンプルで、以下の２ファイルでの実行です

- .gitlab-ci.yaml（MRが上がったことを検知）
- make_merge_request.sh（対象のMRを作成する）

```bash:make_merge_request.sh
#!/bin/bash

set -x
set -eu

PRIVATE_TOKEN="xxxxxxx" # GitLabから取得する
PROJECT_ID="yyyyyyy" # GitLabから確認する
TARGET_BRANCH="production"
SOURCE_BRANCH=${3}-for_${TARGET_BRANCH}
TITLE=clone:${4}
ID=${1}
DIRECTORY="/Users/{gitリポジトリのある場所}"


# ここからはgit branch -D でエラーが発生する可能性があるため、エラーを許容
set +e

## git reposiroryに移動してMR用のbranchを作成する
cd ${DIRECTORY}
git checkout .
git checkout -f ${TARGET_BRANCH}
git branch -D ${SOURCE_BRANCH}
git pull
git checkout -b ${SOURCE_BRANCH}

#　エラーの許容を終了
set -e


# 対象のMRからcommit数を取得する
COMMIT_COUNT=$(curl --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" "https://gitlab.com/api/v4/projects/${PROJECT_ID}/merge_requests/${ID}/commits" | jq -r 'sort_by(..authored_date)' | jq '. | length')


# commit数の数分だけcherry-pickを行う。時系列的に昔のcommitからcherry-pickを行うため、commitの作成時間を昇順でソートしている
for i in `seq ${COMMIT_COUNT}`
    do
        COMMIT_HASH=$(curl --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" "https://gitlab.com/api/v4/projects/${PROJECT_ID}/merge_requests/${ID}/commits" | jq -r 'sort_by(..authored_date)' |  jq ".[$(($i-1))].id")
        COMMIT_HASH=`echo  "${COMMIT_HASH}" | sed "s/\"//g"`
        git cherry-pick ${COMMIT_HASH}
    done

# 全てのcommitを取り込んだブランチをoriginにpushする
git push -f origin ${SOURCE_BRANCH}

# 対象のブランチでmasterブランチ向けにMRを作成する
curl -X POST -H "Private-Token: ${PRIVATE_TOKEN}" -d "title=${TITLE}&source_branch=${SOURCE_BRANCH}&target_branch=${TARGET_BRANCH}" "https://gitlab.com/api/v4/projects/${PROJECT_ID}/merge_requests" | jq .

```

```yml:gitlab-ci.yaml
# This file is a template, and might need editing before it works on your project.
image: openjdk:8-jdk

variables:
  ANDROID_COMPILE_SDK: "28"
  ANDROID_BUILD_TOOLS: "28.0.2"
  ANDROID_SDK_TOOLS: "4333796"

before_script:


stages:
  - build
  - test
  - deploy


MR_clone_job:
    stage: deploy
    tags:
      - mac-ci-runner
    only:
      - merge_requests
    script:
      - echo "hello merge request"
      - if [ "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" == "master" ]; then ./make_branch.sh $CI_MERGE_REQUEST_IID $CI_MERGE_REQUEST_TARGET_BRANCH_NAME $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME $CI_MERGE_REQUEST_TITLE; fi
```

## 特定のbranchにMRが上がったことを検知する
基本的に`.gitlab-ci.yaml`に記載してある以下の部分が対象です。
まず、このCIは`MRがmergeされた時`ではなく`MRが作成,更新された時`に走るものになっています。
Mergeの結果によって制御するものもありますが、そちらはGitLabの有料版のみの機能になるため使用しておりません
参考：https://docs.gitlab.com/ee/ci/merge_request_pipelines/pipelines_for_merged_results/

そのため`if [ "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" == "master" ]`のように、
特定のbranchへのMRの時のみ反応するように作成しています。
（このifを記述しないと、MRがあがる度に反応してしまうので、後述のMRを作成する処理も相まってCIの無限ループ状態になってしまいます）

なお、今回使用している[gitlab-ci.yml内の環境変数](https://docs.gitlab.com/ee/ci/variables/predefined_variables.html)についてはリンク先を参照してください。
これらの変数を引数にして、後述する`./make_branch.sh`の処理を行っております。



```bash
MR_clone_job:
    stage: deploy
    tags:
      - mac-ci-runner
    only:
      - merge_requests
    script:
      - echo "hello merge request"
      - if [ "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" == "master" ]; then ./make_branch.sh $CI_MERGE_REQUEST_IID $CI_MERGE_REQUEST_TARGET_BRANCH_NAME $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME $CI_MERGE_REQUEST_TITLE; fi
```


## 対象のMRと同一のcommit情報を持ったbranchを作成する
ここからは`make_merge_request.sh`の中身の説明です。
この中ではGitLabのAPIを投げる処理があるので、
`PRIVATE_TOKEN`と`PROJECT_ID`をGitLabから取得してきてください。
[Gitlab APIでデータ活用を進めよう](https://qiita.com/okiyuki99/items/a10d1a868a2bf6fe8a7c)に詳しく記載があります。

前半はソースコード内のコメントを見れば何をやっているか大方分かるかと思います。

次に、この箇所で今回の処理が発火した要因となるMRのcommitの数を取得しています。
取得の方法や取れる情報については[GitLabのリファレンス](https://docs.gitlab.com/ee/api/merge_requests.html#create-mr)を参考にしています。
後続のloop処理でのloop回数を宣言するために取得しています。

```bash
# 対象のMRからcommit数を取得する
COMMIT_COUNT=$(curl --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" "https://gitlab.com/api/v4/projects/${PROJECT_ID}/merge_requests/${ID}/commits" | jq -r 'sort_by(..authored_date)' | jq '. | length')
```

そして、それらのcommitを全てcherry-pickしています。
ここで注意なのが、そのままMRの情報を取得すると、新しいcommitが先頭で表示されるので、時系列の項目`.authored_date`でソートすることで、commitを古い順から取得してcherry-pickできるようにしています。

```bash
# commit数の数分だけcherry-pickを行う。時系列的に昔のcommitからcherry-pickを行うため、commitの作成時間を昇順でソートしている
for i in `seq ${COMMIT_COUNT}`
    do
        COMMIT_HASH=$(curl --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" "https://gitlab.com/api/v4/projects/${PROJECT_ID}/merge_requests/${ID}/commits" | jq -r 'sort_by(..authored_date)' |  jq ".[$(($i-1))].id")
        COMMIT_HASH=`echo  "${COMMIT_HASH}" | sed "s/\"//g"`
        git cherry-pick ${COMMIT_HASH}
    done
```

これで最後にbranchをリモートにpushすれば、晴れて同一のcommitを持ったbranchを作成することができます。

## コマンドライン上でMRを作成する
最後に、作成したbranchでMRを作る方法です。
基本的にGitLabのGUIから作成すればいいじゃないかと思いかもしれませんが、`branchだけpushしてMR作るの忘れてた〜`なんてことも発生しかねないので、ここまでは自動化しています。
（逆に、最後のMergeする部分はしっかり目視でRVをするべきだと思うので、その部分の自動化処理は今回は含めておりません）

コマンドラインからMRを作成する方法については、上述したものと同じく[GitLabのリファレンス](https://docs.gitlab.com/ee/api/merge_requests.html#create-mr)を参考にしています。
最初の変数の時点で`TARGET_BRANCH`をmasterにしたり、`TITLE`が区別できるような宣言の仕方をしています。

```bash
# 対象のブランチでmasterブランチ向けにMRを作成する
curl -X POST -H "Private-Token: ${PRIVATE_TOKEN}" -d "title=${TITLE}&source_branch=${SOURCE_BRANCH}&target_branch=${TARGET_BRANCH}" "https://gitlab.com/api/v4/projects/${PROJECT_ID}/merge_requests" | jq .
```


#結果
以上の設定をすることで、冒頭で記載した**目的**を達成することができました。
操作上の動きとしては以下のようになるはずです。

1. masterブランチから修正ブランチを作成する(bugfix/A)`手動`
2. 修正ブランチでmasterブランチ向けにMRを作成する(MR_A)`手動`
3. そのMRに反応して、MR用のCIによりproductionブランチから派生した修正ブランチが作成される(bugfix/A-for_production)`自動`
4. その修正ブランチからproductionブランチにMRが出される(clone:MR_A)`自動`
5. productionブランチ向けにMRが出されたのでMR用のCIが反応するが、MRを作成するシェルは起動しない`自動`

*シェルやGitLabCIに関しては初心者なので何かご意見等いただけると大変勉強になります。

# 参考
#### GitLabに関して
- https://docs.gitlab.com/ee/ci/merge_request_pipelines/pipelines_for_merged_results/
- [gitlab-ci.yml内の環境変数](https://docs.gitlab.com/ee/ci/variables/predefined_variables.html)
- [Gitlab APIでデータ活用を進めよう](https://qiita.com/okiyuki99/items/a10d1a868a2bf6fe8a7c)
- [GitLabのリファレンス](https://docs.gitlab.com/ee/api/merge_requests.html#create-mr)

#### シェルの書き方
- [GitLabでtagとtagの間に含まれるMRリストを取得する](https://takamii.hatenablog.com/entry/2019/10/31/212021)
- [curlでAPIを叩く](https://qiita.com/buntafujikawa/items/758425773b2239feb9a7)


#　追記
運用していく中で、masterからproductionブランチにMRを複製する対象を制御したいという話があがりました。
そこで、MRの際に設定できる`labels`の項目を利用することにしました。
まず、コード上では以下のように設定を変更します。

```yml:gitlab-ci.yml
script:
      - echo "hello merge request"
      - if [ "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" == "master" ] && [ "$CI_MERGE_REQUEST_LABELS" == "sample" ]; then ./make_branch.sh $CI_MERGE_REQUEST_IID $CI_MERGE_REQUEST_TARGET_BRANCH_NAME $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME $CI_MERGE_REQUEST_TITLE; fi

```

つまり、targetBranchがmasterかつ、MRのlabelsが`sample`のものだけを対象にしています。
GitLabの画面上ではMR作成画面の以下の部分で設定可能なパラメータです。
なお、この検知はMergeRequestのCIが回っている間のみになるので、Labelsをつけ忘れてMRを作成してしまった場合は、Pipeline上から再度ジョブを実行する必要があります。
![スクリーンショット 2020-10-21 1.44.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555381/27e269b9-9813-5016-9015-dde375a1c9e0.png)