Skip to content

Fixes #39185 - Track errata applications in database#11690

Merged
jeremylenz merged 1 commit into
Katello:masterfrom
pavanshekar:issue-39185
Jun 15, 2026
Merged

Fixes #39185 - Track errata applications in database#11690
jeremylenz merged 1 commit into
Katello:masterfrom
pavanshekar:issue-39185

Conversation

@pavanshekar

@pavanshekar pavanshekar commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

What are the changes introduced in this pull request?

Fixes the "Host - Applied Errata" report which was generating empty CSV files. The root cause was that the report relied on parsing Foreman task history every time it was generated, which was slow, brittle, and was not reliable. The fix introduces a dedicated database table called katello_errata_applications that persistently tracks errata applications as they happen. A Dynflow middleware component now automatically intercepts errata installation jobs when they complete and extracts relevant details to create database records. The report template has been updated to query this database table directly instead of parsing tasks, making report generation fast and reliable. The implementation includes a hybrid approach that falls back to the legacy task-parsing method if the table doesn't exist yet, ensuring backward compatibility during migration periods.

Considerations taken when implementing this change?

The implementation follows the existing ContentViewHistory pattern to maintain consistency with Katello's established conventions for tracking historical events. The middleware was designed to run in the finalize phase of Dynflow actions to capture the final task state, with robust error handling to ensure recording failures don't break errata installation tasks. The solution handles two different job types: direct installations where errata IDs are specified upfront, and search query installations where IDs are resolved at runtime and extracted from the generated script. The database schema includes proper foreign keys, indexes on frequently queried columns, and a uniqueness constraint to prevent duplicate records. Comprehensive test coverage was added for both the model logic and report generation functionality to prevent future regressions. The test suite was developed completely using Claude Code to ensure thorough coverage of validations, scopes, edge cases, and integration scenarios.

What are the testing steps for this pull request?

  1. Register any new host to Satellite.
  2. Check Installable Errata for that host see if its showing any Errata
  3. Apply Errata by REX from Satellite UI.
  4. Now generate Host Applied Report from Monitor > Report Template > Host Applied Errata
  5. Now check the status of host which is in Green state shows host is upto date now.
  6. Verify it shows the list of errata applied by REX from Satellite

Summary by Sourcery

Track applied errata in a dedicated database model and use it as the primary data source for errata reporting while keeping the legacy task-parsing path as a fallback.

New Features:

  • Introduce a Katello::ErrataApplication model, associations, and scopes to persist host errata applications with metadata such as status, method, task, user, and timestamps.
  • Add Dynflow middleware to automatically record errata applications from completed errata installation tasks into the new tracking table.
  • Expose a new database-backed macro for loading errata application data for report templates, supporting status, type, date range, and host filtering.

Bug Fixes:

  • Fix Host Applied Errata reporting by replacing brittle on-demand task parsing with reliable queries against persistently stored errata application records.

Enhancements:

  • Extend existing template scope helpers to transparently use the new database-backed errata applications loader, falling back to the legacy task-based implementation when the table is unavailable.
  • Add host and erratum associations to reference their errata application history and ensure dependent records are cleaned up appropriately.

Deployment:

  • Add a database migration to create the katello_errata_applications table with appropriate foreign keys, indexes, defaults, and a composite index to support efficient querying.

Tests:

  • Add comprehensive unit tests for the ErrataApplication model validations, uniqueness constraints, scopes, and task-recording logic, including edge cases and error handling.
  • Add tests for the template scope helper to verify the structure, filtering, and options of the errata application data returned from the database-backed loader.

Summary by CodeRabbit

  • New Features

    • Persistent errata application records linked to hosts, errata, optional task/user, method, status and timestamp.
    • Automatic recording of errata applications when relevant update jobs finish; API now returns errata-application hashes (with filters for status, erratum type, date range, host, and optional last-reboot time) and falls back to legacy behavior if DB-backed data is unavailable.
    • DB migration and model associations to connect applications to hosts, errata, and users.
  • Tests

    • Added unit and integration tests for model validations, scopes, API filtering, and task-derived recording.

@coderabbitai

coderabbitai Bot commented Mar 27, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds persistent errata application tracking: a new Katello::ErrataApplication model and migration, host/erratum/user associations, Dynflow middleware to record applications from completed tasks, DB-backed and legacy loaders for template-scope API, and accompanying tests.

Changes

Cohort / File(s) Summary
Core Model & Schema
app/models/katello/errata_application.rb, db/migrate/20260327192122_create_katello_errata_applications.rb
New Katello::ErrataApplication AR model with associations (host, erratum, optional task/user), validations, scopes, uniqueness, helper methods and record_from_task. Migration adds table, FKs, defaults, and indexes (including unique composite index).
Model Associations
app/models/katello/concerns/host_managed_extensions.rb, app/models/katello/erratum.rb, app/models/katello/concerns/user_extensions.rb
Adds has_many :errata_applications on Host concern, Katello::Erratum, and User concern (Host/Erratum: dependent: :destroy; User: dependent: :nullify, inverse_of set).
Dynflow Middleware & Engine
app/lib/actions/middleware/record_errata_application.rb, lib/katello/engine.rb
Adds Actions::Middleware::RecordErrataApplication middleware that on finalize resolves a ForemanTasks::Task (special-case lookup for RemoteExecution::RunHostJob), detects errata-install jobs (template invocation inputs or allowlist of action labels), calls Katello::ErrataApplication.record_from_task, and logs/rescues exceptions. Engine registers this middleware on Dynflow init.
API / Template Scope
app/lib/katello/concerns/base_template_scope_extensions.rb
Updates load_errata_applications Apipie contract to return arrays of Hash and to use application statuses (success,error,warning,cancelled). Runtime dispatch: load_errata_applications_from_db when DB table exists, otherwise load_errata_applications_legacy. Adds DB-backed loader with filters and optional last_reboot_time.
Tests
test/lib/concerns/base_template_scope_extensions_test.rb, test/models/katello/errata_application_test.rb
Adds tests for template-scope API output and filters, and comprehensive model tests for validations, uniqueness, scopes, record_from_task, status/method determination, and edge cases (nil/missing inputs, duplicates).

Sequence Diagram(s)

sequenceDiagram
    participant Task as ForemanTasks::Task
    participant Middleware as Actions::Middleware::RecordErrataApplication
    participant Finder as Katello::ErrataApplication (helpers)
    participant Host as Host::Managed
    participant Erratum as Katello::Erratum
    participant DB as Database

    Task->>Middleware: finalize(event)
    Middleware->>Middleware: resolve task by execution_plan_id / external_id
    Middleware->>Finder: errata_install_job?(task, action)
    alt RemoteExecution::RunHostJob
        Finder->>Task: inspect template_invocation inputs for 'errata'/'Errata search query'
    else Other action types
        Finder->>Task: inspect action.label against allowlist
    end
    alt Task is errata job
        Middleware->>Finder: extract_host_id_from_task(task)
        Middleware->>Finder: extract_errata_ids_from_task(task)
        Middleware->>Host: load host by id
        Middleware->>Erratum: load errata by ids
        Middleware->>Finder: determine_status(task, action)
        Middleware->>Finder: determine_method_from_task(task)
        loop per erratum id
            Middleware->>DB: create Katello::ErrataApplication record
            DB-->>Middleware: created / skipped (duplicate)
        end
    else Not errata
        Middleware-->>Task: no-op
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fixes #39185 - Track errata applications in database' directly and clearly summarizes the main change: introducing persistent database-backed tracking of errata applications to fix a bug.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai sourcery-ai Bot left a comment

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.

Hey - I've found 5 issues, and left some high level feedback:

  • In the migration you declare task_id as a string while the model uses belongs_to :task, class_name: 'ForemanTasks::Task', which expects an integer foreign key; consider switching this to a proper t.references :task (with the correct table and type) or updating the association to match the column type.
  • ErrataApplication.record_from_task currently records for tasks in states running and stopped; if you only want completed applications, narrowing this to the final/terminal states (e.g. just stopped) would avoid persisting partial or in-progress runs.
  • When rescuing ActiveRecord::RecordInvalid in ErrataApplication.record_from_task, the log message only mentions the erratum and host; including the task ID and validation errors in the warning would make troubleshooting skipped records significantly easier.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the migration you declare `task_id` as a `string` while the model uses `belongs_to :task, class_name: 'ForemanTasks::Task'`, which expects an integer foreign key; consider switching this to a proper `t.references :task` (with the correct table and type) or updating the association to match the column type.
