Match existing SBOM components on their organization

🤖 AI wrote this code from my instructions.

Unreviewed AI output often lands on reviewers and costs them time.
This MR is human verified: I read the full diff and ran the tests myself, because your time is as valuable as mine.

What does this MR do and why?

Fixes the existing-component lookup in ee/app/services/sbom/ingestion/tasks/ingest_components.rb.

The lookup built its where values with:

Sbom::Component.by_unique_attributes(*occurrence_map.to_h.values_at(
  :name, :purl_type, :component_type, :organization_id))

OccurrenceMap#to_h has no organization_id key, so that value was always nil. The scope is where(name:, purl_type:, component_type:, organization_id:), and sbom_components.organization_id is NOT NULL, so the query could never match a row.

As a result, existing_record always returned nil and the skip in attributes was unreachable. Every already-known component went through bulk_upsert! again on each ingestion, hitting the ON CONFLICT DO UPDATE path and rewriting updated_at on unchanged rows, plus running a wasted OR-chained query per slice. Commit e4f6455cea82 ("Skip updates for existing components and versions") added this short-circuit to avoid exactly that cost.

This reads as a slip, not intent:

  • attributes builds its own hash and does .merge!(organization_id: organization_id), so the author already knew to_h lacks the key.
  • The class overrides grouping_key_for_map to inject organization_id for the same reason.
  • The sibling task IngestComponentVersions has a comment describing this exact trap.

The fix: take name, purl_type, component_type from to_h, and organization_id from the pipeline, matching what attributes already does.

The old spec example does not update existing component built its existing component from an occurrence map that was deliberately excluded from the maps under test, and gave it the factory's organization instead of the pipeline's. It could never fail, which is how this bug survived. It is replaced by a context whose existing component matches one of the tested maps and carries the pipeline's organization, with two examples: updated_at does not change, and the component is reused while only the remaining three are inserted.

References

None.

How to set up and validate locally

Run the spec:

bundle exec rspec ee/spec/services/sbom/ingestion/tasks/ingest_components_spec.rb

9 examples, all pass. The full ee/spec/services/sbom/ingestion/ directory (228 examples) also passes.

Revert the lookup change in existing_records and run again to see the bug reproduce:

Failure/Error: expect { ingest_components }.not_to change { existing_component.reload.updated_at }
  expected `existing_component.reload.updated_at` not to have changed, but did change from
  2026-09-15 16:06:22.907502000 +0000 to 2026-09-15 16:06:22.911521000 +0000
2 examples, 1 failure

Caveat: the second new example (component reused, only three inserted) passes with or without the fix, since the upsert already resolves to the same row on conflict. It checks that component_id stays unchanged, not proof of the fix by itself.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Merge request reports

Loading
Loading