---
title: "バージョン番号を人が打たないリリース自動化"
pubDatetime: 2026-08-06T00:00:00.000Z
description: "Chrome 拡張のリリースまわりを GitHub Actions と git-cliff で自動化した。人が判断するのは「リリース PR をマージするか」の一点だけになる。動かして初めて出た 5 つの失敗も込みで。"
tags: [GitHub Actions, CI, Rust, Chrome拡張]
canonical: https://shsw228.github.io/hume.com/posts/2026-08-06-release-automation-without-versions/
---

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

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

結果としてこうなった。

```text
機能ブランチ → 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](https://git-cliff.org/) に任せて、**書き込み先は自分で決める**。ワークフロー 2 本と `cliff.toml` で済む。

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

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

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

[GitHub の仕様](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow)で、ワークフローが既定のトークンで起こしたイベントは別のワークフローを起動しない。無限ループ防止のためで、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 コミットだけ除外すればいい。

```toml
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 はその補助にすぎなかったらしい。

解決は前処理。

```toml
[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 を作れない。**

```text
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 の本文に自動で書くようにした。

## できあがったもの

| ワークフロー | 起点 | 役割 |
|---|---|---|
| `build` | PR / main への push | 検証。文書だけの変更では走らない |
| `release-pr` | main への push | 次の版を算出してリリース PR を開く／更新する |
| `release` | main への 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` とできるのは、実務では地味にありがたい。