- `ErrataApplication.record_from_task` currently records for tasks in states `running` and `stopped`; if you only want completed applications, narrowing this to the final/terminal states (e.g. just `stopped`) would avoid persisting partial or in-progress runs.
- When rescuing `ActiveRecord::RecordInvalid` in `ErrataApplication.record_from_task`, the log message only mentions the erratum and host; including the task ID and validation errors in the warning would make troubleshooting skipped records significantly easier.

## Individual Comments

### Comment 1
<location path="app/models/katello/errata_application.rb" line_range="5" />
<code_context>
+  class ErrataApplication < Katello::Model
+    belongs_to :host, class_name: '::Host::Managed', inverse_of: :errata_applications
+    belongs_to :erratum, class_name: 'Katello::Erratum', inverse_of: :errata_applications
+    belongs_to :task, class_name: 'ForemanTasks::Task', optional: true, inverse_of: false
+    belongs_to :user, class_name: '::User', optional: true
+
</code_context>
<issue_to_address>
**issue (bug_risk):** The `task` association does not match the schema and will point to the wrong key by default.

The migration defines `task_id` as a `string`, which likely stores `ForemanTasks::Task.external_id`, but `belongs_to :task` assumes an integer `task_id` referencing `ForemanTasks::Task.id`. This will break calls like `errata_application.task`. Please either configure the association:

```ruby
depends_to :task,
  class_name: 'ForemanTasks::Task',
  primary_key: :external_id,
  foreign_key: :task_id,
  optional: true,
  inverse_of: false
```

or change `task_id` to be a proper foreign key to `foreman_tasks_tasks.id`, depending on the intended relationship.
</issue_to_address>

### Comment 2
<location path="app/models/katello/errata_application.rb" line_range="22" />
<code_context>
+
+    default_scope { order(applied_at: :desc) }
+
+    validates :erratum_id, uniqueness: { scope: [:host_id, :applied_at] }
+
+    # Record errata applications from a completed task
</code_context>
<issue_to_address>
**issue (bug_risk):** The uniqueness validation is not backed by a unique index, which can allow duplicates under concurrency.

The model validates `erratum_id` uniqueness with scope `[:host_id, :applied_at]`, but the DB index on `[:host_id, :erratum_id, :applied_at]` is not unique. Under concurrent inserts, this can still create duplicates. Please change this to a unique index:

```ruby
add_index :katello_errata_applications,
          [:host_id, :erratum_id, :applied_at],
          unique: true,
          name: 'index_errata_apps_host_erratum_date'
```

and update the migration so the database enforces the constraint.
</issue_to_address>

### Comment 3
<location path="app/models/katello/errata_application.rb" line_range="30" />
<code_context>
+    # @return [Array<ErrataApplication>] Created application records
+    def self.record_from_task(task, action = nil)
+      return [] unless task
+      return [] unless task.state.in?(%w[running stopped])
+
+      host_id = extract_host_id_from_task(task)
</code_context>
<issue_to_address>
**issue (bug_risk):** The state filter in `record_from_task` seems inverted relative to the intention to handle completed tasks.

The docstring states this should run for completed tasks, but the guard only allows `running` or `stopped` and returns early for everything else. That both permits processing while still `running` and excludes other terminal states (e.g. `paused`, or future ones). If this is meant for finished tasks only, consider checking explicitly for terminal state(s), e.g.:

```ruby
return [] unless task.state == 'stopped'
```

or using a whitelist of terminal states instead of including `running`.
</issue_to_address>

### Comment 4
<location path="test/lib/concerns/base_template_scope_extensions_test.rb" line_range="24" />
<code_context>
       assert_equal @errata.id.to_s, id
     end
+
+    def test_load_errata_applications_returns_array
+      create_errata_application(@host, @errata)
+      scope = ::Foreman::Renderer.get_scope
</code_context>
<issue_to_address>
**suggestion (testing):** Add coverage for the legacy `load_errata_applications_legacy` path when the table does not exist

The new tests only cover the code path where `Katello::ErrataApplication.table_exists?` is true, so the legacy task-parsing path is now untested. Please add a test that stubs `Katello::ErrataApplication.table_exists?` to `false` and verifies that `load_errata_applications` delegates to `load_errata_applications_legacy` (e.g., by stubbing that method and asserting it was called, or by checking the returned data matches the legacy structure).
</issue_to_address>

### Comment 5
<location path="test/models/katello/errata_application_test.rb" line_range="228" />
<code_context>
+      refute_includes applications, manual_app
+    end
+
+    def test_record_from_task_with_valid_task
+      task = create_task_with_errata(@host, [@erratum.errata_id])
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for additional `record_from_task` extraction and method/status branches

Current tests exercise the main `record_from_task` flow and some guards, but several branches in `ErrataApplication` remain untested:
- `extract_errata_ids_from_task` when IDs come from `input['content']`.
- Script-based extraction (`extract_from_script`) for jobs using `job_features` and `RESOLVED_ERRATA_IDS` in the script.
- Template-input-based extraction (`extract_from_template_input`) when errata are passed as template input values.
- The default branch of `determine_method_from_task` (neither REX nor Katello agent), including verifying that multiple errata IDs in one task yield multiple application records.
Targeted tests for these paths would better protect the parsing logic that populates the tracking table.

Suggested implementation:

```ruby
  class ErrataApplicationTest < ActiveSupport::TestCase
    def setup
      @host = hosts(:one)
      @erratum = katello_errata(:security)
      @user = User.first
    end

    def test_extract_errata_ids_from_task_from_content
      task = {
        'input' => {
          'content' => {
            'errata' => [@erratum.errata_id, 'RHBA-2024:0001']
          }
        }
      }

      ids = ErrataApplication.send(:extract_errata_ids_from_task, task)

      assert_equal [@erratum.errata_id, 'RHBA-2024:0001'], ids
    end

    def test_extract_errata_ids_from_task_from_script
      script = <<~SCRIPT
        #!/bin/bash
        RESOLVED_ERRATA_IDS="#{@erratum.errata_id},RHBA-2024:0002"
        echo "Applying errata"
      SCRIPT

      task = {
        'input' => {
          'job_features' => ['katello_errata_install'],
          'script' => script
        }
      }

      ids = ErrataApplication.send(:extract_errata_ids_from_task, task)

      assert_includes ids, @erratum.errata_id
      assert_includes ids, 'RHBA-2024:0002'
    end

    def test_extract_errata_ids_from_task_from_template_input
      task = {
        'input' => {
          'template_input' => {
            'errata' => "#{@erratum.errata_id},RHBA-2024:0003"
          }
        }
      }

      ids = ErrataApplication.send(:extract_errata_ids_from_task, task)

      assert_equal [@erratum.errata_id, 'RHBA-2024:0003'], ids
    end

    def test_determine_method_from_task_default_branch
      task = {
        'input' => {
          'some' => 'payload'
        }
      }

      method = ErrataApplication.send(:determine_method_from_task, task)

      refute_equal 'remote_execution', method
      refute_equal 'katello_agent', method
      assert method.present?
    end

    def test_record_from_task_creates_multiple_applications_for_multiple_errata_ids
      task = {
        'id' => SecureRandom.uuid,
        'input' => {
          'content' => {
            'errata' => [@erratum.errata_id, 'RHBA-2024:0004']
          }
        }
      }

      created = []
      ErrataApplication.stub(:create!, ->(attrs) { created << attrs }) do
        ErrataApplication.record_from_task(task, @host, @user)
      end

      assert_equal 2, created.size
      created.each do |attrs|
        assert_equal @host, attrs[:host]
        assert_equal @user, attrs[:user]
        assert_equal 'success', attrs[:status]
      end
    end

    def test_create_valid_application

```

These tests assume the following about the implementation:

