下拉重新整理
rails 公開 rails bundler gemfile versioning semver breaking-change

Rails Gemfile 版本管理:慣例、鎖版機制與 Breaking Change

Rails Gemfile 版本寫法慣例、Gemfile.lock 分工機制、breaking change 定義,以及 Engine 開發參考母 app lock 檔、獨立 Gem 開發寬版本範圍與 CI 矩陣測試的完整實務指南。

| 匯入於 2026-04-16 |

三種版本寫法

# 1. 不指定版本
gem "devise"

# 2. 悲觀鎖定(最常用)
gem "devise", "~> 4.9"

# 3. 精確指定
gem "devise", "= 4.9.3"
寫法 優點 缺點
不指定 永遠拿最新版,省事 bundle update 可能帶入 breaking change
~> 4.9(悲觀鎖定) 允許 patch 更新(4.9.x),擋住 major/minor 升級 需手動追版本
= 4.9.3(精確) 完全可重現 連 security patch 都要手動升

Gemfile vs Gemfile.lock 的分工

Rails 慣例是 Gemfile 不寫死版本,鎖版本的事交給 Gemfile.lock

流程說明

你在 Gemfile 寫:
ruby
gem "devise" # 沒指定版本

bundle install,Bundler 找最新版(假設 4.9.4),寫進 Gemfile.lock:

GEM
specs:
devise (4.9.4)
bcrypt (~> 3.1.7)

Gemfile.lock commit 進 git。

同事 clone 專案後跑 bundle install,裝到的是 4.9.4。Bundler 看到 Gemfile.lock 存在,直接照 lock 檔裝,就算 Devise 昨天出了 4.9.5 也不影響。

graph TD
  A[Gemfile<br/>聲明可接受範圍] -->|bundle install| B[Bundler 解析相依]
  B -->|寫入| C[Gemfile.lock<br/>精確記錄實際版本]
  C -->|commit 進 git| D[所有人裝到相同版本]
  E[想升版本時] -->|bundle update devise| C

分工總結

Gemfile      → 聲明「我接受哪些版本」(寬鬆範圍)
Gemfile.lock → 記錄「我現在實際用哪個版本」(精確到小數點)

Gemfile 寫死精確版本(= 4.9.4)只是縮窄聲明範圍,但鎖版本的效果 Gemfile.lock 本來就做到了。兩件事重疊,反而變成維護負擔——每次升 patch 要改兩個地方。


語意化版本號(Semantic Versioning)

主版本.次版本.修補版本
  8  .  1  .  4

主版本升 → 可能有 breaking change(要謹慎)
次版本升 → 新功能,理論上向下相容
修補版本升 → bug fix,安全,放心升

~> 8.1 的效果:允許 8.1.08.1.18.1.99,但擋住 8.2.09.0.0


Breaking Change 是什麼

升級後,原本能跑的程式碼壞掉了。

具體例子

Devise 3.x 有個方法:
ruby
current_user.authenticated? # 回傳 true/false

Devise 4.0 把它改名:
ruby
current_user.authenticatable? # 新名字

程式碼裡有 100 個地方呼叫 authenticated?,升到 4.0 之後全部噴 NoMethodErrorAPI 改了,舊的用法直接壞掉,不是悄悄出 bug,是直接炸。

對比 Non-Breaking Change

# 3.1.0:密碼驗證有個邊界條件 bug
# 3.1.1:修掉那個 bug,其他行為完全一樣

升 3.1.0 → 3.1.1,程式碼一行都不用改,直接受益。這就是為什麼 ~> 3.1 允許自動升 patch,但擋住 4.0。


標準 Rails Gemfile 範例

source "https://rubygems.org"

# Rails 本身用悲觀鎖定,擋住 major 升級
gem "rails", "~> 8.1"

# 資料庫 adapter 也鎖 minor
gem "pg", "~> 1.1"

# Asset / JS
gem "propshaft"
gem "importmap-rails"

# 一般套件——不寫版本,交給 Gemfile.lock
gem "devise"
gem "pundit"
gem "pagy"
gem "image_processing", "~> 1.2"

