diff --git a/.github/workflows/update_lockfiles.yml b/.github/workflows/update_lockfiles.yml index af20bd5e2..1dc9b0e0c 100644 --- a/.github/workflows/update_lockfiles.yml +++ b/.github/workflows/update_lockfiles.yml @@ -1,14 +1,15 @@ name: Update lockfiles -# Generates one committed, checksummed lockfile per test-matrix cell so that CI -# installs are fully pinned (supply-chain hardening). Run manually to create the -# initial lockfiles, and on a schedule to refresh them deliberately. +# Generates the committed, checksummed lockfiles that fully pin CI installs +# (supply-chain hardening): one per test-matrix cell, plus one per e2e app under +# spec/apps. Run manually to create the initial lockfiles, and on a schedule to +# refresh them deliberately. # # Under MISE_ENV=ci the mise-action step installs every Ruby declared in # .mise.ci.toml; bin/relock then sweeps every gem's test-matrix.json — the # single source of truth, shared with the *_test.yml workflows — resolving each -# cell against its matching Ruby via `mise exec`. One invocation regenerates the -# whole lock set. +# cell against its matching Ruby via `mise exec`, and each app against the +# oldest Ruby in the e2e matrix. One invocation regenerates the whole lock set. on: workflow_dispatch: @@ -60,11 +61,15 @@ jobs: - name: Create branch id: create-branch run: | + # Two patterns because an app locks against a committed Gemfile of its + # own rather than a generated wrapper. + LOCKS=('**/gemfiles/*.gemfile.lock' 'spec/apps/*/Gemfile.lock') + # Stage first, then diff the index against HEAD. `git diff` alone only # sees tracked files, so newly generated (untracked) locks — i.e. the # bootstrap run and any filled-in missing cell — would otherwise look # like "no change" and never get pushed. - git add '**/gemfiles/*.gemfile.lock' + git add "${LOCKS[@]}" if git diff --cached --quiet; then echo "No lockfile changes; nothing to do." @@ -76,13 +81,15 @@ jobs: SHORT_SHA="${GITHUB_SHA:0:12}" BRANCH_NAME="lockfiles/update-$(date +%m-%d)-${SHORT_SHA}" - # Which gems changed: the top-level dir of each touched lock. - GEMS=$(git diff --cached --name-only -- '**/gemfiles/*.gemfile.lock' | cut -d/ -f1 | sort -u) + # What changed: the gem name for a matrix lock, the app name for an app. + GEMS=$(git diff --cached --name-only -- "${LOCKS[@]}" \ + | sed -E 's|^spec/apps/([^/]+)/Gemfile\.lock$|\1|; s|^([^/]+)/gemfiles/.*|\1|' \ + | sort -u) # Aggregate dependency version changes across every touched lock. Pair # the removed (-) and added (+) "name (version)" spec lines per gem so # the summary reads "gem: old → new" (deduped across cells). - DEPS=$(git diff --cached --text -U0 -- '**/gemfiles/*.gemfile.lock' \ + DEPS=$(git diff --cached --text -U0 -- "${LOCKS[@]}" \ | { grep -E '^[+-] +[A-Za-z0-9_.-]+ \([0-9]' || true; } \ | sed -E 's/^([+-]) +([A-Za-z0-9_.-]+) \(([^)]+)\).*/\1 \2 \3/' \ | awk '{ if ($1 == "-") old[$2] = $3; else neu[$2] = $3 } @@ -120,11 +127,11 @@ jobs: const deps = (process.env.DEPS || '').trim(); const gemsList = gems.length ? gems.map(g => `\`${g}\``).join(', ') : '_none_'; const depsBlock = deps || '_No dependency version changes (checksum/metadata only)._'; - const prBody = `Automated regeneration of the per-matrix lockfiles used to pin CI dependencies (supply-chain hardening). + const prBody = `Automated regeneration of the committed lockfiles used to pin CI dependencies (supply-chain hardening): one per test-matrix cell, plus the e2e apps. #skip-changelog - ## Gems updated + ## Bundles updated ${gemsList} ## Dependency changes diff --git a/.gitignore b/.gitignore index 302e83806..33692808c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,12 @@ log/ *.gem # Per-matrix lockfiles (/gemfiles/*.gemfile.lock) ARE committed (supply-chain # pinning). The wrapper *.gemfile is regenerated on the fly (identical one-liner), -# so ignore it. Default locally-resolved lockfiles also stay ignored. +# so ignore it. Default locally-resolved lockfiles also stay ignored, except the +# e2e rails-mini app: its lock IS committed so every e2e run pins the same +# dependency set rather than resolving whatever is newest. */gemfiles/*.gemfile Gemfile.lock +!spec/apps/rails-mini/Gemfile.lock .coveralls.yml .ruby-version .ruby-gemset diff --git a/bin/lib/matrix.rb b/bin/lib/matrix.rb index c0ef028c3..16ca9fe84 100644 --- a/bin/lib/matrix.rb +++ b/bin/lib/matrix.rb @@ -8,8 +8,13 @@ # Each cell runs under a matching Ruby provided by mise (https://mise.jdx.dev); # the required Rubies are declared in .mise.ci.toml and installed with # `mise --env ci install`. +# +# The e2e apps under spec/apps are cells too, of a second shape: they have a +# Gemfile of their own rather than a generated wrapper, and no matrix axes. Only +# bin/relock expands them (bin/test runs gem specs, not the apps). require "json" +require "yaml" module Matrix ROOT = File.expand_path("../..", __dir__) @@ -22,9 +27,27 @@ module Matrix "sidekiq" => "SIDEKIQ_VERSION" }.freeze - Cell = Struct.new(:gem, :base, :ruby, :env, :rubyopt, keyword_init: true) do + # Apps whose lock is committed, so that an e2e run pins its dependencies + # instead of resolving whatever is newest — the same reason the gems do. + APP_DIRS = ["spec/apps/rails-mini"].freeze + + # The e2e job matrix, i.e. which Rubies the app locks must satisfy. + E2E_WORKFLOW = ".github/workflows/e2e_tests.yml" + + Cell = Struct.new(:gem, :base, :ruby, :env, :rubyopt, :dir, :gemfile, keyword_init: true) do + def dir + self[:dir] || gem + end + + # Matrix cells resolve through a generated wrapper that eval_gemfile's the + # gem's own Gemfile; apps point straight at theirs. def wrapper - "#{gem}/gemfiles/#{base}.gemfile" + self[:gemfile] || "#{gem}/gemfiles/#{base}.gemfile" + end + + # An app's Gemfile is committed, so only wrappers are ours to (re)write. + def generated_wrapper? + self[:gemfile].nil? end def lock @@ -32,7 +55,7 @@ def lock end def label - "#{gem} / #{base}" + generated_wrapper? ? "#{gem} / #{base}" : base end end @@ -116,6 +139,40 @@ def all_gems Dir.glob(File.join(ROOT, "*", "test-matrix.json")).map { |p| File.basename(File.dirname(p)) }.sort end + def all_apps + APP_DIRS.map { |dir| File.basename(dir) } + end + + # One committed lock per app serves every Ruby in the e2e matrix, so resolve + # under the OLDEST of them: a resolution that installs there installs on the + # newer ones too, not the other way round. The workflow's own matrix is the + # source of truth here, the way test-matrix.json is for the gems. + def oldest_e2e_ruby + @oldest_e2e_ruby ||= begin + rubies = YAML.load_file(File.join(ROOT, E2E_WORKFLOW), aliases: true) + .dig("jobs", "e2e-tests", "strategy", "matrix", "ruby") + abort "Could not read the e2e Ruby matrix from #{E2E_WORKFLOW}" unless rubies.is_a?(Array) && rubies.any? + + rubies.map { |r| r.fetch("flavor") }.min_by { |flavor| Gem::Version.new(flavor) } + end + end + + def app_cells(apps = all_apps) + apps.map do |app| + dir = APP_DIRS.find { |d| File.basename(d) == app } + abort "Unknown app '#{app}'. Known apps: #{all_apps.join(', ')}" unless dir + + Cell.new(base: app, ruby: oldest_e2e_ruby, env: {}, dir: dir, gemfile: "#{dir}/Gemfile") + end + end + + # Resolve a --cell path against the apps. Their lock is /Gemfile.lock, so + # there's no gemfiles/ segment for cell_path_parts to key off. + def app_cell_for(path) + dir = path.delete_prefix("./").sub(%r{/Gemfile(\.lock)?\z}, "") + app_cells.find { |cell| cell.dir == dir } + end + def mise_bin @mise_bin ||= begin found = `sh -lc 'command -v mise' 2>/dev/null`.strip diff --git a/bin/relock b/bin/relock index da7af3363..3c27222c6 100755 --- a/bin/relock +++ b/bin/relock @@ -11,6 +11,11 @@ # so the locks can never drift from the matrix CI actually runs. Edit the matrix # to add/remove a cell, then run this to materialize the gemfiles and locks. # +# The e2e apps under `spec/apps` are covered too. They are ordinary Bundler +# projects — a committed Gemfile, no matrix axes, one lock each — so there is no +# wrapper to generate; only the lock is regenerated. One lock serves every Ruby +# in the e2e job matrix, so each app resolves under the oldest of them. +# # Each cell must resolve against its own Ruby (gemspecs gate on # required_ruby_version), so every cell runs under a matching Ruby provided by # mise (https://mise.jdx.dev). The required Rubies are declared in @@ -18,9 +23,11 @@ # script resolves against those already-installed Rubies and aborts if any are # missing. # -# bin/relock # refresh every cell +# bin/relock # refresh every cell, apps included # bin/relock --gem sentry-ruby # one gem +# bin/relock --app rails-mini # one e2e app # bin/relock --cell sentry-ruby/gemfiles/ruby-3.2_rack-3_redis-5.gemfile +# bin/relock --cell spec/apps/rails-mini/Gemfile # bin/relock --force # delete each lock before resolving # bin/relock -l # list every cell's lock path # @@ -34,14 +41,19 @@ require "optparse" require_relative "lib/matrix" include Matrix -# Shell run under the cell's Ruby. Writes the wrapper, re-resolves, adds -# checksums, and seeds x86_64-linux-gnu into PLATFORMS. +# Shell run under the cell's Ruby. Re-resolves, adds checksums, and seeds +# x86_64-linux-gnu into PLATFORMS. +# +# WRAPPER is set only for per-matrix cells; an app's Gemfile is committed rather +# than generated, so writing one would clobber it. # # When FORCE is set, the existing lock is deleted first so bundler resolves from scratch, use for edge cases. RESOLVE = <<~SH set -euo pipefail mkdir -p "$(dirname "$BUNDLE_GEMFILE")" - echo 'eval_gemfile "../Gemfile"' > "$BUNDLE_GEMFILE" + if [ -n "${WRAPPER:-}" ]; then + echo 'eval_gemfile "../Gemfile"' > "$BUNDLE_GEMFILE" + fi if [ -n "${FORCE:-}" ]; then echo "+ FORCE: removing $BUNDLE_GEMFILE.lock" rm -f "$BUNDLE_GEMFILE.lock" @@ -63,6 +75,7 @@ def run_mise(cell, force: false) # re-source the profile and reset Ruby back to the host default. argv = [mise_bin, "exec", "ruby@#{cell.ruby}", "--", "bash", "-c", RESOLVE] env = cell_env(cell) + env = env.merge("WRAPPER" => "1") if cell.generated_wrapper? env = env.merge("FORCE" => "1") if force env = env.merge("JRUBY" => "1") if cell.ruby.start_with?("jruby") [env, argv] @@ -72,6 +85,7 @@ end opts = { gems: [], + apps: [], cell: nil, list: false, force: false @@ -80,6 +94,7 @@ opts = { parser = OptionParser.new do |o| o.banner = "Usage: bin/relock [options]" o.on("--gem NAME", "Only one gem (repeatable). Default: all.") { |v| opts[:gems] << v } + o.on("--app NAME", "Only one e2e app (repeatable). Default: all.") { |v| opts[:apps] << v } o.on("--cell PATH", "Resolve exactly one cell by its wrapper/lock path.") { |v| opts[:cell] = v } o.on("-l", "--list", "List every cell's lock path, one per line; do nothing.") { opts[:list] = true } o.on("--force", "Delete each lock before resolving (resolve from scratch).") { opts[:force] = true } @@ -90,13 +105,16 @@ parser.parse!(ARGV) # ---- select cells -------------------------------------------------------- if opts[:cell] - # single explicit cell - gem, base = cell_path_parts(opts[:cell]) - abort "--cell must point at a /gemfiles/.gemfile path" unless gem && base - cells = [parse_cell(gem, base)] + cells = [app_cell_for(opts[:cell])].compact + if cells.empty? + gem, base = cell_path_parts(opts[:cell]) + abort "--cell must point at a /gemfiles/.gemfile or /Gemfile path" unless gem && base + cells = [parse_cell(gem, base)] + end +elsif opts[:gems].empty? && opts[:apps].empty? + cells = discover_cells(all_gems) + app_cells else - gems = opts[:gems].empty? ? all_gems : opts[:gems] - cells = discover_cells(gems) + cells = discover_cells(opts[:gems]) + app_cells(opts[:apps]) end if cells.empty? @@ -152,9 +170,9 @@ cells.each_with_index do |cell, i| env, argv = run_mise(cell, force: opts[:force]) cell_started = Time.now - # chdir into the gem dir to match CI's working-directory (avoids stray - # files and keeps any .bundle/ config local to the gem). - ok = system(env, *argv, chdir: File.join(ROOT, cell.gem)) + # chdir into the gem/app dir to match CI's working-directory (avoids stray + # files and keeps any .bundle/ config local to it). + ok = system(env, *argv, chdir: File.join(ROOT, cell.dir)) elapsed = fmt_duration(Time.now - cell_started) if ok diff --git a/spec/apps/rails-mini/Gemfile b/spec/apps/rails-mini/Gemfile index 595eeca09..1203bae85 100644 --- a/spec/apps/rails-mini/Gemfile +++ b/spec/apps/rails-mini/Gemfile @@ -11,8 +11,8 @@ gem 'activerecord' gem 'activejob' gem 'sqlite3' -gem 'sentry-ruby', path: Pathname(__dir__).join("../../..").realpath -gem 'sentry-rails', path: Pathname(__dir__).join("../../..").realpath +gem 'sentry-ruby', path: '../../..' +gem 'sentry-rails', path: '../../..' gem 'sidekiq' gem 'resque' gem 'delayed_job_active_record' diff --git a/spec/apps/rails-mini/Gemfile.lock b/spec/apps/rails-mini/Gemfile.lock new file mode 100644 index 000000000..7123c50fa --- /dev/null +++ b/spec/apps/rails-mini/Gemfile.lock @@ -0,0 +1,327 @@ +PATH + remote: ../../.. + specs: + sentry-rails (7.0.0) + railties (>= 5.2.0) + sentry-ruby (~> 7.0.0) + sentry-ruby (7.0.0) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + logger + +GEM + remote: https://rubygems.org/ + specs: + actionpack (8.1.3.1) + actionview (= 8.1.3.1) + activesupport (= 8.1.3.1) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actionview (8.1.3.1) + activesupport (= 8.1.3.1) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.3.1) + activesupport (= 8.1.3.1) + globalid (>= 0.3.6) + activemodel (8.1.3.1) + activesupport (= 8.1.3.1) + activerecord (8.1.3.1) + activemodel (= 8.1.3.1) + activesupport (= 8.1.3.1) + timeout (>= 0.4.0) + activesupport (8.1.3.1) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.1.2) + builder (3.3.0) + concurrent-ruby (1.3.8) + connection_pool (3.0.2) + crass (1.0.7) + delayed_job (4.2.0) + activesupport (>= 3.0, < 9.0) + benchmark + logger + delayed_job_active_record (4.1.11) + activerecord (>= 3.0, < 9.0) + delayed_job (>= 3.0, < 5) + drb (2.2.3) + erb (6.0.7) + erubi (1.13.1) + et-orbi (1.4.2) + tzinfo + fugit (1.13.0) + et-orbi (~> 1.4) + raabro (~> 1.4) + globalid (1.4.0) + activesupport (>= 6.1) + i18n (1.15.2) + concurrent-ruby (~> 1.0) + io-console (0.9.2) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.21.2) + logger (1.7.0) + loofah (2.25.2) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + minitest (6.0.6) + drb (~> 2.0) + prism (~> 1.5) + mono_logger (1.1.2) + multi_json (1.21.1) + mustermann (3.1.1) + nio4r (2.7.5) + nokogiri (1.19.4-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.19.4-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.19.4-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-linux-musl) + racc (~> 1.4) + pp (0.6.4) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + puma (8.0.2) + nio4r (~> 2.0) + raabro (1.5.0) + racc (1.8.1) + rack (3.2.7) + rack-protection (4.2.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-session (2.1.2) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.3.1) + rack (>= 3) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.1) + loofah (~> 2.25, >= 2.25.2) + 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) + railties (8.1.3.1) + actionpack (= 8.1.3.1) + activesupport (= 8.1.3.1) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rake (13.4.2) + rbs (4.2.0) + logger + prism (>= 1.6.0) + tsort + rdoc (8.0.0) + erb + prism (>= 1.6.0) + rbs (>= 4.0.0) + tsort + redis (6.0.0) + redis-client (= 0.30.1) + redis-client (0.30.1) + connection_pool + redis-namespace (1.11.0) + redis (>= 4) + reline (0.7.0) + io-console (~> 0.5) + resque (3.0.2) + base64 (~> 0.1) + logger + mono_logger (~> 1) + multi_json (~> 1.0) + redis (>= 4.0) + redis-namespace (~> 1.6) + sinatra (>= 2.0) + securerandom (0.4.1) + sidekiq (8.1.7) + connection_pool (>= 3.0.0) + json (>= 2.16.0) + logger (>= 1.7.0) + rack (>= 3.2.0) + redis-client (>= 0.29.0) + sinatra (4.2.1) + logger (>= 1.6.0) + mustermann (~> 3.0) + rack (>= 3.0.0, < 4) + rack-protection (= 4.2.1) + rack-session (>= 2.0.0, < 3) + tilt (~> 2.0) + solid_queue (1.7.0) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11) + railties (>= 7.1) + thor (>= 1.3.1) + sqlite3 (2.9.6-aarch64-linux-gnu) + sqlite3 (2.9.6-aarch64-linux-musl) + sqlite3 (2.9.6-arm-linux-gnu) + sqlite3 (2.9.6-arm-linux-musl) + sqlite3 (2.9.6-arm64-darwin) + sqlite3 (2.9.6-x86_64-darwin) + sqlite3 (2.9.6-x86_64-linux-gnu) + sqlite3 (2.9.6-x86_64-linux-musl) + thor (1.5.0) + tilt (2.9.0) + timeout (0.6.1) + tsort (0.2.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uri (1.1.1) + useragent (0.16.11) + zeitwerk (2.8.3) + +PLATFORMS + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + actionpack + activejob + activerecord + delayed_job_active_record + puma + railties + rake + resque + sentry-rails! + sentry-ruby! + sidekiq + solid_queue + sqlite3 + +CHECKSUMS + actionpack (8.1.3.1) sha256=974cb7154548e81f470b1b0f247b99cb38e87825899dca58610596e2817723d0 + actionview (8.1.3.1) sha256=2da68b8414c47b43bfbed1ce69c5afe1c04f78c267aacb5660a4cab5ca12cfb6 + activejob (8.1.3.1) sha256=1c8dd275df930df40deecffec63d913a550a33fd94bd298f69721dd96939954a + activemodel (8.1.3.1) sha256=99cc02ce2faec371d14440949d85787ebd23a907c9baef0a9d4bcd4d21888f88 + activerecord (8.1.3.1) sha256=0a2fb6c28f4938f6b013a3a549bec0a7e37d535f3dc8990e804bcc3258c0403b + activesupport (8.1.3.1) sha256=85458765f25ea48b9019c46b6bb3fa5683197bf4280d9f06710a6e8d7a831376 + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f + bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785 + concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1 + connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a + crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295 + delayed_job (4.2.0) sha256=51dc1f68bb9084398317884c2178debd38c13a7eb48d8aa4ff4775465d7713ef + delayed_job_active_record (4.1.11) sha256=64f34a6d50316dd7a7df1daf7c6e04ad888c0e86938762318f6f52522b7c1ba8 + drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 + erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92 + erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 + et-orbi (1.4.2) sha256=bb555dae668419cb24caa2a293a170e58be6d4df1e017c51f5030bdc133cd20c + fugit (1.13.0) sha256=a4f093fce740da52f216740a5041e2a594ea763cdb89e8b2754ca4399634ab18 + globalid (1.4.0) sha256=037f12fbf1d9d7a014d501c2d5c77356fd4ddd96d7a7991d6700bba96706f427 + i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5 + io-console (0.9.2) sha256=efa74f891dd03c0939a931dfc6e74c2813d904763d456ea9762b0525e748db08 + irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 + json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + loofah (2.25.2) sha256=2007f746959ac65552456e04b433e83deb22759ab38c838b4445c70e43425918 + minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1 + mono_logger (1.1.2) sha256=2e359def7007f5c908aadd953687991fe667995d14ae5f0d10dda76e3e8670f7 + multi_json (1.21.1) sha256=e6126a31808e3b4d19f483c775ceac34df190dffa62adfb63a165ee14ba68080 + mustermann (3.1.1) sha256=4c6170c7234d5499c345562ba7c7dfe73e1754286dcc1abb053064d66a127198 + nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 + nokogiri (1.19.4-aarch64-linux-gnu) sha256=1269fb644a6de405057a53dd5c762b1209b43ca7424f839454d3dbc677c31a8f + nokogiri (1.19.4-aarch64-linux-musl) sha256=35c65b9ce72b3bb03207bdbe7067915019dc18c1b9b59139684bd6690fdd01af + nokogiri (1.19.4-arm-linux-gnu) sha256=a301313e38bb065d68239e79734bcd6f56fb6efaacebde29e9abf2a4735340ca + nokogiri (1.19.4-arm-linux-musl) sha256=588923c101bcfa78869734d247d25b598674323e7f22474fc468f6e5647311eb + nokogiri (1.19.4-arm64-darwin) sha256=a46db9853286e6597b36ebc6953817d15acf3a299583eb3f89fdc6f91dd63527 + nokogiri (1.19.4-x86_64-darwin) sha256=7fd17057d3e1f00e9954a74b3cd76595d3d4a5ef233b7ed9599047c204f70551 + nokogiri (1.19.4-x86_64-linux-gnu) sha256=379fae440b28915e3f19d752ce2dcf8465ed2b2fbefd2a7ca0dd497bc981a06a + nokogiri (1.19.4-x86_64-linux-musl) sha256=17dfb7c1fa194ae02fbf7c51a7afc8d278045ab3fdacfd86f91d02d7b274470b + pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570 + prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 + puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb + raabro (1.5.0) sha256=3f998a7bc84f9c84df3ab580634d2e0a5bda4f0841168d56035f529c9877440a + racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f + rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7 + rack-protection (4.2.1) sha256=cf6e2842df8c55f5e4d1a4be015e603e19e9bc3a7178bae58949ccbb58558bac + rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8 + rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 + rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 + rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d + rails-html-sanitizer (1.7.1) sha256=e797a7c9b01e567307e317c576b49ab4168017e63eea4dba9ce3cb587e2f22c2 + railties (8.1.3.1) sha256=2388a232579a00cefea4487de66c8553c3408c1300abdc6cf1799d86ffb04487 + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674 + rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469 + redis (6.0.0) sha256=de71c10edd106986b759ec7ecdd08b63b9c0ee7414a0d0c1da73d31ba2bccda6 + redis-client (0.30.1) sha256=5151bc5c7bbfe48623732cdae3b900d8a22dc691cc7cdfacfb351ac55116522d + redis-namespace (1.11.0) sha256=e91a1aa2b2d888b6dea1d4ab8d39e1ae6fac3426161feb9d91dd5cca598a2239 + reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d + resque (3.0.2) sha256=da3c0166d59826a2696f621925ef1fc13778106538e2735571f2bdb703dee896 + securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 + sentry-rails (7.0.0) + sentry-ruby (7.0.0) + sidekiq (8.1.7) sha256=91ad48c7e6c32e1846bf1d5b74c27948758049b93fed0cb539f8d586c7de3415 + sinatra (4.2.1) sha256=b7aeb9b11d046b552972ade834f1f9be98b185fa8444480688e3627625377080 + solid_queue (1.7.0) sha256=6566b70b801d1c317c81bba7bcdd5677c019afac584a30374b4164002ca356d3 + sqlite3 (2.9.6-aarch64-linux-gnu) sha256=d8b1f7d23efd7abac285775a9566562fc7debfef79d594e3a20354406fb7907c + sqlite3 (2.9.6-aarch64-linux-musl) sha256=3579e1c98cdc7ff5c3722847bb63ed4e1efb7ff675cb5e1e48ef2d4da5fb3bc9 + sqlite3 (2.9.6-arm-linux-gnu) sha256=33541500e3615da02afe54a9cc38b17a6985d3cf9d8b76d6d0a83002f114e7ec + sqlite3 (2.9.6-arm-linux-musl) sha256=c5490af48bb228fefa54314e9541375c3907e70f8109f3881b5ff97e1c93ae33 + sqlite3 (2.9.6-arm64-darwin) sha256=849b5d7f795e60fe25076d62c72dd722beb45b3850b516ad978d60ee848ec15b + sqlite3 (2.9.6-x86_64-darwin) sha256=b5842fea77781c14da03fa7bc0feb82db03a69e135affcb6f5399cbd2797a5f3 + sqlite3 (2.9.6-x86_64-linux-gnu) sha256=613188ce02f614126ddbc38c5e217ccffd6306d0dcd9adca9764547aa890a634 + sqlite3 (2.9.6-x86_64-linux-musl) sha256=d493b11818a3573387a1d56e1ee8fa00da23a683a7a1cc063e7a0feeed843abf + thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 + tilt (2.9.0) sha256=da5735d0280bba96e9a91041bb14aee435ccad5c17b0fa519249ae543d9aa3a5 + timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 + zeitwerk (2.8.3) sha256=2c85125a8467ce069e20123d1e709a08955c9d29c118c25b46b7b7fafdbb92e5 + +BUNDLED WITH + 4.0.11