GitHub Immutable Releases(不変リリース)は、公開したリリースのアセット(配布ファイル)とGitタグを以後変更できなくする機能です。2025年8月26日にパブリックプレビュー、2025年10月28日に一般提供(GA)になりました。設定画面の「Enable release immutability」にチェックを入れれば有効になりますが、公開後にアセットを追加するワークフローは422エラーで止まります。本記事では、固定される範囲、有効化の手順、draft経由の公開、gh release verifyによる検証を、公式ドキュメントと2026年11月7日の実行結果で整理します。
まとめ:Immutable Releasesを有効化する前に押さえる6点
- 固定されるのはアセットとタグだけです。タイトル、リリースノート、プレリリース/Latestの指定は公開後も変更できます。
- 有効化はリポジトリの Settings → Releases、またはOrganizationの Settings → Repository → General で行います。対象は有効化以降に公開するリリースだけで、無効化しても不変になったリリースは元に戻りません。
- 公開済みリリースへのアセット追加は
Cannot upload assets to an immutable releaseで失敗します。draftで作成→アセット添付→公開の順に変えます。 - draftから公開したプレリリースでは、
releaseイベントのprereleasedが発火しません。プレリリースで動かす後続ワークフローはpublishedで受け、公開処理にはGITHUB_TOKENではなくPATなどを使います。 - 不変リリースには署名付きのリリース証明が自動で付き、
gh release verifyとgh release verify-assetで検証します。gh attestation verifyが既定で見るのはビルド来歴のほうです。gh CLIは2.102.0以降を推奨します。 - リリースを削除すればタグも削除できますが、同じタグ名は再利用できません。同名リポジトリを作り直しても同じです。
Immutable Releasesで固定される範囲と変更できる項目
公式ドキュメント「Immutable releases」は、保護対象をタグとアセットの2つに限っています。タグは公開時点のコミットに固定され、リリースが存在する間は移動も削除もできません。添付したバイナリやアーカイブは変更も削除もできません。操作ごとの可否は次のとおりです。
| 操作 | 不変リリースでの可否 |
|---|---|
| アセットの追加・差し替え・削除 | 不可 |
| タグの移動(別コミットへの付け替え) | 不可 |
| リリースが存在する間のタグ削除 | 不可 |
| タイトル・リリースノートの編集 | 可 |
| プレリリース/Latestの切り替え | 可 |
| リリースの削除とその後のタグ削除 | 可 |
| 削除したタグと同名のタグの再作成 | 不可 |
同名タグを再利用できない制約は、リポジトリを削除して同じ名前で作り直した場合にも適用されます。公式ドキュメントはこれを「repository resurrection attacks」への対策と説明しています。
不変かどうかはリリースページのタイトル下に表示される「Immutable」ラベルで判別できます。APIではリリースオブジェクトのimmutableフィールド(boolean)を見ます。リリース機能そのものの使い方は「GitHub Releasesの使い方|タグとの違い・リリースノート自動生成・Latestの決まり方」で扱っています。
Enable release immutabilityの有効化手順(リポジトリ・Organization・REST API)
リポジトリ単位の設定画面
リポジトリのトップから Settings を開き、「Releases」セクションの Enable release immutability にチェックを入れます。公式ドキュメントは「immutability will only apply to future releases」と注意しており、既存のリリースは対象外です(原文は「Preventing changes to your releases」)。
Organization単位のポリシー
Organizationの Settings を開き、サイドバーの「Code, planning, and automation」にある Repository → General を選びます。「Releases」セクションの No policy ドロップダウンを All repositories または Selected repositories に変えると、配下のリポジトリに強制されます。Selected repositories では歯車ボタンから対象を選びます。
REST APIでの状態確認と有効化
多数のリポジトリへの一括適用はREST APIで行います。リポジトリ側はAdministration権限、Organizationのポリシー変更はclassic PATならadmin:orgスコープが必要です。
# 状態確認(enabled と enforced_by_owner を返す)
gh api repos/OWNER/REPO/immutable-releases
# リポジトリで有効化・無効化
gh api -X PUT repos/OWNER/REPO/immutable-releases
gh api -X DELETE repos/OWNER/REPO/immutable-releases
# Organization配下の全リポジトリに強制(all / none / selected)
gh api -X PUT orgs/ORG/settings/immutable-releases -f enforced_repositories=all
REST APIのドキュメントは「無効なら404」と記載していますが、2026年11月7日に無効のリポジトリへGETを実行すると、200で{"enabled":false,"enforced_by_owner":false}が返りました。スクリプトでは200応答のenabledの値で判定し、404などの非成功応答は認証・権限の問題として別に扱います。enforced_by_ownerがtrueなら、Organization側のポリシーで強制されており、リポジトリ側ではチェックを外せません。
有効化後の不変リリースの一覧確認と件数集計
どのリリースが不変になっているかは、gh CLI v2.83.0(2025年11月4日)でgh release listに追加されたisImmutableフィールドで数えられます。
# 不変リリースだけを表示
gh release list -R cli/cli -L 300 --json tagName,isImmutable,publishedAt \
--jq '.[] | select(.isImmutable) | "\(.tagName) \(.publishedAt)"'
# 不変リリースの件数
gh release list -R cli/cli -L 300 --json isImmutable --jq '[.[] | select(.isImmutable)] | length'
2026年11月7日にcli/cliで実行すると、211件のリリースのうち不変は15件で、すべてv2.93.0(2026年5月27日)以降でした。-Lの既定は30件なので、古いリリースまで数えるときは件数を指定します。
既存リリースと無効化したときの扱い
GA時のchangelogは、有効化後の挙動を次のように定めています。
- 有効化以降に公開したリリースはすべて不変になります。
- 有効化前のリリースは「remain mutable unless you republish them」、つまり公開し直さない限り変更可能なままです。
- 無効化しても、有効だった期間に作ったリリースは不変のままです。
無効化で解除できないため、「試しに有効化して、問題があれば戻す」運用は成り立ちません。誤ったアセットを添付して公開した場合は、別のタグ名(例:v1.2.1)で出し直します。旧リリースはリリースノートで移行先を案内して残すか、削除します。どちらでもバージョン番号を1つ消費します。
アセット追加が422エラーになる原因とdraft経由の公開手順
Cannot upload assets to an immutable releaseの発生条件
REST APIのリリース作成エンドポイントはアセットを受け付けないため、多くのリリース自動化は「リリースを公開→アセットをアップロード」の2段階で動きます。不変リリースでは公開した瞬間にアセットが固定されるため、2段階目が失敗します。softprops/action-gh-releaseのIssue #653(2025年9月1日起票)には、実際のワークフローで出た次のエラーが記録されています。
Error: Cannot upload assets to an immutable release. - https://docs.github.com/rest
gh CLIによるdraft作成・アセット添付・公開の手順
公式ドキュメントが推奨する手順は、draftで作成し、アセットをすべて添付してから公開する流れです。draftは公開前なので不変の制約を受けません。
set -e
gh release create v1.2.0 --draft --title "v1.2.0" --notes-file CHANGELOG.md
gh release upload v1.2.0 dist/app_linux_amd64.tar.gz dist/checksums.txt
gh release edit v1.2.0 --draft=false
アセットを引数に渡す1コマンドの形(gh release create v1.2.0 dist/* --notes-file CHANGELOG.md)なら、この分割は不要です。gh CLIのソース(pkg/cmd/release/create/create.go)は、アセットがあり--draft指定が無いとき、リリースをいったんdraftで作成し、全アセットのアップロード完了後に公開します。アップロードに失敗すると公開へ進まず、draftの削除を試みます。
アセットの生成をワークフローのジョブ間で受け渡す場合は、「GitHub Actionsのアーティファクト|受け渡しの設計と同名不可・保持期間の決め方」の設計と組み合わせ、公開ジョブを最後の1回に集約します。
プレリリースをdraft経由で公開するときのreleaseイベント
draft経由の公開には副作用が1つあります。GitHub Actionsの「Events that trigger workflows」は、draftから公開したプレリリースではprereleasedが発火せず、publishedだけが発火すると明記しています。on: release: types: [prereleased]でベータ版の配布ジョブを動かしている構成は、draft経由に変えた時点で何も起動しなくなります。
on:
release:
types: [published] # 正式版とプレリリースの両方で発火する
jobs:
notify-beta:
if: github.event.release.prerelease
runs-on: ubuntu-latest
steps:
- env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: printf '%s\n' "prerelease $RELEASE_TAG"
softprops/action-gh-releaseは、v2.5.0(2025年12月1日)でアセットのアップロードが終わるまでdraftにする方式へ変えました。ただしプレリリースだけはdraftを経由せずに公開します。draftから公開すると後続ワークフローのイベントが変わるためで、PR #763にその理由が書かれています。READMEは、不変リリースを有効にしたリポジトリでアセット付きのプレリリースを作る場合、draft: trueで作成して後から公開し、後続ワークフローをrelease.publishedで受けるよう案内しています。同じREADMEは、既定のGITHUB_TOKENで作ったリリースでは別のワークフローが起動しないため、releaseイベントで後続を動かすならPATを使うよう注記しています。prerelease: trueだけを指定したままでは、公開後のアセットのアップロードで失敗します。ワークフローのイベント設計は「GitHub Actionsのトリガー(on)|イベント選定とbranches・pathsフィルタの設計」で扱っています。
主要リリースツールの対応状況
| ツール | 対応した版 | 不変リリースでの動き |
|---|---|---|
| gh release create | アセット指定時は内部でdraft経由 | そのまま使える |
| softprops/action-gh-release | v2.5.0(2025-12-01) | 正式版はdraft経由・プレリリースは要draft: true |
| ncipollo/release-action | v1.19.0(2025-09-01) | draft経由に対応・現行版ではimmutableCreate: trueを指定 |
| GoReleaser | v2.18.0(2026-08-24) | preflightで公開済みの不変タグを事前検知 |
softprops/action-gh-releaseの現行はv3.0.4(2026年11月5日)です。GoReleaserはrelease.preflight.fail_on_error: trueで、同じタグが不変リリースとして公開済みの状態などをビルド前に検出して止めます。既定のfalseでは警告だけです。
リリース証明(attestation)とgh release verifyでの検証
不変リリースを公開すると、タグ・コミットSHA・各アセットのダイジェストを含むリリース証明が自動で作られます。形式はSigstore bundleで、gh CLI以外のSigstore互換ツールでも検証できるとchangelogに明記されています。以下は、2026年10月26日に公開された不変リリース cli/cli v2.103.3 に対して、2026年11月7日に gh 2.96.0 で実行した結果です。
gh release verifyによるリリース証明の検証
$ gh release verify v2.103.3 -R cli/cli
Resolved tag v2.103.3 to sha1:63837fb778d6a39f02be0ce76ff89f2bc8d74bdc
Loaded attestation from GitHub API
✓ Release v2.103.3 verified!
Assets
gh_2.103.3_checksums.txt sha256:f756e7859d90dbb5e85aae5194206c37da220bec9bac23d3d659078a8e766fdc
gh_2.103.3_linux_386.deb sha256:5b3b310d80e6bdb144d256b4c3794f01eac0cf3f6df44e893e5c85573f1138e5
実際の出力では、この後に残りのアセットのダイジェストが続きます。--format jsonを付けると証明の中身が取れ、bundleの形式はapplication/vnd.dev.sigstore.bundle.v0.3+jsonです(9月16日にv2.101.0で確認)。
検証に使うgh CLIは2.102.0以降を推奨します。2.92.0以前には、gh release verify・gh release verify-asset・gh attestationの実行時に、GitHubのトークンをAPI以外のホスト(TUFリポジトリのミラー)へ送ってしまう脆弱性(CVE-2026-48501、深刻度High、2026年5月29日公開)があり、2.93.0で修正されました。また2.51.0〜2.101.0には、gh attestation verifyが--signer-workflowを前方一致で照合する問題(GHSA-wjmr-j3rp-mh2g、深刻度Low、2026年9月30日公開)があり、2.102.0で修正されています。トークン送信の問題がある旧版でこれらのコマンドを実行済みの場合、アドバイザリはgh CLIで使っていた認証トークンの失効、2.93.0への更新、セキュリティログと監査ログの確認を案内しています。
gh release verify-assetによるダウンロード済みファイルの照合
手元のファイルがリリースのアセットと一致するかはverify-assetで確かめます。ダウンロードしたチェックサムファイルをそのまま照合すると成功し、末尾に1行追記したファイルは失敗しました(終了コード1)。
$ gh release verify-asset v2.103.3 gh_2.103.3_checksums.txt -R cli/cli
Calculated digest for gh_2.103.3_checksums.txt: sha256:f756e7859d90dbb5e85aae5194206c37da220bec9bac23d3d659078a8e766fdc
Resolved tag v2.103.3 to sha1:63837fb778d6a39f02be0ce76ff89f2bc8d74bdc
Loaded attestation from GitHub API
✓ Verification succeeded! gh_2.103.3_checksums.txt is present in release v2.103.3
$ cp gh_2.103.3_checksums.txt t.txt && echo x >> t.txt
$ gh release verify-asset v2.103.3 t.txt -R cli/cli
attestation for v2.103.3 does not contain subject sha256:20e76c0f1203af87f9734d0f5da61fe681ba7a2d009f3f4a9788ad5d251c4cb9
リリースページの「Source code (zip)」「Source code (tar.gz)」はダウンロード要求時に生成されるため、公式ドキュメントのとおりverify-assetの対象外です。ソースを検証したい場合は、タグが解決されたコミットSHAと照合します。
ビルド来歴(SLSA)の証明とgh attestation verifyとの違い
配布元がactions/attest-build-provenanceなどでビルド来歴も付けている場合、不変リリースのアセットには証明が2種類並びます。cli/cliのv2.103.3で証明APIを引くと、gh_2.103.3_linux_amd64.tar.gzにはinitiatorがuserとgithubの2件、gh_2.103.3_checksums.txtにはgithubの1件だけが返りました。
| 項目 | ビルド来歴 | リリース証明 |
|---|---|---|
| 作成者 | 配布元のワークフロー | GitHub(公開時に自動) |
| predicateType | slsa.dev/provenance/v1 |
in-toto.io/attestation/release/v0.2 |
| 証明書のSAN | ワークフローファイルのURL | https://dotcom.releases.github.com |
| 検証コマンド | gh attestation verify |
gh release verify-asset |
# ビルド来歴:どのワークフローでビルドされたかを確かめる
$ gh attestation verify gh_2.103.3_linux_amd64.tar.gz --repo cli/cli --format json \
--jq '.[].verificationResult.statement.predicateType'
https://slsa.dev/provenance/v1
# ビルド来歴を付けていないファイルは gh attestation verify では見つからない
$ gh attestation verify gh_2.103.3_checksums.txt --repo cli/cli
Error: no attestations found
--predicate-type https://in-toto.io/attestation/release/v0.2を指定しても、gh 2.96.0のgh attestation verifyはリリース証明を見つけられませんでした。リリース証明は「GitHubのリリースに含まれているか」、ビルド来歴は「どのワークフローで作られたか」を示し、互いの代わりにはなりません。GoReleaserのIssue #6814(2026年8月)では、miseによるインストール時に、期待したワークフローの識別子とリリース証明の証明書識別子が一致せず、検証が失敗したと報告されています。CIでは、確かめたいのがどちらかを決めてコマンドを選びます。
リリース証明がない旧バージョンの検証失敗
cli/cliは2026年5月26日にマージしたPRで「v2.93.0以降のリリースを不変にする」とREADMEに追記しています。直前のv2.92.0を検証すると、証明が存在しないため次のように失敗します。
$ gh release verify v2.92.0 -R cli/cli
no attestations for tag v2.92.0 (sha1:6c470f60803784e1558b626022677c53dccb6016)
CIにgh release verifyを組み込む場合、配布元が不変化する前のバージョンに固定していると検証で止まります。先にimmutableフィールドで確認します。依存関係の棚卸しは「SBOMとは?ソフトウェア部品表の目的・フォーマットと作成・運用の判断を解説」の手順と併せて進められます。
GitHub Actionsのメジャータグ(v1)との両立
アクションの配布では、利用者がuses: owner/action@v1と書けるよう、v1タグを最新のパッチ版へ付け替える運用が一般的です。不変リリースではタグを動かせないため、この運用が壊れるように見えますが、固定されるのはリリースに紐づいたタグだけです。
GitHub Actionsの公式ドキュメント「Using immutable releases and tags to manage your action’s releases」は、次の使い分けを示しています。
v1.0.0のような版固有のタグには、GitHubのリリースを作成して不変にする。v1やv1.1のような後で動かすタグには、リリースを作らずGitタグだけを置く。- メジャー・マイナーのタグは、最新の互換版を指すよう
git tag -fとgit push -f --tagsで付け替える。
つまり@v1は不変リリースの保護を受けません。2025年3月のtj-actions/changed-files侵害では、攻撃者が既存のタグをまとめて悪性コミットへ付け替えました(経緯は「tj-actions/changed-files侵害(CVE-2025-30066)とreviewdog侵害の経緯・影響確認・再発防止」)。同アクションは現在、v47.0.6などの版固有タグを不変リリースで公開しています。利用者側の対策は、GitHubのSecure use referenceによれば、2026年10月28日にGAになったworkflow lockfileで依存アクションのコミットを記録するのが最優先で、採用できないリポジトリでは完全長のコミットSHAで固定します(手順は「SHA Pinningとは?GitHub ActionsをコミットSHAで固定する方法【Enforce設定・pinact・ロックファイル】」)。名前の似た「Immutable Actions」(アクションをOCIパッケージで配布する構想)は別機能で、ロードマップのissue #592は2025年12月11日に completed で閉じられた後、12月18日に not_planned へ変更されています。ワークフローの権限設計は「GitHub Actionsのpermissions|GITHUB_TOKENの既定権限と最小権限の設計」で扱っています。
有効化を急がないほうがよいリポジトリの条件
配布物を第三者がダウンロードするOSSやCLIツールでは、不変リリースは有効化すべき機能です。ただし、2026年3月のaxios事件で起きたnpmへの悪性バージョンの新規公開は、Immutable Releasesだけでは防げません(経緯は「axiosにマルウェア混入(1.14.1・0.30.4)|感染確認・対処と再発防止の設定」)。次の条件に当てはまるリポジトリでは、先にワークフローを直さないと運用が止まります。
- 同じタグでリリースを作り直す運用がある:
nightlyやlatestのような固定名タグに毎回リリースを作ると、2回目以降は同名タグを再利用できません。GoReleaserは2026年4月26日の告知でnightlyビルドのタグをv2.16.0-abc1234-nightly形式の一意な名前へ変えています。 - 公開後にアセットを足すジョブがある:OSごとのビルドを並列ジョブで個別にアップロードする構成は、draftへの集約が先に必要です。
- GitHub Enterprise Serverで配布している:不変リリースは3.20.0(2026年3月17日リリース)で追加されましたが、同版のリリースノートは「Release attestations are not supported on GHES」と明記しています。3.21・3.22のリリースノートに対応開始の記載はない一方、3.22の版別ドキュメントには
gh release verifyの手順が載っており、資料間で食い違っています。利用者側の検証を前提にするなら、自社の版で実際に検証できるかを先に確かめます。
社内限定のリポジトリで、リリースを配布経路として使っていない場合は、有効化の効果は限定的です。リリース自動化の組み直しをCI/CD全体の見直しとあわせて進める場合は、一創のDevOps・CI/CD導入支援でもご相談を受けています。サプライチェーン対策全体のなかでの優先度は「ソフトウェアサプライチェーンセキュリティとは?開発工程に組み込む防御と2026年規制対応の実装判断」で整理しています。
よくある質問
Immutable Releasesを有効化すると既存のリリースも変更できなくなりますか?
いいえ。対象は有効化以降に公開したリリースだけで、既存のリリースは公開し直さない限り変更できるままです。
不変リリースのタイトルやリリースノートは修正できますか?
修正できます。固定されるのはアセットとタグだけで、タイトル、リリースノート、プレリリース/Latestの指定は公開後も変更できます。
不変リリースを削除すれば同じタグで出し直せますか?
出し直せません。リリースを削除すればタグは削除できますが、同じタグ名は再利用できず、リポジトリを作り直しても同じです。別のバージョン番号で公開します。
Immutable Releasesを無効化すると不変のリリースは元に戻りますか?
戻りません。無効化しても、有効だった期間に公開したリリースは不変のままです。無効化の効果は、それ以降に公開するリリースにだけ及びます。
GitHub Actionsでアセットをアップロードすると422エラーになるのはなぜですか?
公開済みの不変リリースにはアセットを追加できないためです。リリースをdraftで作成してアセットを添付し、最後に公開する手順へ変えます。アセットを引数に渡すgh release createは、この順序を内部で自動的に行います。softprops/action-gh-releaseでプレリリースを作る場合はdraft: trueでアセットを添付し、完了後に別のステップまたは手動操作で公開します。