ソフトウェアのバージョニングのあれこれ

アプリにしろクラウドサービスにしろ、ソフトウェアをリリースするときに付けるバージョン。どんな体系にするのか、チームで開発するとたびたび意見が分かれる。意見が出るならまだいいチーム。ともすれば、誰も何も考えてなく適当に付けて後で困る。ここらで、ソフトウェアのバージョニングについて整理してみよう。

# なぜバージョンをつけるのか

なぜあなたはソフトウェアにバージョンを付けなければならないのか。誰のためか。ひとつは、あなたがリリースしたソフトウェアの利用者のためだ。
あなたがリリースしたのがライブラリであれば、そのライブラリを組み込んだアプリケーションの開発者のため。Webサービスをアップデートしたのなら、そのWebサービスの利用者のため。

ソフトウェアは一度リリースして終わり、ということには決してならない。必ず更新が入る。更新前と更新後で区別されてなければ、利用者にとって不便だし、場合によっては致命的だ。

もう一つ、開発チーム（もしくは保守・運用チーム）にとってもなくてはならない。このユーザーと開発チームという二つの用途のずれが、たびたび意見が分かれる原因かなと思う。

# あるシステムの場合

例えばこんなサービスをリリースしたとしよう。
>あなたのモバイルアプリにこのSDKを組み込めば、いい感じにユーザーの行動を分析していい感じにレポートするよ。レポートはWebサイトから見られるよ。

みたいな。名前は…『分析ちゃん』にしとこうか。なんでもいいけど。

きっとこんな感じのシステム構成になるんじゃないかな。
- iOS, Android向けのSDK
- SDKからのリクエストをオンラインで受けるWeb API
- ユーザーの行動を非同期に分析するJobとかWorkerとか呼ばれそうな何か
- レポートを表示するWebサイト
- レポートを表示するWebサイトから呼ばれるGraphQLとかでできてそうなAPI

いわゆる分散システムだ。さて、このシステムに更新が入ったら、バージョンいくつになる？どこのバージョンを上げる？

# プロダクトオーナーにとって

プロダクトオーナー、すなわちそのプロダクトやサービスを提供する代表者にとって、リリースは大事な晴れ舞台である。「さぁ、こんな素敵なアップデートをしましたよ！みんな使って！」というお披露目の機会。ここでアガらねればいつアガる。

なので、プロダクトオーナーにとってのプロダクトのバージョンは多かれ少なかれマーケティング色が出る。1.0から1.1へのバージョンアップより、2.0へのバージョンアップの方がすごそうな感じがする。
1.0, 2.0, 3.0と地道に上げていってインパクトが欲しくなったら、いっそぶっ飛んだバージョンを付けたくなるかもしれない。『分析ちゃん95』とか、『分析ちゃんXP』とかどうだろう？

プロダクトオーナーはユーザーの代弁者であり、ユーザーはシステム構成の中のコンポーネントにどんな変更があったかなんて興味はない。プロダクトオーナーが書くリリースノートにも、内部のコンポーネントの変更内容を書くべきではない。「グラフ描画用ライブラリほげほげのバージョンを1.3から1.4に上げた」なんてユーザーは誰も知りたいないよね？
プロダクトオーナーが考えるべきなのは、プロダクト全体のバージョニングなのだ。

さて、分析ちゃんXPとか付けられる強心臓なひとは別にして、継続して開発するプロダクトにまじめにバージョニングするとすれば、無難なのは以下の三つかと思う。

- `1.0`, `1.1`, `1.2`, ..., `2.0`, `2.1`, ..., `3.0`, `3.1`, ...
- `1.0`, `2.0`, `3.0`, ....
- `July 2023`, `August 2023`, ...

最初のはマイナー番号をあげているスタイル。これは、メジャー番号をあげるマイルストーンをはっきり持っているプロダクトに都合がいい。逆にそれがないと、『分析ちゃん1.87』みたいに「今さらいつ2.0に上げればいいかわからない...」みたいになる。

継続的にサービスを改善していくぜ、なスタイルのプロダクトには二つ目がやりやすい。ただ、『分析ちゃん36.0』になる頃にはリリースのインパクトも薄くなり、パンドラの箱を開けて『分析ちゃんVista』にしたくなるかも。

プロダクトの軸がクラウドサービスであれば、いっそバージョニングしない、というのもアリ。それが三つ目のスタイル。リリースノートに並ぶのはリリース月とかリリース日。潔くていいんじゃないかな。

# ライブラリ開発者にとって

ライブラリやSDKといった、何か別のプログラムに組み込まれる部品のバージョンには唯一の解がある。セマンティックバージョニング (https://semver.org/lang/ja/ ) だ。

>バージョンナンバーは、メジャー.マイナー.パッチ とし、バージョンを上げるには、
>
>APIの変更に互換性のない場合はメジャーバージョンを、
>後方互換性があり機能性を追加した場合はマイナーバージョンを、
>後方互換性を伴うバグ修正をした場合はパッチバージョンを上げます。
>プレリリースやビルドナンバーなどのラベルに関しては、メジャー.マイナー.パッチ の形式>を拡張する形で利用することができます。

このスキームによって、利用する側は `1.3.*` みたいな記述で更新に伴うリスクを抑えつつ最新のパッチをシステマチックに当てることができる。
システマチックにできるのが重要なので、これに逸脱したバージョニングをされると混乱を生み、大変迷惑になる。そのためライブラリやSDKのバージョニングはこれ一択だ。異論は認めない。

# Web APIの開発者にとって

Web APIにとってバージョンには二つの異なる意味がある。ひとつはリリースのたびにインクリメンタルに上げていくものでチーム内の管理のためには大切ではあるが、もう一つの方がより重要である。それは、インタフェースのバージョンだ。

Web APIにとってのユーザーは、それを呼び出すクライアントアプリケーションである。『分析ちゃん』の場合は、iOS, Android向けのSDKがAPIのクライアントになる。
クライアントにとって重要なのは後方互換性、すなわち「呼び方変えなくても以前と同じように動くよね？」を保証してくれるかだ。
なぜなら、ライブラリなら改めてビルドするまで勝手に入れ替わることはないが、Web APIではクライアントの知らないタイミングでアップデートがされるので、いきなり「このパラメーターは無効になりました。代わりにこれを使ってね。」とか言われても困るのだ。

そういうわけで、Web APIで互換性のない変更を行う場合は、クライアントが意識的に切り替えられるようURL等にバージョンを埋め込むのがお約束になった。どこに埋め込むか、おおよそ三つの流派がある。
- URLパス `https://api.example.com/v2/`とか `https://v2.api.example.com/`
- HTTPヘッダー `X-Api-Version: 2`
- URLクエリ `https://api.example.com?version=2023-07-08`

一つ目のURLパスに入れると、クライアントが意識的に使うバージョンを切り替えることが期待される。`https://api.example.com/v3/`をリリースしたからといって、v2を利用しているクライアントがすぐにv3を使い始めたりはしない。必然、クライアントが新しいバージョンを使い始めるまで古いバージョンを延命させなければいけない宿命を背負う。

HTTPヘッダーやURLクエリに埋め込む方式では、省略した場合は最新（もしくはstable）のバージョンが使われることが期待される（そうならなければいけない、というわけでもないが）。その点でURLパスに入れる方法よりは、早めに古いバージョンのEOL（サポート終了）ができるかもしれない。

インターフェースの互換性のためにURLパス方式を、インタフェースは変わらないものの振る舞いが変わるものにはHTTPヘッダーやURLクエリ方式を、というように二つを組み合わせるのもいいだろう。
たとえば、『分析ちゃんAPI』は互換性のないリリースがあってv2を出したが、それ以降は分析に使うAIモデルの更新がたびたびあるとしよう。クライアントによっては必ずしも最新のAIモデルを好まないかもしれない。そんなクライアントは、`https://api.bunsekichan.com/v2/analyze?model=2023-07-08`みたいな呼び方がいいだろう。

長々と書いたものの、原則として、Web APIでは可能なかぎり後方互換性は保つのが吉だ。うっかり作ってしまったv1のイケてないインターフェースを書き直したい気持ちは痛いほどわかる。わかるけど、複数バージョンを運用し続ける苦労に比べればマシだ。

あなたがv2をリリースした後も、クライアントはv1を使い続け、あなたはv2とv1をメンテナンスし続けなければいけないだろう。v1のEOLを宣言しようとも、クライアントはそのメッセージに気付かない。そしてv1をシャットダウンしたとに、烈火の如くクレームを投げてくるだろう。

# テストまたは運用担当者にとって

リリース前にテスト担当者がプロダクトをテストしてバグを見つけたとき、またはリリース済みのプロダクトに不具合の報告が運用者に届いたとき、彼らはバグ報告のチケットを書く。チケットにはこんな内容が記載されるはずだ。
- タイトル
- 重要度
- 状態
- 発生バージョン
- 修正バージョン

彼らにとって、どのバージョンで発生する不具合か、は重要な関心事だ。「2.0.0~2.1.0では発生するけど2.1.1で修正されたよ」という情報が記録されてないと、何個も同じようなチケットが作られてしまう。

モバイルアプリやデスクトップアプリでは古いバージョンが使われる状況は日常だ。それらには先に説明したセマンティックバージョンを付けておいて、バグ報告者にはそれを記載してもらおう。

リリース前のテストで検知されるバグは、修正されたバージョンが発生したバージョンと同じかもしれない。だって、まだリリースされてないのだから。すると、たびたびこんなことが起こる。

テスト担当者：「次リリースするこの機能でバグあったよ」
開発担当者：「オッケー。さっき修正したから確認よろ」
テスト担当者：「直ってないよ」
開発担当者：「ごめん。テスト環境にデプロイするの忘れてたわ」

再現手順が複雑なバグでこれくらうと非効率甚だしい。辛い。でもひとはミスをするもの。
この手の問題は、バージョン番号に`2.0.3.7e527bc`のようにビルド番号まで含めると防げる。テスト担当者はテストする前にバージョンを確認できるから、「さっきのバージョンから変わってないよ」って突き返せる。ビルドやCIを少しがんばって、自動で採番できるようにしておこう。

WebアプリやWeb APIでもいま動いているバージョンを確認できるといいのだが、いまのところこれぞ標準、といった方法はないようだ。

HTTPレスポンスヘッダーに毎回入れるか、もしくは`https://api.bunsekichan.com/v2/version`といったエンドポイントにGETするとバージョンを返すような仕組みにしてもいいだろう。

# まとめ

ソフトウェア、特に分散システムのバージョンについて、案外複雑なんだよ、ってのを見てきた。
まとめとして、『分析ちゃん』のあるリリースではどんなリリースノートになるかひとつの例をあげて締める。

### ユーザー向けのリリースノート
```
2023.07.09 リリース
  新機能
    - ...
    - ...
  バグフィックス
    - ...
    - ...
  SDK update
    - iOS 3.1.0
    - Android 3.1.0
```

### 内部向けリリースノート
内部向けはJIRAなどのチケット管理システムに作ってもらおう。GitHubでもいい。手間暇かけるものじゃないから機械的にすまそう。
```
iOS/Android SDK 3.1.0
  feature
    - ...
  bugfix
    - ...
```
```
Web API 1.5.0
  feature
    - ...
  bugfix
    - ...
```
```
Worker 2.4.0
  feature
    - ...
  bugfix
    - ...
```
```
Dashboard Web 1.1.0 
  no release
```
```
Dashboard API 1.2.1
  bugfix
    - ...
```