つばくろぐ @takamii228

知は力なり

GitLab APIを使ってtag名とtagのタイムスタンプをcsv形式で出力する

リリース頻度のデータを分析するために、GitLabのtag名とtagを作成した時のタイムスタンプの一覧が欲しくなったのでGitLab API経由で取得するシェルを書きました。

#!/bin/sh

BASE_URL="xxxx"
API_TOKEN="xxxx"
PROJECT_ID="xxxxx"

# 全tag一覧を取得するための最大ページ数を取得する
TOTAL_PAGES=`curl -D - -s -o /dev/null -X GET -H "PRIVATE-TOKEN: ${API_TOKEN}" "${BASE_URL}/api/v4/projects/${PROJECT_ID}/repository/tags?per_page=100" | grep "X-Total-Pages" | tr -d "X-Total-Pages: " | tr -d "\r"` 

# タグとタグのタイムスタンプをカンマ区切りで取得する
RESULT=""

for i in `seq 1 ${TOTAL_PAGES}`
do
  RESULT=${RESULT}`curl -X GET -H "PRIVATE-TOKEN: ${API_TOKEN}" "${BASE_URL}/api/v4/projects/${PROJECT_ID}/repository/tags?per_page=100&page=${i}" | jq -r '.[] | (.name|tostring) + "," + (.commit.authored_date|tostring)'`"\n"
done

# UTCで返却されるので、JSTに変換し出力する
for line in ${RESULT}
do
  TAG_NAME=`echo ${line} | cut -d ',' -f1`
  TIMESTAMP_UTC=`echo ${line} | cut -d ',' -f2`
  TIMESTAMP_JST=`date -v+9H -j -f "%Y-%m-%dT%T.000Z" ${TIMESTAMP_UTC} +%Y-%m-%dT%T.000Z`
  echo ${TAG_NAME},${TIMESTAMP_JST}
done

tag一覧の取得は/api/v4/projects/${PROJECT_ID}/repository/tagsのエンドポイントを叩くと取れます。

https://docs.gitlab.com/ce/api/

tag一覧の取得はページングのハンドリングが必要なので、レスポンスヘッダーのX-Total-Pagesからページ数を取得し、最終的にはfor文で回してjqで加工したものを連結します。

タイムゾーンの設定によってはUTCで結果が帰ってくるので、最後にdateコマンドでJSTに変換しています。

上記のシェルを実行すると以下のようなcsvが取得できます。

1.5.0,2019-09-30T10:59:49.000Z
1.4.0,2019-09-26T15:35:21.000Z
1.3.2,2019-09-20T19:00:11.000Z
1.3.1,2019-09-17T18:21:30.000Z
1.3.0,2019-09-11T21:01:37.000Z
1.2.0,2019-09-03T22:08:34.000Z
1.1.0,2019-08-27T22:41:38.000Z
1.0.2,2019-08-20T22:37:28.000Z
1.0.1,2019-08-13T22:13:56.000Z
1.0.0,2019-08-06T21:38:05.000Z
0.3.0,2019-07-30T21:16:30.000Z
0.2.0,2019-07-23T22:38:51.000Z
0.1.0,2019-07-17T15:30:01.000Z

あとはこれをSpreadSheetなりExcelなりPythonなりで集計すればリリース頻度の分析の集計ができます。

master中心のブランチ戦略で開発している話

ここ数年、CI/CDパイプラインを構築したりブランチ戦略を考えるロールで仕事をすることが多いのですが、ブランチ戦略はいつも頭を悩ませていました。

当初はGit FlowGitLab Flowの採用を考えたりしたのですが、ブランチやマージの戦略がちょっと重厚でめんどうなだという印象を持っていました。

「releaseブランチとかpre-productionとか管理面倒だからぶっちゃけmasterだけでよくね?」とふと思い、master中心のシンプルなブランチ戦略を考え、実践してみました。するとそれなりにうまく立ち回ることができるようになっていたので、整理もかねてまとめておきます。

対象とするプロジェクト

本ブランチ戦略は、以下のような開発チームに対してうまく機能できると考えています(※もちろんこの方法が最適な解とは限りません)

  • バックエンド・フロントエンド・スマホアプリなど開発領域ごとにチームが別れている
  • サービスを成立させるためには各チームが独立にリリースすることはできず、最終的には資材の同期を取る必要がある
  • 先に開発を終えたチームは次の案件の開発に着手しており、追加開発とリリース用の2つの資材を管理する必要がある
  • 開発チームメンバーは数人から20人程度
  • masterをcleanに保つためのCIが整っている

基本的な考え方

