YAMAGUCHI::weblog

噛み付き地蔵に憧れて、この神の世界にやってきました。マドンナみたいな男の子、コッペです。

Goのリリースプロセスとブランチ戦略

はじめに

こんにちは!Google Cloudでオブザーバビリティの担当をしているものです。CVE-2021-44228のおかげでバタバタしていますがみなさんはお元気ですか?

このエントリーはpyspa Advent Calendar 2021の15日目の記事です。昨日は @moriyoshit さんの「Goのロギングライブラリ 2021年冬」でした。めちゃめちゃ調べてあって良い記事でした。Goでログライブラリの選定をする際にはこちらをまず読むと良さそうです。

2021.12.21 追記: 穴が空いていたのでGo Advent Calendar 2021 その1の14日目の記事にもしました。

さて、今日は本当は「Goならわかる確定申告第三表」という記事を書こうと思ったのですが、まだ確定申告の時期ではないのでそれは辞めにします。そのかわり、今日はGo 1.18がめでたくベータ版リリースとなったので、Goのリリースプロセスとブランチ戦略の紹介をしようと思います。

TL;DR

Goは2月と8月にメジャーリリースを行います。リリースに際してはメインブランチから各バージョン用リリースブランチをフォークしてリリースします。概ねGoのレポジトリのWikiに説明が書いてあるので興味があったらこれを読んでください。

リリース

新規バージョンのリリーススケジュール

リリーススケジュールに関してはGoのGitHubレポジトリのWikiに詳細に書いてあるので、これを読めばすべてわかります。

github.com

英語を読むのが面倒という方向けに簡単に要約をするとこうなります。(この図を見ながら読んでください。図は上のWikiから引用。)

https://github.com/golang/go/wiki/images/release-cycle.png

  • メジャーリリース*1は半年ごとのサイクルで行われ、毎年2月と8月にリリースされる
  • リリースサイクルは3ヶ月ごとに前半(緑の部分)と後半(青の部分)に分けられ、前半は新規機能開発含めたすべての開発、後半はバグ修正とドキュメント更新のみとなる
  • リリースサイクル後半の2ヶ月めでベータ版(Beta)、3ヶ月めでリリース候補版(Release Candidate, RC)がリリースされる
    • ベータ版は早めに実環境で利用してもらい、バグの発見とその修正を行う意図でのリリース
    • リリース候補版はほぼリリース版と機能的には同様で大きなバグはないという想定で、リリースまではドキュメント修正が主
    • リリース候補版で修正が入る場合は相当致命的なバグがあった場合のみ
    • ベータ版もリリース候補版も多く版を重ねないように多くても2週間に1度しか出さないようにする
  • リリース候補版で無事に致命的なバグがないと確認できたら正式リリース版をリリース

こういう背景を知っていると、たとえばGo 1.11でのベータ版がbeta 3まで行ったときに「今回はバグが多いな」といったニュアンスが読み取れます。

既存バージョンのメンテナンス

基本的にリリース版は「新規機能とその追加によって起きたバグの修正がなされた」と判断された状態でリリースされるので、本来であれば修正が必要ないはずです。しかしながらバグがないソフトウェアというのは通常はありえないので、だいたい正式リリース後にバグが発見されます。

このリリース後のバグの修正、というのは各正式リリース版のブランチ(release-branch.go1.x)で行われるわけではなく、メインブランチ(=次バージョン用の開発ブランチ)で行われた修正をcherry pickで各ブランチにバックポートする形で行われます。この対応が行われるのは最新2バージョンのみです。(例えばいま1.17がリリースされているので、バックポートは1.17と1.16に対して行われる。)

おおよそマイナーリリースは1ヶ月ごとに行われるので、パッチバージョンは5前後まで行くのが目安だということがわかります。(ただし後述のとおりセキュリテイ修正リリースが行われる場合はその限りではありません。)

またパフォーマンスや挙動ではなく、セキュリティに関する修正の場合はポリシーが異なります。セキュリティ関連のバグは3種類に分けられていて

  • PUBLIC: 影響が大きくない、あるいはすでに知られているバグで、修正は公開した状態で行われる
  • PRIVATE: セキュリテイ観点で問題があると思われるもので隠したほうがいいと判断したもの。修正は非公開で行われ、その内容はリリースの3日〜7日前にアナウンスされる。
  • URGENT: ゼロデイなどGoのエコシステム全体に影響を及ぼすような深刻なバグ。修正は非公開で行われる。

