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


目的

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

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

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

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

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

完成した全体の構成

構成としてはシンプルで、以下の2ファイルでの実行です

  • .gitlab-ci.yaml(MRが上がったことを検知)
  • make_merge_request.sh(対象のMRを作成する)
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 .

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内の環境変数についてはリンク先を参照してください。
これらの変数を引数にして、後述する./make_branch.shの処理を行っております。

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_TOKENPROJECT_IDをGitLabから取得してきてください。
Gitlab APIでデータ活用を進めように詳しく記載があります。

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

次に、この箇所で今回の処理が発火した要因となるMRのcommitの数を取得しています。
取得の方法や取れる情報についてはGitLabのリファレンスを参考にしています。
後続のloop処理でのloop回数を宣言するために取得しています。

# 対象の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できるようにしています。

# 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のリファレンスを参考にしています。
最初の変数の時点でTARGET_BRANCHをmasterにしたり、TITLEが区別できるような宣言の仕方をしています。

# 対象のブランチで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に関して

シェルの書き方

 追記

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

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上から再度ジョブを実行する必要があります。