master中心のブランチ戦略のベースとなる考え方は以下の通りです。

  • masterが最新の安定版となるようにコミットしていく(※ リリース版ではなく機能開発の最新)
  • ブランチは本開発、追加開発、リリース版などの開発の流れ(= Flow)を表す
  • ブランチはどこかのブランチにマージするか、マージしない場合は独立に更新を続け最終的には捨てる
  • この機能を入れる資材と入れない資材が必要、というときにブランチを切って開発の流れを分岐させる
  • リリースバージョンはタグに紐づけるようにし、ブランチと実行環境は紐付けない

これら原則に従うことで、以下のようなメリットが得られます。

  • 管理するブランチが少なくなるため、ブランチの管理やマージ先の手間をへらすことができる
  • 「masterブランチを最新の安定版」とするため、Pull Reuquest / Merge Requestのマージ先はhotfix以外はすべてmasterになり、追加開発時のマージ先誤りの事故を防ぐことができる
  • ブランチと実行環境が分離されるため、環境管理とは独立して資材のバージョン管理ができる

以下、実際にリリースの前・リリース後のブランチ戦略を説明していきます。

リリース前のブランチ戦略

リリース前のブランチ戦略は非常にシンプルです。masterブランチに対して、featureブランチ・bugfixブランチをマージすることで機能追加を反映していきます。

f:id:takamii228:20191126204756p:plain

初期開発時に利用するブランチは以下の通りです。

