バージョン管理自動化がAPI更新作業をどう効率化するか解説

ここから始めよう - API更新作業の自動化でバージョン管理が驚くほど効率化する実践ヒント

  1. CI/CDパイプラインにAPIバージョン検証スクリプトを毎回走らせる

    手動確認ゼロ、リリース前に互換性崩れを即座に発見できる

  2. セマンティックバージョニング(MAJOR.MINOR.PATCH)を全API変更で適用

    どこが変わったか一目瞭然、利用者も安心して追従できる

  3. 公開API仕様の差分抽出ログを毎回残し、履歴として30日間保管

    不意の破壊的変更も即座に把握、過去との比較や原因追跡が容易

  4. [非互換時] 自動通知&マイグレーションガイド配布率90%以上目指す

    `知らぬ間`の障害激減、移行作業のストレス最小化につながる

バージョン管理の迷走とちょっとした絶望

# Swiftパッケージのバージョニング自動化! [[Image created by author using Procreate]] (この記事では、シェルスクリプトの基本的な知識を前提としています)

## バージョン管理の課題

共有ライブラリのバージョン管理って、時に本当にめんどうなことがあるんだよね。ああ、今さら言うまでもないけど…たとえば社内プロジェクトの場合もそうだし、公開しているパッケージをメンテナンスしている場合も同じで、プロジェクトが大きくなればなるほど変更内容とか、その影響範囲をきちんと把握するのが途端に難しくなる。実は、一人で開発してる場合ですら油断できなくて、大幅な仕様変更したつもりなのにメジャーバージョンだけ上げ忘れて、そのままリリースしちゃった……みたいなこともあり得るわけで。あれ? なんか話が逸れてしまったな。でもまあ、それくらいバージョニングって地味に神経使う作業だったりする。

## 解決策:バージョニングの自動化

じゃあ、どうすればいいんだろう——って考えた結果、自動化しかないんじゃないかな、と。いや、本当に。他にも方法はあるかもしれないけど…。この記事では主に、自分自身やチーム全体でセマンティックバージョニング(Semantic Versioning)の整合性を保つために役立つ具体的な自動化手法について紹介したいと思う。例えばバージョン付与そのものを全部自動でやってくれる方法とか、手動で提案されたバージョン番号が正しいかチェックする仕組みとかね。おっと脱線。でも要はSwiftパッケージ全体の一貫性をちゃんと維持しやすくなるよ、という話。

## Swift Package Manager のバージョニング理解

Swift Package Manager(SPM)は[セマンティック・バージョニング](semver)という規格に沿って運用されているわけだけど、「1.0.1-alpha」みたいなサフィックス付きの表現も理論上OK。ただし、SPMの場合はコア部分のみ――つまり数字三つ並び(version core)の方が相性良いっぽい。うーん、この辺り細かい話になってくるので混乱しそうだけど…結局、多くの場合は余計なサフィックスより素直に「x.y.z」形式だけ意識しておいた方が安心できる気がする。それとも他にも何か落とし穴あるかな?いや、大丈夫そうだ。本筋戻ります。

自動でやっちゃう?手動確認もアリなバージョニング入門

[g . , `1.0.1`)。バージョンのコアだけでタグをつけると、うーん…なんかシンプルすぎる気もするけど、まあそれはさておき、ライブラリ利用者がSPMの`.upToNextMinor(from:)`や`.upToNextMajor(from:)`みたいな書き方で安心してサポート範囲を指定できるようになるんだよね。あれ?でも本当に全部うまくいくのかな…ま、大体大丈夫だと思う。

## どのように実現できるか?

正直手順って言われても頭こんがらがっちゃう時あるんだけど、一応自動化の流れを分解してみると――あ、ちょっと脱線しそうになった、戻らないと。

まず、

1. **API状態の比較**:最新でタグ付けされたバージョンと今のコードベースからパブリックインターフェースをごそっと抽出してくる。なんか面倒そうだけど実際はツール使えばすぐ終わることも多い。
2. **差分の分析**:そのあと追加とか変更とか削除された部分を特定する作業に入る。こういう地味な工程ほど意外と重要だったりするんだよなぁ。ま、それはともかく。
3. **バージョニングルールの適用**:見つかった違いを基準に、「これならパッチアップかな」「いやこれはマイナーアップ?」みたいな判断を下す流れ(ふー…妙に疲れる手順だけど慣れるしかない)。

- **パッチ**(x .)【注意事項】,

自動でやっちゃう?手動確認もアリなバージョニング入門

Swift Package Manager流セマンティックVer考


[x . Z) - バグ修正とか、まあパブリックAPIには影響しないような内部の細かい変更ってやつね。なんだろう、あんまり目立たないけど大事なやつ。で、**マイナー** (x . Y . x) っていうのは…えっと、後方互換性が保たれてる新機能らしい。つまり今まで動いてたコードを壊さずにちょっと何か増える感じ?そういう意味合いかな。でも本当に壊れないのか時々疑わしい気もしてきて…あ、ごめん戻すね。本筋は「新機能追加」だけど既存コードそのまま使えると。

あと **メジャー** (X . x . x)、こっちは利用者側が絶対に対応しなくちゃいけない破壊的な変更。要するにアップデートしたら前と同じ書き方じゃ動かなくなるから注意ってことだよね。こういうの来ると軽くため息出る…。でも避けて通れない時あるし。で、この三つバージョニング原則通りに次のバージョン番号を決めて提案します。

## スクリプト基盤と引数解析

うーん、それでスクリプトの基本設定なんだけど──いや、その前になんとなくコーヒー飲みたい気分(今関係ない話)。はい戻ります。

#!/usr/bin/env bash

set -Eeuo pipefail
trap 'error_handler ${FUNCNAME-main context} ${LINENO} $?' ERR

# 定数

readonly CALL_DIR="$PWD"
readonly SCRIPT_NAME=$(basename -s ".sh" "$0")
readonly TEMP_DIRECTORY="tmp$RANDOM"

# 関数

function error_handler() {
echo "$SCRIPT_NAME.sh: in '$1()', line $2: error: $3"
reset
exit 1
}

function main() {
# 後ほど内容を追加します。
}

# エントリーポイント

while [[ $# -gt 0 ]]; do
case $1 in
# パブリックインターフェース作成用デバイス。`xcodebuild archive` の `-destination` 引数としてサポートされる値をセットしてください。
# 例: `platform=iOS Simulator,name=iPhone 14,OS=17.0`
-d|--device)
DESTINATION=${2}
shift 2
;;
# 派生データパス(まあ任意の場合もある)。
-r|--derived-data-path)
DERIVED_DATA_PATH=${2}
ARCHIVE_PATH="$DERIVED_DATA_PATH/archive"
shift 2
;;
# パッケージスキーム名について。一部複雑になる場合あり。
# 例:パッケージ名が `ClientService` のケースでは、内部ターゲットとして `ClientServiceDTOs` や `ClientServiceAPI` とか含むことも多い。
# この場合ターゲット名は `ClientService-Package` にする必要あり、と。
-s|--scheme)
SCHEME=${2}
shift 2
;;
*)
echo "Unknown parameter: '${1

API比較ってどう始める?初期ステップざっくり解説

[g . `./.build`]って、あー、そういえば、1つのランナー上でスクリプトが並列CIジョブとして走るときにデータ競合しちゃう現象を防ぐために必要なんだよね。いや、実際めんどくさいんだけどさ、ここ大事だから無視できない。さて、それはさておき、今後使うヘルパー関数を何個か追加していくことにする。…えっと話が逸れたけど戻ろう。

function reset() {
cd "$CALL_DIR"
rm -rf "$TEMP_DIRECTORY" > /dev/null
}

# Package.swiftからパッケージ名を抽出
function swift_package_name() {
swift package describe --type json | jq -r .name
}

これらのヘルパー関数には二つほど意味がある気がする。reset()は一時ファイルを片付けて元いたディレクトリへ戻る働きを持っていて、まあ当たり前っちゃ当たり前なんだけど…。うーん、swift_package_name()について言えば、Swift標準のパッケージ記述機能とjqによるJSON解析を利用してPackage.swiftからパッケージ名を取り出す感じになる。ああ、それとjqがちゃんと入ってるか確認したほうがいいかもね。

## 現在のバージョンタグを見つける

比較対象となる最新バージョンも調べなきゃいけない。何というか、このスクリプトはアクティブなブランチ(HEAD)からGit履歴をごそごそ漁って最新のバージョンタグを探し出す仕組みになっているわけで…。

function get_current_version_tag_name() {
local current_branch_name
local last_reference_tag_name

current_branch_name=$(git rev-parse --abbrev-ref HEAD)
last_reference_tag_name=$(git tag --merged="$current_branch_name" --list --sort=-version:refname "[0-9]*.[0-9]*.[0-9]*" | head -n 1)
cat

API比較ってどう始める?初期ステップざっくり解説

スクリプトの骨組みと引数パズル、はじめに設定したいこと

今のブランチにマージされたタグ、その中でsemverパターン3にフィットするやつを全部…まあ、いちいち数えるのも面倒だけど、とりあえず一覧で出す。えっと、次はそれらをバージョン順(`-version:refname` ね)で降順に並べるわけ。つまり、新しいものが先頭になって、古いやつは後ろへ追いやられる形だ。ところで、この「降順」って昔からどうしても混乱しちゃうんだよなぁ…ま、それはさておき、そのリストから一番最初(要するに最新)の1つだけ抜き出すという流れ。

## パブリックAPIインターフェース生成

Swiftにはさ、パブリックAPIファイルを作るための機能が最初からあるんだよね。実際のところ、使ってみた人いる?自分は一度だけ手探りで触ったことある気がするけど…。まあどうでもいいか。さてと、この機能ってパッケージタイプによって2通りくらいアプローチが考えられるっぽい。一瞬迷ったけど、それぞれ事情が違うし選び方にも癖があるかもしれないなぁ…。

gitタグとのにらめっこ~どのバージョンが最新なのか問題~

Appleのフレームワーク、例えば`UIKit`とかを使ったパッケージについて思い出したけどさ、
function build_public_interface() {
xcodebuild \
-archivePath "$ARCHIVE_PATH" \
-derivedDataPath "$DERIVED_DATA_PATH" \
-destination "$DESTINATION" \
-scheme "$SCHEME" \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
SKIP_INSTALL=NO \
OTHER_SWIFT_FLAGS="-no-verify-emitted-module-interface" \
archive | xcbeautify
}
…まあ、こういうやり方なんだけど、うーん、macOSでしか動かないって点は一応念頭に置いておいたほうがよさそう。あれ?前にも同じことで困った気がするな。でも本筋戻るね。

純粋なSwiftパッケージ(つまりユニバーサル的存在)の時は話が変わるというか——
function build_public_interface() {
swift build \
--build-path "$DERIVED_DATA_PATH" \
-Xswiftc -enable-library-evolution \
-Xswiftc -emit-module-interface \
-Xswiftc -no-verify-emitted-module-interface | xcbeautify

gitタグとのにらめっこ~どのバージョンが最新なのか問題~

public API抽出:xcodebuild派、それともswift build?そしてファイル探しゲーム


## 公開インターフェースの抽出

パッケージをビルドしてから、その公開インターフェースを抜き出す必要があるんだけど、えっと、これは結構面倒くさい作業だったりする。まあ、昔は手でやってたんだけど今は違う。っていうか、最近眠いな…。さて、このプロセス自動化のために使う関数が下記だ。

function get_public_interface() {
local package_name
local public_interface_directory
local build_output

if ! build_output=$(build_public_interface 2>&1); then
echo "❌ Build failed. Error details:" >&2
echo "$build_output" >&2
echo "Cannot complete the build due to the compile error. Check logs above." >&2
exit 1
fi

package_name=$(swift_package_name)
public_interface_directory=$(find "./$DERIVED_DATA_PATH/" -name "${package_name}.swiftinterface")
cat

一時ディレクトリ魔改造&ビルドキャッシュ爆破タイム

一時的にディレクトリをどうするかとか、derived-dataの掃除とか、まあ正直この辺でうっかり忘れがちなんだけど…。えっと、スクリプトの冒頭ではローカル変数をいくつも宣言していて、current_branch_nameだのcurrent_public_interfaceだの…もう名前だけで頭がゴチャゴチャしてくる。TEMP_DIRECTORYやDERIVED_DATA_PATHも「./」で始まってたら妙なことになるから、sedで微修正してるところは地味に重要だったりする。あぁ、この手間は地味に効いてくるんだよね。

そうそう、一応キャッシュファイル絡みでも油断できないので、derived dataディレクトリ自体はrm -rfでまっさらに削除しちゃう。ほんと、これやらずにハマった経験ある人、多いんじゃない?私も前に…いや、その話はいいか。さて次へ。

タグ付け済みバージョンと現状との差分を取るためには、一時コピー作成が要るわけだけど…ここでもgit clone使ってブランチ指定して静か~にクローン(quietオプションね)。submoduleにも気を配ってて抜かりなし。ただし呼び出し元ディレクトリ(CALL_DIR)へ戻ったり移動したりと落ち着きなくcdしてて、あーまたミスったかなと思いつつ……まあ大丈夫だった。

それからpublic interface取得用の関数実行。このあたりちょっとややこしいと思うんだけど、「version」と「current」の両方についてインターフェース内容取得。その後diff比較用として、それぞれswiftinterfaceファイルとして保存。この時コメント行(//で始まるもの)はgrep --invert-matchで省いちゃう。…API本体とは関係ないからね。でも逆説的に言えば、「コメント消えて焦った」ってことも最初はありそう。

最後の差分分析――ここが肝心なのさ。「diff」で2つのインターフェースファイル突き合わせて、「^<」ならbreaking changes、「^>」ならadditive changes、と判定。それぞれgrep -c -i で件数カウントするという寸法。でも何というか、このへん曖昧な記憶になりがちなので、自信なくてもechoで確認した方が多分安心。semantic_version_regexによるバージョン抽出も忘れずに。

もしバージョン名がお約束通りじゃなかった場合は、そのまま表示。ちゃんと3段階(major, minor, patch)揃ってればdiff結果次第で増減処理。一度メジャー上げたらminor/patchゼロクリアなのもお決まりなんだけどさ、本当にこれ絶対ミスできないところだよね…。

そういえばクリーンアップ処理にも一応触れておこう。「reset」で一時ディレクトリ内を整理整頓。でもbash reset命令そのものを書いてしまう癖、もう直したい…。

使い方例――これは普通にchmod +xして実行、と書いてあるけど、CI/CD環境だとうっかりパーミッション忘れる人多い気がする。しかしながら、そのNEXT_VERSION変数をタグ化&pushまで自動化できちゃうので便利と言えば便利なんだけど……ま、自動化すると余計不安になったりもしない?

結局のところ、このシェルスクリプト+Swiftビルトインツール連携術によって、人為的な凡ミス回避&一貫したバージョニング運用につながる仕組みとなっています。「これ本当に必要?」と疑いたくなるくらい細部まで詰めてあるけど、不思議とこういう面倒臭さのお陰でヒューマンエラー減った実感はある。不満?いや…疲れるけど助かってます、多分…。コード全文は[my GitHub repository]参照とのこと。

一時ディレクトリ魔改造&ビルドキャッシュ爆破タイム

diffでAPI変化を丸裸にする:バージョン決定までのジグザグ道

今後の改善点か…。うーん、現状提示されているソリューション自体はまあまあしっかりしてる印象だけど、やっぱりまだまだ伸びしろってあるもんだね。まず、依存関係の分析について言えば、`Package.swift` をスキャンして互換性に影響する可能性がある依存バージョンの変動をちゃんと見つけられる仕組みがほしい、とふと思った。いや、突然話逸れるけどさ、最近パッケージ周りで地味に困ったこと多くて…でも本題戻そう。この機能が実装されたら予期せぬトラブル減る気がする。

それから複数ターゲット対応ね。現状だと単一ターゲット向けには良い感じなんだけど、「scheme」内の全ターゲットを網羅的にスキャンできたらもっと便利になるはず。正直、一個一個手作業とか面倒じゃない?全部の公開インターフェースファイルを差分取ってくれればありがたいよなあ。でも途中で思いついたんだけど、それ設定ミスったらカオスになりそう…ま、大丈夫か。

さらに、カスタムAPI分析も重要だと思うんだよなぁ。例えばメソッドとかenum内でデフォルト値付き新規パラメータ追加した場合、それを非破壊的変更として判定できるような、高度で細やかな解析機能―こういうやつ、ちょっと憧れる。でもさすがに難易度高い?いや無理ではないっぽい。

最後にベータ版への対応について触れておきたい。議論中のスクリプトって基本的には成熟済みパッケージ前提なんだけど、破壊的変更時は `MAJOR` バージョン上げ方式にもきちんと対応してる。そのへん…まぁ特段問題にはならないとは思うけど、一応念頭に置いておきたい気分になる瞬間もあるわけで…。ま、ともかくこれからも改良余地は山ほどあるって話でした。

後片付け、CI例、これから欲しい機能(妄想含む)

ベータリリースだと、なんか破壊的な変更が起きた場合に `MINOR` バージョンが 5 に上がっちゃうんだよね。まあ…別に驚くことでもないのかもしれないけど、ちょっと気になる。あ、話戻すと、**複数デバイス対応**の話をしたい。一部パッケージには Linux とか macOS 専用のプラットフォーム固有 API が存在していて、これ地味にややこしい。

でさ、今の場合だと開発者はサポートされているそれぞれのプラットフォームごとにスクリプトを一つずつ実行して、その結果出てくるバージョン同士を比較しなきゃならないんだって。面倒だよねえ…。うーん、なんでこんな仕組みにしたんだろう?いや、ごめん愚痴っぽくなった。でも現実としてはそうなんだからしょうがない。

GitHub ユーザー向けの話だけど、一からスクリプトを書く代わりに、自分のリポジトリで公開されてる既製アクション「Filozoff/action-swift-propose-next-version」ってやつを使うこともできるらしい。ちなみに [自分のライブラリ] でも使用例を見ることができるし、興味あれば覗いてみてもいいかも。ま、それはさておき…何事もまず試してみるしかないよね…。

Related to this topic:

Comments