Skip to content

Upgrade madmin to v2.x#371

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

Upgrade madmin to v2.x#371
etagwerker wants to merge 9 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.

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.

Comment thread spec/requests/madmin/projects_spec.rb Outdated
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
JuanVqz added a commit that referenced this pull request Jul 20, 2026
Production (points-production) is crashing on boot with:

  madmin-1.2.8/lib/madmin/resource.rb:39:in 'attribute':
  uninitialized constant OpenStruct (NameError)

Ruby 3.4 removed ostruct from the default gems (it is now a bundled gem
that must be declared in the Gemfile). madmin 1.2.8 uses OpenStruct
without requiring "ostruct", so on Ruby 3.4 the constant is undefined,
Puma fails to boot, and every request returns 503 (H10 App crashed).

madmin's rescue in resource.rb re-raises the NameError as a misleading
"couldn't find attribute 'id'" ArgumentError, which masks the real cause.

Declaring gem "ostruct" makes Bundler.require load it at boot so
OpenStruct is defined. Applied to Gemfile.lock and Gemfile.next.lock.

This is a hotfix to restore production; the madmin 2.x upgrade (#371)
is the longer-term fix and removes the incompatibility entirely.
etagwerker and others added 7 commits July 20, 2026 11:57
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>
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>
The compressor re-parses compiled CSS and evaluates calc()/min() unit
arithmetic (e.g. 100vh - 160px), raising "Incompatible units" on valid
CSS. It was only disabled in production, but madmin request specs compile
assets in the test env too. Move the setting to application.rb so all
envs skip it.
@JuanVqz
JuanVqz force-pushed the feature/upgrade-madmin branch from 0895b6b to f166fac Compare July 20, 2026 17:58
JuanVqz added 2 commits July 20, 2026 12:00
The previous version branched at runtime on @pagy.respond_to?(:series_nav)
to support both the pagy 43+ object helpers and the older pagy_nav/pagy_info
module helpers. pagy is pinned to 43.x in both Gemfile.lock and
Gemfile.next.lock, so the else branch was dead code that never executed.

Call the pagy 43 object helpers directly (series_nav/info_tag), dropping
the feature-detection branch.
madmin 2.x renders the full project title in the index table, not a
20-char truncation. Assert the full title instead of title.truncate(20),
which produced 'Dietrich, Reichel...' and never matched the rendered
'Dietrich, Reichel and Lueilwitz'.
@@ -1,3 +1,6 @@
<%# madmin's npm_rails_version helper was removed in 2.x; reproduce it here.
Pins @rails/* unpkg packages to the matching Rails version (or "main" for prereleases). %>
<% npm_rails_version = Rails.version.match?(/\A\d+\.\d+\.\d+\z/) ? Rails.version : "main" %>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This app is dual-booted; should we use the NextRails.next? instead of the Rails.version.match?(/\A\d+\.\d+\.\d+\z/)

In fact, why are we adding this now, how npm_rails_version was resolved in the past if we didn't set that variable.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@JuanVqz JuanVqz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

approving, I'll merge in the afternoon. (because Im leaving and do not have time to see it work in prod)

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.

3 participants