PUBLICとPRIVATEに分類されたものは他のセキュリティ関連ではないバグの修正とまとめられてマイナーリリースで行われます。URGENTに分類されたものは、その修正のみのマイナーリリースが行われます。以前はセキュリティ修正リリースは分類に関係なく単体でリリースされていたのですが、今年の3月に提出された提案によってプロセスが改められました。(過去のリリースを見るとパッチバージョンの番号がすごく多いのはセキュリテイ修正が多かったことによるものだとわかります。)

詳細はこちらを参照してください。 go.dev

修正と新規機能の作成

さてリリーススケジュールとその中身がわかったところで、次は開発による変更がどのように行われているか、というところです。開発は大きく分けて2つの種類があります。一つは機能改善やバグの修正、もう一つは新規機能開発です。

修正

セキュリティ関連でない修正はまず問題をIssue Trackerに登録します。

github.com

Issue Trackerに登録するとトリアージが行われます。トリアージが行われた際には次のどれかのラベルが付与されることになります。

  • NeedsInvestigation: 問題の原因がよくわからないので原因究明のための調査が必要
  • NeedsDecision: 問題の原因はわかっているもののどう対応するか判断しかねているもの
  • NeedsFix: 問題の原因はわかっていて、対応方法もわかっているもの

NeedsFixにラベルされたものはいつでもコードを書いて修正して良いもので、基本的には開発のメインブランチに送られているパッチはこのラベルがついた問題に対するものです。

パッチを提案するためにはコントリビューションのワークフローに則らなければいけません。現在は大きく分けて2つの方法でパッチを送ることができます。一つはGitHub経由、もう一つはGerrit経由です。*2

GitHub経由でパッチを送る場合はいわゆるGitHub Flowに沿って行います。golang/goをforkしてブランチを切ってPull Requestを作成です。コードレビュー自体はGerrit上で行われ、そこでのコメントがGitHubのPull Requestにミラーされます。

Gerrit経由でパッチを送る場合はChange (Change List) というものを作成します。Gerritの解説をするとそれだけで終わってしまうので、ここではGitHubでのPull Requestに相当する単位だと理解してください。

どちらもコードレビューが終わり修正が承認されると1コミットとしてメインブランチにpushされます。

新規機能

一方で1.18に入ったGenericsのように、言語の機能自体の大きな変更はProposalが必要になります。まずは簡単に提案の概要をIssue Trackerで起票します。起票するとGitHubのプロジェクトで管理されていきます。

まずはIncomingとなり、Goチームのレビューが始まるのを待ちます。そして週次のProposalレビューの対象になると、ラベルがActiveに変更になります。(どれが取り上げられたかはGoチームの議事録に記録されます)レビューの対象の提案はIssue Tracker上やgolang-devのメーリングリストで議論が行われます。もし大きな設計書がなくてもコードが書けそうな提案であればその場でLikely Acceptになります。

もし詳細が必要であると判断された場合にはDesign Docを書くように依頼されます。どのような機能か、その機能を提案する背景、実装例含む設計案を説明したDesign Docを作成し、再度レビューが始まります。Design Docは専用のレポジトリで管理されています。(GitHubにもミラーがある)

たとえばGo 1.18に入ったtype parameterでの例はこのような形です。

Design Docを含めた深い議論の末、晴れてLikely Acceptになったのちに、1週間特に異論が見られないようであれば、めでたくAcceptedとなり、いよいよ実際にその実装を行うためのマイルストーンが設定されます。Proposalはマイルストーンの単位となり、実装は細かな修正としてバグの修正のときと同様のプロセスでパッチが作成されます。

なお、Declineの場合はパッチが作成されないので、ここではそのプロセスは割愛しましたが、Declineの場合はその理由と共に決定がなされ、チケットが閉じられます。またHoldという状態もあり、これは議論が止まってしまって判断がつかないような状況です。議論が再開すればActiveに戻ります。

ブランチ戦略

基本

パッチがどのように作成されるかがわかったところで、いよいよブランチ戦略です。まず単発で取り込まれるような通常の修正の場合です。

go.googlesource.com

すでに上の節でも触れていますが、Goのレポジトリのブランチは基本的に master が常に最新です。そしてすべての修正はこの master に対して行われます。

f:id:ymotongpoo:20211215230908p:plain

上の図は上から下に時間が流れていると解釈してください。一番上ではまさにバージョン1.xのための開発を行っています。(リリースサイクルの前半)

先に紹介したように、すべての修正はGerritでレビューされます。Gerritの線上にある四角はGerritでのChange(GitHub上ではPull Requestに見える)を表しています。区別するために新規開発や新規実装のためのChangeは紫色に、バグ修正、ドキュメント更新、セキュリティ修正のためのChangeは水色にしています。