1. `extract_errata_ids_from_task`, `extract_from_script`, and `extract_from_template_input` are private class methods on `ErrataApplication` invoked by `extract_errata_ids_from_task(task)` and accept a `Hash`-like `task` with an `'input'` key.
2. The script-based extraction looks for `RESOLVED_ERRATA_IDS` in the `input['script']` value and splits it into individual errata IDs.
3. Template-input-based extraction expects `input['template_input']['errata']` as a comma-separated string of errata IDs.
4. `determine_method_from_task` accepts the same Hash-like `task` structure and returns a non-empty string; the default branch is any case where it does not return `'remote_execution'` or `'katello_agent'`.
5. `record_from_task` accepts `(task, host, user)` where `task` can be a Hash with at least `'id'` and `'input'` keys, uses `extract_errata_ids_from_task` internally, and calls `create!` once per erratum ID with `:host`, `:user`, and `:status` in the attributes.

If your implementation differs, you should adjust:
- The shape of the `task` Hashes to match the real task object (for example, using a Dynflow task or a Struct that responds to `input`, etc.).
- The keys used in `input` (`'content'`, `'template_input'`, `'job_features'`, `'script'`) to align with how the extraction methods actually read data.
- The `record_from_task` call signature and the expectations on the attributes passed to `create!` (e.g., `:host_id` instead of `:host`, different `:status` values, or additional required keys like `:method` or `:applied_at`).

You may also want to:
- Replace the direct `send(:extract_errata_ids_from_task, task)` calls with tests that go through `record_from_task` if those helpers are not intended to be part of the test surface.
- Use existing task factories or helper methods (e.g. `create_task_with_errata`) if they already encapsulate the correct Dynflow task structure, adapting these tests to build on those helpers instead of raw Hashes.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

class ErrataApplication < Katello::Model
belongs_to :host, class_name: '::Host::Managed', inverse_of: :errata_applications
belongs_to :erratum, class_name: 'Katello::Erratum', inverse_of: :errata_applications
belongs_to :task, class_name: 'ForemanTasks::Task', optional: true, inverse_of: false

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.

issue (bug_risk): The task association does not match the schema and will point to the wrong key by default.

The migration defines task_id as a string, which likely stores ForemanTasks::Task.external_id, but belongs_to :task assumes an integer task_id referencing ForemanTasks::Task.id. This will break calls like errata_application.task. Please either configure the association:

depends_to :task,
  class_name: 'ForemanTasks::Task',
  primary_key: :external_id,
  foreign_key: :task_id,
  optional: true,
  inverse_of: false

or change task_id to be a proper foreign key to foreman_tasks_tasks.id, depending on the intended relationship.

Comment thread app/models/katello/errata_application.rb Outdated
Comment thread app/models/katello/errata_application.rb Outdated
Comment thread test/lib/concerns/base_template_scope_extensions_test.rb Outdated
refute_includes applications, manual_app
end

def test_record_from_task_with_valid_task

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.

suggestion (testing): Add tests for additional record_from_task extraction and method/status branches

Current tests exercise the main record_from_task flow and some guards, but several branches in ErrataApplication remain untested:

  • extract_errata_ids_from_task when IDs come from input['content'].
  • Script-based extraction (extract_from_script) for jobs using job_features and RESOLVED_ERRATA_IDS in the script.
  • Template-input-based extraction (extract_from_template_input) when errata are passed as template input values.
  • The default branch of determine_method_from_task (neither REX nor Katello agent), including verifying that multiple errata IDs in one task yield multiple application records.
    Targeted tests for these paths would better protect the parsing logic that populates the tracking table.

Suggested implementation:

  class ErrataApplicationTest < ActiveSupport::TestCase
    def setup
      @host = hosts(:one)
      @erratum = katello_errata(:security)
      @user = User.first
    end

    def test_extract_errata_ids_from_task_from_content
      task = {
        'input' => {
          'content' => {
            'errata' => [@erratum.errata_id, 'RHBA-2024:0001']
          }
        }
      }

      ids = ErrataApplication.send(:extract_errata_ids_from_task, task)

      assert_equal [@erratum.errata_id, 'RHBA-2024:0001'], ids
    end

    def test_extract_errata_ids_from_task_from_script
      script = <<~SCRIPT
        #!/bin/bash
        RESOLVED_ERRATA_IDS="#{@erratum.errata_id},RHBA-2024:0002"
        echo "Applying errata"
      SCRIPT

      task = {
        'input' => {
          'job_features' => ['katello_errata_install'],
          'script' => script
        }
      }

      ids = ErrataApplication.send(:extract_errata_ids_from_task, task)

      assert_includes ids, @erratum.errata_id
      assert_includes ids, 'RHBA-2024:0002'
    end

    def test_extract_errata_ids_from_task_from_template_input
      task = {
        'input' => {
          'template_input' => {
            'errata' => "#{@erratum.errata_id},RHBA-2024:0003"
          }
        }
      }

      ids = ErrataApplication.send(:extract_errata_ids_from_task, task)

      assert_equal [@erratum.errata_id, 'RHBA-2024:0003'], ids
    end

    def test_determine_method_from_task_default_branch
      task = {
        'input' => {
          'some' => 'payload'
        }
      }

      method = ErrataApplication.send(:determine_method_from_task, task)

      refute_equal 'remote_execution', method
      refute_equal 'katello_agent', method
      assert method.present?
    end

    def test_record_from_task_creates_multiple_applications_for_multiple_errata_ids
      task = {
        'id' => SecureRandom.uuid,
        'input' => {
          'content' => {
            'errata' => [@erratum.errata_id, 'RHBA-2024:0004']
          }
        }
      }

      created = []
      ErrataApplication.stub(:create!, ->(attrs) { created << attrs }) do
        ErrataApplication.record_from_task(task, @host, @user)
      end

      assert_equal 2, created.size
      created.each do |attrs|
        assert_equal @host, attrs[:host]
        assert_equal @user, attrs[:user]
        assert_equal 'success', attrs[:status]
      end
    end

    def test_create_valid_application

These tests assume the following about the implementation:

  1. extract_errata_ids_from_task, extract_from_script, and extract_from_template_input are private class methods on ErrataApplication invoked by extract_errata_ids_from_task(task) and accept a Hash-like task with an 'input' key.
  2. The script-based extraction looks for RESOLVED_ERRATA_IDS in the input['script'] value and splits it into individual errata IDs.
  3. Template-input-based extraction expects input['template_input']['errata'] as a comma-separated string of errata IDs.
  4. determine_method_from_task accepts the same Hash-like task structure and returns a non-empty string; the default branch is any case where it does not return 'remote_execution' or 'katello_agent'.
  5. record_from_task accepts (task, host, user) where task can be a Hash with at least 'id' and 'input' keys, uses extract_errata_ids_from_task internally, and calls create! once per erratum ID with :host, :user, and :status in the attributes.

If your implementation differs, you should adjust:

  • The shape of the task Hashes to match the real task object (for example, using a Dynflow task or a Struct that responds to input, etc.).
  • The keys used in input ('content', 'template_input', 'job_features', 'script') to align with how the extraction methods actually read data.
  • The record_from_task call signature and the expectations on the attributes passed to create! (e.g., :host_id instead of :host, different :status values, or additional required keys like :method or :applied_at).

You may also want to:

  • Replace the direct send(:extract_errata_ids_from_task, task) calls with tests that go through record_from_task if those helpers are not intended to be part of the test surface.
  • Use existing task factories or helper methods (e.g. create_task_with_errata) if they already encapsulate the correct Dynflow task structure, adapting these tests to build on those helpers instead of raw Hashes.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

🧹 Nitpick comments (2)
app/lib/katello/concerns/base_template_scope_extensions.rb (2)

248-251: Re-enable the other metrics after the legacy helper.

Line 325 only turns Metrics/MethodLength back on. Metrics/AbcSize, Metrics/CyclomaticComplexity, and Metrics/PerceivedComplexity stay disabled until Line 415, which widens the suppression beyond load_errata_applications_legacy.

Suggested fix
       result
       end
       # rubocop:enable Metrics/MethodLength
