Skip to content

v1.8.2 — Critical: back-matter promotion UUID collision fix

Choose a tag to compare

@clem-field clem-field released this 27 May 18:53

v1.8.2 — Critical: back-matter promotion crashes on cross-doc UUID collision

Production hotfix. Single critical fix. Unblocks v1.8.0 / v1.8.1
deploys that crashed mid-migration with:

ActiveRecord::RecordInvalid: Validation failed: Uuid has already been taken

Root cause

back_matter_resources.uuid carries a GLOBAL unique index. The v1.8.0
back-matter promotion stored the original OSCAL source uuid directly
as BMR.uuid. Two docs that legitimately reference the same source
uuid (very common across SSP / SAR / SAP / CDEF for shared NIST
800-53 references) crashed the second doc's INSERT. ECS task
exit-1'd at ~50s into the migration, before any health-check grace
period would matter. Loop.

Mid-failure state was even worse: previous failed deploys left
partial BMR rows in the database. Resume-from-partial hit the same
crash because the per-doc dedup check only looked at the local doc
(idempotent per-doc, NOT global).

The v1.8.0 release notes claimed both migrations were "idempotent
on re-run" — only per-doc idempotency was implemented. Test
fixtures used per-doc-unique UUIDs so the spec coverage missed the
cross-doc collision case.

Fix

Match the SAR cross-doc copy pattern already used by
sar_documents_controller#copy_back_matter_into_sar:

  • BMR.uuid: SecureRandom.uuid (globally fresh, no collision)
  • source uuid preserved in resource_data['source_uuid']

Per-doc dedup check (for idempotent re-runs) now matches BOTH:

  • back_matter_resources.uuid = source_uuid
    (legacy v1.8.0 imports that succeeded)
  • back_matter_resources.resource_data->>'source_uuid' = source_uuid
    (v1.8.2+ imports)

All three upgrade states recover cleanly:

Fresh v1.8.2 installs promote with fresh uuid +
source_uuid metadata
Already-completed v1.8.0 deploys re-run skips (legacy uuid
dedup hit)
Mid-failed v1.8.1 deploys resume continues from where
it left off; no duplicate
creates against existing
partial rows

Files changed

app/services/concerns/back_matter_promotable.rb
Runtime parser path uses fresh uuid + source_uuid stash. Dual-
key dedup helper covers both legacy and new BMRs.

db/migrate/20260526120000_promote_cdef_back_matter_imports.rb
db/migrate/20260526200000_promote_remaining_back_matter_imports.rb
Existing v1.8.0 migration bodies updated in place. Anyone who
already ran them cleanly is unaffected (schema_migrations
records them done; the rake never re-runs).

app/models/sparc_config.rb
VERSION 1.8.1 -> 1.8.2

Specs

spec/services/concerns/back_matter_promotable_spec.rb (new, 9)
Cross-doc UUID collision regression (the original crash);
fresh-uuid + source_uuid metadata preservation; per-doc
idempotency via both legacy uuid path and new resource_data
path; resume-from-partial-v1.8.1 (the actual production state);
unchanged behavior on v4 skip / nil input / audit emission.

Test result

bundle exec rspec 2541 examples, 0 failures, 2 pending (+9 new)
bin/rubocop 0 offenses

Upgrade notes

  • No schema changes. No data destruction.
  • Stuck on v1.7.x because v1.8.0/v1.8.1 crashed: deploy v1.8.2
    directly. Migration resumes from partial state.
  • Already on v1.8.0/v1.8.1 with the migration completed
    successfully: v1.8.2 is a no-op on existing data.
  • Newly-imported back-matter going forward will have BMR.uuid
    distinct from the OSCAL source UUID. The source is recoverable
    from resource_data['source_uuid']. The OSCAL export uses
    BMR.uuid as the resource uuid (unchanged behavior).

What's next (v1.8.3)

Architectural fix so this class of bug stops killing deploys
regardless of any future migration bug: deferred data migrations
as a Solid Queue job, container comes up immediately, migration
runs in background with admin status page + structured log
emission. Already designed; v1.8.3 will land as a normal feature
PR after v1.8.2 stabilizes in production.

🤖 Generated with Claude Code