Changeの中の三角はPatch Set(GitHub上ではPull Request内のコミット)です。開発者は原則としてコミットは必ずGerritに対して行うような形になります。そしてレビューによってChangeが承認されると、ChangeはGit内の1コミットとして master にpushされます。(GitHubでPull RequestしたとしてもGerrit経由でpushされるためGitHub上ではforce closedしたように見える)

そして、リリースサイクルの後半に入るときにRelease freezeがアナウンスされ、新規開発や新規実装の新しいChangeはいったん受け入れを停止し、バグ修正、ドキュメント修正、セキュリティ修正のみが master に入ります。次のメールは1.18のためのRelease freeze宣言時の例です。

そしていよいよベータ版リリースとなったときに release-branch.go1.x のブランチが切られます。しかしすべての修正は相変わらず master に送られ、必要なものは都度 cherry-pick しながら release-branch.go1.x に取り込まれます。

そのまま開発を続けていって、リリース候補版が出され、そして晴れてバージョン 1.x のリリースとともに、次バージョン 1.(x+1) の開発がスタートします。このタイミングで新規機能や新規実装のChangeが取り込み可能になります。

もちろん次バージョンの開発を続ける中で、現バージョンにも影響するセキュリティ修正やバグ修正もあるので、その場合はまた cherry-pick で release-branch.go.1.xrelease-branch.go.1.(x-1) の各ブランチ(最新2バージョン)にバックポートして、マイナーリリースの際にパッチバージョンを上げてリリースしていきます。(ここでバージョン名でGitのTagを打つ)

大きな新機能の開発

上記が基本的な流れなのですが、Goのレポジトリを覗くとその規則からは外れたブランチが見られます。ざっと一覧で表示してみましょう。

dev.cc
dev.cmdgo
dev.debug
dev.fuzz
dev.garbage
dev.gcfe
dev.go2go
dev.inline
dev.link
dev.power64
dev.regabi
dev.ssa
dev.tls
dev.typealias
dev.typeparams
dev.types

すべて dev.foobar という命名規則になっていることがわかります。これらのブランチの後半の単語をよく見てみると見覚えがあるような単語ばかりです。これらのブランチは大きな新機能のために作られた特別な開発用ブランチで、このブランチは master とは違い Release freeze 期間においても新規開発を取り込めるブランチとなっています。

例えばFuzzingの開発ブランチである dev.fuzz の、あるコミットを見てみます。

4651d6b267 - go - Git at Google

commit 4651d6b267818b0e0d128a5443289717c4bb8cbc
Author: Katie Hockman <katie@golang.org>
Date:   Wed Dec 2 14:37:49 2020 -0500

    [dev.fuzz] internal/fuzzing: handle and report crashers
    
    Change-Id: Ie2a84c12f4991984974162e74f06cfd67e9bb4d7
    Reviewed-on: https://go-review.googlesource.com/c/go/+/274855
    Trust: Katie Hockman <katie@golang.org>
    Run-TryBot: Katie Hockman <katie@golang.org>
    Reviewed-by: Jay Conrod <jayconrod@google.com>

日付を見ると去年の12月となっています。12月は上のリリースサイクルで説明したように Release freeze 期間ですね。このようにして、Goでは安定したリリースを行いつつ、重要な新規機能の開発は止めないようなプロセスになっています。

おわりに

というわけで、Goのリリースプロセスとブランチ戦略について大まかに説明しました。本記事によって、Goチームからのアナウンスがより身近に感じられていただけたら幸いです。今日話さなかった内容はまだまだあって、たとえば次のような話題があります。

  • dev ブランチの master へのマージプロセス
  • Gerritにpushされた際に行われるテストとそのインフラ
  • リリースの際のアーティファクトビルドプロセス

これらはまた触れる機会があれば紹介したいと思います。

オープンソースプロジェクトは様々な点で勉強になることがありますが、あまり注目されていない(と思われる)リリース戦略もその一つだと思います。もしGo以外のプロジェクトのリリースプロセスの解説記事があればぜひ読んでみたいので、ご存知でしたら教えてください。

ところでPythonでのリリースといえばパッケージングですね。明日は世界でも指折りにPythonのパッケージングに詳しい @aodag です。*3

参照

*1:Goはまだ1系しか出していないので、マイナーバージョンが更新されるリリースをメジャーリリースと呼んでいる

*2:当初はGerritのみだったのですが、Gerritに不慣れな方が多く、多くの要望があったためGitHubとGerritを同期するbotを導入して、どちらでも取り込めるようにしています。現在でもGerritが正であることは変わっていません。

*3:本人が出たがらないから勝手に宣伝するけど、いままで@aodag以上にPythonのパッケージングに詳しい人は海外のカンファレンスや会社の同僚を含めて会ったことがない。