+      # rubocop:enable Metrics/AbcSize
+      # rubocop:enable Metrics/CyclomaticComplexity
+      # rubocop:enable Metrics/PerceivedComplexity

Also applies to: 325-325

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/katello/concerns/base_template_scope_extensions.rb` around lines 248
- 251, The RuboCop disables for Metrics/AbcSize, Metrics/CyclomaticComplexity,
and Metrics/PerceivedComplexity were not re-enabled after the legacy helper;
locate the legacy helper method load_errata_applications_legacy and where
Metrics/MethodLength is turned back on and add corresponding rubocop:enable
lines for Metrics/AbcSize, Metrics/CyclomaticComplexity, and
Metrics/PerceivedComplexity so all four rules are re-enabled immediately after
that helper (ensuring the suppression scope only covers the legacy helper).

347-357: Keep the DB-only helper internal unless you want to support it as public API.

Adding Apipie docs here exposes load_errata_applications_from_db as a template macro, but unlike load_errata_applications it has no table_exists? guard. That lets callers bypass the compatibility wrapper and hit the DB-only path directly on systems where the migration is not finished yet.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/katello/concerns/base_template_scope_extensions.rb` around lines 347
- 357, The apipie doc block currently exposes the DB-only helper
load_errata_applications_from_db as a public template macro; remove or hide that
apipie documentation so callers cannot invoke the DB-only path directly, or
alternatively add the same table existence guard used by
load_errata_applications (call table_exists? before touching DB and raise/return
fallback when false) to load_errata_applications_from_db to prevent bypassing
the compatibility wrapper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/lib/katello/concerns/base_template_scope_extensions.rb`:
- Around line 369-387: The DB-backed branch using Katello::ErrataApplication
bypasses the prior permission check that load_resource(..., permission:
'view_foreman_tasks') enforced; restore equivalent authorization by applying the
same readable/authorized scope before further filtering (e.g. use the same
permission symbol 'view_foreman_tasks' and the same policy/scope helper used by
load_resource) to the Katello::ErrataApplication relation so the subsequent
where/since/up_to/host_id filters only operate on records the caller is allowed
to view.

In `@app/models/katello/errata_application.rb`:
- Around line 58-60: The current rescue in ErrataApplication that rescues
ActiveRecord::RecordInvalid and assumes every validation failure is a duplicate
should be tightened: in the rescue for ActiveRecord::RecordInvalid (the block
that logs "Skipped duplicate: #{erratum.errata_id} on #{host.name}"), inspect
the exception.record.errors (or errors.details) to determine if the failure is
actually the uniqueness violation for the errata/host combination (e.g., error
on :errata_id or :host_id with :taken/has already been taken), and only
swallow/log as a duplicate in that case; for any other validation errors
re-raise the exception (or log full validation errors) so real data problems
aren't silently lost. Ensure the rescue references ActiveRecord::RecordInvalid
and uses exception.record.errors to distinguish uniqueness errors from other
validation failures.
- Around line 95-99: The code in extract_from_script relies on
execution_plan.actions[1] which is brittle; replace that hardcoded index by
scanning execution_plan.all_planned_actions for the action that contains a
script (e.g., action.try(:input).try(:[], 'script')). In extract_from_script,
iterate over execution_plan.all_planned_actions, pull the first non-nil script
string (or concatenate/scan them) and then search its lines for the '#
RESOLVED_ERRATA_IDS=' marker, preserving the existing parsing logic (split on
'=', split by ',' and strip/reject blank). Apply the same change to the other
occurrence mentioned (base_template_scope_extensions.rb) so both use
all_planned_actions rather than actions[1].

In `@db/migrate/20260327192122_create_katello_errata_applications.rb`:
- Line 17: The non-unique index index_errata_apps_host_erratum_date allows
duplicate rows under concurrent writes; change the migration to enforce DB-level
uniqueness by creating a unique constraint/index on [:host_id, :erratum_id,
:applied_at] (e.g., add or replace the existing t.index call with a unique index
or use add_index with unique: true) so the database rejects duplicates and
matches any application-level validation in the Katello::ErrataApplications
table; ensure the index name remains 'index_errata_apps_host_erratum_date' (or
drop the old non-unique index first if needed) to avoid duplicate-index
conflicts during migration.

---

Nitpick comments:
In `@app/lib/katello/concerns/base_template_scope_extensions.rb`:
- Around line 248-251: The RuboCop disables for Metrics/AbcSize,
Metrics/CyclomaticComplexity, and Metrics/PerceivedComplexity were not
re-enabled after the legacy helper; locate the legacy helper method
load_errata_applications_legacy and where Metrics/MethodLength is turned back on
and add corresponding rubocop:enable lines for Metrics/AbcSize,
Metrics/CyclomaticComplexity, and Metrics/PerceivedComplexity so all four rules
are re-enabled immediately after that helper (ensuring the suppression scope
only covers the legacy helper).
- Around line 347-357: The apipie doc block currently exposes the DB-only helper
load_errata_applications_from_db as a public template macro; remove or hide that
apipie documentation so callers cannot invoke the DB-only path directly, or
alternatively add the same table existence guard used by
load_errata_applications (call table_exists? before touching DB and raise/return
fallback when false) to load_errata_applications_from_db to prevent bypassing
the compatibility wrapper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 98fce92f-061d-4dd5-a5de-7018f6f4fd43

📥 Commits

Reviewing files that changed from the base of the PR and between 2b1e044 and 2f94da17745777de913e664b74039a038c31ded7.

📒 Files selected for processing (9)
  • app/lib/actions/middleware/record_errata_application.rb
  • app/lib/katello/concerns/base_template_scope_extensions.rb
  • app/models/katello/concerns/host_managed_extensions.rb
  • app/models/katello/errata_application.rb
  • app/models/katello/erratum.rb
  • db/migrate/20260327192122_create_katello_errata_applications.rb
  • lib/katello/engine.rb
  • test/lib/concerns/base_template_scope_extensions_test.rb
  • test/models/katello/errata_application_test.rb

Comment thread app/lib/katello/concerns/base_template_scope_extensions.rb Outdated
Comment thread app/models/katello/errata_application.rb Outdated
Comment thread app/models/katello/errata_application.rb
Comment thread db/migrate/20260327192122_create_katello_errata_applications.rb Outdated

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (3)
test/lib/concerns/base_template_scope_extensions_test.rb (1)

8-10: Consider using a fixture instead of User.first for reliability.

User.first may return nil if the users table is empty or if the database order changes. Using a named fixture (like users(:admin) or similar) would be more reliable and explicit.

Proposed fix
     `@host` = hosts(:one)
     `@errata` = katello_errata(:security)
     `@bugfix` = katello_errata(:bugfix)
-    `@user` = User.first
+    `@user` = users(:admin)
   end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/lib/concerns/base_template_scope_extensions_test.rb` around lines 8 -
10, Replace the brittle User.first usage in the test setup with a named fixture:
change the `@user` assignment (in the setup for BaseTemplateScopeExtensionsTest)
from User.first to a specific fixture reference such as users(:admin) or
users(:one); ensure the test includes the users fixture (fixtures :users) if not
already present so `@user` reliably references the intended fixture.
app/models/katello/errata_application.rb (1)

20-20: Consider removing default_scope to avoid unexpected query behavior.

default_scope can cause confusion and unexpected behavior when composing queries, particularly when using unscoped is forgotten or when the scope interferes with joins. Consider removing it and explicitly ordering where needed, or using a named scope like recent that can be applied intentionally.

Proposed fix
-    default_scope { order(applied_at: :desc) }
+    scope :recent, -> { order(applied_at: :desc) }

Then update callers to use .recent explicitly where ordering is desired.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/katello/errata_application.rb` at line 20, Remove the model-level
default_scope that orders by applied_at to avoid hidden query behavior; replace
it with an explicit named scope (e.g., def self.recent or scope :recent, -> {
order(applied_at: :desc) }) in the Katello::ErrataApplication model and update
all call sites that rely on the implicit ordering to call .recent explicitly
(search for usages of Katello::ErrataApplication, ErrataApplication, and any
code that expected default ordering) so queries and joins no longer inherit the
ordering unexpectedly.
app/lib/katello/concerns/base_template_scope_extensions.rb (1)

396-411: find_each may conflict with default_scope ordering.

find_each internally uses primary key ordering for batching and ignores any custom ORDER BY clause. The default_scope { order(applied_at: :desc) } on ErrataApplication will be overridden by find_each, which may lead to unexpected ordering in the results. If ordering matters for the report output, consider using .unscoped or removing the default_scope as suggested earlier.

Proposed fix if ordering matters
-        applications.find_each(batch_size: 1000) do |app|
+        applications.reorder(nil).find_each(batch_size: 1000) do |app|

Or if ordered output is needed, use in_batches instead:

applications.in_batches(of: 1000) do |batch|
  batch.each do |app|
    # ...
  end
end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/katello/concerns/base_template_scope_extensions.rb` around lines 396
- 411, The report uses applications.find_each(batch_size: 1000) which ignores
the ErrataApplication default_scope ordering (order(applied_at: :desc)), causing
unexpected output order; change the batching to preserve order by either calling
applications.in_batches(of: 1000) and iterating each batch with batch.each (so
applied_at ordering is respected) or use applications.unscoped.find_each if you
intentionally want primary-key ordering, or remove the default_scope on
ErrataApplication; update the loop in base_template_scope_extensions.rb where
applications.find_each is used to one of these options to ensure the desired
ordering.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/lib/katello/concerns/base_template_scope_extensions.rb`:
- Around line 396-411: The report uses applications.find_each(batch_size: 1000)
which ignores the ErrataApplication default_scope ordering (order(applied_at:
:desc)), causing unexpected output order; change the batching to preserve order
by either calling applications.in_batches(of: 1000) and iterating each batch
with batch.each (so applied_at ordering is respected) or use
applications.unscoped.find_each if you intentionally want primary-key ordering,
or remove the default_scope on ErrataApplication; update the loop in
base_template_scope_extensions.rb where applications.find_each is used to one of
these options to ensure the desired ordering.

In `@app/models/katello/errata_application.rb`:
- Line 20: Remove the model-level default_scope that orders by applied_at to
avoid hidden query behavior; replace it with an explicit named scope (e.g., def
self.recent or scope :recent, -> { order(applied_at: :desc) }) in the
Katello::ErrataApplication model and update all call sites that rely on the
implicit ordering to call .recent explicitly (search for usages of
Katello::ErrataApplication, ErrataApplication, and any code that expected
default ordering) so queries and joins no longer inherit the ordering
unexpectedly.

In `@test/lib/concerns/base_template_scope_extensions_test.rb`:
- Around line 8-10: Replace the brittle User.first usage in the test setup with
a named fixture: change the `@user` assignment (in the setup for
BaseTemplateScopeExtensionsTest) from User.first to a specific fixture reference
such as users(:admin) or users(:one); ensure the test includes the users fixture
(fixtures :users) if not already present so `@user` reliably references the
intended fixture.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1260da11-d78d-4166-8a1b-e8d56fd1b2ae

📥 Commits

Reviewing files that changed from the base of the PR and between 2f94da17745777de913e664b74039a038c31ded7 and 4033cc93f9a804b6707ab2c7d3580ba07ee871a5.

📒 Files selected for processing (9)
  • app/lib/actions/middleware/record_errata_application.rb
  • app/lib/katello/concerns/base_template_scope_extensions.rb
  • app/models/katello/concerns/host_managed_extensions.rb
  • app/models/katello/errata_application.rb
  • app/models/katello/erratum.rb
  • db/migrate/20260327192122_create_katello_errata_applications.rb
  • lib/katello/engine.rb
  • test/lib/concerns/base_template_scope_extensions_test.rb
  • test/models/katello/errata_application_test.rb
✅ Files skipped from review due to trivial changes (1)
  • db/migrate/20260327192122_create_katello_errata_applications.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/models/katello/errata_application_test.rb

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/lib/actions/middleware/record_errata_application.rb`:
- Around line 32-43: The non-REX branch of errata_install_job? currently only
checks for action.label including 'Actions::Katello::Host::Erratum::Install',
which misses legacy ApplicableErrataInstall jobs; update the check in
errata_install_job? (the method in record_errata_application.rb) to also match
'Actions::Katello::Host::Erratum::ApplicableErrataInstall' (or more robustly
match the common namespace 'Actions::Katello::Host::Erratum::') so both Install
and ApplicableErrataInstall are treated as errata-install tasks, ensuring parity
with the legacy/template logic that references ApplicableErrataInstall.

In `@app/lib/katello/concerns/base_template_scope_extensions.rb`:
- Around line 369-389: Replace the array materialization via pluck(:id) with SQL
subqueries: for the authorized set, change the pre-check from
authorized_host_ids = ::Host::Managed.authorized('view_hosts').pluck(:id) and
return [] if authorized_host_ids.empty? to using the relation (e.g. auth_hosts =
::Host::Managed.authorized('view_hosts')) and short-circuit with return [] if
auth_hosts.none?; then apply the subquery in the ErrataApplication where clause
as applications =
Katello::ErrataApplication.includes(includes_list).where(host_id:
auth_hosts.select(:id)). Similarly, replace host_ids =
::Host.search_for(host_filter).pluck(:id) and applications =
applications.where(host_id: host_ids) with applications =
applications.where(host_id: ::Host.search_for(host_filter).select(:id)). Keep
the rest of the status/since/up_to logic unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5cd54644-aea2-41d6-a46f-c3927d96f1f3

📥 Commits

Reviewing files that changed from the base of the PR and between 4033cc93f9a804b6707ab2c7d3580ba07ee871a5 and 875b9a127d1d0dc8d30e41e2fcf8ae6692392a84.

📒 Files selected for processing (9)
  • app/lib/actions/middleware/record_errata_application.rb
  • app/lib/katello/concerns/base_template_scope_extensions.rb
  • app/models/katello/concerns/host_managed_extensions.rb
  • app/models/katello/errata_application.rb
  • app/models/katello/erratum.rb
  • db/migrate/20260327192122_create_katello_errata_applications.rb
  • lib/katello/engine.rb
  • test/lib/concerns/base_template_scope_extensions_test.rb
  • test/models/katello/errata_application_test.rb
✅ Files skipped from review due to trivial changes (1)
  • db/migrate/20260327192122_create_katello_errata_applications.rb
🚧 Files skipped from review as they are similar to previous changes (4)
  • lib/katello/engine.rb
  • app/models/katello/erratum.rb
  • test/lib/concerns/base_template_scope_extensions_test.rb
  • app/models/katello/errata_application.rb

Comment thread app/lib/actions/middleware/record_errata_application.rb Outdated
Comment thread app/lib/katello/concerns/base_template_scope_extensions.rb Outdated

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (2)
app/lib/katello/concerns/base_template_scope_extensions.rb (1)

377-383: Consider handling invalid date parsing gracefully.

Time.zone.parse(since) and Time.zone.parse(up_to) will raise ArgumentError if the string is not a valid date format. The legacy implementation uses string interpolation in the search query which may handle this differently.

Consider wrapping in error handling or validating input:

Suggested defensive parsing
         if since.present?
-          applications = applications.since(Time.zone.parse(since))
+          begin
+            applications = applications.since(Time.zone.parse(since))
+          rescue ArgumentError
+            Rails.logger.warn("Invalid 'since' date format: #{since}")
+          end
         end

         if up_to.present?