ブランチ名 ブランチの切り元・マージ先 用途
master - 最新の安定版ブランチ
feature/* master 機能追加用ブランチ。CIが通ったもののみマージする。チケット番号と紐付ける。masterにマージしたら削除する。
bugfix/* master テスト・QA等で発覚したバグの修正用のブランチ。CIが通ったもののみマージする。チケット番号と紐付ける。masterにマージしたら削除する。

このとき、成長するブランチはmasterブランチのみであり、チームメンバはmasterブランチのみを追いかけていればよいです。

検証環境へのリリース資材を作成する場合はtagでバージョンとコミットハッシュを紐付けることでバージョン管理ができます(図のv0.1.0、v0.1.1)。

リリース後のブランチ戦略

ここでのリリース後、とは自分のチームの開発範囲のリリースを指します。すなわちバックエンドチームであればバックエンドのAPIのみのリリースを指します。

バックエンドは順調に開発が進み先に環境へリリースできた一方で、フロントエンドは画面の仕様調整に難航しスケジュールが遅れてしまった、というケースはよくあると思います(逆もしかり)。

このときバックエンドチームはフロントエンドチームの待ちの状態になるため、開発チームの余ったリソースを使って先に追加開発に着手し始めるケースがよくあります。このとき、masterブランチのみの運用だと本来のリリースには含めてはいけない資材が混じってしまう可能性があるため、フロントエンドの改修がバックエンドに波及したときのhotfixの対応を受け入れるために、masterとは別のproductionブランチを作成し2つの流れを作って運用します。

productionブランチを作っておくことで、フロントエンドチームと同期をとったときの不具合のフローと追加開発のフローを分けて管理することができます(下図)。

f:id:takamii228:20191126211825p:plain

利用するブランチは以下の通りです。 リリース後のブランチはリリース前にあったブランチに加えてproductionブランチとhotfixブランチが追加されています。

ブランチ名 ブランチの切り元・マージ先 用途
master - 最新の安定版ブランチ
production master 最新のリリース版、リリースごとに作成する。リリース版にのみ緊急に反映させたいパッチがあればあてる。追加開発版のリリースが行われたら今まで使っていたproductionブランチは削除し、リリース対象のタグから新しいproductionブランチを作成する。
feature/* master 機能追加用ブランチ。CIが通ったもののみマージする。チケット番号と紐付ける。masterにマージしたら削除する。
bugfix/* master テスト・QA等で発覚したバグの修正用のブランチ。CIが通ったもののみマージする。チケット番号と紐付ける。masterにマージしたら削除する。
hotfix/* production / master リリース版バグ修正用のブランチ。CIが通ったもののみマージする。チケット番号と紐付ける。productionにのみパッチをあてる場合はproductionから切ってproductionにマージし削除する。production / master両方に必要なパッチの場合はそれぞれの根本から2本パッチを充ててそれぞれにマージし削除する。

productionブランチは外部チームへの提供可能なバージョンのタグを切ったときに作成します。productionブランチにはリリースに必要なパッチや緊急のhotfixのみの変更が当てられます。hotfixのパッチは場合によってはデグレを防ぐためにmasterとproduction両方に当てる必要があります(※後述)。追加開発向けの機能追加はリリース前と同様です。

リリース版で発覚したバグ修正のhotfixは、追加開発を行っている最新版のmasterに入れる場合と入れない場合とで以下のように対応を分けます。

  • リリース版および最新版にもパッチを入れる場合(例:恒久的なバグ対応)
    • hotfixブランチをproductionから切って、productionにマージする
    • hotfixブランチをmasterからも切って、productionブランチへのhotfixと同様の変更を加えmasterにマージする(cherry-pickなどを使う)
  • リリース版のみに入れるパッチの場合(例:ライブラリの緊急パッチ等の暫定対処)
    • hotfixブランチをproductionから切って、productionにのみマージする

hotfixの対応は一見複雑そうに見えますが、「masterが常に追加開発の最新版である」こと、「productionブランチがリリース版の最新版である」ことを考えると意外とシンプルにまとまっているのではないでしょうか。

なお、productionブランチはmasterには不要な変更を含んでいる場合があるため、masterにはマージしません。次のリリース版のtagが打たれたらまた新しいproductionブランチを作成します。

まとめ

Gitのブランチ戦略はGiit FlowやGitLab Flow等がありますが、どちらも扱うブランチの種類が多かったりデプロイ先の環境と密結合に語られていて扱いにくい印象を持っていました。

「masterを最新の安定版ブランチ」とする、というこのmaster中心のブランチ戦略を使うことで、シンプルで運用しやすい資材管理ができるようになったと個人的には考えています。興味を持たれた方はぜひ実践してみてください。

GitLabでtagとtagの間に含まれるMRリストを取得する

GitLabであるタグに含まれるMRのリストをトレースする必要があったので、GitLabのAPIとjqを使ってtagとtagに間含まれるMRリストを出力して、Markdown形式のリリースノートを出力するシェルを組みました。

#!/bin/sh

BASE_URL="GITLAB_HOSTNAME"
API_TOKEN="GITLAB_APITOKEN"
PROJECT_ID="xx"
PROJECT_NAME="yyy"
REPOSITOEY_NAME="zzz"

TAG_FROM_AUTHORD_DATE=`curl -X GET -H "PRIVATE-TOKEN: ${API_TOKEN}" "${BASE_URL}/api/v4/projects/${PROJECT_ID}/repository/tags/${1}" | jq -r .commit.authored_date`
TAG_TO_AUTHORD_DATE=`curl -X GET -H "PRIVATE-TOKEN: ${API_TOKEN}" "${BASE_URL}/api/v4/projects/${PROJECT_ID}/repository/tags/${2}" | jq -r .commit.authored_date`

# tagを切ったコミットハッシュのMRが含まれない可能性があるため1分ずらす
TAG_FROM_AUTHORD_DATE=`date -v+1M -j -f "%Y-%m-%dT%T.000Z" ${TAG_FROM_AUTHORD_DATE} +%Y-%m-%dT%T.000Z`
TAG_TO_AUTHORD_DATE=`date -v+1M -j -f "%Y-%m-%dT%T.000Z" ${TAG_TO_AUTHORD_DATE} +%Y-%m-%dT%T.000Z`

curl -X GET -H "PRIVATE-TOKEN: ${API_TOKEN}" "${BASE_URL}/api/v4/projects/${PROJECT_ID}/merge_requests?state=merged&updated_after=${TAG_FROM_AUTHORD_DATE}&updated_before=${TAG_TO_AUTHORD_DATE}&target_branch=master&view=simple&per_page=100" | jq -r 'sort_by(.iid)| .[] | "- [" + (.title|tostring) + "](" + (.web_url|tostring) + ")"'

使い方は引数に2つのタグを入れるだけです。

# 実行方法と出力サンプル
$ ./generate-releasenote.sh tag1 tag2
- [MR Title1](${BASE_URL}/${PROJECT_NAME}/${REPOSITOEY_NAME}/merge_requests/mr_id1)
- [MR Title2](${BASE_URL}/${PROJECT_NAME}/${REPOSITOEY_NAME}/merge_requests/mr_id2)
- [MR Title3](${BASE_URL}/${PROJECT_NAME}/${REPOSITOEY_NAME}/merge_requests/mr_id3)

中でやってるのは以下の通りです。

  1. tag1、tag2が打たれたタイムスタンプを取得する
  2. 1で取得したタイムスタンプをdateコマンドで1分後ろにずらす
  3. 2を使ってMRリストを取得するAPIリクエストを投げてタイトルとIDのリストを取得する
  4. jqで加工してMarkdown形式で出力する

tag1、tag2のタイムスタンプをそのまま使ってしまうとtag1・tag2がマージコミットだった場合に出力されるMRリストに含まれない可能性があるのでdateコマンドで1分後ろにずらしています。

またクエリにヒットするのはタイムスタンプの間に更新があったMRリストなので、もしマージされたあとにコメント書いたりして更新されたものがあるとヒットしてしまいます。このへんは運用で除外するように対処しました。

あとはMR一覧を取得するAPIは一度に取得できる数がデフォルトだと20件なので、per_pageで最大値である100を指定しています。

これを日々行うことで、リリース前後でどのbugfixがどのタグに含まれているか追跡するのが楽になりました。

Merge requests API | GitLab