開発のリードタイム削減のために入れたgithub actionsたちをご紹介！ (実装例付き)

<!---
メリークリスマス！！ 今年もまさかmarkdownを素で読んでくださる方がいるとは思っていませんでしたよ....!!
よかったらLGTMお願いしますー！！
--->

この記事は[モチベーションクラウドシリーズ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/mcs) 6日目の投稿です。

https://qiita.com/advent-calendar/2022/mcs

# はじめに

私のチームではプロダクト開発の生産性指標としてFour keys metricsを利用しています。
Four Keys Metricsは DevOps Research and Assesment (DORA) チームが確立した開発組織のパフォーマンスに関する指標です。[^1]
[^1]:https://cloud.google.com/blog/products/devops-sre/the-2019-accelerate-state-of-devops-elite-performance-productivity-and-scaling を参照ください

その中の一つである変更のリードタイムは下記のように定義されています。

> 変更のリードタイム - commit から本番環境稼働までの所要時間[^2]
[^2]:https://cloud.google.com/blog/ja/products/gcp/using-the-four-keys-to-measure-your-devops-performance を参照ください

このリードタイムを削減するために作成した、3つのgithub actionsを実際のワークフローの中身とともに紹介できればと思います。

# リードタイム削減に向けた方針

私のチームではgithubのPull Requestを使っているので、リードタイムを分解すると

```md
初回コミットからブランチがマージされPull Requestがクローズされるまでの時間
+
開発のメインブランチが本番環境に反映されるまでの時間
```
になることから、
前者の初回コミットからブランチがマージされるまでの時間というのをとにかく状況を見えるようにすることにこだわりました。

私のチームではまずこの「初回コミットからブランチがマージされPull Requestがクローズされるまでの時間」を`変更のリードタイム`としてみていくことにしました。

自分自身がコードを書く際も、ついつい自分がOpenしたPull Requestを放置していたことも多かったです。
また、マージした際も「なんとなく長かった気がするなー」といったような感覚でしか長さを感じることができていませんでした。

そこで、開発ブランチへのマージの促進、結果のフィードバック、チーム全体での状況の可視化を行うためのワークフローの追加を行いました。

:::note
あくまでも実装例ですが、よろしければご参考になさってください
見づらく拙い記法もあるかと思いますので、コメントいただけると幸いです
:::

# 1. Pull Requestが作成されてから時間がたつとお知らせ

放置されているPull Requestを作成者に伝えるようにしました。
ルールとしては、平日9時にOpenになってから2日以上経過しているPull Request上で作成者に対して2日経過している旨をコメントするようにしました。
またPull Request一覧で見たさいにわかりやすく絞り込めるように`2d-passed`というラベルも付与しています。

```yml
name: 作成されてからマージされていないPRをチェック
on:
  schedule:
    # AM 09:00, 月-金
    - cron: '0 0 * * 1-5'

jobs:
  set-label-and-comment:
    runs-on: ubuntu-latest
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - name: checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - name: 作成して2日以上経っているのに2d-passed labelのついてないPRにコメントしてラベルをつける
        run: |
          # 適宜変更してください
          LABEL="2d-passed"
          # 適宜変更してください
          DATE=$(date +'%Y-%m-%d' --date '2 day ago')
          gh pr list -L 100 \
            --search "is:pr is:open created:<${DATE} -label:${LABEL}" \
            --json url,author \
            --jq '.[] | [.url, .author.login] | join(" ")' |
            xargs -r -n2 bash -c 'gh pr comment $0 -b "@$1 マージを促進するコメント" && gh pr edit --add-label "2d-passed" $0'
```

こんな形でコメントがされます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/885081/a7a7ad45-36e1-b9d2-8f2f-25ec6c40f0f5.png)

<font color=#dddddd>私のチームではマージを促進するコメントを投票で決めたところ「良い時計してますなあ」になりましたw</font>


# 2. マージされたあとに初回のコミットから何時間でされたかお知らせ

Pull Request作成者が最初のコミットをしてから何時間でbaseブランチにマージされたかを計算し、コメントするようにしました。
具体的な数字をコメントすることで、作成者だけではなくレビュワーにもフィードバックされ、レビューまでの時間などを意識させることができたかなと思います。

```yml
name: 初回コミットからマージまでの時間を計測してコメント
on:
  pull_request:
    branches:
      - develop # baseブランチの指定
    types: [closed]

jobs:
  lead-time:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - name: checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - name: calc time
        run: |
          gh api graphql -f id="${{ github.event.pull_request.node_id }}" -f query='
            query ($id: ID!) {
              node(id: $id) {
                ... on PullRequest {
                  mergedAt
                  commits(first: 1) {
                    nodes {
                      commit {
                        authoredDate
                      }
                    }
                  }
                }
              }
            }
          ' |
          jq -r '.data.node | .mergedAt, .commits.nodes[0].commit.authoredDate | fromdate | strftime("%s")' |
          xargs -n 2 |
          awk '{printf "%d", ($1 - $2) / 60 / 60 }'|
          xargs -I{} echo LEAD_TIME_HOUR={} >> $GITHUB_ENV
      - name: comment
        run: |
          gh pr comment ${{ github.event.pull_request.html_url }} \
          -b "@${{ github.event.pull_request.user.login }} お疲れ様でした!!🍺 リードタイムは$(expr ${LEAD_TIME_HOUR} / 24)日 (${LEAD_TIME_HOUR}時間)でした!!🎉"

```

こんな形でコメントされます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/885081/c995429e-21d3-25ed-eab9-e566ecb2590c.png)


ちなみにgh cliはめちゃくちゃ便利でなんでもできます。

https://cli.github.com/

この初回コミットからマージまでの時間計測には `gh api graphql`を使用しており、githubの公開されているGraphQLをcliから簡単に実行することができます。
Github GraphQL APIでどんな情報が取得できるか試すにはこちらの[エクスプローラ](https://docs.github.com/ja/graphql/overview/explorer)を試すのがおすすめです。

https://docs.github.com/ja/graphql/overview/explorer

# 3. 結果を分析するために情報を取得して蓄積

各Pull Requestがマージされてクローズされる時、時間のコメントだけではなく、時間、変更数などを合わせてspreadsheetに書き込むようにしています。

具体的には

1. Pull Resquestがクローズされたら関連する情報を整形して送信
1. 送信先はspreadsheet上のGoogle Apps Scriptでシートに追加

のステップで情報の蓄積を行なっています。

以下の情報を取得しています。

- リポジトリ名
- Pull Requestのタイトル
- 作成者
- url
- baseブランチ
- headブランチ
- 初回コミット日時
- 作成日時
- 初回レビュー日時
- マージ日時
- 追加行数
- 削除行数

```yml:github actions
name: SpreadsheetにPull Requestの情報を送信
on:
  pull_request:
    types:
      - closed
jobs:
  metrix-info:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - name: checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - name: post github info
        run: |
          gh api graphql -f id="${{ github.event.pull_request.node_id }}" -f query='
            query ($id: ID!) {
              node(id: $id) {
                ... on PullRequest {
                  repository {
                    name
                  }
                  title
                  author {
                    login
                  }
                  url
                  baseRefName
                  headRefName
                  commits(first: 1) {
                    nodes {
                      commit {
                        authoredDate
                      }
                    }
                  }
                  createdAt
                  mergedAt
                  additions
                  deletions
                  reviews(first: 1) {
                    nodes {
                      ... on PullRequestReview {
                        createdAt
                      }
                    }
                  }
                }
              }
            }
          ' |
          # tokenで認証を行なっている
          jq -r --arg token ${{ secrets.GAS_POST_API_METRIX_TOKEN }} '.data.node |
            {
              data: {
                repository: .repository.name,
                title: .title,
                user: .author.login,
                url: .url,
                base: .baseRefName,
                head: .headRefName,
                firstCommittedAt: .commits.nodes[0].commit.authoredDate,
                createdAt: .createdAt,
                firstReviewedAt: .reviews.nodes[0].createdAt,
                mergedAt: .mergedAt,
                additions: .additions,
                deletions: .deletions
              },
              type: "lead_time",
              token: $token
            } |
            @json' |
          curl -L -H "Content-Type: application/json" -d @- "https://script.google.com/macros/s/*******/exec"

```
<details><summary>受信先のGoogle Apps Script例 (少し長いのでこちらをクリック)</summary>

```js
function doPost(e) {
  const postData = JSON.parse(e.postData.contents);
  // ここでtoken認証
  if (postData.token !== PropertiesService.getScriptProperties().getProperty('GAS_POST_API_METRIX_TOKEN')) {
    return (createResponse('invalid token error'))
  }

  switch (postData.type) {
    case 'lead_time':
      insertLeadTimeData(postData);
      return (createResponse('ok'))
      break;
    default:
      return (createResponse('invalid type error'))
  }
}

function insertLeadTimeData(postData) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('シート名');
  const data = postData.data
  sheet.appendRow([
    data.repository,
    data.title,
    data.user,
    data.url,
    data.base,
    data.head,
    data.firstCommittedAt,
    data.createdAt,
    data.firstReviewedAt,
    data.mergedAt,
    data.additions,
    data.deletions
  ]);
}

function createResponse(message) {
  const output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);
  output.setContent(JSON.stringify({ message: message }));
  return output;
}
```
</details>

こんな形で蓄積されていっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/885081/d551e9a2-9a8f-1679-2140-c8a5d4324863.png)


spreadsheetに蓄積することでメンバー間の比較やチーム全体の変化を可視化できるようになりました。
結果の可視化はGoogleの[Looker Studio](https://support.google.com/looker-studio/answer/6283323?hl=JA)で行い、他のFour keys metricsと一緒にダッシュボードにしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/885081/632047f2-6d7e-e7b2-4009-2f68ba04a77f.png)

Four keys metricsのダッシュボードについては[弊社テックブログ](https://link-and-motivation.hatenablog.com/?_ga=2.72213527.564245223.1670225889-1385780718.1670225889)に詳細を投稿予定ですので、読者登録いただけると幸いです。

# 導入してみてどうだったか
元々5~10日ほどだったマージまでの時間が平均値、中央値ともに1日前後になりました。
もちろんこのワークフローたちのみの結果ではなく、ペアプロによるレビューコスト削減、Pull Requestのサイズ軽減、レビューファーストなチームの文化づくりの結果だとは思います。
マージまでの時間が減ることで、コンフリクトも起きづらくなり、開発者体験も向上したのではないかなと思います。