みなさんこんにちは、電通国際情報サービス(ISID)Xイノベーション本部ソフトウェアデザインセンターの佐藤太一です。
この記事では、Git を使った仕事のやり方(以降は Git ワークフローと記載)を設計する上での検討事項を説明します。 これによって、読者の皆さんがGitワークフローを適切に定義できるようになることを主たる目的としています。
また、筆者の能力不足によって記載しきれなかった考慮事項について、より深く Git を使いこなしている識者からの指摘を受ける機会を得ることを副次的な目的とします。 この記事には書かれていないものの、検討すべき事項について知見のある方はブログ記事を書いたり、Twitter等のSNSで指摘してくださるとありがたいです。
はじめに
この記事は、Git や GitHub の基本的な使い方を読者が理解していることを前提に書いています。 つまり、Git サーバとしての GitHub だけでなく、その機能を十分に使うことを想定しています。
もし基本的な使い方に不安があるなら、まずは以下のサイトや書籍で学習してください。
- Git でのバージョン コントロールの概要
- 半日程度でgitについて把握できるMicrosoftの教育コンテンツです。
- サル先生の Git 入門
- Git の初歩の初歩を図解しながら学習できるサイトです
- 改訂 2 版 わかばちゃんと学ぶ Git 使い方入門
- 漫画で Git を学習するならこの本がおすすめです
- 新しい領域として Git を学ぶならとっつき易さで選ぶのがいいと個人的には考えています
- いちばんやさしい Git&GitHub の教本 人気講師が教えるバージョン管理&共有入門
- Git と GitHub をまとめて学習するならこの本がおすすめです
基本的な考え方
Gitのワークフローを決めるのは仕事の質を上げるためです。失敗の可能性を低減し、また失敗したとしても労せずやり直せることで効率よく働けるようにしましょう。
Gitのワークフローを検討する際、常に意識してほしいのは作りこみ過ぎないということです。 まず、起きてもいない問題のすべてを事前に対応しようとしないでください。また、プロジェクトメンバーがワークフローの習熟に使う時間をできるかぎり減らしましょう。 プロジェクトメンバーが複雑すぎるワークフローに振り回されると仕事の生産性は確実に低減し、その結果として仕事の質が落ちていきます。
Gitのワークフローは自分のプロジェクトにおいて問題が起きてもやり直せるギリギリまで軽量化しましょう。
Git ワークフロー設計における考慮事項
この記事の主題となるGitワークフロー設計における考慮事項を説明します。
ここでは、Gitを中心にした仕事のやり方全体を指してGit ワークフローと呼んでいます。
少し抽象的な話が続きますので、Gitワークフローについて考えたことがないなら、記事の後半にある Git ワークフローの実際を読んでから戻ってくると少しわかり易くなるかもしれません。
チームの人数
仕事のやり方を考えるのですから、まずチームがどのようなメンバーで構成されているのか検討しましょう。 例えば、2,3人のチームでは労せずお互いのやっていることを正確に把握できるので、複雑なワークフローは必要ありません。ミスはするものとしてお互い補いあう想定で働くと効率がいいです。 こういった少人数であれば、最低限のケアレスミスを防ぐためだけにワークフローを定義しましょう。
メンバーが10人を超えたあたりで、お互いの仕事内容を正確に把握しあうのは難しくなります。 これくらいの人数から、Gitワークフローを検討する上でメンバーの入れ替わり頻度について考慮しましょう。 数年間固定されていて気心の知れたメンバー10人と、この3か月で集まった10人では採用すべきワークフローに多少の違いはでるでしょう。 メンバーが固定されているチームでは少人数のチームと同じようなGitワークフローで十分です。変に手順を難しくするのはやめましょう。 メンバーが流動的なチームでは作業記録を残しやすい形でワークフローを定義すると、問題が発生した際に対処し易くなります。
メンバーが30人を超えると大抵のプロジェクトでお互いの仕事内容を正確に把握しあうのは不可能です。 チームを2つか3つに分割した上で役割と責任範囲を明確にした作業分担が必要になるでしょう。 メンバーは流動的で少なくとも四半期に一回程度は誰かが抜けて、誰かが増えます。 つまり、ワークフローに習熟していないメンバーがいつでも一定数存在することを前提にワークフロー操作が失敗しても他のチームにその影響が派生しないことを重視しましょう。 こういう状況ではチーム同士の成果を結合するワークフローはどうしても複雑なものになりがちです。
monorepoの検討
Gitでは小さい単位の開発成果物ごとにリポジトリを作る方が大抵の場合うまくいきます。 例えば、サーバアプリケーションとクライアントアプリケーション、デプロイスクリプト、マニュアル類、設計ドキュメントなどをそれぞれを別なリポジトリとして管理するとよいでしょう。 成果物の種類毎にリポジトリが分かれていれば、それらを更新するメンバーの役割と責任範囲が自然と明確で分かり易くなります。
例えば、クライアントアプリケーションの開発メンバーには、サーバアプリケーションのリポジトリに対して読み取り専用の権限を付与することを考えてみましょう。まず、コードの読むことはできるので問題発生時に分析はできます。 一方で、クライアントアプリケーションの修正のつもりでサーバアプリケーションに修正を加えてしまうといったミスは発生しなくなります。
一方で、リポジトリが分かれていると問題になるケースもあります。 考えてみてください。.protoやJSON Schemaのように、サーバアプリケーションとクライアントアプリケーションが通信するためのプロトコルを定義するIDLは、どちらのリポジトリに置くのがいいでしょうか?それとも、通信プロトコルの定義は設計ドキュメントとセットで新しいリポジトリを作るべきでしょうか?また、クライアントアプリケーションとしてモバイルアプリケーションを実装することにしました、AndroidとiOSではリポジトリを分けるべきでしょうか? このように何かあるたびにリポジトリを増やしていくと、リポジトリの増加は止まりません。 リポジトリが増えると手元の開発環境を適切な状態に維持するための手順は煩雑になります。また、CIやCDなどのビルドプロセスも複雑になります。
これに対して、開発成果物を全て単一のリポジトリ内に収めてしまうのがmonorepoです。 単一のリポジトリ内にあらゆる成果物を入れて管理すれば、メンバーの新規追加時における難しさは大きく低減します。 monorepoを採用することによって、まず新しいメンバーが既存の成果物を探し回るために必要なコストが低減します。 そして、ドキュメントやソースコードの間で相対パスを使ったリンクを張れます。種類の成果物同士で情報の連携が取り易いことは、仕様に対する理解の速度を早めます。 Googleのように依存ライブラリも含めて全てmonorepoに入れてしまうような組織もあります。
対応を検討すべきデメリットはいくつかあります。
- ファイル数やコミット数が多くなるので、単純にリポジトリが大きくなる
- タグの対象範囲が分かりづらくなり易い
- タグの命名規則をしっかり考える必要がある
- メンバーが編集できる成果物の範囲を限定するのが難しい
参考文献
- Git で monorepo を扱う際の課題とヒント
- Why Google Stores Billions of Lines of Code in a Single Repository
- monorepo.tools
プロジェクト管理ツールとの連携
ソフトウェア開発をするためにGitを使うのですから、プロジェクト管理ツールとGitワークフローは密接に関連します。 GitHubにはタスク管理機能が備わっていますが、ある一定以上の規模で利用しようとするには機能不足です。 ボトムアップな管理をするにも、例えばタスクの分割と集約の機能がなかったり、チケットの検索クエリが貧弱だったりします。 トップダウンな管理という意味では、例えばタスクの進捗状況を俯瞰するのは非常に難しいですし、承認フローを構築したりWBSを引いたりといったことも出来ません。
ステークホルダーとなる組織が複数あり、プロジェクトとの関係性において一定の複雑さがあるならプロジェクト管理ツールを導入すべきです。 理想的には、ソフトウェア開発系のタスクとそれ以外のタスクにおける情報連携を自動的に実施するのが望ましいでしょう。つまり、プロジェクト管理ツールに定義したワークフローとGitを使ったワークフローを上手く連携させるのが望ましいというわけです。
基本的にはプロジェクト管理ツール側にGitHub上のPull Requestやブランチの情報を取込んでいく形になります。 例えば、広く使われているプロジェクト管理ツールであるJiraにはスマートコミットという仕組みがあり、コミットメッセージを所定の書式で記述するとJiraのIssueが持つ状態を操作できます。
プロジェクト管理ツール側がWebhookに対応しているならPull Requestから情報を送信できます。 また、GitHub Actionsを使うならPull Requestでなくても情報をプロジェクト管理ツールに送信できます。
Pull Request
Gitはそれぞれの開発者が持っているリポジトリは完全に独立しており対等なものです。 SVNやCVSといった構成管理ツールを使う場合、開発者のローカルにチェックアウトされた成果物は作業用のコピーであることとは対照的です。
一方で、内部統制がしっかりと効いた開発を行うなら、プロジェクトの進捗と厳密に同期していると言えるリポジトリを一つ決めます。 その中心となるリポジトリをGitHub上に置くと使える仕組みがPull Request(PR)です。他のツールではMerge Requestと呼ばれることもあります。 PRを使うと、コードレビューとそれにひもづく定型作業の自動化(CI)をサーバ上で実行できます。
GitHubを使うプロジェクトでは、Gitワークフローを設計するならPRをどうやって使うかが議論の中心になるでしょう。
もしPRを使うのであれば、Issue Templateを用意するとPR自体の質を底上げできます。
ブランチ名
Gitを利用する際にブランチ名を工夫すると、CIで作業を自動化し易くなります。 ワークフローの設計次第ではもう少しルールを追加しますが、基本的にはこれだけです。
- 作業内容を想像し易い単語をブランチ名に含める
- ブランチ名は基本的に半角英数だけを使う
- 意味の区切りは
/
を使う
区切り文字として /
を使うのは、GitKrakenを始めとしたGUIクライアントが /
区切りでブランチを階層表示してくれるからです。
この画像では dev/my-task
、dev/your-task
に加えて折りたたまれた test/awesome
ブランチがあります。
頻出する定型業務の自動化(CI/CD サービスとの統合)
Gitのワークフローを作りこむ理由は、定型業務を自動化してより効率よく働くためです。 どんな作業をCIの中に組込みたいのかをしっかりと検討することはワークフロー設計をする上で大切です。
ここでは、参考のためにCIへ組み込むと便利な定型業務を列挙しますが、最初から全てに取り組もうとしないでください。 ご自分のプロジェクトにおいて明らかに恩恵があると思われるものを少量取り入れるのがおすすめです。 PRに伴って動作するCIワークフローのパフォーマンスが悪いと開発効率は確実に低減しますので、あれもこれもと盛り込み過ぎないようにしましょう。
また、筆者が知らなかったり、列挙できなかった定型業務は非常に多岐にわたることが予想されます。 読者の皆さんがご自分のプロジェクトで定型化している業務をSNSやブログで共有して下さることを期待しています。
ビルド
CIに組み込む作業として一番最初に思い浮かぶのがビルドです。 ここでいうビルドは、ソースコードから実行可能な状態に変換する過程で発生する定型的な業務全般を指しています。
静的解析
コンパイル型のプログラミング言語ならコンパイラがある程度の静的解析に基づくエラーを出力してくれます。 大抵のプログラミング言語には、Lintと呼ばれるタイプのソースコードの妥当性を検証するツールがあります。
特に筆者が好きなのは、ベストプラクティスに基づいたコードへ矯正するタイプの静的解析ツールです。
使うことが明確に非推奨というわけではないけども、新しいコードではそのような書き方をしない方がよいという知識を複数のプログラミング言語にわたって学習し続けるのは非常に難しいです。
例えば、ReactのuseEffectに[]
を指定すべきケースは限定的で基本的に何か値を設定したほうがいいとか。
goは1.13でerrorの望ましい書き方が変わったとか。
アプリケーションとしては動作する以上、望ましくないスタイルのコードを書いていても気が付きませんよね。しかし、望ましくないスタイルのコードはソフトウェアの品質を確実に低減させます。
こういったことを学習するきっかけをえるためにLintツールはPRからトリガーされるタスクに組み込むのが望ましいでしょう。
ソースコードの自動生成
ソースコードの自動生成は、それを実施すべきかどうかを判断するのが難しいタスクです。 ジェネリックスやテンプレートなど言語の機能を利用することで、継続的なソースコードの自動生成は不要にできる場合もあります。 また、自動生成したソースコードをGitのような構成管理ツールにチェックインするかどうかも注意深く検討してください。
自動生成したソースコードをチェックインする場合には、リポジトリのサイズが大きくなり易い代わりに、CIの実行時間を抑えられます。 一方で、自動生成したソースコードをチェックインしない場合には、リポジトリのサイズを小さく保てる代わりにCIの実行時間が長くなります。
筆者としては、ソースコードの自動生成は開発者のローカルマシンで実行しPRを出してリポジトリにチェックインする方式を好んで使っています。それは、基本的には望ましくないことだと分かっていても、生成物に対して手を入れたいという要求を完全に消すことはできないからです。
単体テスト
データベースやアプリケーションサーバを使わない単体テストは、PRからトリガーされるタスクとして実行したいタスクです。開発者が実施するテストのあり方を学びたい人には和田さんのスライドをおすすめしておきます。
QuickCheckのようなパラメタライズドテストや、コードカバレッジの計測は実行コストが高いのでそれほど頻繁に行わなくてもいいかもしれません。そういう高コストなテストは、Gitワークフローからは切り離して夜中や早朝に定期実行するようなアプローチをおすすめします。
結合テスト
ここではデータベースやアプリケーションサーバ、リバースプロキシなどを構成した上で実施するテストを結合テストとします。 テストに時間がかかりすぎるとか、テストが終わらなくてタイムアウトするとか言った問題はスローテスト問題と呼ばれています。 長期間にわたって価値を生み出すソフトウェアを実装していくにあたって、スローテスト問題は避けられません。 スローテスト問題に対する基本的なアプローチは、品質を担保できるギリギリの頻度で実行し、テストの実行コストと品質のトレードオフにおける妥当な割合を見つけることです。
機械学習によってテストの実行対象を賢く選択してくれるLaunchableは、この問題に対する画期的な解決策を提供してくれるかもしれないと期待しています。
筆者の個人的な好みは、ナイトリービルドのような形で一日に数回は結合テストを実行するものの、日常的なPRトリガーのワークフローからは外すことです。
ドキュメントの自動生成
Javadocのようなドキュメントコメントから開発者向けドキュメントを自動生成するタスクもまたビルドタスクの一部としてよく組込みます。
フレームワークやライブラリのように繰り返し再利用されるものを実装する際にはドキュメントコメントをしっかり書くのが望ましいでしょう。一方で、アプリケーションコードのドキュメントコメントは必要最小限に留めておくのがおすすめです。 筆者の経験上、コメント部分は動作しないためかコードと整合性をもってメンテナンスされづらく、その結果コメントとコードの内容にズレがおきることでコードを理解する妨げになる事の方が多いからです。
ドキュメントの自動生成は多くても一日に一回程度、大抵の場合は一週間に一回くらいの頻度で実施すれば事足りるので、PRから実行されるCIタスクには組み込まないことが多いです。 代わりに、クーロンジョブのような形で定期的に実行してPRを自動生成する方が低コストです。
今のGitHub Pagesにはアクセス制御機能があり、許可された開発者だけが閲覧できるサイトを簡単に構築できることは知っておいてください。
パッケージング
パッケージングは、Dockerコンテナイメージや、jarのような形式でアーカイブされたファイルを作ることです。 単にアーカイブすればいいものから署名が必要なものまで、言語やデプロイ先によって実行すべきタスクは様々です。
ただ、大きな傾向としてパッケージングはリリース作業の一部として実行するタスクです。
パッケージングとして実施すべきタスクの内容は極めて複雑で多岐にわたるため、今回はそういうものがあるとだけ言っておきます。
依存ライブラリのアップデート
現代的なソフトウェア開発においてオープンソースソフトウェア(OSS)の利用は避けられません。 そして、OSSのライブラリは高頻度にアップデートされます。特に緊急性の高いセキュリティパッチと呼ばれるようなアップデートは実施しなければなりません。
とはいえ、定期的な依存ライブラリのアップデートを手動でやっていると時間がどれだけあっても不足します。つまり、自動化するしかありません。そのためには、比較的高品質な回帰テストが必要になります。
依存ライブラリのアップデートを自動的にやってくれるサービスとしては、Renovateや、GitHubに標準で組み込まれているDependabotを使うのが良いでしょう。これらのサービスを利用するとライブラリをアップデートするPRを自動的に作ってくれます。
自動的に作られるPRをGitワークフローの中でどういう位置づけにするのかは、丁寧に検討してください。深く考えずに導入すると、無限に生成されるPRに押しつぶされてしまいますよ。
これは、筆者がメンテナンスしている社内ツールの回帰テストが不安定なせいで自動マージが上手く動作せず蓄積してしまったPRの山です。
システムテスト
Gitのワークフローを検討する上で、システムテストの位置づけは整理するのが望ましいでしょう。 まずは、ワークフローの一部にシステムテストを組み込むのかどうかを検討してください。 システムテストの自動化は極めて深遠な話題ですので、大抵のプロジェクトでは組み込まないという結論になるでしょう。
ソフトウェアが価値を生み長生きするならば、規模は大きくなり複雑になっていきます。 その中でシステムテストの自動化は取り組むべき課題の一つです。 Gitワークフローの一部としてシステムテストを実行すべき状況になっていないかを、ときどき気にかけてください。
アプリケーション運用との統合
Gitワークフローとアプリケーション運用の統合については、パッケージングの話題でありデプロイの話題です。 少なくとも、ワークフローの終了ステータスをどのような形で、いつ誰に通知するのかは検討してください。
伝統的にはショートメッセージサービス(SMS)や電子メールを使っていますよね。 現代的にはSlackやTeams、LINEなどチャットツールへのメッセージ送信となるでしょう。
また、アプリケーション運用側からGitに対する情報提供をどのように実施するのかを検討するのも忘れないでください。 基本的にはプロジェクト管理ツールに対するタスクの起票(GitHub Issuesなど)を介して行うことになるはずです。
Git ワークフローの実際
ここからは、ワークフロー設計の参考になるようなGitワークフローをいくつか紹介します。 ただし、具体的な作業手順については説明していません。参考文献に詳細な説明のあるページをリンクしておきましたので参考にしてください。
ここで紹介しているものは、あくまでも基本的な型であって、これをそのまま導入すべきものではありません。 とはいえ、一定の複雑さがあるワークフローを導入する際、最初のうちは基本的な型のまま何も変えずに導入することがGitワークフローのあり方をチームとして理解する助けになるでしょう。
この説明では、デフォルトのブランチをmainブランチと記載しています。 以前は、GitHubにおけるデフォルトのブランチ名はmasterでしたが、世相を反映して現在はmainをデフォルトのブランチ名とすることが推奨されています。
シングルブランチモデル
シングルブランチモデルは、開発者はサーバのリポジトリをcloneしたらmainブランチに変更作業をコミットしていき、作業が終わり次第mainブランチをサーバにpushする方式です。
これは、SVNからGitへソースコード管理ツールを移行するチームが一時的に使う方式として検討する余地があります。 単にSVNをGitで代替し、基本的なコマンドの使い方や動作を理解する期間を設けることでツールの移行に伴う生産性の低下を緩やかにできます。
一方でシングルブランチモデルはソフトウェアの設計や、作業分担次第ではあるものの非常にコンフリクトが発生し易い使い方です。 push前に必ずrebaseしてローカルディレクトリ内でコンフリクトを解決する。もしくは、同じファイルを同時に複数の人が修正しないよう作業範囲を調整しなければなりません。 つまり、この方式を採用してプロジェクト運営を行うならプロジェクトマネージャーが、仕様の変更や追加に伴うファイルの変更範囲を厳密に理解している必要があります。
チームが2,3人で担当範囲が明確なら、この方式でGitを使い始めるのがいいでしょう。 コンフリクトさえ発生しなければ、覚えなければならないGit固有の知識が最小限で済みます。
参考文献
GitHub Flow
GitHub Flowでは、サーバのリポジトリをcloneしたらmainブランチから作業用のフィーチャブランチへswitchします。 フィーチャブランチでの作業が終わったら、それをサーバにpushしPRを使ってmainブランチにマージします。 この方式では、gitの3-way mergeが上手く働くのでシングルブランチモデルよりも断然コンフリクトが発生し辛くなります。
原義的なGitHub Flowではmainブランチにマージされた変更は本番環境を含む何らかの実行環境にデプロイすることが重要なのですが、そこまでやらなくても十分に利便性を享受できます。
参考文献
GitLab Flow
ブランチの管理とデプロイをより丁寧に対応付ける形のワークフローがGitLab Flowです。 開発者がPRを使ってmainブランチに変更をマージしていくことはGitHub Flowと同じです。
GitLab Flowでは、デプロイ作業を実施するブランチを専用に設けることで、開発におけるPRのマージとデプロイの周期やタイミングを分離します。 mainブランチにマージされたアプリケーションがテスト環境にデプロイされ、そこで問題が見つからなければ本番環境にデプロイされるというようなワークフローを想定すると、このような形になるでしょう。
大規模な開発では、テスト環境が複数用意されているものです。 テストの目的や、担当者の責任範囲によって環境が分離されているようなら環境に対応したブランチを用意した上で、それらのブランチへのマージをトリガーにデプロイを実行するでしょう。
パッケージソフトウェアのビジネスでは、リリース済みで新機能の追加がないとしても、セキュリティの問題や深刻なバグへの対応を実施するためパッチを適用して保守していくことが望まれます。 このような状況にGitLab Flowの考え方を適用すると、それぞれのリリースに応じてブランチを作り、mainブランチから必要な変更だけを取り出してマージしていく(チェリーピック)ことになります。
このバージョンブランチを伸ばしていく保守方針は変更コストが高くなり易いので、なるべく選ばない方がいいでしょう。 しかし、これはソフトウェアの収益モデルと直接関連するので、ビジネスオーナーと開発者が丁寧な議論を実施すべき話題です。
参考文献
Gitflow (A successful Git branching model)
Gitflowはここまで説明してきたワークフローの集大成となるものです。
開発者は、サーバのリポジトリをcloneしたらdevlopブランチから作業用のフィーチャブランチへswitchします。 フィーチャブランチでの作業が終わったら、それをサーバにpushしPRを使ってdevelopブランチにマージします。 必要な変更作業が終わったら、リリースの担当者がdevelopブランチから新しいreleaseブランチをswitchし、アプリケーションのリリースに伴う作業を実施した成果をコミットします。ここでコミットされるものは、例えば、軽微な作業ミスの修正やバグの対応、ドキュメントの修正などです。 そういった作業が全て終了したら、releaseブランチをサーバにpushしPRを使ってmainブランチにマージしてタグを打ちます。 そうです、Gitflowにおけるmainブランチはアプリケーションの静止点を記録するためだけのブランチなのです。
ただし、緊急対応を行うホットフィックスブランチだけは例外です。 緊急対応を行う開発者は、mainブランチから作業用のホットフィックスブランチへswitchします。 作業が終わったらホットフィックスブランチをサーバにpushしPRを使ってdevelopブランチとmainブランチの両方へマージします。 その上で、mainブランチにタグを打ちアプリケーションをリリースします。
Gitflowでは非常に複雑な状況が想定されています。またそれぞれの開発者がGitに対してかなり習熟していることが前提のワークフローです。 中身を詳細に理解した上で、全ての要素が必要であると判断できるなら、リリースまでの作業を何度か実施してみてください。それでもなおプロジェクトの運営方針と合致していると確信できる場合のみGitflowを採用してください。
参考文献
まとめ
筆者は、Gitワークフローの設計で最も重要なのは作りこみ過ぎないことだと考えています。 そのために、Gitワークフローにおける検討事項を適切に把握しましょう。 みなさんのプロジェクトにおいて対応すべき課題を丁寧に把握した上でプロジェクトメンバーの生産性を最大限に発揮できるワークフローを設計してください。
具体的にどのワークフローを使えばいいか分からないなら、まずはGitHub Flowを試してみてください。 GitHub Actionsを使って簡単なビルドを行いつつ、コードレビューをするとGitを使うことの利便性を享受できます。
この記事によって、読者の皆さんがご自分のチーム内におけるGitワークフローを改善するきっかけになれば、これ以上に嬉しいことはありません。 非常に長いエントリでしたが、最後までお読みいただきありがとうございます。
執筆:@sato.taichi、レビュー:@yamashita.tsuyoshi (Shodoで執筆されました)