Skip to content

Upgrade to Rails 7.1 - #11195

Draft
lhellebr wants to merge 1 commit into
theforeman:developfrom
lhellebr:rails-7.1-upgrade
Draft

Upgrade to Rails 7.1#11195
lhellebr wants to merge 1 commit into
theforeman:developfrom
lhellebr:rails-7.1-upgrade

Conversation

@lhellebr

@lhellebr lhellebr commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Bumps the rails gem to ~> 7.1.0. config.load_defaults is deliberately kept at '7.0' (see comment in config/application.rb) — loading 7.1 defaults activates ~20 behavior changes at once (including ActiveRecord::Encryption switching to SHA-256, which affects already-encrypted data) across core and every plugin, and should be its own separate, incremental effort.

Depends on domcleal/as_deprecation_tracker#5 (Rails 7.1 support for the deprecators registry) being merged and released. bundler.d/test.rb currently points at a fork/branch of that gem as a temporary safety net so CI doesn't silently lose deprecation-tracking coverage in the meantime; this must be reverted to the plain ~> 1.6 constraint once a release containing that fix is out, before this PR can be merged.

Also blocked on auditing the ~30 bundled plugins for their own Rails 7.1 compatibility.

Compatibility fixes

  • alias_attribute: replaced aliases targeting non-attribute methods (associations) or manually-overridden methods with explicit reader/writer methods, since Rails 7.2 drops support for those patterns (arch, hostname, os, hostgroup_parameters, mailer_method, override_values/_ids/_order, select_title, to_label/to_s).
  • serialize: serialize :attr, Typeserialize :attr, type: Type (positional type arg deprecated).
  • database_cleaner: unmaintained, references ActiveRecord::Base directly → database_cleaner-active_record.
  • ActiveRecord::MigrationContext: no longer accepts the old ActiveRecord::SchemaMigration class as its schema_migration arg (app/registries/foreman/plugin.rb).
  • assert_deprecated/assert_not_deprecated: now require an explicit deprecator argument.
  • basic_rest_response_test pagination test: redirect_to no longer sets a default HTML body (rails/rails@c2e756a9); assert on @response.redirect? instead.
  • SubnetTest uniqueness test: Rails 7.1 skips the uniqueness DB check for a persisted?, unchanged attribute already covered by a unique index (rails/rails#45149), trusting the DB already enforced it on insert — an assumption FactoryBot.build_stubbed violates (same issue as thoughtbot/factory_bot#1634). Switched to build.
  • Api::V2::RegistrationControllerTest: RecordNotFound now calls #inspect on the id, a security fix for ANSI escape injection into logs via a crafted id (CVE-2025-55193 / GHSA-76r7-hhxj-r776). String ids are now quoted in the message.
  • EncryptValue#decrypt_field: MessageEncryptor's refactor (rails/rails#47326) makes decrypt_and_verify always raise MessageEncryptor::InvalidMessage on a corrupt message regardless of cipher; non-AEAD ciphers (aes-256-cbc, used here) previously raised MessageVerifier::InvalidSignature. Now rescuing both so decryption failures keep degrading gracefully.

Test plan

  • Full bin/rails test suite passes (verified locally; CI will confirm)
  • as_deprecation_tracker fork PR merged/released, then revert bundler.d/test.rb to ~> 1.6

:if => ->(host) { host.managed }
end

# Defined explicitly (rather than via alias_attribute) because operatingsystem

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm thinking whether we could utilize a small helper in ApplicationRecord that would be something like:

  def self.alias_association(*aliases, original)
    aliases.each do |alias_name|
      alias_method alias_name, original
      alias_method "#{alias_name}=", "#{original}="
    end
  end

That would enable you to just write alias_association :os, :operatingsystem (and so on) in the models. Thoughts?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion! I've been going back and forth on this.

On one hand, the explicit methods are verbose but very transparent - each one is self-contained, easy to grep for, and immediately obvious what it does without needing to know about a helper. They also handle the cases that don't fit the simple getter+setter pattern (e.g. MailNotification#mailer_method reads via self[:method] to avoid shadowing Object#method, LookupKey#value_before_type_cast is a single reader delegating to a manually-overridden method, and ApplicationRecord#to_label/to_s aren't association aliases at all). So the helper would only cover a subset of the replacements - the rest would still need explicit methods.

On the other hand, for the straightforward association aliases (os/operatingsystem, arch/architecture, hostname/name), a helper like yours would definitely cut the boilerplate.

One thing I'd suggest if we do go with a helper: use define_method instead of alias_method:

def self.alias_association(*aliases, original)
  aliases.each do |alias_name|
    define_method(alias_name) { send(original) }
    define_method("#{alias_name}=") { |value| send("#{original}=", value) }
  end
end

alias_method copies the method body at the time it's called - it's a snapshot. If a plugin later prepends a module that redefines operatingsystem (which plugins do via config.to_prepare, i.e. after the model class body has already been evaluated), the os alias would still point to the pre-override version. define_method creates a live delegation that always dispatches to the current definition of the target method, so plugin overrides are picked up correctly. I couldn't find an existing plugin that actually overrides any of these specific methods today, but given Foreman's plugin ecosystem, define_method feels like the safer default.

