Skip to content
hume.com
Go back

バージョン番号を人が打たないリリース自動化

Chrome 拡張のリポジトリで、リリースまわりを一通り自動化した。目標はひとつだけ。

バージョン番号を人が打たない。

結果としてこうなった。

機能ブランチ → PR → main にマージ
   ↓ 自動
「Release 1.1.0」PR が立つ(版の書き換え + CHANGELOG)
   ↓ 人間の判断はここだけ
   ↓ 自動
タグ + GitHub Release + zip

人がやることは「このリリース PR をマージするか」の判断だけになった。

版の正をどこに置くか

最初に決めるのはここだった。バージョンの正は、配布物が読むファイルに置く。

Chrome 拡張ならストアが読む extension/manifest.json がそれにあたる。Rust のワークスペースでもあるので Cargo.toml にも版はあるが、そちらは実質どうでもいい。

「Cargo を正にして、CI がビルド時に manifest へ書き込む」という手もある。でもそれだとリポジトリ内の manifest が常に古い。開発中に unpacked で読み込んだときの表示もズレるし、リポジトリを見た人が版を判断できない。

上げ幅の算出と CHANGELOG 生成は git-cliff に任せて、書き込み先は自分で決める。ワークフロー 2 本と cliff.toml で済む。

タグ起点にはできなかった

素直に考えると「リリース PR をマージ → ボットがタグを push → タグ起点のワークフローがビルドして公開」だが、これは動かない

Events triggered by the GITHUB_TOKEN will not create a new workflow run

GitHub の仕様で、ワークフローが既定のトークンで起こしたイベントは別のワークフローを起動しない。無限ループ防止のためで、PAT を置けば回避できるが、そのためにトークンを増やしたくない。

そこでタグ起点をやめた。リリースのワークフローは main への push で走り、

  1. manifest.json の版にタグが無いか調べる
  2. 無ければ検証 → タグ作成 → Release 作成

とする。タグはこのジョブ自身が作る。「未リリースの版があるか」を毎回見るだけなので、リリース PR 以外の push では判定して即終わる。

squash マージをやめた

最初は squash にした。1 PR = 1 コミットになるので CHANGELOG が PR 単位で綺麗に出る。

でもこれをやると、上げ幅が PR タイトルの種別ひとつで決まる[refactor] というタイトルの PR に機能が混ざっていたら patch にしかならない。

merge コミット方式なら、ブランチ内の個々のコミット([feat][fix])が main の履歴に残るので、上げ幅は実際の変更から計算される。CHANGELOG からは merge コミットだけ除外すればいい。

commit_parsers = [
  { message = "^Merge (pull request|branch|remote)", skip = true },
  # ...
]

独自のコミット規約でも通る

このリポジトリのコミットは [種類] 件名 の日本語で、conventional commits ではない。

git-cliff には custom_minor_increment_regex があるので、これで ^\[feat\] を拾えばいいと思った。思ったが、動かなかった。

CHANGELOG のグループ分けは正しいのに、バージョンだけ patch のまま。[feat] が入っているのに 1.0.1 が出る。グループ名で判定させる書き方も試したが同じだった。

原因は conventional_commits = false にしていたこと。git-cliff が上げ幅を決めるための「型」を持っていない状態で、custom regex はその補助にすぎなかったらしい。

解決は前処理。

[git]
conventional_commits = true
commit_preprocessors = [
  { pattern = "^\\[feat!\\]", replace = "feat!:" },
  { pattern = "^\\[feat\\]",  replace = "feat:" },
  { pattern = "^\\[fix\\]",   replace = "fix:" },
]

[feat]feat: に変換してから標準の判定に載せる。これで 1.1.0 が出た。コミットの書き方は変えていない。変換は git-cliff の中だけで起きる。

副産物として、conventional 形にすると commit.message が種別を除いた本文になるので、テンプレートの文字列処理が要らなくなった。

動かして初めて分かったこと

机上で組んだときは「まあ動くだろう」と思っていたが、実際に回したら 5 回落ちた。

1. Release の二重作成。 履歴を直してタグを再 push したら、gh release create が「同じタグの Release が既にある」で落ちた。既存なら gh release upload --clobber に分岐させた。

2. リリース PR に 62MB のバイナリが入った。 git add -A が、共通ステップが作業ツリーに展開した binaryen(libbinaryen.a が 61.71MB)を巻き込んでいた。GitHub から「50MB を超えています」と警告が出て気付いた。add するファイルを列挙し、展開先も $RUNNER_TEMP に移した。

3. Actions が PR を作れない。

GitHub Actions is not permitted to create or approve pull requests

リポジトリ設定(Settings > Actions > General)の話で、コードでは直せない。

4. 機能を入れても patch。 上に書いた conventional_commits の件。CHANGELOG は正しいのに版だけ間違うという気付きにくい壊れ方だった。

5. YAML が壊れた。 PR 本文に埋め込んだ --- が YAML の文書区切りと解釈された。しかも検証コマンドをパイプで繋いでいたせいで終了コードが握り潰され、壊れたまま push した

どれも動かさなければ出てこないものばかりで、これがいちばんの収穫だったかもしれない。

ボットが作った PR の CI

リリース PR の CI が awaiting approval で止まる。これはボットが作った PR のワークフローには承認が要るという仕様で、押しても押さなくてもいい。

うちでは押さない方針にした。マージ後のリリースジョブが同じ検証(lint・テスト・パッケージ)を通してから公開するので、通らなければタグも Release も作られない。二重に検証しても得るものがない。その旨をリリース PR の本文に自動で書くようにした。

できあがったもの

ワークフロー起点役割
buildPR / main への push検証。文書だけの変更では走らない
release-prmain への push次の版を算出してリリース PR を開く/更新する
releasemain への push未リリースの版があればビルドしてタグと Release を作る

上げ幅は [feat] で minor、[feat!]BREAKING CHANGE で major、それ以外は patch。

Chrome ウェブストアに出す zip は必ず CI がビルドしたものになるので、「公開ソースとビルド手順を示す」という審査への説明もそのまま通る。WASM は読めないので、そこは説明できる状態にしておきたかった。

余談: Chrome のバージョン表記

semver をそのまま持ち込めない。Chrome 拡張の版は 1〜4 個の整数(各 0〜65535、先頭ゼロ不可)だけで、-rc.1 のような接尾辞は使えない。

代わりに 4 桁目が使える。「中身は同じだが審査の差し戻し対応で再アップロードしたい」ときに 1.2.0.1 とできるのは、実務では地味にありがたい。


Share this post:

Next Post
app-ads.txt を Cloudflare Pages の pages.dev で通す