Bundle drift detection and correction (#2101)
Gemfile.saas evals Gemfile, so shared gems should have identical versions in both lockfiles. This adds bin/bundle-drift to detect and fix drift: * `bin/bundle-drift check` compares shared gem versions * `bin/bundle-drift correct` seeds Gemfile.lock from Gemfile.saas.lock and re-locks, letting Bundler prune SaaS-only gems while preserving shared versions Adds drift check to bin/ci and GitHub CI. Corrects existing drift.
This commit is contained in:
@@ -7,6 +7,20 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
gemfile-drift:
|
||||||
|
name: Gemfile drift
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: .ruby-version
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- name: Check for lockfile drift
|
||||||
|
run: bin/bundle-drift check
|
||||||
|
|
||||||
security:
|
security:
|
||||||
name: Security
|
name: Security
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
+22
-43
@@ -117,8 +117,8 @@ GEM
|
|||||||
specs:
|
specs:
|
||||||
action_text-trix (2.1.15)
|
action_text-trix (2.1.15)
|
||||||
railties
|
railties
|
||||||
addressable (2.8.8)
|
addressable (2.8.7)
|
||||||
public_suffix (>= 2.0.2, < 8.0)
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
ast (2.4.3)
|
ast (2.4.3)
|
||||||
autotuner (1.1.0)
|
autotuner (1.1.0)
|
||||||
aws-eventstream (1.4.0)
|
aws-eventstream (1.4.0)
|
||||||
@@ -151,8 +151,8 @@ GEM
|
|||||||
brakeman (7.1.1)
|
brakeman (7.1.1)
|
||||||
racc
|
racc
|
||||||
builder (3.3.0)
|
builder (3.3.0)
|
||||||
bundler-audit (0.9.3)
|
bundler-audit (0.9.2)
|
||||||
bundler (>= 1.2.0)
|
bundler (>= 1.2.0, < 3)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
capybara (3.40.0)
|
capybara (3.40.0)
|
||||||
addressable
|
addressable
|
||||||
@@ -172,7 +172,7 @@ GEM
|
|||||||
bigdecimal
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
date (3.5.1)
|
date (3.5.0)
|
||||||
debug (1.11.0)
|
debug (1.11.0)
|
||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.3.8)
|
||||||
@@ -185,15 +185,11 @@ GEM
|
|||||||
tzinfo
|
tzinfo
|
||||||
faker (3.5.2)
|
faker (3.5.2)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
ffi (1.17.2)
|
|
||||||
ffi (1.17.2-aarch64-linux-gnu)
|
ffi (1.17.2-aarch64-linux-gnu)
|
||||||
ffi (1.17.2-aarch64-linux-musl)
|
ffi (1.17.2-aarch64-linux-musl)
|
||||||
ffi (1.17.2-arm-linux-gnu)
|
ffi (1.17.2-arm-linux-gnu)
|
||||||
ffi (1.17.2-arm-linux-musl)
|
ffi (1.17.2-arm-linux-musl)
|
||||||
ffi (1.17.2-arm64-darwin)
|
ffi (1.17.2-arm64-darwin)
|
||||||
ffi (1.17.2-x86-linux-gnu)
|
|
||||||
ffi (1.17.2-x86-linux-musl)
|
|
||||||
ffi (1.17.2-x86_64-darwin)
|
|
||||||
ffi (1.17.2-x86_64-linux-gnu)
|
ffi (1.17.2-x86_64-linux-gnu)
|
||||||
ffi (1.17.2-x86_64-linux-musl)
|
ffi (1.17.2-x86_64-linux-musl)
|
||||||
fugit (1.12.1)
|
fugit (1.12.1)
|
||||||
@@ -223,7 +219,7 @@ GEM
|
|||||||
actionview (>= 7.0.0)
|
actionview (>= 7.0.0)
|
||||||
activesupport (>= 7.0.0)
|
activesupport (>= 7.0.0)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.18.0)
|
json (2.17.1)
|
||||||
jwt (3.1.2)
|
jwt (3.1.2)
|
||||||
base64
|
base64
|
||||||
kamal (2.9.0)
|
kamal (2.9.0)
|
||||||
@@ -260,7 +256,6 @@ GEM
|
|||||||
mini_magick (5.3.1)
|
mini_magick (5.3.1)
|
||||||
logger
|
logger
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.9)
|
|
||||||
minitest (5.26.2)
|
minitest (5.26.2)
|
||||||
mission_control-jobs (1.1.0)
|
mission_control-jobs (1.1.0)
|
||||||
actioncable (>= 7.1)
|
actioncable (>= 7.1)
|
||||||
@@ -272,7 +267,7 @@ GEM
|
|||||||
railties (>= 7.1)
|
railties (>= 7.1)
|
||||||
stimulus-rails
|
stimulus-rails
|
||||||
turbo-rails
|
turbo-rails
|
||||||
mittens (0.3.1)
|
mittens (0.3.0)
|
||||||
mocha (2.8.2)
|
mocha (2.8.2)
|
||||||
ruby2_keywords (>= 0.0.5)
|
ruby2_keywords (>= 0.0.5)
|
||||||
msgpack (1.8.0)
|
msgpack (1.8.0)
|
||||||
@@ -293,9 +288,6 @@ GEM
|
|||||||
net-protocol
|
net-protocol
|
||||||
net-ssh (7.3.0)
|
net-ssh (7.3.0)
|
||||||
nio4r (2.7.5)
|
nio4r (2.7.5)
|
||||||
nokogiri (1.18.10)
|
|
||||||
mini_portile2 (~> 2.8.2)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.18.10-aarch64-linux-gnu)
|
nokogiri (1.18.10-aarch64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.10-aarch64-linux-musl)
|
nokogiri (1.18.10-aarch64-linux-musl)
|
||||||
@@ -306,8 +298,6 @@ GEM
|
|||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.10-arm64-darwin)
|
nokogiri (1.18.10-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.10-x86_64-darwin)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.18.10-x86_64-linux-gnu)
|
nokogiri (1.18.10-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.10-x86_64-linux-musl)
|
nokogiri (1.18.10-x86_64-linux-musl)
|
||||||
@@ -329,10 +319,10 @@ GEM
|
|||||||
actionpack (>= 7.0.0)
|
actionpack (>= 7.0.0)
|
||||||
activesupport (>= 7.0.0)
|
activesupport (>= 7.0.0)
|
||||||
rack
|
rack
|
||||||
psych (5.3.0)
|
psych (5.2.6)
|
||||||
date
|
date
|
||||||
stringio
|
stringio
|
||||||
public_suffix (7.0.0)
|
public_suffix (6.0.2)
|
||||||
puma (7.1.0)
|
puma (7.1.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
@@ -345,7 +335,7 @@ GEM
|
|||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
rack-test (2.2.0)
|
rack-test (2.2.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rackup (2.3.1)
|
rackup (2.2.1)
|
||||||
rack (>= 3)
|
rack (>= 3)
|
||||||
rails-dom-testing (2.3.0)
|
rails-dom-testing (2.3.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
@@ -356,7 +346,7 @@ GEM
|
|||||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.3.1)
|
rake (13.3.1)
|
||||||
rdoc (6.17.0)
|
rdoc (6.16.1)
|
||||||
erb
|
erb
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
tsort
|
tsort
|
||||||
@@ -366,10 +356,10 @@ GEM
|
|||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
rexml (3.4.4)
|
rexml (3.4.4)
|
||||||
rouge (4.6.1)
|
rouge (4.6.1)
|
||||||
rqrcode (3.1.1)
|
rqrcode (3.1.0)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
rqrcode_core (~> 2.0)
|
rqrcode_core (~> 2.0)
|
||||||
rqrcode_core (2.0.1)
|
rqrcode_core (2.0.0)
|
||||||
rubocop (1.81.7)
|
rubocop (1.81.7)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (~> 3.17.0.2)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
@@ -388,7 +378,7 @@ GEM
|
|||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rubocop (>= 1.75.0, < 2.0)
|
rubocop (>= 1.75.0, < 2.0)
|
||||||
rubocop-ast (>= 1.47.1, < 2.0)
|
rubocop-ast (>= 1.47.1, < 2.0)
|
||||||
rubocop-rails (2.34.1)
|
rubocop-rails (2.34.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
@@ -427,18 +417,13 @@ GEM
|
|||||||
fugit (~> 1.11)
|
fugit (~> 1.11)
|
||||||
railties (>= 7.1)
|
railties (>= 7.1)
|
||||||
thor (>= 1.3.1)
|
thor (>= 1.3.1)
|
||||||
sqlite3 (2.8.1)
|
sqlite3 (2.8.0-aarch64-linux-gnu)
|
||||||
mini_portile2 (~> 2.8.0)
|
sqlite3 (2.8.0-aarch64-linux-musl)
|
||||||
sqlite3 (2.8.1-aarch64-linux-gnu)
|
sqlite3 (2.8.0-arm-linux-gnu)
|
||||||
sqlite3 (2.8.1-aarch64-linux-musl)
|
sqlite3 (2.8.0-arm-linux-musl)
|
||||||
sqlite3 (2.8.1-arm-linux-gnu)
|
sqlite3 (2.8.0-arm64-darwin)
|
||||||
sqlite3 (2.8.1-arm-linux-musl)
|
sqlite3 (2.8.0-x86_64-linux-gnu)
|
||||||
sqlite3 (2.8.1-arm64-darwin)
|
sqlite3 (2.8.0-x86_64-linux-musl)
|
||||||
sqlite3 (2.8.1-x86-linux-gnu)
|
|
||||||
sqlite3 (2.8.1-x86-linux-musl)
|
|
||||||
sqlite3 (2.8.1-x86_64-darwin)
|
|
||||||
sqlite3 (2.8.1-x86_64-linux-gnu)
|
|
||||||
sqlite3 (2.8.1-x86_64-linux-musl)
|
|
||||||
sshkit (1.24.0)
|
sshkit (1.24.0)
|
||||||
base64
|
base64
|
||||||
logger
|
logger
|
||||||
@@ -453,9 +438,8 @@ GEM
|
|||||||
thruster (0.1.16)
|
thruster (0.1.16)
|
||||||
thruster (0.1.16-aarch64-linux)
|
thruster (0.1.16-aarch64-linux)
|
||||||
thruster (0.1.16-arm64-darwin)
|
thruster (0.1.16-arm64-darwin)
|
||||||
thruster (0.1.16-x86_64-darwin)
|
|
||||||
thruster (0.1.16-x86_64-linux)
|
thruster (0.1.16-x86_64-linux)
|
||||||
timeout (0.5.0)
|
timeout (0.4.4)
|
||||||
trilogy (2.9.0)
|
trilogy (2.9.0)
|
||||||
tsort (0.2.0)
|
tsort (0.2.0)
|
||||||
turbo-rails (2.0.20)
|
turbo-rails (2.0.20)
|
||||||
@@ -497,11 +481,6 @@ PLATFORMS
|
|||||||
arm-linux-gnu
|
arm-linux-gnu
|
||||||
arm-linux-musl
|
arm-linux-musl
|
||||||
arm64-darwin
|
arm64-darwin
|
||||||
arm64-linux
|
|
||||||
ruby
|
|
||||||
x86-linux-gnu
|
|
||||||
x86-linux-musl
|
|
||||||
x86_64-darwin
|
|
||||||
x86_64-linux
|
x86_64-linux
|
||||||
x86_64-linux-gnu
|
x86_64-linux-gnu
|
||||||
x86_64-linux-musl
|
x86_64-linux-musl
|
||||||
|
|||||||
Executable
+128
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# Checks that Gemfile.lock and Gemfile.saas.lock are in sync for shared dependencies.
|
||||||
|
# Since Gemfile.saas evals Gemfile, shared gems should have identical versions.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bin/bundle-drift [check] # check for drift (default subcommand)
|
||||||
|
# bin/bundle-drift correct # restore alignment (Gemfile.saas.lock is authoritative)
|
||||||
|
require "bundler"
|
||||||
|
require "fileutils"
|
||||||
|
|
||||||
|
GEMFILE_LOCK = "Gemfile.lock"
|
||||||
|
GEMFILE_SAAS_LOCK = "Gemfile.saas.lock"
|
||||||
|
|
||||||
|
class GemfileDriftChecker
|
||||||
|
def initialize
|
||||||
|
@oss_lockfile = parse_lockfile(GEMFILE_LOCK)
|
||||||
|
@saas_lockfile = parse_lockfile(GEMFILE_SAAS_LOCK)
|
||||||
|
end
|
||||||
|
|
||||||
|
def check
|
||||||
|
find_drift.tap do
|
||||||
|
report it
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def parse_lockfile(path)
|
||||||
|
Bundler::LockfileParser.new(File.read(path))
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_drift
|
||||||
|
oss_specs, saas_specs = specs_hash(@oss_lockfile), specs_hash(@saas_lockfile)
|
||||||
|
shared_gems = oss_specs.keys & saas_specs.keys
|
||||||
|
|
||||||
|
shared_gems.filter_map do |name|
|
||||||
|
oss_version, saas_version = oss_specs[name], saas_specs[name]
|
||||||
|
if oss_version != saas_version
|
||||||
|
{ name: name, oss: oss_version, saas: saas_version }
|
||||||
|
end
|
||||||
|
end.sort_by { |d| d[:name] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def specs_hash(lockfile)
|
||||||
|
lockfile.specs.to_h { |spec| [ spec.name, spec.version.to_s ] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def report(drift)
|
||||||
|
if drift.empty?
|
||||||
|
puts "✓ Gemfile.lock and Gemfile.saas.lock are in sync"
|
||||||
|
else
|
||||||
|
puts "✗ Gemfile lock files have drifted!\n\n"
|
||||||
|
|
||||||
|
name_width = [ drift.map { |d| d[:name].length }.max, "Gem".length ].max
|
||||||
|
oss_width = [ drift.map { |d| d[:oss].length }.max, "Gemfile.lock".length ].max
|
||||||
|
saas_width = [ drift.map { |d| d[:saas].length }.max, "Gemfile.saas.lock".length ].max
|
||||||
|
|
||||||
|
puts " #{"Gem".ljust(name_width)} #{"Gemfile.lock".ljust(oss_width)} Gemfile.saas.lock"
|
||||||
|
puts " #{"-" * name_width} #{"-" * oss_width} #{"-" * saas_width}"
|
||||||
|
|
||||||
|
drift.each do |d|
|
||||||
|
puts " #{d[:name].ljust(name_width)} #{d[:oss].ljust(oss_width)} #{d[:saas]}"
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "\nRun 'bin/bundle-drift correct' to restore alignment."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class GemfileDriftCorrector
|
||||||
|
def correct
|
||||||
|
drift = GemfileDriftChecker.new.check
|
||||||
|
return puts "\nNothing to correct." if drift.empty?
|
||||||
|
|
||||||
|
puts "\nRestoring alignment (Gemfile.saas.lock is authoritative)...\n\n"
|
||||||
|
|
||||||
|
# Save original for diff
|
||||||
|
original_content = File.read(GEMFILE_LOCK)
|
||||||
|
|
||||||
|
# Seed Gemfile.lock with Gemfile.saas.lock - Bundler will use these as version hints
|
||||||
|
FileUtils.cp(GEMFILE_SAAS_LOCK, GEMFILE_LOCK)
|
||||||
|
|
||||||
|
# Re-lock: Bundler prunes SaaS-only gems while preserving shared versions
|
||||||
|
puts "▸ Re-locking Gemfile (seeded from Gemfile.saas.lock)"
|
||||||
|
unless system("BUNDLE_GEMFILE=Gemfile bundle lock")
|
||||||
|
File.write(GEMFILE_LOCK, original_content)
|
||||||
|
abort("Failed to lock Gemfile. Restored original.")
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "\n▸ Verifying alignment"
|
||||||
|
new_drift = GemfileDriftChecker.new.check
|
||||||
|
|
||||||
|
if new_drift.empty?
|
||||||
|
puts "\n✓ Lock files are now in sync"
|
||||||
|
show_diff(original_content, File.read(GEMFILE_LOCK))
|
||||||
|
else
|
||||||
|
puts "\n✗ Lock files still have drift after correction."
|
||||||
|
puts " Bundler couldn't resolve to matching versions."
|
||||||
|
puts " Restoring original Gemfile.lock."
|
||||||
|
File.write(GEMFILE_LOCK, original_content)
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def show_diff(original, corrected)
|
||||||
|
require "tempfile"
|
||||||
|
|
||||||
|
Tempfile.create("gemfile-lock-original") do |f|
|
||||||
|
f.write(original)
|
||||||
|
f.flush
|
||||||
|
|
||||||
|
diff = `diff -u #{f.path} #{GEMFILE_LOCK} 2>/dev/null`
|
||||||
|
unless diff.empty?
|
||||||
|
puts "\nChanges made to Gemfile.lock:"
|
||||||
|
puts diff
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
case command = ARGV[0] || "check"
|
||||||
|
when "check"
|
||||||
|
exit 1 unless GemfileDriftChecker.new.check.empty?
|
||||||
|
when "correct"
|
||||||
|
GemfileDriftCorrector.new.correct
|
||||||
|
else
|
||||||
|
abort "Usage: bin/bundle-drift [check|correct]"
|
||||||
|
end
|
||||||
@@ -11,6 +11,7 @@ CI.run do
|
|||||||
|
|
||||||
step "Style: Ruby", "bin/rubocop"
|
step "Style: Ruby", "bin/rubocop"
|
||||||
|
|
||||||
|
step "Gemfile: Drift check", "bin/bundle-drift check"
|
||||||
step "Security: Gem audit", "bin/bundler-audit check --update"
|
step "Security: Gem audit", "bin/bundler-audit check --update"
|
||||||
step "Security: Importmap audit", "bin/importmap audit"
|
step "Security: Importmap audit", "bin/importmap audit"
|
||||||
step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
|
step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
|
||||||
|
|||||||
Reference in New Issue
Block a user