-          applications = applications.up_to(Time.zone.parse(up_to))
+          begin
+            applications = applications.up_to(Time.zone.parse(up_to))
+          rescue ArgumentError
+            Rails.logger.warn("Invalid 'up_to' date format: #{up_to}")
+          end
         end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/katello/concerns/base_template_scope_extensions.rb` around lines 377
- 383, Wrap the Time.zone.parse calls for the since and up_to inputs in
defensive parsing so invalid date strings don't raise ArgumentError; e.g. parse
into a local variable (parsed_since = Time.zone.parse(since) rescue nil) and
only call applications.since(parsed_since) if parsed_since.present? Do the same
for up_to (parsed_up_to = Time.zone.parse(up_to) rescue nil) before calling
applications.up_to(parsed_up_to), and optionally log or return a validation
error when parsing fails. Ensure references to Time.zone.parse, the since/up_to
variables, and applications.since/applications.up_to are used so you modify the
correct spots.
app/models/katello/errata_application.rb (1)

20-20: Consider avoiding default_scope for ordering.

default_scope { order(applied_at: :desc) } can lead to unexpected behavior in queries, especially when joining with other tables or when you need a different ordering. It requires explicit unscoped or reorder calls to override.

Consider using an explicit scope instead:

-    default_scope { order(applied_at: :desc) }
+    scope :recent_first, -> { order(applied_at: :desc) }

Then apply .recent_first explicitly where needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/katello/errata_application.rb` at line 20, The model currently
uses default_scope { order(applied_at: :desc) } which can cause surprising query
behavior; replace it with an explicit scope (e.g., scope :recent_first, -> {
order(applied_at: :desc) }) inside the ErrataApplication model and remove the
default_scope, then update all call sites that rely on the implicit ordering to
use .recent_first (or call .reorder(...) where different ordering is required)
so queries and joins behave predictably.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/lib/katello/concerns/base_template_scope_extensions.rb`:
- Around line 377-383: Wrap the Time.zone.parse calls for the since and up_to
inputs in defensive parsing so invalid date strings don't raise ArgumentError;
e.g. parse into a local variable (parsed_since = Time.zone.parse(since) rescue
nil) and only call applications.since(parsed_since) if parsed_since.present? Do
the same for up_to (parsed_up_to = Time.zone.parse(up_to) rescue nil) before
calling applications.up_to(parsed_up_to), and optionally log or return a
validation error when parsing fails. Ensure references to Time.zone.parse, the
since/up_to variables, and applications.since/applications.up_to are used so you
modify the correct spots.

In `@app/models/katello/errata_application.rb`:
- Line 20: The model currently uses default_scope { order(applied_at: :desc) }
which can cause surprising query behavior; replace it with an explicit scope
(e.g., scope :recent_first, -> { order(applied_at: :desc) }) inside the
ErrataApplication model and remove the default_scope, then update all call sites
that rely on the implicit ordering to use .recent_first (or call .reorder(...)
where different ordering is required) so queries and joins behave predictably.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: df48aa53-e428-42ca-8f01-91918ddb8d9f

📥 Commits

Reviewing files that changed from the base of the PR and between 875b9a127d1d0dc8d30e41e2fcf8ae6692392a84 and 63a263e763c175e787c0aa42bfeff9440c4037c5.

📒 Files selected for processing (9)
  • app/lib/actions/middleware/record_errata_application.rb
  • app/lib/katello/concerns/base_template_scope_extensions.rb
  • app/models/katello/concerns/host_managed_extensions.rb
  • app/models/katello/errata_application.rb
  • app/models/katello/erratum.rb
  • db/migrate/20260327192122_create_katello_errata_applications.rb
  • lib/katello/engine.rb
  • test/lib/concerns/base_template_scope_extensions_test.rb
  • test/models/katello/errata_application_test.rb
✅ Files skipped from review due to trivial changes (1)
  • db/migrate/20260327192122_create_katello_errata_applications.rb
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/models/katello/concerns/host_managed_extensions.rb
  • test/lib/concerns/base_template_scope_extensions_test.rb

@coderabbitai coderabbitai Bot 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.

♻️ Duplicate comments (2)
app/lib/katello/concerns/base_template_scope_extensions.rb (1)

446-453: ⚠️ Potential issue | 🟠 Major

Use the ProxyAction script in the legacy search-query fallback.

This branch now reads RunHostJob, but the resolved errata marker is extracted from ::Actions::RemoteExecution::ProxyAction in app/models/katello/errata_application.rb, Lines 102-114. When katello_errata_applications is not available yet, katello_errata_install_by_search tasks will resolve to [] here and disappear from the compatibility report path.

Proposed fix
-          action = task.execution_plan_action.all_planned_actions(::Actions::RemoteExecution::RunHostJob).first
-          script = action.try(:input).try(:[], 'script') || ''
+          proxy_action = task.execution_plan_action
+                           .all_planned_actions(::Actions::RemoteExecution::ProxyAction)
+                           .find { |planned_action| planned_action.try(:input).try(:[], 'script').present? }
+          script = proxy_action.try(:input).try(:[], 'script') || ''
           found = script.lines.find { |line| line.start_with? '# RESOLVED_ERRATA_IDS=' } || ''
-          (found.chomp.split('=', 2).last || '').split(',')
+          (found.chomp.split('=', 2).last || '').split(',').map(&:strip).reject(&:blank?)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/katello/concerns/base_template_scope_extensions.rb` around lines 446
- 453, In errata_ids_from_template_invocation, the fallback currently only
inspects a RunHostJob action for the resolved errata marker so tasks using the
legacy ProxyAction are missed; update the lookup in
errata_ids_from_template_invocation to try finding a
::Actions::RemoteExecution::ProxyAction (in addition to
::Actions::RemoteExecution::RunHostJob) and read its input['script'] the same
way (look for the '# RESOLVED_ERRATA_IDS=' line, split on '=', and return the
CSV as before) so the legacy search-query tasks produce the same []/ids result
as the code in app/models/katello/errata_application.rb expects.
app/models/katello/errata_application.rb (1)

71-80: ⚠️ Potential issue | 🟠 Major

Normalize ApplicableErrataInstall input before extracting host and errata.

These helpers read task.input directly, but Actions::Katello::Host::Erratum::ApplicableErrataInstall stores the real payload on its planned Install action. app/lib/katello/concerns/base_template_scope_extensions.rb, Lines 430-434 already handles that task shape. Without the same normalization here, those jobs pass the middleware filter but record_from_task returns [], so nothing gets persisted.

Proposed fix
+    def self.normalized_task_input(task)
+      return {} unless task
+      return task.input || {} unless task.label == 'Actions::Katello::Host::Erratum::ApplicableErrataInstall'
+
+      install_action = task.execution_plan_action
+                         &.all_planned_actions(::Actions::Katello::Host::Erratum::Install)
+                         &.first
+      install_action&.input || {}
+    end
+
     # Extract host ID from task input
     def self.extract_host_id_from_task(task)
-      input = task.input
+      input = normalized_task_input(task)
       return nil unless input
 
       if input['host'].is_a?(Hash)
         input['host']['id']
       elsif input['host_id']
@@
     # Extract errata IDs from task
     def self.extract_errata_ids_from_task(task)
-      input = task.input
+      input = normalized_task_input(task)
       return [] unless input
 
       errata = input['errata'] || input['content']
       return errata if errata.present?

