From 2f0c4ae1d02accbd107e500d73cb4b65561dbe9d Mon Sep 17 00:00:00 2001 From: Riley Seaburg Date: Mon, 1 Dec 2025 14:37:19 -0600 Subject: [PATCH 1/8] Add Cargo caching and library linkage verification to CircleCI - Add restore/save cache for Cargo target and registry directories - Use Cargo.lock checksum for cache key to ensure proper invalidation - Verify Rust library linkage with ldd before deploy - Fail fast if library has unresolved dependencies - Should significantly speed up Rust builds on subsequent runs --- .circleci/config.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4f672380d..7fed0ba83 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -55,12 +55,42 @@ jobs: rustc --version cargo --version + - restore_cache: + keys: + - v1-cargo-{{ checksum "ext/widget_renderer/Cargo.lock" }} + - v1-cargo- + - run: name: Build widget renderer (Rust) command: | source $HOME/.cargo/env cargo build --release --manifest-path ext/widget_renderer/Cargo.toml + - run: + name: Verify Rust native library linkage + command: | + set -euo pipefail + LIB=ext/widget_renderer/target/release/libwidget_renderer.so + if [ -f "$LIB" ]; then + echo "Found built rust library; verifying linkage..." + if ldd "$LIB" 2>&1 | grep -q "not found"; then + echo "ERROR: Rust library has unresolved dependencies (ldd shows 'not found')." + ldd "$LIB" || true + exit 1 + else + echo "Rust library linkage looks good" + fi + else + echo "No Rust library built - skipping linkage verification" + fi + + - save_cache: + paths: + - ext/widget_renderer/target + - ~/.cargo/registry + - ~/.cargo/git + key: v1-cargo-{{ checksum "ext/widget_renderer/Cargo.lock" }} + # Download and cache dependencies - restore_cache: keys: From decad7339d32b7de8b0b6652f3f983a87e861b34 Mon Sep 17 00:00:00 2001 From: Riley Seaburg Date: Wed, 10 Dec 2025 13:29:28 -0600 Subject: [PATCH 2/8] Fix cx_collections export_csv 500 error - Handle nil organization in User#cx_collections to prevent NoMethodError - Add eager loading for service_provider.organization and cx_collection_details - Add missing database indexes on cx_collections, cx_collection_details, cx_responses Fixes the 'We're sorry, but something went wrong' error at: /admin/cx_collections/export_csv --- app/models/cx_collection.rb | 7 +++++-- app/models/user.rb | 4 +++- ...251210192727_add_indexes_to_cx_collections.rb | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20251210192727_add_indexes_to_cx_collections.rb diff --git a/app/models/cx_collection.rb b/app/models/cx_collection.rb index 36c270631..3781c5c5b 100644 --- a/app/models/cx_collection.rb +++ b/app/models/cx_collection.rb @@ -87,7 +87,10 @@ def duplicate!(new_user:) end def self.to_csv - collections = all.includes(:organization, :service_provider, :service, :user).references(:organization).order(:fiscal_year, :quarter, 'organizations.name') + collections = all + .includes(:organization, :service, :user, :cx_collection_details, service_provider: :organization) + .references(:organization) + .order(:fiscal_year, :quarter, 'organizations.name') attributes = %i[ id @@ -118,7 +121,7 @@ def self.to_csv csv << attributes collections.each do |collection| - csv << attributes = [ + csv << [ collection.id, collection.name, collection.organization_id, diff --git a/app/models/user.rb b/app/models/user.rb index 683d4c505..9fce23b96 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -19,7 +19,9 @@ class User < ApplicationRecord def cx_collections user_org = organization - user_parent_org = user_org&.parent + return CxCollection.none if user_org.nil? + + user_parent_org = user_org.parent CxCollection.where(cx_collections: { organization_id: [user_org.id, user_parent_org&.id].compact }) end diff --git a/db/migrate/20251210192727_add_indexes_to_cx_collections.rb b/db/migrate/20251210192727_add_indexes_to_cx_collections.rb new file mode 100644 index 000000000..faec71f0d --- /dev/null +++ b/db/migrate/20251210192727_add_indexes_to_cx_collections.rb @@ -0,0 +1,16 @@ +class AddIndexesToCxCollections < ActiveRecord::Migration[8.0] + def change + # cx_collections table - missing all FK indexes + add_index :cx_collections, :organization_id + add_index :cx_collections, :user_id + add_index :cx_collections, :service_provider_id + add_index :cx_collections, :service_id + + # cx_collection_details table - missing FK index + add_index :cx_collection_details, :cx_collection_id + + # cx_responses table - missing FK indexes + add_index :cx_responses, :cx_collection_detail_id + add_index :cx_responses, :cx_collection_detail_upload_id + end +end From 446fc3c2cb3cced317539728e3104b835494f543 Mon Sep 17 00:00:00 2001 From: Riley Seaburg Date: Wed, 10 Dec 2025 13:59:06 -0600 Subject: [PATCH 3/8] Update schema.rb with new indexes for CircleCI --- db/schema.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/db/schema.rb b/db/schema.rb index 2017dc8bd..7c9b8635d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -97,6 +97,7 @@ t.text "trust_question_text" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["cx_collection_id"], name: "index_cx_collection_details_on_cx_collection_id" end create_table "cx_collections", force: :cascade do |t| @@ -124,6 +125,10 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.datetime "submitted_at" + t.index ["organization_id"], name: "index_cx_collections_on_organization_id" + t.index ["service_id"], name: "index_cx_collections_on_service_id" + t.index ["service_provider_id"], name: "index_cx_collections_on_service_provider_id" + t.index ["user_id"], name: "index_cx_collections_on_user_id" end create_table "cx_responses", force: :cascade do |t| @@ -149,6 +154,8 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "external_id" + t.index ["cx_collection_detail_id"], name: "index_cx_responses_on_cx_collection_detail_id" + t.index ["cx_collection_detail_upload_id"], name: "index_cx_responses_on_cx_collection_detail_upload_id" end create_table "digital_product_versions", force: :cascade do |t| From 2a919afa87248f316c1f29a063a76ec2059f5120 Mon Sep 17 00:00:00 2001 From: Riley Seaburg Date: Wed, 10 Dec 2025 14:16:25 -0600 Subject: [PATCH 4/8] Update schema version to include new migration --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 7c9b8635d..275383302 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_07_17_034402) do +ActiveRecord::Schema[8.0].define(version: 2025_12_10_192727) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" From 39a848ccfaf7148af07a25d41872bdd24edeead5 Mon Sep 17 00:00:00 2001 From: Riley Seaburg Date: Thu, 11 Dec 2025 13:16:55 -0600 Subject: [PATCH 5/8] Add tests for User#cx_collections method --- spec/models/user_spec.rb | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0193f7923..49b8f6bc1 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -165,6 +165,44 @@ end end + describe "#cx_collections" do + let(:user_with_org) { FactoryBot.create(:user, organization: organization) } + let(:service_provider) { FactoryBot.create(:service_provider, organization: organization) } + let(:service) { FactoryBot.create(:service, organization: organization, service_provider: service_provider) } + + context "when user has no organization" do + it "returns an empty collection" do + user_without_org = User.new(email: "test@example.gov") + user_without_org.organization = nil + result = user_without_org.cx_collections + expect(result).to eq(CxCollection.none) + expect(result.count).to eq(0) + end + end + + context "when user has an organization" do + it "returns cx_collections for the user's organization" do + cx_collection = FactoryBot.create(:cx_collection, organization: organization, service: service, service_provider: service_provider, user: user_with_org) + result = user_with_org.cx_collections + expect(result).to include(cx_collection) + end + end + + context "when user's organization has a parent organization" do + let(:parent_org) { FactoryBot.create(:organization, name: "Parent Org", domain: "parent.gov", abbreviation: "PARENT") } + let(:child_org) { FactoryBot.create(:organization, name: "Child Org", domain: "child.gov", abbreviation: "CHILD", parent: parent_org) } + let(:child_user) { FactoryBot.create(:user, organization: child_org) } + let(:parent_service_provider) { FactoryBot.create(:service_provider, organization: parent_org) } + let(:parent_service) { FactoryBot.create(:service, organization: parent_org, service_provider: parent_service_provider) } + + it "includes cx_collections from the parent organization" do + parent_cx_collection = FactoryBot.create(:cx_collection, organization: parent_org, service: parent_service, service_provider: parent_service_provider, user: child_user) + result = child_user.cx_collections + expect(result).to include(parent_cx_collection) + end + end + end + describe "#ensure_organization" do before do @org2 = Organization.create(name: "Subdomain Example", domain: "sub.example.gov", abbreviation: "SUB") From f376cab291d37152b52900cd75baec3f42e506cb Mon Sep 17 00:00:00 2001 From: Riley Seaburg Date: Mon, 15 Dec 2025 10:27:32 -0600 Subject: [PATCH 6/8] Fix widget renderer load when native lib missing (#1913) --- config/initializers/widget_renderer.rb | 5 ++++- ext/widget_renderer/lib/widget_renderer.rb | 23 ++++++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/config/initializers/widget_renderer.rb b/config/initializers/widget_renderer.rb index 608c188b1..69e2c6813 100644 --- a/config/initializers/widget_renderer.rb +++ b/config/initializers/widget_renderer.rb @@ -2,7 +2,7 @@ begin # Try loading the precompiled Rutie extension. require_relative '../../ext/widget_renderer/lib/widget_renderer' - + # Verify the class was properly defined if defined?(WidgetRenderer) && WidgetRenderer.respond_to?(:generate_js) Rails.logger.info "WidgetRenderer: Rust extension loaded successfully! generate_js method available." @@ -14,6 +14,9 @@ rescue LoadError => e Rails.logger.warn "Widget renderer native library not available: #{e.message}" Rails.logger.warn 'Rust extension must be built during staging; falling back to ERB template rendering.' +rescue SystemExit => e + Rails.logger.error "Widget renderer exited during load: #{e.message}" + Rails.logger.warn 'Falling back to ERB template rendering' rescue StandardError => e Rails.logger.error "Widget renderer failed to load: #{e.class}: #{e.message}" Rails.logger.error e.backtrace.join("\n") if e.backtrace diff --git a/ext/widget_renderer/lib/widget_renderer.rb b/ext/widget_renderer/lib/widget_renderer.rb index c6cd72a44..6c4142352 100644 --- a/ext/widget_renderer/lib/widget_renderer.rb +++ b/ext/widget_renderer/lib/widget_renderer.rb @@ -9,13 +9,6 @@ puts "WidgetRenderer: root=#{root}" puts "WidgetRenderer: __dir__=#{__dir__}" -# If a stale module exists, remove it so Rutie can define or reopen the class. -if defined?(WidgetRenderer) && WidgetRenderer.is_a?(Module) && !WidgetRenderer.is_a?(Class) - Object.send(:remove_const, :WidgetRenderer) -end -# Ensure the constant exists as a Class so rb_define_class will reopen it instead of erroring on Module. -WidgetRenderer = Class.new unless defined?(WidgetRenderer) && WidgetRenderer.is_a?(Class) - # Check for library file extensions based on platform lib_extensions = %w[.so .bundle .dylib] lib_names = lib_extensions.map { |ext| "libwidget_renderer#{ext}" } @@ -84,6 +77,13 @@ end end end + + # If a stale module exists, remove it so Rutie can define or reopen the class. + if defined?(WidgetRenderer) && WidgetRenderer.is_a?(Module) && !WidgetRenderer.is_a?(Class) + Object.send(:remove_const, :WidgetRenderer) + end + # Ensure the constant exists as a Class so rb_define_class will reopen it instead of erroring on Module. + WidgetRenderer = Class.new unless defined?(WidgetRenderer) && WidgetRenderer.is_a?(Class) else puts 'WidgetRenderer: Library not found in any checked path. Listing root contents:' # List files in root to help debug @@ -104,6 +104,9 @@ else puts "WidgetRenderer: target/release directory does not exist at #{release_dir}" end + + # No native library available; let caller handle fallback. + raise LoadError, 'WidgetRenderer native library not found' end # Rutie expects the project root and appends /target/release/lib.so @@ -112,4 +115,8 @@ puts "WidgetRenderer: Initializing Rutie with path: #{path}" -Rutie.new(:widget_renderer).init 'Init_widget_renderer', path +begin + Rutie.new(:widget_renderer).init 'Init_widget_renderer', path +rescue SystemExit => e + raise LoadError, "WidgetRenderer native init exited: #{e.message}" +end From 2b1fb3a6aebfea2134d8145638d76f73942dd0bd Mon Sep 17 00:00:00 2001 From: Riley Seaburg Date: Mon, 15 Dec 2025 11:09:22 -0600 Subject: [PATCH 7/8] Fix User#cx_collections specs for Service owner (#1915) --- spec/models/user_spec.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 49b8f6bc1..9b41ac73f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -168,7 +168,7 @@ describe "#cx_collections" do let(:user_with_org) { FactoryBot.create(:user, organization: organization) } let(:service_provider) { FactoryBot.create(:service_provider, organization: organization) } - let(:service) { FactoryBot.create(:service, organization: organization, service_provider: service_provider) } + let(:service) { FactoryBot.create(:service, organization: organization, service_provider: service_provider, service_owner_id: user_with_org.id) } context "when user has no organization" do it "returns an empty collection" do @@ -192,8 +192,9 @@ let(:parent_org) { FactoryBot.create(:organization, name: "Parent Org", domain: "parent.gov", abbreviation: "PARENT") } let(:child_org) { FactoryBot.create(:organization, name: "Child Org", domain: "child.gov", abbreviation: "CHILD", parent: parent_org) } let(:child_user) { FactoryBot.create(:user, organization: child_org) } + let(:parent_service_owner) { FactoryBot.create(:user, organization: parent_org) } let(:parent_service_provider) { FactoryBot.create(:service_provider, organization: parent_org) } - let(:parent_service) { FactoryBot.create(:service, organization: parent_org, service_provider: parent_service_provider) } + let(:parent_service) { FactoryBot.create(:service, organization: parent_org, service_provider: parent_service_provider, service_owner_id: parent_service_owner.id) } it "includes cx_collections from the parent organization" do parent_cx_collection = FactoryBot.create(:cx_collection, organization: parent_org, service: parent_service, service_provider: parent_service_provider, user: child_user) From bbcb0ac69aaa623a444deb931c7a2a26c7d0e298 Mon Sep 17 00:00:00 2001 From: Riley Seaburg Date: Thu, 18 Dec 2025 13:07:18 -0600 Subject: [PATCH 8/8] Fix production manifest for rust buildpack (#1918) --- touchpoints.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/touchpoints.yml b/touchpoints.yml index e20b17f88..3f59c2101 100644 --- a/touchpoints.yml +++ b/touchpoints.yml @@ -1,6 +1,7 @@ applications: - name: touchpoints memory: 2G + disk_quota: 2G command: bundle exec rake cf:on_first_instance db:migrate && bundle exec rails s -b 0.0.0.0 -p $PORT -e $RAILS_ENV env: AWS_SES_ACCESS_KEY_ID: @@ -20,12 +21,12 @@ applications: TOUCHPOINTS_GTM_CONTAINER_ID: TOUCHPOINTS_WEB_DOMAIN: touchpoints.app.cloud.gov buildpacks: - - rust_buildpack + - https://github.com/rileyseaburg/rust-buildpack-touchpoints.git - nodejs_buildpack - ruby_buildpack services: - touchpoints-prod-db - - touchpoints-prod-redis + - touchpoints-redis-service - touchpoints-prod-s3 - touchpoints-prod-deployer routes: