リリース頻度を毎週から毎日にしてみた

目次

はじめに

こんにちは、NeWork 開発チームの藤野です。普段はオンラインワークスペースサービス NeWork のエンジニアリングマネジメントをしています。

この記事では、それまで毎週新バージョンのリリースをしていた NeWork Web 版のリリース頻度を(最大で)毎日に変更した事例を紹介します。

NeWork とは

NeWork はコロナ禍で誕生したオンラインワークスペースサービスです。

従来の Web 会議ツールとは異なり、手軽に話しかけられることを重視したサービスになっており、Web・デスクトップアプリ・モバイルアプリで提供されています。

サービスの基盤は GC(Google Cloud) 上で提供しており、フロントエンドは Next.js 、バックエンドは Node.js+Express を採用しています。

音声基盤は NTT Com の別チームが開発している SkyWay を利用しています。

リリース頻度変更の背景

それまでの運用

NeWork では 2021 年 7 月以降(ほぼ)毎週のリリースを実現し、大きなインパクトのある機能だけでなく、細かな改善やバグ修正を継続して実施してきました。

具体的には git flow ライクなブランチ運用をしながら毎週リリース作業をしていました。

  • develop から feature ブランチをきって開発し、完了したら develop にマージ。develop にマージされると自動で stg 環境にデプロイ
  • 毎週水曜のリリース日
    • 朝 : develop ブランチから main ブランチにマージ
    • 日中 : NeWork チーム全員で stg 環境を普段利用し、致命的なバグがないことを普段利用の範囲で確認
    • 夕方 : リリースタグを付与して prod 環境に自動デプロイし、動作確認
  • 重大なバグがあれば、main ブランチから hotfix ブランチをきって修正
    • 上長に状況を説明し、許可を得た上でリリース

課題

しかし 2 年以上この運用を続けてきていて以下の課題があると感じていました。

  • お客さまからのフィードバックに即応してもすぐリリースできない
  • バグがあっても重大なものじゃなければ、次のリリースまで待つ必要がある
  • リリース直前にバタつくことがある
    • 来週まで延期したくないので、どうしても今週リリースしておきたい → 水曜の午後まで develop ブランチから main ブランチにマージできないこともあった
      • この駆け込み乗車によってバグがあるままリリースしてしまう可能性があった
  • 複数のスクラムチームのスケジュールに縛りを与える
    • スプリントレビューを通過したものだけリリースになるので、必然とスプリントの終了日が固定され重複する → 複数チームのステークホルダーは全部のイベントに参加できなくなる
  • リリース作業の一部は手動で面倒 & ミスする可能性がある
    • develop ブランチから main ブランチへのマージを忘れたことがある
    • (起きたことはないが) 付与するタグのフォーマットを間違える可能性がある

それ以外でも、DevOps Research and Assessment が提唱する Four Keys のうちの「デプロイの頻度」「変更のリードタイム」を改善すべきと考えていました。

実現方法

上述の背景を考慮して、2023 年 9 月から以下を毎日完全自動で実施する運用に変更しました。

  1. 平日の夕方に、前日の夕方以降で develop ブランチにマージされたすべての変更を main ブランチに自動でマージする
    • main ブランチへのマージをトリガーに stg 環境へ自動デプロイ
    • NeWork チームは基本的に stg 環境を普段から利用しているので、普段使いの範囲の中で致命的なバグがないか丸一日確認できる
  2. 翌日の夕方、main ブランチにリリースタグを自動で付与
    • リリースタグをトリガーに prod 環境へ自動デプロイ

具体的には日時で起動する以下のようなワークフローを GitHub Actions で用意して実現しました。

name: daily-release

on:
  # 平日 18:00 JST
  schedule:
    - cron: "0 9 * * 1-5"

run-name: daily-release/${{ github.event.schedule }}

# release タグが JST ベースの日付になるようにタイムゾーンを設定
env:
  TZ: "Asia/Tokyo"

jobs:
  daily-release:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          ref: main
          # 全部 fetch しないと rev-list が動作しない
          fetch-depth: 0
          # token を指定することで protected branch の設定を bypass できるようにする
          token: ${{ secrets.GH_TOKEN }}
      # 最新のタグと最新のコミットを取得
      - name: get latest tag and commit
        id: get_latest_tag_and_commit
        run: |
          echo latest_tag_commit=$(git rev-list --tags --max-count=1) >> $GITHUB_OUTPUT
          echo latest_commit=$(git rev-parse HEAD) >> $GITHUB_OUTPUT
      - uses: actions/setup-node@v3
        with:
          node-version: "20"
      - run: npm install @holiday-jp/holiday_jp
      - uses: actions/github-script@v3
        id: is_holiday
        with:
          script: |
            const holiday_jp = require(`${process.env.GITHUB_WORKSPACE}/node_modules/@holiday-jp/holiday_jp`)
            core.setOutput('holiday', holiday_jp.isHoliday(new Date()));
      # Release tag の付与
      - name: Create release tag
        id: create_release_tag
        if: |
          steps.get_latest_tag_and_commit.outputs.latest_tag_commit != steps.get_latest_tag_and_commit.outputs.latest_commit &&
          vars.DAILY_RELEASE == 'true' &&
          steps.is_holiday.outputs.holiday != 'true'
        env:
          # token を指定することで release 後の workflow が起動するようにする
          # デフォルトのリポジトリが所有するトークンでは他のワークフローがトリガーされないのは
          # GitHub Actionsの安全性のための仕様
          GH_TOKEN: ${{ secrets.GH_TOKEN }}
        run: |
          release_tag=`deployment/scripts/get_release_version.sh`
          latest_tag=$(gh release list -L 1 | awk '{print $3}')
          gh release create $release_tag --title $release_tag --target main --generate-notes --notes-start-tag $latest_tag
          echo released_version_url=`gh release view --json url -q .url` >> $GITHUB_OUTPUT
      - name: Create pull request
        if: vars.DAILY_MERGE == 'true'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        id: create_pull_request
        run: |
          release_tag=stg.`deployment/scripts/get_release_version.sh day "+%Y%m%d%H%M"`
          echo release_tag=$release_tag >> $GITHUB_OUTPUT
          latest_tag=$(gh release list -L 1 | awk '{print $3}')
          release_note=`gh api /repos/${{ github.repository }}/releases/generate-notes -f tag_name=dummy_tag -f target_commitish=develop -f previous_tag_name=${latest_tag} --jq .body | grep -Ev "dummy_tag$" | sed -e :a -e '/^\n*$/{$d;N;ba}'`
          # || true をいれて失敗しても stderr.txt に書き込みがされるようにしている
          gh pr create --title "【定期実行】 $release_tag" --body "${release_note}" --base main --head develop -l 'auto merge' > /tmp/stdout.txt 2> /tmp/stderr.txt || true
          echo pull_request_uri=`cat /tmp/stdout.txt` >> $GITHUB_OUTPUT
          rm -f /tmp/stdout.txt
        continue-on-error: true
      - name: merge remote/main into develop
        if: ${{ steps.create_pull_request.outputs.pull_request_uri != '' }}
        run: |
          git config --local user.email "github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          git checkout develop
          git merge main
      - name: push changes to remote repository
        if: ${{ steps.create_pull_request.outputs.pull_request_uri != '' }}
        uses: ad-m/github-push-action@master
        with:
          # bypass できるユーザで実行することが重要
          github_token: ${{ secrets.GH_TOKEN }}
          branch: develop
      - name: Merge pull request
        if: ${{ steps.create_pull_request.outputs.pull_request_uri != '' }}
        id: merge_pull_request
        env:
          # token を指定することで release 後の workflow が起動するようにする
          # デフォルトのリポジトリが所有するトークンでは他のワークフローがトリガーされないのは
          # GitHub Actionsの安全性のための仕様
          GH_TOKEN: ${{ secrets.GH_TOKEN }}
        # push が反映されるように5秒待ってからマージ
        run: |
          sleep 5s
          gh pr merge ${{ steps.create_pull_request.outputs.pull_request_uri }} --merge --subject "Merge to main for ${{ steps.create_pull_request.outputs.release_tag }}" --body "Merge to main for ${{ steps.create_pull_request.outputs.release_tag }}"
      - name: Get PR error
        if: ${{ steps.create_pull_request.outputs.pull_request_uri == '' }}
        id: create_pull_request_error
        run: |
          echo error=$(cat /tmp/stderr.txt) >> $GITHUB_OUTPUT
          rm -f /tmp/stdout.txt /tmp/stderr.txt
      - name: Send Slack Notification
        uses: 8398a7/action-slack@v3
        if: always()
        with:
          status: custom
          fields: job,took
          custom_payload: |
            {
              attachments: [
                {
                  color: '${{ job.status }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning',
                  text: `Github Daily Release result: ${{ job.status }} in ${process.env.AS_TOOK}`,
                },
                {
                  color: '${{ steps.create_release_tag.conclusion }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning',
                  text: `Release : ${{ steps.create_release_tag.conclusion }} ${{ steps.create_release_tag.outputs.released_version_url }}`,
                },
                {
                  color: '${{ steps.create_pull_request.outputs.pull_request_uri }}' === '' ? 'danger' : 'good',
                  text: `PR Creation : ${{ steps.create_pull_request.outputs.pull_request_uri }} ${{ steps.create_pull_request_error.outputs.error }}`,
                },
                {
                  color: '${{ steps.merge_pull_request.conclusion }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning',
                  text: `PR Merge : ${{ steps.merge_pull_request.conclusion }}`,
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ vars.SLACK_WEBHOOK_URL }}

解説

日次でワークフローが起動するようにする

GitHub Actions のトリガーを schedule を使ってワークフローを自動起動するようにしています。

on:
  # 平日 18:00 JST
  schedule:
    - cron: "0 9 * * 1-5"

main ブランチの HEAD にタグが付与されていなければ付与する

以下の手順でリリースタグを付与しています。

  1. main ブランチ HEAD のコミットハッシュと最新のリリースタグがふられているコミットハッシュを取得
  2. 上述の値が合致していない = リリースできるものがあると判断してリリースタグを付与
    • リリースタグの付与をトリガーに別のワークフローで prod にデプロイが走るようにしてあります
    • リリースタグの付与を secrets.GITHUB_TOKEN でやってしまうと別ワークフローの起動をトリガーできないので、個人のトークンを利用しています
    • リリースタグの計算はスクリプトを用意して自動計算するようにしています
# 最新のタグと最新のコミットを取得
- name: get latest tag and commit
  id: get_latest_tag_and_commit
  run: |
    echo latest_tag_commit=$(git rev-list --tags --max-count=1) >> $GITHUB_OUTPUT
    echo latest_commit=$(git rev-parse HEAD) >> $GITHUB_OUTPUT
# Release tag の付与
- name: Create release tag
  id: create_release_tag
  if: |
    steps.get_latest_tag_and_commit.outputs.latest_tag_commit != steps.get_latest_tag_and_commit.outputs.latest_commit
  env:
    # token を指定することで release 後の workflow が起動するようにする
    # デフォルトのリポジトリが所有するトークンでは他のワークフローがトリガーされないのは
    # GitHub Actionsの安全性のための仕様
    GH_TOKEN: ${{ secrets.GH_TOKEN }}
  run: |
    release_tag=`deployment/scripts/get_release_version.sh`
    latest_tag=$(gh release list -L 1 | awk '{print $3}')
    gh release create $release_tag --title $release_tag --target main --generate-notes --notes-start-tag $latest_tag

余談ではありますが、 .github/release.yml と PR への自動タグ付与ワークフローを利用して、それっぽいリリースノートを自動生成するようにもしています。

develop に差分があれば main へのマージを自動で行う

  1. develop ブランチから main ブランチへの Pull Request 作成
    • 作成時のタイトルはリリースのときにも使ったタグ計算ロジックを応用しています
    • Pull Request の description もリリースノートを自動算出するロジックを応用しています
  2. 作成がエラーにならなければ (基本的にはマージできる差分があるとき) マージする
    • main ブランチへのマージをトリガーに別のワークフローで stg にデプロイが走るようにしてあります
    • マージを secrets.GITHUB_TOKEN でやってしまうと別ワークフローの起動をトリガーできないので、個人のトークンを利用しています
- name: Create pull request
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  id: create_pull_request
  run: |
    release_tag=stg.`deployment/scripts/get_release_version.sh day "+%Y%m%d%H%M"`
    echo release_tag=$release_tag >> $GITHUB_OUTPUT
    latest_tag=$(gh release list -L 1 | awk '{print $3}')
    release_note=`gh api /repos/${{ github.repository }}/releases/generate-notes -f tag_name=dummy_tag -f target_commitish=develop -f previous_tag_name=${latest_tag} --jq .body | grep -Ev "dummy_tag$" | sed -e :a -e '/^\n*$/{$d;N;ba}'`
    # || true をいれて失敗しても stderr.txt に書き込みがされるようにしている
    gh pr create --title "【定期実行】 $release_tag" --body "${release_note}" --base main --head develop -l 'auto merge' > /tmp/stdout.txt 2> /tmp/stderr.txt || true
    echo pull_request_uri=`cat /tmp/stdout.txt` >> $GITHUB_OUTPUT
    rm -f /tmp/stdout.txt
  continue-on-error: true

...(中略)...

- name: Merge pull request
  if: ${{ steps.create_pull_request.outputs.pull_request_uri != '' }}
  id: merge_pull_request
  env:
    # token を指定することで release 後の workflow が起動するようにする
    # デフォルトのリポジトリが所有するトークンでは他のワークフローがトリガーされないのは
    # GitHub Actionsの安全性のための仕様
    GH_TOKEN: ${{ secrets.GH_TOKEN }}
  run: |
    gh pr merge ${{ steps.create_pull_request.outputs.pull_request_uri }} --merge --subject "Merge to main for ${{ steps.create_pull_request.outputs.release_tag }}" --body "Merge to main for ${{ steps.create_pull_request.outputs.release_tag }}"

これによって、こんな Pull Request の作成・マージまで自動で行っています。

細かな工夫点

main の内容を develop に自動で取り込む

main ブランチに直接行った hotfix やその他のコミットを develop に取り込むのも自動化しています。

そのために専用のステップを設けてマージ・プッシュをしています。

- name: merge remote/main into develop
  if: ${{ steps.create_pull_request.outputs.pull_request_uri != '' }}
  run: |
    git config --local user.email "github-actions[bot]@users.noreply.github.com"
    git config --local user.name "github-actions[bot]"
    git checkout develop
    git merge main
- name: push changes to remote repository
  if: ${{ steps.create_pull_request.outputs.pull_request_uri != '' }}
  uses: ad-m/github-push-action@master
  with:
    # bypass できるユーザで実行することが重要
    github_token: ${{ secrets.GH_TOKEN }}
    branch: develop

余談ではありますが、我々の運用では develop ブランチは基本的に Pull Request なしでコミットできない設定にしてあるので、予めそのルールを迂回できるユーザを設定して、そのユーザのトークンを利用してプッシュするようにしています。

祝日はリリースしないようにする

この運用のひとつの肝は、自動テストに頼りきらず(音声系の機能もあるため自動テスト一本に頼れるほど充実していないのが原因ですが)、stg 環境をチーム全体で丸一日にわたって通常利用したうえでリリースするところにあります。

なので、単純に月曜から金曜まで毎日実行してしまうと、祝日はまったく確認しないままリリースすることになってしまいます。

この問題に対応するため、以下のステップで祝日かどうかを判定しています。

- uses: actions/setup-node@v3
  with:
    node-version: "20"
- run: npm install @holiday-jp/holiday_jp
- uses: actions/github-script@v3
  id: is_holiday
  with:
    script: |
      const holiday_jp = require(`${process.env.GITHUB_WORKSPACE}/node_modules/@holiday-jp/holiday_jp`)
      core.setOutput('holiday', holiday_jp.isHoliday(new Date()));
# Release tag の付与
- name: Create release tag
  id: create_release_tag
  if: |
    steps.is_holiday.outputs.holiday != 'true'

自動リリース・自動 develop → main マージの制御

全部自動で動くのは基本とても良いのですが、リリースやマージを制御したいことも稀にあるかもしれません。

  • stg 環境で事前に確認していたら大きなバグ見つけちゃった
  • stg 環境での確認をもっと長めにしたい
  • 働いている人が少ない状態ではリリースしたくない (年末年始とか)

この要望に対して、ワークフロー自体を無効化してもいいのですが、各機能だけ個別に制御できるようにしています。(今のところ一度も活躍してないですが)

具体的には、 GitHub Actions 変数 を利用してこの値を変更するだけで制御可能にしています。

Slack にリリース結果を通知する

全部自動だと予期せぬエラーに気づけず大変です。

なので、以下のアクションがそれぞれうまくいったかどうかを Slack で通知するようにしています。

- name: Send Slack Notification
  uses: 8398a7/action-slack@v3
  if: always()
  with:
    status: custom
    fields: job,took
    custom_payload: |
      {
        attachments: [
          {
            color: '${{ job.status }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning',
            text: `Github Daily Release result: ${{ job.status }} in ${process.env.AS_TOOK}`,
          },
          {
            color: '${{ steps.create_release_tag.conclusion }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning',
            text: `Release : ${{ steps.create_release_tag.conclusion }} ${{ steps.create_release_tag.outputs.released_version_url }}`,
          },
          {
            color: '${{ steps.create_pull_request.outputs.pull_request_uri }}' === '' ? 'danger' : 'good',
            text: `PR Creation : ${{ steps.create_pull_request.outputs.pull_request_uri }} ${{ steps.create_pull_request_error.outputs.error }}`,
          },
          {
            color: '${{ steps.merge_pull_request.conclusion }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning',
            text: `PR Merge : ${{ steps.merge_pull_request.conclusion }}`,
          }
        ]
      }
  env:
    SLACK_WEBHOOK_URL: ${{ vars.SLACK_WEBHOOK_URL }}

これによって、こんな風に失敗した理由や作成されたリリースタグ・PR の情報を通知してくれます。

stg 環境に変更内容を通知する

上述のワークフロー外でやっているので詳しく説明できないですが、我々が開発・普段利用している NeWork の機能のひとつであるワークスペース全体へのメッセージ機能を利用して変更内容を通知しています。 ここでも GitHub のリリースノート作成機能を利用して通知内容を作成しています。

これによって NeWork チームのメンバーが今日この後リリースされる内容を把握し、必要に応じて重点的な確認やバグ出しをしてくれることを狙って実施しています。

その他の考慮

上記の運用への変更を検討時、以下のような点についても考慮したうえで実行にうつしました。

上司への事前説明の省略

それまでは、週に 1 回のリリースの前に内容の説明と実際のチケットや PR のリンクを共有して上司の許可を得た上でリリースという手順を踏んでいました。

頻度があがるとこれは難しくなったのですが、上司に相談したところ、運用が改善するならぜひやっていこうとのお言葉をいただき、内容の共有は週に一度かつ事後で良い(=リリースの判断に関して権限委譲して頂いた?)となりました。

スプリントレビュー前のリリース

NeWork は開発をスクラムで進めていました。それまでは、リリース日前日にスプリントレビューがあったので、その場でリリース可否を決めていました。 1

リリース頻度があがることでこれまでと同様のメンバーのレビューがはいったうえでリリース可否を決めるという運用はできなくなりました。

ただ、スプリントプランニングにて実装する内容は事前に合意していること、リリース前に一日 stg 環境で確認ができること、リリース後に問題があってもすぐ直す or 切り戻しすればいいという意識あわせをチーム全体で行い、このフロー自体をなくしました。

また、そもそもスプリントレビューしないでリリースしていいのという疑問もチーム内であがりましたが、NTT Com の技術顧問でもある吉羽さんの情報も参考に、そのような縛りがないことを確認したうえですすめました。

www.ryuzee.com

リリースノート

NeWork Web 版は リリースノート を公開しています。

それまではリリース毎にプロモーションチームがユーザに伝わりやすい言葉でリリースノートを作り、リリースと同時に公開していました。

これもリリース頻度があがることで同じ運用を維持することが難しいのは明らかでした。

これについては、リリースノートを後追いで出す運用にすることで対処しました。

一方でプロモーションの理由でリリースノートや サービスサイトご利用ガイド 等とタイミングを揃えたりものについては開発のロードマップレベルであわせてリリースすることにしています。

昨年から feature flag も活用して、重要な機能のロールアウトとデプロイを分離できているのも一役買っています。

品質面

これまでは長いと一週間弱 stg 環境に将来リリースする機能が先行してデプロイされており、その中でバグが見つかりリリースまでの直して対処したこともありました。

リリース頻度があがることでこの確認期間が短くなり、品質が悪化するのではとの声もありましたが、逆に駆け込み乗車を撲滅し、必ず確認期間が一定以上設けられるようになること・確認すべき対象が常にアナウンスされることからほぼ影響がないと判断しました。

リリース頻度を変えてみて

2023 年の 9 月からこの運用をしています。

元々課題に感じていた点・懸念していた点・それ以外の影響について振り返ってみます。

  • 課題 1 : ユーザからのフィードバックや軽微なバグに即応できない → 最短で翌日にはリリースできるようになって対応速度は確実に向上しました。
  • 課題 2 : リリース直前のバタつき → リリース頻度があがることで無理やりリリースにねじ込むことがほぼ 0 になりました。
  • 課題 3 : スクラムチームのスケジュールの縛り → これについては、スケジュール変更がちょっと面倒なこともあり、まだ実行に移せていません。次回チーム構成を変更する際に改めて意識していきたいと考えています。
  • 課題 4 : リリース作業の一部が手動 → 完全に自動化され快適になりました。
  • 考慮 1 : 品質面の劣化 → 今のところ以前の運用であれば防げたはずのバグの流出は一切なく、むしろ駆け込み乗車撲滅の恩恵が大きいと感じています。現在は並行して E2E テストの充実化も進めているので、この懸念はさらに小さくなっていっています。
  • 副産物 : チームの残業時間が他チームと比較して目立って減りました。(この施策だけの影響ではないかもしれませんが、) リリース作業の完全自動化の恩恵と思っています。

基本的に良い影響しか出ていないので、今後も継続していこうと思っています。

おわりに

この記事では、リリース頻度を毎週から毎日単位に変更した事例について紹介させていただきました。

ただ、毎日リリースできればゴールではないので、引き続き自動テストの範囲拡充・信頼性向上とそれに伴うリリース頻度の向上をさらに検討できればと思っています。(そもそもリリース頻度がすべてではないですが)

NeWork はどなたでも無料でお試しできますので、もしプロダクトや使われている技術に興味を持っていただけたらぜひ触ってみてください。

また、2024 年 1 月現在、NeWork では一緒に開発を進めてくれる仲間を募集しています。詳細は以下のリンクをご覧ください。皆さまのご応募を心からお待ちしています!

hrmos.co


  1. スクラムガイド にも「スプリントレビューのことを価値をリリースするための関門とみなすべきではない」とある通り、いわゆるアンチパターンです。
© NTT Communications Corporation All Rights Reserved.