Skip to content

Upgrade madmin to v2.x#371

Open
etagwerker wants to merge 6 commits into
mainfrom
feature/upgrade-madmin
Open

Upgrade madmin to v2.x#371
etagwerker wants to merge 6 commits into
mainfrom
feature/upgrade-madmin

Conversation

@etagwerker

Copy link
Copy Markdown
Member

Motivation / Context

Using madmin 1.2.8 with ruby 3.4 is raising this error:

2026-07-17T03:04:59.902183+00:00 app[web.1]: /app/vendor/bundle/ruby/3.4.0/gems/madmin-1.2.8/lib/madmin/resource.rb:39:in 'Madmin::Resource.attribute': uninitialized constant #<Class:Madmin::Resource>::OpenStruct (NameError)
2026-07-17T03:04:59.902183+00:00 app[web.1]:
2026-07-17T03:04:59.902183+00:00 app[web.1]: attributes[name] = OpenStruct.new(
2026-07-17T03:04:59.902183+00:00 app[web.1]: ^^^^^^^^^^
2026-07-17T03:04:59.902184+00:00 app[web.1]: from /app/app/madmin/resources/estimate_resource.rb:3:in '<class:EstimateResource>'
2026-07-17T03:04:59.902185+00:00 app[web.1]: from /app/app/madmin/resources/estimate_resource.rb:1:in '<top (required)>'
2026-07-17T03:04:59.902185+00:00 app[web.1]: from <internal:/app/vendor/ruby-3.4.10/lib/ruby/3.4.0/rubygems/core_ext/kernel_require.rb>:37:in 'Kernel#require'
2026-07-17T03:04:59.902185+00:00 app[web.1]: from <internal:/app/vendor/ruby-3.4.10/lib/ruby/3.4.0/rubygems/core_ext/kernel_require.rb>:37:in 'Kernel#require'
2026-07-17T03:04:59.902185+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/zeitwerk-2.8.2/lib/zeitwerk/core_ext/kernel.rb:26:in 'Kernel#require'
2026-07-17T03:04:59.902185+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/zeitwerk-2.8.2/lib/zeitwerk/cref.rb:62:in 'Module#const_get'
2026-07-17T03:04:59.902185+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/zeitwerk-2.8.2/lib/zeitwerk/cref.rb:62:in 'Zeitwerk::Cref#get'
2026-07-17T03:04:59.902186+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/zeitwerk-2.8.2/lib/zeitwerk/loader/eager_load.rb:175:in 'block in Zeitwerk::Loader::EagerLoad#actual_eager_load_dir'
2026-07-17T03:04:59.902186+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/zeitwerk-2.8.2/lib/zeitwerk/loader/file_system.rb:51:in 'block in Zeitwerk::Loader::FileSystem#ls'
2026-07-17T03:04:59.902186+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/zeitwerk-2.8.2/lib/zeitwerk/loader/file_system.rb:40:in 'Array#each'
2026-07-17T03:04:59.902186+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/zeitwerk-2.8.2/lib/zeitwerk/loader/file_system.rb:40:in 'Zeitwerk::Loader::FileSystem#ls'
2026-07-17T03:04:59.902186+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/zeitwerk-2.8.2/lib/zeitwerk/loader/eager_load.rb:170:in 'Zeitwerk::Loader::EagerLoad#actual_eager_load_dir'
2026-07-17T03:04:59.902186+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/zeitwerk-2.8.2/lib/zeitwerk/loader/eager_load.rb:18:in 'block (2 levels) in Zeitwerk::Loader::EagerLoad#eager_load'
2026-07-17T03:04:59.902187+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/zeitwerk-2.8.2/lib/zeitwerk/loader/eager_load.rb:17:in 'Hash#each'
2026-07-17T03:04:59.902187+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/zeitwerk-2.8.2/lib/zeitwerk/loader/eager_load.rb:17:in 'block in Zeitwerk::Loader::EagerLoad#eager_load'
2026-07-17T03:04:59.902187+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/zeitwerk-2.8.2/lib/zeitwerk/loader/eager_load.rb:11:in 'Thread::Mutex#synchronize'
2026-07-17T03:04:59.902187+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/zeitwerk-2.8.2/lib/zeitwerk/loader/eager_load.rb:11:in 'Zeitwerk::Loader::EagerLoad#eager_load'
2026-07-17T03:04:59.902187+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/zeitwerk-2.8.2/lib/zeitwerk/loader.rb:456:in 'block in Zeitwerk::Loader.eager_load_all'
2026-07-17T03:04:59.902187+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/zeitwerk-2.8.2/lib/zeitwerk/registry/loaders.rb:10:in 'Array#each'
2026-07-17T03:04:59.902188+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/zeitwerk-2.8.2/lib/zeitwerk/registry/loaders.rb:10:in 'Zeitwerk::Registry::Loaders#each'
2026-07-17T03:04:59.902188+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/zeitwerk-2.8.2/lib/zeitwerk/loader.rb:454:in 'Zeitwerk::Loader.eager_load_all'
2026-07-17T03:04:59.902189+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/railties-8.0.5/lib/rails/application/finisher.rb:79:in 'block in <module:Finisher>'
2026-07-17T03:04:59.902190+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/railties-8.0.5/lib/rails/initializable.rb:32:in 'BasicObject#instance_exec'
2026-07-17T03:04:59.902190+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/railties-8.0.5/lib/rails/initializable.rb:32:in 'Rails::Initializable::Initializer#run'
2026-07-17T03:04:59.902190+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/railties-8.0.5/lib/rails/initializable.rb:61:in 'block in Rails::Initializable#run_initializers'
2026-07-17T03:04:59.902190+00:00 app[web.1]: from /app/vendor/ruby-3.4.10/lib/ruby/3.4.0/tsort.rb:231:in 'block in TSort.tsort_each'
2026-07-17T03:04:59.902190+00:00 app[web.1]: from /app/vendor/ruby-3.4.10/lib/ruby/3.4.0/tsort.rb:353:in 'block (2 levels) in TSort.each_strongly_connected_component'
2026-07-17T03:04:59.902191+00:00 app[web.1]: from /app/vendor/ruby-3.4.10/lib/ruby/3.4.0/tsort.rb:434:in 'TSort.each_strongly_connected_component_from'
2026-07-17T03:04:59.902191+00:00 app[web.1]: from /app/vendor/ruby-3.4.10/lib/ruby/3.4.0/tsort.rb:352:in 'block in TSort.each_strongly_connected_component'
2026-07-17T03:04:59.902191+00:00 app[web.1]: from /app/vendor/ruby-3.4.10/lib/ruby/3.4.0/tsort.rb:350:in 'Rails::Initializable::Collection#each'
2026-07-17T03:04:59.902191+00:00 app[web.1]: from /app/vendor/ruby-3.4.10/lib/ruby/3.4.0/tsort.rb:350:in 'Method#call'
2026-07-17T03:04:59.902191+00:00 app[web.1]: from /app/vendor/ruby-3.4.10/lib/ruby/3.4.0/tsort.rb:350:in 'TSort.each_strongly_connected_component'
2026-07-17T03:04:59.902191+00:00 app[web.1]: from /app/vendor/ruby-3.4.10/lib/ruby/3.4.0/tsort.rb:229:in 'TSort.tsort_each'
2026-07-17T03:04:59.902192+00:00 app[web.1]: from /app/vendor/ruby-3.4.10/lib/ruby/3.4.0/tsort.rb:208:in 'TSort#tsort_each'
2026-07-17T03:04:59.902192+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/railties-8.0.5/lib/rails/initializable.rb:60:in 'Rails::Initializable#run_initializers'
2026-07-17T03:04:59.902194+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/railties-8.0.5/lib/rails/application.rb:444:in 'Rails::Application#initialize!'
2026-07-17T03:04:59.902194+00:00 app[web.1]: from /app/config/environment.rb:5:in '<top (required)>'
2026-07-17T03:04:59.902194+00:00 app[web.1]: from config.ru:3:in 'Kernel#require_relative'
2026-07-17T03:04:59.902195+00:00 app[web.1]: from config.ru:3:in 'block in <main>'
2026-07-17T03:04:59.902195+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/rack-2.2.23/lib/rack/builder.rb:116:in 'Kernel#eval'
2026-07-17T03:04:59.902195+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/rack-2.2.23/lib/rack/builder.rb:116:in 'Rack::Builder.new_from_string'
2026-07-17T03:04:59.902195+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/rack-2.2.23/lib/rack/builder.rb:105:in 'Rack::Builder.load_file'
2026-07-17T03:04:59.902195+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/rack-2.2.23/lib/rack/builder.rb:66:in 'Rack::Builder.parse_file'
2026-07-17T03:04:59.902196+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/puma-7.2.1/lib/puma/configuration.rb:421:in 'Puma::Configuration#load_rackup'
2026-07-17T03:04:59.902196+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/puma-7.2.1/lib/puma/configuration.rb:314:in 'Puma::Configuration#app'
2026-07-17T03:04:59.902196+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/puma-7.2.1/lib/puma/runner.rb:160:in 'Puma::Runner#load_and_bind'
2026-07-17T03:04:59.902196+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/puma-7.2.1/lib/puma/single.rb:44:in 'Puma::Single#run'
2026-07-17T03:04:59.902197+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/puma-7.2.1/lib/puma/launcher.rb:208:in 'Puma::Launcher#run'
2026-07-17T03:04:59.902197+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/puma-7.2.1/lib/puma/cli.rb:73:in 'Puma::CLI#run'
2026-07-17T03:04:59.902197+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/puma-7.2.1/bin/puma:10:in '<top (required)>'
2026-07-17T03:04:59.902197+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/bin/puma:25:in 'Kernel#load'
2026-07-17T03:04:59.902197+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/bin/puma:25:in '<top (required)>'
2026-07-17T03:04:59.902198+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/bundler-2.4.13/lib/bundler/cli/exec.rb:58:in 'Kernel.load'
2026-07-17T03:04:59.902198+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/bundler-2.4.13/lib/bundler/cli/exec.rb:58:in 'Bundler::CLI::Exec#kernel_load'
2026-07-17T03:04:59.902198+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/bundler-2.4.13/lib/bundler/cli/exec.rb:23:in 'Bundler::CLI::Exec#run'
2026-07-17T03:04:59.902198+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/bundler-2.4.13/lib/bundler/cli.rb:492:in 'Bundler::CLI#exec'
2026-07-17T03:04:59.902198+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/bundler-2.4.13/lib/bundler/vendor/thor/lib/thor/command.rb:27:in 'Bundler::Thor::Command#run'
2026-07-17T03:04:59.902198+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/bundler-2.4.13/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in 'Bundler::Thor::Invocation#invoke_command'
2026-07-17T03:04:59.902198+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/bundler-2.4.13/lib/bundler/vendor/thor/lib/thor.rb:392:in 'Bundler::Thor.dispatch'
2026-07-17T03:04:59.902199+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/bundler-2.4.13/lib/bundler/cli.rb:34:in 'Bundler::CLI.dispatch'
2026-07-17T03:04:59.902199+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/bundler-2.4.13/lib/bundler/vendor/thor/lib/thor/base.rb:485:in 'Bundler::Thor::Base::ClassMethods#start'
2026-07-17T03:04:59.902199+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/bundler-2.4.13/lib/bundler/cli.rb:28:in 'Bundler::CLI.start'
2026-07-17T03:04:59.902199+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/bundler-2.4.13/exe/bundle:45:in 'block in <top (required)>'
2026-07-17T03:04:59.902199+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/bundler-2.4.13/lib/bundler/friendly_errors.rb:117:in 'Bundler.with_friendly_errors'
2026-07-17T03:04:59.902210+00:00 app[web.1]: from /app/vendor/bundle/ruby/3.4.0/gems/bundler-2.4.13/exe/bundle:33:in '<top (required)>'
2026-07-17T03:04:59.902210+00:00 app[web.1]: from /app/bin/bundle:3:in 'Kernel#load'
2026-07-17T03:04:59.902210+00:00 app[web.1]: from /app/bin/bundle:3:in '<main>'
2026-07-17T03:04:59.909381+00:00 app[web.1]: D, [2026-07-17T03:04:59.895356 #2] DEBUG -- sentry: [Sentry::SessionFlusher] thread killed
2026-07-17T03:04:59.909381+00:00 app[web.1]: D, [2026-07-17T03:04:59.895526 #2] DEBUG -- sentry: Shutting down background worker

QA / Testing Instructions

Smoke test madmin interface.


I will abide by the code of conduct.

Upgraded sass-rails from 5.0 to 6.0 (which uses sassc-rails 2.1) to support
modern CSS features like rgb() syntax used in madmin 2.3.3. Updated SCSS files
to properly escape calc() and min() expressions using unquote() to prevent
SassC from trying to evaluate unit-mixed expressions at compile time. Added
variables import to project.scss since each SCSS file is compiled independently.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
etagwerker and others added 4 commits July 16, 2026 23:26
The SassC/libsass CSS compressor re-parses already-compiled CSS and evaluates
arithmetic inside calc()/min() (e.g. `100vh - 160px`), raising "Incompatible
units: 'px' and 'vh'" on otherwise valid CSS. This only manifests during
production precompilation, since the compressor does not run in development.

Setting css_compressor to nil explicitly (sprockets-rails defaults to :sass
when sassc is present) skips CSS compression while keeping terser for JS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cover every route under /madmin with integration (request) specs:

- Authorization: unauthenticated and non-admin users are redirected to
  the root path; admins get access across all controllers.
- Dashboard: GET /madmin renders successfully (guards against layout /
  partial regressions like the removed npm_rails_version helper).
- Each resource (projects, stories, estimates, users, version_jumps):
  index/new/show, plus create/update/destroy where those routes exist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
madmin 2.x dropped the madmin/pagy/_nav partial and renders pagination via
pagy's own helpers. The app's overridden index.html.erb still rendered the
old partial, raising ActionView::MissingTemplate for any resource list with
more than one page (pagy's default limit is 20).

Replace the stale partial render with pagy's helpers, mirroring the gem's
own index template (series_nav/info_tag on pagy 43+, pagy_nav/pagy_info on
older versions). Add a request spec that creates 21 records to exercise the
multi-page path, which the single-record index specs never hit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Upgrades the app’s admin interface dependency (madmin) from 1.x to 2.x to restore compatibility with Ruby 3.4, and updates supporting views/assets/tests to match the newer madmin + pagy + sass-rails behavior.

Changes:

  • Bump madmin to ~> 2.0 (and update related dependencies like pagy, sass-rails, sprockets in the lockfile).
  • Update madmin view overrides to work with madmin 2.x (pagination rendering + replacing removed npm_rails_version helper behavior).
  • Add request specs covering madmin authorization and key CRUD/navigation routes, plus a pagination regression spec; adjust production CSS compressor behavior and SCSS output for modern CSS functions.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
spec/requests/madmin/authorization_spec.rb Adds request coverage to ensure non-admins are redirected and admins can access madmin routes.
spec/requests/madmin/dashboard_spec.rb Smoke test for the madmin dashboard and shared layout rendering.
spec/requests/madmin/estimates_spec.rb Request coverage for estimates index/new/show/destroy in madmin.
spec/requests/madmin/pagination_spec.rb Regression test for pagination rendering when results exceed one page.
spec/requests/madmin/projects_spec.rb Request coverage for projects index/new/show/destroy in madmin.
spec/requests/madmin/stories_spec.rb Request coverage for stories CRUD routes in madmin.
spec/requests/madmin/users_spec.rb Request coverage for users index/new/show/destroy in madmin.
spec/requests/madmin/version_jumps_spec.rb Request coverage for version_jumps CRUD routes in madmin.
Gemfile Bumps madmin and sass-rails constraints to match the new dependency requirements.
Gemfile.lock Locks updated versions for madmin 2.x and its dependency graph (notably pagy/sprockets-related changes).
config/environments/production.rb Disables the Sass-based CSS compressor in production to avoid invalid calc/min evaluation errors.
app/views/madmin/application/index.html.erb Reworks pagination rendering to avoid the removed madmin/pagy/_nav partial and support newer pagy APIs.
app/views/madmin/application/_javascript.html.erb Restores npm_rails_version behavior locally for importmap URLs after upstream helper removal.
app/assets/stylesheets/stories.scss Prevents Sass re-processing of CSS min() by emitting it as a literal string.
app/assets/stylesheets/project.scss Imports shared variables to ensure $white and other variables are available under the updated Sass toolchain.
app/assets/stylesheets/4-molecules/_floors.scss Prevents Sass re-processing of CSS calc() by emitting it as a literal string.
app/assets/stylesheets/1-utilities/_basic.scss Prevents Sass re-processing of CSS calc() by emitting it as a literal string.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

get "/madmin/projects"

expect(response).to have_http_status(:ok)
expect(response.body).to include(project.title.truncate(20))
@etagwerker
etagwerker requested a review from JuanVqz July 17, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants