Four keys を計測する CLI ツールを作った

# はじめに

Four keys とはソフトウェア開発の生産性を測定するのに利用される以下の4つの指標のことである（[参考](https://www.devops-research.com/quickcheck.html)）。

- デプロイ頻度（Deployment Frequency） ソフトウェアのデプロイ頻度
- 変更リードタイム（Lead time for changes） ある変更をソフトウェアに適用してから、その変更がリリースされるまでの時間
- 障害修正時間（Time to restore） ソフトウェアに障害が発生してから、その障害が修正されるまでにかかった時間
- 障害率（Change failure rate）ソフトウェアのデプロイのうち障害が発生したデプロイの割合

これらの指標を簡易に測定するための CLI ツールを作成した。

https://github.com/hmiyado/four-keys

この記事では、この CLI ツールについて紹介する。

# 使い方

## インストール

[Releases](https://github.com/hmiyado/four-keys/releases) の最新バージョンから自分の環境に合わせた実行ファイルをダウンロードする。

## 利用方法

利用方法は、大きく2種類ある。

- デフォルト: Four Keys を計算する。
- releases: Four Keys の計算に利用する各リリースの詳細を表示する。
- timeSeries: 一定期間ごとの Four Keys を計算する。

以下でコマンドの詳細な利用方法を説明する。
説明中に jq コマンドを利用しているが、これは別途インストールする必要がある。

### デフォルト: Four Keys を計算

```sh
$ four-keys | jq
{
  "option": {
    "since": "2022-06-18T20:41:47.377195+09:00",
    "until": "2022-07-18T20:41:47.377196+09:00"
  },
  "deploymentFrequency": 0.1,
  "leadTimeForChanges": {
    "value": 5.447465277777778,
    "unit": "day"
  },
  "timeToRestore": {
    "value": 0,
    "unit": "day"
  },
  "changeFailureRate": 0
}
```

デフォルトでは、直近1ヶ月（ option.since から option.until までの期間）の Four Keys を計算する。

deploymentFrequency は期間中の1日あたりのデプロイ頻度である。
値が1であれば毎日デプロイしていることを示す。
上記の例では、0.1 となっており、期間が1ヶ月（30日）であることからリリース回数は3回と考えられる。

leadTimeForChanges は各リリースにおいて変更がリリースされるまでの時間の平均値である。
単位も同時に出力している。
上記の例では、およそ 5.4 日となっている。
deploymentFrequency の値とも総合すると、 1ヶ月の間に3回リリースがあり、これらのリリースの leadTimeForChanges の平均値が 5.4 日ということになる。

timeToRestore は各リリースでの障害修正時間の平均値である。
上記の例では障害が発生していないため 0 となっている。

changeFailureRate はデプロイが発生した割合である。
上記の例ではやはり障害が発生していないため 0 となっている。

### releases: Four Keys の計算に利用する各リリースの詳細を表示

```sh
$ four-keys releases --since 2022-07-01 | jq 
{
  "option": {
    "since": "2022-07-01T00:00:00Z",
    "until": "2022-08-31T23:09:09.991488+09:00"
  },
  "releases": [
    {
      "tag": "v2.1.0",
      "date": "2022-08-03T00:12:16+09:00",
      "leadTimeForChanges": {
        "value": 4.060879629629629,
        "unit": "day"
      },
      "result": {
        "isSuccess": true,
        "timeToRestore": null
      }
    },
# 中略
    {
      "tag": "v0.0.1",
      "date": "2022-07-12T23:28:17+09:00",
      "leadTimeForChanges": {
        "value": 16.335208333333334,
        "unit": "day"
      },
      "result": {
        "isSuccess": true,
        "timeToRestore": null
      }
    }
  ]
}
```

releases ではそれぞれのリリース（タグ）ごとの情報を出力できる。
1回のリリースに含まれる情報は以下の通り。

- tag タグの名前
- date タグが作成された日時
- leadTimeForChanges そのタグより前で、時間的にひとつ前のタグより後の、最も古いコミットが作成されてからそのタグが作成されるまでの時間
- result リリースの結果
- result.isSuccess そのリリースが成功したかどうか。そのタグ以降の途中のコミットのメッセージに `hotfix` が含まれる場合、リリースが失敗したとする（コミットメッセージの判定はオプションで変更可能）
- result.timeToRestore 前のリリースが失敗していた場合に、復旧までかかった時間
1つのタグで1回リリースしている

### timeSeries: 一定期間ごとの Four Keys を計算

```sh
$ four-keys timeSeries --repository https://github.com/hmiyado/four-keys --since 2022-10-01 --until 2022-12-31 --interval month | jq
{
  "option": {
    "since": "2022-10-01T00:00:00Z",
    "until": "2022-12-31T23:59:59Z"
  },
  "items": [
    {
      "time": "2022-12-01T00:00:00Z",
      "deploymentFrequency": 0,
      "leadTimeForChanges": 0,
      "timeToRestore": 0,
      "changeFailureRate": 0
    },
    {
      "time": "2022-11-01T00:00:00Z",
      "deploymentFrequency": 1,
      "leadTimeForChanges": 12.61,
      "timeToRestore": 0,
      "changeFailureRate": 0
    },
    {
      "time": "2022-10-01T00:00:00Z",
      "deploymentFrequency": 3,
      "leadTimeForChanges": 20.960555555555555,
      "timeToRestore": 0,
      "changeFailureRate": 0
    }
  ]
}
```

timeSeries では一定期間ごとに Four Keys を計算できる。
デフォルトでは毎月の値を出力する。
`--interval` オプションに `day` `week` `month` のいずれかを指定することで期間を変更できる。
`week` の場合は日曜日始まりで、 `month` の場合は毎月1日始まりとなる。
いずれのオプションを指定した場合も、 `--since` と `--until` で指定した期間が `--interval` で指定した期間に満たない場合はエラーとなる。

deploymentFrequency は、その集計期間内でのデプロイ回数を表す。
`--interval` が `day` であれば1日のうちの、 `month` であれば1ヶ月のうちのデプロイ回数を集計する。
上記のコマンド例では、2022年10月に3回デプロイしていることがわかる。

leadTimeForChanges と timeToRestore の単位は時間（hour）である。
上記のコマンド例では、2022年10月の leadTimeForChanges がおよそ 20.96 時間となる。
デプロイ回数は3回なので、3回のリリースの leadTimeForChanges の平均が 20.96 時間ということである。


### その他の利用可能なオプション

```sh
$ four-keys -h
# 重要なものだけ抜粋
   --accessToken value       GitHub access token to clone private repository (default: no access token)
   --fixCommitPattern value  commit that message matches fixCommitPattern is regarded fix commit (default: hotfix)
   --ignorePattern value     ignore releases that matches the pattern(regex)
   --repository value        the remote repository url. repository will be cloned in memory (default: local repository of current directory)
   --since value             the start date to query releases (inclusive) (default: 1 month ago)
   --until value             the end date to query releases (inclusive) (default: now)
```

fixCommitPattern にはリリースを修復したと判断するためのコミットメッセージのパターンを正規表現を指定できる。
デフォルトでは `hotfix` だが、 `^\[fix\]` とすればコミットメッセージの先頭に `[fix]` がある場合、といった形に変更できる。

ignorePattern には、集計時に無視するタグの名前のパターンを正規表現で指定できる。
releases コマンドでも表示されなくなる。
RC や誤って採番したタグを除外するために利用することを想定している。

repository には、外部リポジトリの URL を指定できる。
ファイルシステム上にはクローンしないので、環境がクリーンなまま集計できる。
ただし、集計には時間がかかる。
accessToken も同時に指定すると、 GitHub からプライベートリポジトリを clone できるようになる。

since と until で集計のための期間を指定できる。
デフォルトでは直近1ヶ月だが、状況によって自由に変更できる。

## 制約

利用にあたっていくつか制約がある。

- リリースの判定がタグに依存
- GitHub の情報を参照しない

まず、リリースの判定がタグに依存している。
そのため、タグを付与せずにリリースしている場合には集計することができない。
例えば、main ブランチにマージしたらタグを付与せずに自動でリリースしているようなケースが挙げられる。

また、GitHub の情報を参照していない。
これにより、リリースが復旧したかどうかに issue や PR に付随する情報を利用できない。
Git ではマージ時のブランチ名は管理していないため、これも厳密には GitHub 上の情報になる（ただし、 Git のデフォルトではマージ時のコミットメッセージにブランチ名が記載される）。
GitHub 上で hotfix 関連の情報が完結していて Git のコミットメッセージに反映されていない場合には、リリースの復旧を判定できず、障害修正時間や障害率を集計することができない。

# 他ツールとの比較

興味のある方向けに、いくつか関連するツールとの比較を紹介する。

|ツール|環境|備考|
|:--|:--|:--|
| 今回作成したツール |ローカル|ある1つのリポジトリで計測できる。とりあえず設定不要で動かせる|
| [Trendyol/four-key](https://github.com/Trendyol/four-key) | ローカル | 複数のリポジトリを設定してまとめて計測できる。多少事前の設定が必要。最近更新が止まっている様子|
| [GoogleCloudPlatform/fourkeys](https://github.com/GoogleCloudPlatform/fourkeys) | GCP | GitHub とも連携できるようだ |
| [Findy Teams](https://findy-teams.com/) | SaaS | 契約が必要だが、Four keys 以外もメトリクスを計測できる |
| [merged-pr-stat](https://github.com/shibayu36/merged-pr-stat) / [dmps](https://github.com/isanasan/dmps) | ローカル |  PR 単位での統計情報を計測できる。Four keys のようなリリース単位での計測ではない（[コメント](https://qiita.com/hmiyado/items/fb9b0409ab479942ad4c#comment-3420a93775b9b64ea233)より追記） |

# 実装について

## ツール作成の背景

既存ツールをいくつか調べながら、以下の特徴をもったツールがほしいと感じた。

- CLI でサクッと動かして試せる
  - ツール自体もサクッと利用可能になる
- 計算式を調整できる

前述したツールはこれらの要求にマッチしなかったため、自分で作ることにした。

## 技術

ツールの作成には Golang を選択した。
いろいろな環境で動く実行ファイルを手軽に生成できそうなイメージがあったこと、 git 操作をある程度簡便にできそうなライブラリがあったことが理由だ。

https://github.com/go-git/go-git

Golang で使い捨て以上のコードを書くのは初めてだったが、かなり言語側の慣習として定められているところが多く、書きやすかった。
特に、テストを書きやすいのはよかった。
テストを書くために必要なステップが定められているので、ただテストを書くというところに集中できた。
一方で、あまり高度な API がなく、最近の言語では map や reduce で完結するような処理を可変な変数と for 文でなんとか書かないといけないのは少し大変だった。

go-git は git 操作を扱うには便利だった。
in-memory にリポジトリをクローンしていろいろできる機能が特に良く、テストに必要な様々な条件のリポジトリをローカルのファイルシステム上に置かずに取り扱えた。
一方で実行速度が遅い部分があり、このツールの中で最も遅い部分の処理（あるリリースの最も古いコミットを特定する処理）で直接 git を使うようにしたら 1.5 倍高速になった。
枯れた技術の優秀さを目の当たりにした気分だった。