つばくろぐ @takamii228

知は力なり

GitLab CEでMerge Request時のCIを事前にマージしてから実行する

最近のお仕事ではCIにGitLab CIを使っています。

Merge Request時にGitLab CIを実行して、CI上で静的解析やビルド・テストを実行してコードをクリーンに保つようにしています。

しかしMerge Requestがしばらく放置されてしまい、当時はCIのPipelineが成功していたとしても、ターゲットブランチが先に進んでしまってからマージした場合、マージ後のCIが失敗してしまうケースがあります。

これを防ぐためにはコンフリクトを直すときのように常にローカルでfetch / rebaseしたり最新のターゲットブランチをマージして再Pushして解決することができますが、なかなか面倒です。

githubやgitbucketにはこれを防ぐ機能として、マージ先のブランチが先に進んでしまっていた場合にマージ先のoriginの情報をマージ対象のブランチにマージする Update branch 機能があり、PRのレビュー画面で実行することができます。

github.blog

GitLabにも似たような機能がないかと探していたら、こんな機能を見つけました。

https://docs.gitlab.com/ce/ci/merge_request_pipelines/pipelines_for_merged_results/index.html

It’s possible for your source and target branches to diverge, which can result in the scenario that source branch’s pipeline was green, the target’s pipeline was green, but the combined output fails.

By having your merge request pipeline automatically create a new ref that contains the merge result of the source and target branch (then running a pipeline on that ref), we can better test that the combined result is also valid.

GitLab can run pipelines for merge requests on this merged result. That is, where the source and target branches are combined into a new ref and a pipeline for this ref validates the result prior to merging.

マージ対象のブランチにマージしてからPipelineを実行してくれるらしい。

これこれ、と思ってReposirtoyの設定を見てみたら、設定が見当たらない。

おかしいなと思ってよく見てみると・・・

f:id:takamii228:20190727155021p:plain

あー課金ユーザ向けだった。。。

仕方ないので似たようなことができるように自作しました。

まずgitlab-ci.ymlの中でMerge Request実行時には最初にupdate-branch.shを実行するように設定します。

// Merge Request時はこっちを実行する
mergeRequestBuild:
  stage: build
  tags:
    - ci-runner
  script:
    - ./update-branch.sh
    - ./lint.sh
    - ./build.sh
  only:
    - merge_requests

// ブランチPush時はこっちを実行する
branchBuild:
  stage: build
  tags:
    - ci-runner
  script:
    - ./lint.sh
    - ./build.sh

update-branch.shでは最新のターゲットブランチとマージ対象のブランチを作成して、マージ対象のブランチにターゲットブランチをマージします。

#!/bin/sh

set -x
set -e

TARGET_BRANCH=$CI_MERGE_REQUEST_TARGET_BRANCH_NAME
SOURCE_BRANCH=$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME

if [ -n "$SOURCE_BRANCH" ]; then
    # CIの実行ごとにブランチを再利用しないようにコミットごとにテンポラリのブランチ名を作成する
    TMP_SOURCE_BRANCH=$SOURCE_BRANCH-`git rev-parse --short HEAD`-`date "+%Y%m%d%H%M%S"`
    TMP_TARGET_BRANCH=$TARGET_BRANCH-`git rev-parse --short HEAD`-`date "+%Y%m%d%H%M%S"`

    # Merge対象のブランチをテンポラリのブランチ名でチェックアウトする
    git checkout -f -b $TMP_SOURCE_BRANCH HEAD

    # Merge元のブランチをテンポラリのブランチ名でチェックアウトする
    git checkout -f -b $TMP_TARGET_BRANCH origin/$TARGET_BRANCH

    # Merge対象のブランチにMerge元のブランチをマージする
    git merge $TMP_SOURCE_BRANCH -m "merge $CI_COMMIT_REF_NAME into develop"
fi

Merge Request対象のブランチ名はCI_COMMIT_REF_NAMEから取得することができました。

https://docs.gitlab.com/ee/ci/variables/predefined_variables.html

gitalb ciでは毎回git checkout HEADでローカル環境のチェックアウトをしているため、CI上でブランチのマージを実行する場合は明示的にブランチを切ってcheckoutする必要がありました。

また今回はGitLab CI Runnerはshellで実行しているため、ビルド実行後のブランチ情報は毎回残ってしまいます。そのため毎回の実行でブランチ名が一位になるようにコミットハッシュ+実行日時をいれるようにしました。

コンフリクトが発生する場合は失敗mergeReuqestBuildは失敗するのでConflictが発生していることがCIの実行時に気づけるようになりました。