Also applies to: 83-97

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/katello/errata_application.rb` around lines 71 - 80, The helpers
(extract_host_id_from_task and the corresponding extract_errata_from_task) read
task.input directly but must first normalize the payload the same way
app/lib/katello/concerns/base_template_scope_extensions.rb does: detect when
task.input contains an Action wrapper with a planned_action and replace input
with that planned action's payload before extracting host or errata; update
extract_host_id_from_task and the other helper to check for input['action'] &&
input['action']['planned_action'] (and use the planned_action payload) so they
read the real Install action payload rather than the outer wrapper.
🧹 Nitpick comments (1)
test/models/katello/errata_application_test.rb (1)

228-349: Cover the planned-action and script extraction paths in this suite.

These tests only hit the explicit task.input['errata'] case. One ApplicableErrataInstall case and one katello_errata_install_by_search case would lock down the two branches most likely to regress in this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/models/katello/errata_application_test.rb` around lines 228 - 349, Add
tests that exercise the planned-action and script-extraction branches by
creating tasks whose input mimics the two alternative shapes: (1) a
planned_actions array containing an action entry with name
"ApplicableErrataInstall" and the errata ids (use a new helper like
create_task_with_planned_action mirroring create_task_with_errata but stubbing
task.input['planned_actions'] => [{'name' => 'ApplicableErrataInstall', 'errata'
=> [errata_id]}]); and (2) a script/search form for
katello_errata_install_by_search (create_task_with_script_search that stubs
task.input to include a script or params structure with errata IDs and uses
label 'Actions::Katello::Host::Erratum::Install' or the script name
'katello_errata_install_by_search'). Ensure tests call
ErrataApplication.record_from_task and assert expected applications, and stub
task.user as in create_task_with_errata.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/lib/katello/concerns/base_template_scope_extensions.rb`:
- Around line 446-453: In errata_ids_from_template_invocation, the fallback
currently only inspects a RunHostJob action for the resolved errata marker so
tasks using the legacy ProxyAction are missed; update the lookup in
errata_ids_from_template_invocation to try finding a
::Actions::RemoteExecution::ProxyAction (in addition to
::Actions::RemoteExecution::RunHostJob) and read its input['script'] the same
way (look for the '# RESOLVED_ERRATA_IDS=' line, split on '=', and return the
CSV as before) so the legacy search-query tasks produce the same []/ids result
as the code in app/models/katello/errata_application.rb expects.

In `@app/models/katello/errata_application.rb`:
- Around line 71-80: The helpers (extract_host_id_from_task and the
corresponding extract_errata_from_task) read task.input directly but must first
normalize the payload the same way
app/lib/katello/concerns/base_template_scope_extensions.rb does: detect when
task.input contains an Action wrapper with a planned_action and replace input
with that planned action's payload before extracting host or errata; update
extract_host_id_from_task and the other helper to check for input['action'] &&
input['action']['planned_action'] (and use the planned_action payload) so they
read the real Install action payload rather than the outer wrapper.

---

Nitpick comments:
In `@test/models/katello/errata_application_test.rb`:
- Around line 228-349: Add tests that exercise the planned-action and
script-extraction branches by creating tasks whose input mimics the two
alternative shapes: (1) a planned_actions array containing an action entry with
name "ApplicableErrataInstall" and the errata ids (use a new helper like
create_task_with_planned_action mirroring create_task_with_errata but stubbing
task.input['planned_actions'] => [{'name' => 'ApplicableErrataInstall', 'errata'
=> [errata_id]}]); and (2) a script/search form for
katello_errata_install_by_search (create_task_with_script_search that stubs
task.input to include a script or params structure with errata IDs and uses
label 'Actions::Katello::Host::Erratum::Install' or the script name
'katello_errata_install_by_search'). Ensure tests call
ErrataApplication.record_from_task and assert expected applications, and stub
task.user as in create_task_with_errata.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9cd723e9-9c6e-44c0-92e9-a8ec0cf9bb4a

📥 Commits

Reviewing files that changed from the base of the PR and between 63a263e763c175e787c0aa42bfeff9440c4037c5 and 6c77a8a7a2adba6a31b770427eef22fa1ddcef9e.

📒 Files selected for processing (10)
  • app/lib/actions/middleware/record_errata_application.rb
  • app/lib/katello/concerns/base_template_scope_extensions.rb
  • app/models/katello/concerns/host_managed_extensions.rb
  • app/models/katello/concerns/user_extensions.rb
  • app/models/katello/errata_application.rb
  • app/models/katello/erratum.rb
  • db/migrate/20260327192122_create_katello_errata_applications.rb
  • lib/katello/engine.rb
  • test/lib/concerns/base_template_scope_extensions_test.rb
  • test/models/katello/errata_application_test.rb
✅ Files skipped from review due to trivial changes (2)
  • db/migrate/20260327192122_create_katello_errata_applications.rb
  • test/lib/concerns/base_template_scope_extensions_test.rb
🚧 Files skipped from review as they are similar to previous changes (3)
  • lib/katello/engine.rb
  • app/models/katello/erratum.rb
  • app/models/katello/concerns/host_managed_extensions.rb

@ianballou

ianballou commented Mar 31, 2026

Copy link
Copy Markdown
Member

Before jumping in to a new implementation of tracking errata applications, was the old way tested to ensure it was properly reading errata applications from remote execution jobs? I want to make sure the user-reported issue really was because jobs were cleaned up, and not because there's a legitimate bug around reading the applicability jobs (assuming they are not cleaned up).

Edit: we chatted, and the feature did work when the jobs existed.

@ianballou ianballou 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.

Took a first glance. The performance aspect of this is something I'd like to know more about. Is this putting significantly more load on postgres than anything else if a user applies multiple errata to, say, 1,000 hosts at once?

Comment thread db/migrate/20260327192122_create_katello_errata_applications.rb
Comment thread test/models/katello/errata_application_test.rb Outdated
Comment thread db/migrate/20260327192122_create_katello_errata_applications.rb Outdated
Comment thread db/migrate/20260327192122_create_katello_errata_applications.rb Outdated
@pavanshekar pavanshekar force-pushed the issue-39185 branch 2 times, most recently from 2931287 to e44a1fd Compare March 31, 2026 23:20

@ianballou ianballou 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.

I'm thinking about the rebuild scenario. For a host, if it's provisioned via Foreman, users can rebuild them, which means the OS is likely completely reimaged.

Question then - today, if a host is rebuilt, is the applied errata report for that host the same after rebuild? Or is there any cleanup? Does this PR match that experience?

We may want to consider clearing the history data on rebuild since applied errata no longer apply to the host when it is reimaged. I can't think why someone would care about errata applications once a machine has abandoned its previous OS. This might be worth a question to the community.


I realized too that this report is a bit broken for image mode machines, but I don't think we have to worry too much about it since users shouldn't really be applying errata to those immutable machines.

@pavanshekar

Copy link
Copy Markdown
Contributor Author

I'm thinking about the rebuild scenario. For a host, if it's provisioned via Foreman, users can rebuild them, which means the OS is likely completely reimaged.

Question then - today, if a host is rebuilt, is the applied errata report for that host the same after rebuild? Or is there any cleanup? Does this PR match that experience?

We may want to consider clearing the history data on rebuild since applied errata no longer apply to the host when it is reimaged. I can't think why someone would care about errata applications once a machine has abandoned its previous OS. This might be worth a question to the community.

I realized too that this report is a bit broken for image mode machines, but I don't think we have to worry too much about it since users shouldn't really be applying errata to those immutable machines.

You're right, when a host is rebuilt, the errata application history is no longer relevant since it applied to the old OS. But in this PR the old data won't be cleared. It will be cleaned up only when the host is deleted entirely (due to dependent: :destroy on the host association).

@ianballou ianballou 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.

Some feedback collected outside this PR:

  1. When a machine is rebuilt, the errata application history should be destroyed. Users are likely not interested in seeing errata applications from before a host was rebuilt since that data is gone. If they really need this info, they can go through the old task history, assuming it isn't cleaned up. There likely are build hooks Katello can use for this.
  2. With this history existing, we can now improve the applied errata report to tell if an applied erratum is still applicable to a host. This would be an indicator to admins that they need to go inspect why the application didn't result in the patch state that they expected. This would need to be addressed via the report template in the Foreman repo. This can be done now or filed as a future RFE.

return [] unless proxy_action

script = proxy_action.input['script'] || ''
found = script.lines.find { |line| line.start_with?('# RESOLVED_ERRATA_IDS=') } || ''

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.

Because of issues like https://projects.theforeman.org/issues/38384, these resolved IDs may not always be 100% accurate. I was hoping we could stop using that magic comment as part of this refactor somehow, but didn't really have the details of a solution. I think it's worth calling out here, though.

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 flagging this, Jeremy. I looked into a few alternatives: re-running the advisory query after the job completes risks capturing changed host state, storing resolved IDs as task input doesn't fix the root issue since we're still calling advisory_ids(), and parsing yum/dnf output would require handling OS-specific formats.

The magic comment is only used for install-by-search, the other extraction methods are solid. Do you think it's worth addressing this in this PR, or should we file a follow-up to tackle it alongside https://projects.theforeman.org/issues/38384 ?

@jeremylenz jeremylenz Apr 3, 2026

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.

Yeah, definitely a follow-up, I just wanted to bring it to your attention. I wonder if there's a way we can get into the internals of the dnf command (since Katello proxies all those requests anyway) and directly figure out what errata are being applied that way, when the content is actually requested. Just a thought.

@jeremylenz jeremylenz Apr 3, 2026

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.

The magic comment is only used for install-by-search, the other extraction methods are solid.

Kind of unfortunate since everything in the new UI uses the install-by-search templates. It's still like 98% accurate though, so not too bad.

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.

The dnf internals idea is interesting. That could give us ground truth.

Comment thread app/lib/katello/concerns/base_template_scope_extensions.rb Outdated
@pavanshekar pavanshekar force-pushed the issue-39185 branch 2 times, most recently from 9dbbf57 to c04b6e9 Compare April 7, 2026 18:32
Comment thread app/models/katello/errata_application.rb Outdated

@jeremylenz jeremylenz 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.

Tested again

  • rake task runs and generates with correct string errata IDs
  • applying an errata via REX correctly creates new ErrataApplications
  • Deleting the RunHostsJob tasks and re-running the report correctly shows the errata applications, even when the tasks are missing

Just a couple more questions.

Comment thread lib/katello/tasks/upgrades/4.21/populate_errata_applications.rake Outdated
Comment thread lib/katello/tasks/upgrades/4.21/populate_errata_applications.rake Outdated
@jeremylenz

Copy link
Copy Markdown
Member

@aidenfine I don't really have any more comments here, but I think it would be a good idea to re-run your performance test since things changed a bit.

@pavanshekar pavanshekar force-pushed the issue-39185 branch 2 times, most recently from 973915e to c7cd8f7 Compare June 10, 2026 14:54
applied_at: applied_at,
status: status
)
[application]

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.

Why does this return an array if it's only ever 1 or 0 items returned?

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 changed record_from_task to return the object directly or nil instead of an array.

# Extract errata IDs from task
# @param task [ForemanTasks::Task] The task to extract from
# @param input_values_map [Hash] Optional pre-loaded map of template_invocation_id => errata_value
def self.extract_errata_ids_from_task(task, input_values_map = nil)

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.

Suggested change
def self.extract_errata_ids_from_task(task, input_values_map = nil)
def self.extract_errata_ids_from_task(task)

input_values_map is not used anymore.

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.

Removed the unused input_values_map parameter from extract_errata_ids_from_task and extract_from_template_input since bulk loading happens in the rake script now.

return [] unless proxy_action

script = proxy_action.input['script'] || ''
found = script.lines.find { |line| line.start_with?('# RESOLVED_ERRATA_IDS=') } || ''

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.

If there isn't one already, is it possible to add a unit test that ensures the magic errata IDs comment is there in the job template somehow?

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.

Added unit tests to verify the magic comment pattern exists in the search-based job template.


# Helper method to bulk-load template invocation input values
# Only loads legacy template input ('errata'). Modern templates require Dynflow.
bulk_load_template_input_values = lambda do |tasks|

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 find the use of lamdas here a bit difficult to read, could these just be methods? I don't see the lambdas closing over any variables.

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 converted all lambdas to methods as suggested. Changed from lambda do |params| to def self.method_name(params) and updated all calls from .call() to direct method invocation.

@ianballou

Copy link
Copy Markdown
Member

I don't think I have more comments than the above, and I'll rely on Jeremy's functional testing. +1 to having one more run of the perf test due to the recent changes.

@pavanshekar

pavanshekar commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

I added bulk SQL extraction to query the dynflow_actions table directly for both host_id and errata_ids in a single pass per batch. This eliminates individual task.input calls that were loading Dynflow execution plans and causing corrupted plan errors from historical data. The migration now uses only 2 bulk SQL queries per batch - one for search-based jobs from Dynflow actions table and one for list-based jobs from template_invocation_input_values table.

@pavanshekar

Copy link
Copy Markdown
Contributor Author

Latest performance test run:

Migration complete: 3062851 created, 0 skipped, 0 errors
 INFO  dynflow : start terminating throttle_limiter...
 INFO  dynflow : start terminating client dispatcher...
 INFO  dynflow : stop listening for new events...
 INFO  dynflow : start terminating clock...

real	17m48.890s
user	14m51.758s
sys	1m32.877s

@ianballou

Copy link
Copy Markdown
Member

I don't think I have more comments than the above, and I'll rely on Jeremy's functional testing. +1 to having one more run of the perf test due to the recent changes.

Latest performance test run:

Migration complete: 3062851 created, 0 skipped, 0 errors
 INFO  dynflow : start terminating throttle_limiter...
 INFO  dynflow : start terminating client dispatcher...
 INFO  dynflow : stop listening for new events...
 INFO  dynflow : start terminating clock...

real	17m48.890s
user	14m51.758s
sys	1m32.877s

Nice, for tens of thousands of hosts each with a year's worth of patching jobs, 17 minutes is fine.

@jeremylenz jeremylenz 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.

Works for me!

ACK 👍

@jeremylenz

Copy link
Copy Markdown
Member

foreman_rh_cloud failures are unrelated.

@pavanshekar

Copy link
Copy Markdown
Contributor Author

I added scoped_search to the ErrataApplication model and updated the report template helper to use scoped_search for date filtering instead of Time.zone.parse. This enables users to use relative dates like "3 weeks ago" when generating the Applied Errata report via hammer or API. The change only affects report template rendering and doesn't impact the runtime middleware or migration code paths.

@jeremylenz jeremylenz 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.

Re-tested and still works as expected

@ianballou ianballou 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.

Just one bit of unused code, otherwise +1. We can merge once that's taken care of.

template_content = File.read(template_path)

# Verify the magic comment pattern is present in the template
assert_match(/# RESOLVED_ERRATA_IDS=/, template_content,

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.

Normally I wouldn't want to have the test read that the code is there or not, but in this case, I think it's a helpful gate.

Comment thread lib/katello/tasks/upgrades/4.21/populate_errata_applications.rake Outdated
Comment thread lib/katello/tasks/upgrades/4.21/populate_errata_applications.rake Outdated
@pavanshekar

Copy link
Copy Markdown
Contributor Author

Fixed the migration counter to accurately report created vs skipped records by counting before/after all parallel threads complete, eliminating the race condition.

First run:

Migration complete: 3062851 created, 0 skipped, 0 errors                                                                                     
   INFO  dynflow : start terminating throttle_limiter...                                                                                       
   INFO  dynflow : start terminating client dispatcher...                                                                                      
   INFO  dynflow : stop listening for new events...                                                                                            
   INFO  dynflow : start terminating clock...                                                                                                  
                                                                                                                                               
  real    17m25.991s                                                                                                                           
  user    14m32.525s                                                                                                                           
  sys    1m28.467s 

Second run:

Migration complete: 0 created, 3062851 skipped, 0 errors
 INFO  dynflow : start terminating throttle_limiter...
 INFO  dynflow : start terminating client dispatcher...
 INFO  dynflow : stop listening for new events...
 INFO  dynflow : start terminating clock...

real	17m19.107s
user	14m17.035s
sys	1m49.268s

@ianballou ianballou 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.

I found a blocker:

Comment thread lib/katello/engine.rb Outdated
@@ -120,6 +120,12 @@ def self.register_scheduled_task(task_class, cronline)
world.middleware.use ::Actions::Middleware::KeepLocale
end
end

# Register middleware to track errata applications
ForemanTasks.dynflow.config.on_init do |world|

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.

On my test Satellite, new errata application records were not being created. AI analysis came up with the following:

"""
ForemanTasks.dynflow.config.on_init do |world| defaults to on_init(true), which pushes the callback to @on_executor_init. These callbacks only fire when config.remote? is false (line 43 of dynflow/rails.rb). But in Satellite's dynflow-sidekiq architecture, the worker processes that actually execute REX/errata jobs set config.remote = true, so the executor-only callbacks never fire there. Only the orchestrator (which doesn't execute jobs) gets them.
"""

Suggested change
ForemanTasks.dynflow.config.on_init do |world|
ForemanTasks.dynflow.config.on_init(false) do |world|

I do not see records when applying errata via "Install errata by search query" without this change.

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.

Implemented on_init(false) as suggested. Verified that errata application records are now being created at runtime.

@ianballou ianballou 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.

Thanks!

@jeremylenz jeremylenz merged commit 8b4d283 into Katello:master Jun 15, 2026
31 checks passed
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.

4 participants