gem "dotenv-rails"

group :development, :test do
  gem "debug"
  gem "rspec-rails"
  gem "factory_bot_rails"
end

group :development do
  gem "rubocop-rails-omakase", require: false
end

版本寫法的判斷規則

套件類型 慣例寫法 理由
rails 本身 ~> 8.1 major 升級有 breaking change,要主動決定
資料庫 adapter ~> 1.1 跟 Rails 版本有相依性
一般應用套件 不寫版本 交給 Gemfile.lock 鎖,升版本時再決定
有已知 breaking change 的套件 ~> x.y 防禦性寫法

Engine 開發:先看 Gemfile.lock

當同仁要新開發 Rails Engine,先查母 app 的 Gemfile.lock,不要等 bundle install 衝突再調整。

Gemfile.lock 是母 app 目前實際跑的版本,是唯一的事實來源。Engine 最終要掛進這個 app,.gemspec 的版本範圍必須和 lock 檔相容。

開發流程

Step 1:查母 app 的 Gemfile.lock

# Gemfile.lock 片段
rails (8.1.2)
pg (1.5.6)
devise (4.9.4)
sidekiq (7.3.0)

Step 2:engine 的 .gemspec 對齊這些版本

# my_engine.gemspec
Gem::Specification.new do |s|
  s.name    = "my_engine"
  s.version = "0.1.0"

  # 參考 lock 檔的 rails 版本,用悲觀鎖定
  s.add_dependency "rails",  "~> 8.1"
  s.add_dependency "devise", "~> 4.9"
end

Step 3:母 app 用本地路徑引入 engine

# 母 app 的 Gemfile
gem "my_engine", path: "../my_engine"

bundle install。此時若有衝突,通常是 .gemspec 版本範圍和 lock 檔不相容,調整範圍即可。

為什麼不等衝突再調

  • 衝突訊息往往不直觀,追根源需要時間
  • 版本差距大時,可能要連帶升降其他 gem,影響範圍擴大
  • 多個 engine 同時開發,衝突會疊加

先看 Gemfile.lock 只要 5 分鐘,能省掉後面可能好幾小時的除錯。


開發獨立 Gem:沒有母 App 可以參考

開發獨立 gem(不是掛進特定 app)時,沒有 Gemfile.lock 可以參考,也不應該有

.gemspec 是唯一的聲明

# my_gem.gemspec
Gem::Specification.new do |s|
  s.name    = "my_gem"
  s.version = "0.1.0"

  # 範圍要夠寬,讓使用者的 app 能相容
  s.add_dependency "rails", ">= 7.0", "< 9.0"

  # 開發時才需要,不影響使用者
  s.add_development_dependency "rspec"
  s.add_development_dependency "rubocop"
end

為什麼不能寫 ~> 8.1 gem 被裝進別人的 app,如果鎖 ~> 8.1,用 Rails 7.x 的人就無法安裝。範圍越窄,潛在使用者越少。

Gemfile.lock 不 commit

獨立 gem 的 Gemfile.lock本地開發用的暫存檔,加進 .gitignore

# .gitignore
Gemfile.lock

Ruby 社群的強制慣例。gem 被裝進各種不同的 app,每個 app 的相依樹不同,commit 本地的 lock 檔沒有意義,反而造成困惑。

多版本相容性測試

用 CI 矩陣對每個支援的 Rails 版本跑一次測試:

# .github/workflows/test.yml
strategy:
  matrix:
    rails: ["7.1", "7.2", "8.0", "8.1"]

Engine vs 獨立 Gem 對比

Rails Engine(掛進母 app) 獨立 Gem
版本參考來源 母 app 的 Gemfile.lock 無,自己決定支援範圍
.gemspec 範圍 對齊 lock 檔,範圍較窄 盡量寬,支援多版本
Gemfile.lock commit 進 git 加進 .gitignore
測試策略 跑過母 app 的測試套件即可 CI 矩陣,多版本並行測試

© 2025-2026 Nickle Cheng Built with Ruby Ruby on Rails