I can either leave it as it is or add the helper either as part of this PR or as a followup - what do you think?

@lhellebr

Copy link
Copy Markdown
Contributor Author

Minor docs bug this PR will cause: https://redhat.atlassian.net/browse/SAT-49436

Bumps the rails gem to ~> 7.1.0 (config.load_defaults stays at '7.0'
deliberately, see comment in config/application.rb) and fixes the
resulting compatibility breaks:

- alias_attribute: replace aliases that target non-attribute methods
  or manually-overridden methods with explicit reader/writer methods,
  since Rails 7.2 will stop supporting those (arch, hostname, os,
  hostgroup_parameters, mailer_method, override_values/_ids/_order,
  select_title, to_label/to_s).
- serialize :attr, Type -> serialize :attr, type: Type (positional
  arg deprecated).
- database_cleaner (unmaintained, references ActiveRecord::Base
  directly) -> database_cleaner-active_record.
- ActiveRecord::MigrationContext no longer accepts the old
  ActiveRecord::SchemaMigration class as its schema_migration arg.
- assert_deprecated/assert_not_deprecated now require an explicit
  deprecator argument.
- as_deprecation_tracker temporarily points at a fork/branch with
  Rails 7.1 deprecators-registry support, pending upstream release
  (github.com/domcleal/as_deprecation_tracker/pull/5).
- basic_rest_response_test's pagination test no longer matches
  redirect_to's response body, which Rails 7.1 stopped populating
  by default (rails/rails@c2e756a9); assert on @response.redirect?
  instead.
- SubnetTest: FactoryBot.build_stubbed fakes persisted? without ever
  inserting a row, but Rails 7.1's uniqueness validator now skips its
  DB check for a persisted, unchanged attribute already covered by a
  unique index (rails/rails#45149), trusting the DB already enforced
  it on insert. That trust is misplaced for build_stubbed objects.
  Same issue/fix as thoughtbot/factory_bot#1634; use build instead.
- Api::V2::RegistrationControllerTest: RecordNotFound now calls
  #inspect on the id (rails/rails@dd6fcc43), a security fix for ANSI
  escape injection into logs via a crafted id (CVE-2025-55193 /
  GHSA-76r7-hhxj-r776, backported to activerecord 7.1.5.2+). String
  ids (as opposed to Integer) are now quoted in the message.
- EncryptValue#decrypt_field: MessageEncryptor's refactor
  (rails/rails#47326) made decrypt_and_verify always raise
  MessageEncryptor::InvalidMessage on a corrupt message, regardless
  of cipher; non-AEAD ciphers (aes-256-cbc, used here) previously
  raised MessageVerifier::InvalidSignature instead. Rescue both so
  decryption failures keep degrading gracefully instead of raising.
@ekohl

ekohl commented Aug 26, 2026

Copy link
Copy Markdown
Member

To test plugins against this PR you can use https://github.com/theforeman/actions#foreman-plugin-ruby-tests. So update the plugin's workflow to use foreman_version: refs/pull/11195/head.

# search for a metric - e.g.:
# Host::Managed.with("failed") --> all reports which have a failed counter > 0
# Host::Managed.with("failed",20) --> all reports which have a failed counter > 20
scope :with, lambda { |*arg|

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.

I might be blind or missing something, but why do we drop with scopes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Rails 7.1 added ActiveRecord::QueryMethods#with (native CTE support). Both ConfigReport and Host::Managed had their own unrelated scope :with, which now collides and raises ArgumentError at boot under 7.1.

I initially considered renaming to with_metric, just as we have with_status, but I've found no usage of this and it doesn't seem to be a public, documented interface either. So unless I am wrong and it actually is used, I would prefer deleting it to just renaming.

Comment thread config/application.rb

module Foreman
class Application < Rails::Application
# Intentionally not bumped to '7.1' yet even though the rails gem itself is.

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.

We should load_defaults for 7.1 here. All of the new settings that break the app should be overridable if we're not ready to use them, see the current pattern below. Ideally with follow ups or at least comments for the future.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree we should load 7.1, however I've chosen the path of least resistance here (loading 7.0) to support Rails 7.1 ASAP and intended to do the bump, with required fixes, in a separate PR. Do we prefer doing it all in this PR?

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.

I think it would be interesting to know what breaks, especially in plugins but we certainly can break it up. Usually we use Refs #redmine - ... instead of Fixes #redmine - for that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Does it mean you would not consider the (future) redmine ticket created from "Upgrade foreman to Rails 7.1" JIRA ticket a complete fix? Because it uses defaults from 7.0, as a kind of compatibility layer so we don't have to change everything immediately?
My opinion is that redmine will be fixed by this PR including the 7.0 defaults because it, indeed, upgrades foreman to Rails 7.1. But if you disagree, then obviously I will have to include changes for 7.1 config to ship it completely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants