Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
b70a34b
Merge pull request #1898 from GSA/feature/rust-widget-renderer
rileyseaburg Nov 20, 2025
539f661
Tighten ignore for env/vars files
rileyseaburg Nov 21, 2025
bb3cd67
Look for widget artifact in workspace target dir
rileyseaburg Nov 21, 2025
8af0c02
Fix widget build script app root detection
rileyseaburg Nov 21, 2025
2db8e84
Make widget build script locate ext directory flexibly
rileyseaburg Nov 21, 2025
51bbc6a
Link rutie against shared ruby in runtime build
rileyseaburg Nov 21, 2025
36b2721
Avoid rutie static link; skip linking when building at runtime
rileyseaburg Nov 21, 2025
a80e494
Prefer cached Rust toolchain in runtime build
rileyseaburg Nov 21, 2025
2eaaf05
Ensure widget .so is included in CF bits
rileyseaburg Nov 21, 2025
6143f92
Copy shipped widget .so into target/release at runtime
rileyseaburg Nov 21, 2025
fa7dbbe
Let Rutie define WidgetRenderer (drop Ruby module wrapper)
rileyseaburg Nov 22, 2025
d598548
Load widget renderer from lib/widget_renderer
rileyseaburg Nov 22, 2025
8d0bbd1
Remove stale WidgetRenderer module before Rutie init
rileyseaburg Nov 22, 2025
936da01
Guard WidgetRenderer constant as class before Rutie init
rileyseaburg Nov 22, 2025
a23cc09
Allow app-staging host in staging manifest
rileyseaburg Nov 22, 2025
1fa69f2
Use app-staging as asset host to satisfy SRI
rileyseaburg Nov 22, 2025
c1ecb52
Scope session cookie domain via optional env
rileyseaburg Nov 22, 2025
d98161b
Point LOGIN_GOV_REDIRECT_URI to app-staging host
rileyseaburg Nov 22, 2025
9c96767
Pass question text field to Rust renderer
rileyseaburg Nov 22, 2025
faf7e69
Clean up cx collections export tests and fix service csv list
rileyseaburg Nov 22, 2025
b420feb
Merge branch 'feature/rust-widget-renderer' into develop
rileyseaburg Nov 22, 2025
4580b73
Bump Ruby to 3.4.7
rileyseaburg Nov 24, 2025
6cc6804
Build Rust widget renderer in CircleCI
rileyseaburg Nov 24, 2025
4b566d9
Use cargo build for widget renderer in CI
rileyseaburg Nov 24, 2025
8ff23e8
Coerce nil booleans before calling Rust renderer
rileyseaburg Nov 24, 2025
2dfa28e
Skip Rust widget renderer in test env to stabilize specs
rileyseaburg Nov 24, 2025
0c32cce
Default element_selector for widget renderer
rileyseaburg Nov 24, 2025
609891d
Stabilize form permissions spec expectations
rileyseaburg Nov 24, 2025
c6fe6d2
Revert Ruby target to 3.2.8 for CF buildpack
rileyseaburg Nov 24, 2025
1b0c575
Use prebuilt widget renderer .so before compiling at runtime
rileyseaburg Nov 24, 2025
b92b49b
Fix widget renderer build artifact detection
rileyseaburg Nov 24, 2025
0081849
Make widget renderer tolerate null booleans
rileyseaburg Nov 24, 2025
d48eeb4
Add logging and extra fallback for widget renderer .so in pre-start
rileyseaburg Nov 24, 2025
cf2a312
Allow widget renderer pre-start to build if no prebuilt lib
rileyseaburg Nov 24, 2025
0395cf6
Set HOME to /home/vcap before building widget renderer
rileyseaburg Nov 24, 2025
fc0ebbe
Harden widget renderer runtime build paths
rileyseaburg Nov 24, 2025
42d9cc4
Trigger CircleCI deploy
rileyseaburg Nov 24, 2025
21c7ccf
Guard CF deploy steps to a single parallel node
rileyseaburg Nov 24, 2025
16004a5
Precompile done.svg so the landing page renders
rileyseaburg Nov 24, 2025
1bea5e4
Link done.svg in manifest and clean precompile entry
rileyseaburg Nov 24, 2025
ee4a044
Allow asset fallback in staging
rileyseaburg Nov 24, 2025
4203ce3
Enable Rack::Attack middleware
rileyseaburg Nov 24, 2025
12b0b2f
Stabilize digital product create feature spec
rileyseaburg Nov 24, 2025
2e448c4
Loosen digital product path assertion
rileyseaburg Nov 24, 2025
82b503d
Avoid runtime Rust build in widget renderer
rileyseaburg Nov 24, 2025
2c78f55
Increase memory allocation to 2G for Rust widget renderer
rileyseaburg Nov 25, 2025
273f1d0
Fix SRI CORS issue by adding crossorigin attribute to asset tags
rileyseaburg Nov 25, 2025
54b8531
Add CORS headers for static assets to support SRI cross-origin requests
rileyseaburg Nov 25, 2025
2af401e
Add prebuilt Linux libwidget_renderer.so for Cloud Foundry deployment
rileyseaburg Nov 25, 2025
051deb6
Add debug logging for WidgetRenderer initialization
rileyseaburg Nov 25, 2025
73380ed
Add debugging output to widget_renderer.rb
rileyseaburg Dec 1, 2025
0480a2b
Fix: Copy library to expected location when found in workspace target…
rileyseaburg Dec 1, 2025
d3e0271
Fix: Set LD_LIBRARY_PATH for libruby.so at runtime on Cloud Foundry
rileyseaburg Dec 1, 2025
6a9cac1
Fix flaky test: explicitly set service to twitter for digital_service…
rileyseaburg Dec 1, 2025
3596c94
Add retry logic to CF deploy scripts to handle staging race conditions
rileyseaburg Dec 1, 2025
e7b114a
Add deployment wait logic to prevent CF supersession errors
rileyseaburg Dec 1, 2025
3475fe0
Add CF env-based deploy lock to serialize concurrent pipelines
rileyseaburg Dec 1, 2025
14288a9
Build Rust extension at runtime on CF to fix libruby.so linking
rileyseaburg Dec 1, 2025
e304dff
Check library linkage before using - rebuild if libruby not found
rileyseaburg Dec 1, 2025
2f0c4ae
Add Cargo caching and library linkage verification to CircleCI
rileyseaburg Dec 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .cfignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
/public/packs-test
/node_modules

# Ignore Rust build artifacts
# Ignore Rust build artifacts, but keep the prebuilt widget library
target/
ext/widget_renderer/target/
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

The negation pattern on line 44 won't work as intended because line 43 already ignores the parent directory ext/widget_renderer/target/. In gitignore-style patterns, once a parent directory is ignored, you cannot un-ignore it with a negation pattern before un-ignoring its contents.

Consider removing line 43 (ext/widget_renderer/target/) and keeping only line 42 (target/) which already covers this path. The specific negations on lines 45-46 should then work correctly.

Suggested change
ext/widget_renderer/target/

Copilot uses AI. Check for mistakes.
!ext/widget_renderer/target/
!ext/widget_renderer/target/release/
!ext/widget_renderer/target/release/libwidget_renderer.so
!ext/widget_renderer/libwidget_renderer.so
82 changes: 79 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
#
version: 2.1

# Cancel redundant builds when new commits are pushed
# This prevents multiple pipelines from racing to deploy
# Note: This must also be enabled in CircleCI project settings
# Settings > Advanced > Auto-cancel redundant builds

jobs:
build:
docker:
- image: cimg/ruby:3.4.7-browsers # Updated to match Gemfile Ruby version
- image: cimg/ruby:3.2.8-browsers # Matches deployed Ruby version in CF
environment:
RAILS_ENV: test
PGHOST: 127.0.0.1
Expand Down Expand Up @@ -41,6 +46,51 @@ jobs:

- checkout

- run:
name: Install Rust toolchain
command: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
echo 'source $HOME/.cargo/env' >> $BASH_ENV
source $HOME/.cargo/env
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:
Expand Down Expand Up @@ -91,11 +141,37 @@ jobs:

- run:
name: Deploy Sidekiq worker servers
command: ./.circleci/deploy-sidekiq.sh
command: |
# Only deploy from a single parallel node to avoid concurrent CF pushes.
if [ "${CIRCLE_NODE_INDEX:-0}" != "0" ]; then
echo "Skipping Sidekiq deploy on parallel node ${CIRCLE_NODE_INDEX}"
exit 0
fi
# Remove prebuilt Rust library - it must be built on CF with the correct Ruby paths
# The library built on CircleCI links against /usr/local/lib/libruby.so.3.2
# but on CF, Ruby is in /home/vcap/deps/*/ruby/lib/
echo "Removing prebuilt Rust library (will be rebuilt on CF)..."
rm -rf ext/widget_renderer/target/release/libwidget_renderer.so 2>/dev/null || true
rm -f ext/widget_renderer/libwidget_renderer.so 2>/dev/null || true
./.circleci/deploy-sidekiq.sh
no_output_timeout: 30m

- run:
name: Deploy web server(s)
command: ./.circleci/deploy.sh
command: |
# Only deploy from a single parallel node to avoid concurrent CF pushes.
if [ "${CIRCLE_NODE_INDEX:-0}" != "0" ]; then
echo "Skipping web deploy on parallel node ${CIRCLE_NODE_INDEX}"
exit 0
fi
# Wait for Sidekiq deployment to complete before starting web deploy
sleep 120
# Remove prebuilt Rust library - it must be built on CF with the correct Ruby paths
echo "Removing prebuilt Rust library (will be rebuilt on CF)..."
rm -rf ext/widget_renderer/target/release/libwidget_renderer.so 2>/dev/null || true
rm -f ext/widget_renderer/libwidget_renderer.so 2>/dev/null || true
./.circleci/deploy.sh
no_output_timeout: 30m

cron_tasks:
docker:
Expand Down
128 changes: 125 additions & 3 deletions .circleci/deploy-sidekiq.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,135 @@
# a non-zero exit code
set -e

# Acquire a deployment lock using CF environment variable
# This prevents multiple pipelines from deploying simultaneously
acquire_deploy_lock() {
local app_name="$1"
local lock_name="DEPLOY_LOCK"
local lock_value="${CIRCLE_BUILD_NUM:-$$}_$(date +%s)"
local max_wait=600 # 10 minutes max
local wait_interval=30
local waited=0

echo "Attempting to acquire deploy lock for $app_name..."

while [ $waited -lt $max_wait ]; do
# Check if there's an existing lock
local current_lock=$(cf env "$app_name" 2>/dev/null | grep "$lock_name:" | awk '{print $2}' || echo "")

if [ -z "$current_lock" ] || [ "$current_lock" == "null" ]; then
# No lock exists, try to acquire it
echo "Setting deploy lock: $lock_value"
cf set-env "$app_name" "$lock_name" "$lock_value" > /dev/null 2>&1 || true
sleep 5 # Small delay to handle race conditions

# Verify we got the lock
current_lock=$(cf env "$app_name" 2>/dev/null | grep "$lock_name:" | awk '{print $2}' || echo "")
if [ "$current_lock" == "$lock_value" ]; then
echo "Deploy lock acquired: $lock_value"
return 0
fi
fi

# Check if lock is stale (older than 15 minutes)
local lock_time=$(echo "$current_lock" | cut -d'_' -f2)
local now=$(date +%s)
if [ -n "$lock_time" ] && [ $((now - lock_time)) -gt 900 ]; then
echo "Stale lock detected (age: $((now - lock_time))s), clearing..."
cf unset-env "$app_name" "$lock_name" > /dev/null 2>&1 || true
continue
fi

echo "Deploy lock held by another process ($current_lock), waiting ${wait_interval}s... (waited ${waited}s)"
sleep $wait_interval
waited=$((waited + wait_interval))
done

echo "Warning: Could not acquire lock after ${max_wait}s, proceeding anyway..."
return 0
}

# Release the deployment lock
release_deploy_lock() {
local app_name="$1"
local lock_name="DEPLOY_LOCK"
echo "Releasing deploy lock for $app_name..."
cf unset-env "$app_name" "$lock_name" > /dev/null 2>&1 || true
}

# Wait for any in-progress deployments to complete before starting
wait_for_deployment() {
local app_name="$1"
local max_wait=600 # 10 minutes max
local wait_interval=15
local waited=0

echo "Checking for in-progress deployments of $app_name..."

while [ $waited -lt $max_wait ]; do
# Get deployment status - look for ACTIVE deployments
local status=$(cf curl "/v3/deployments?app_guids=$(cf app "$app_name" --guid)&status_values=ACTIVE" 2>/dev/null | grep -o '"state":"[^"]*"' | head -1 || echo "")

if [ -z "$status" ] || [[ "$status" == *'"state":"FINALIZED"'* ]] || [[ "$status" == *'"state":"DEPLOYED"'* ]]; then
echo "No active deployment in progress, proceeding..."
return 0
fi

echo "Deployment in progress ($status), waiting ${wait_interval}s... (waited ${waited}s of ${max_wait}s)"
sleep $wait_interval
waited=$((waited + wait_interval))
done

echo "Warning: Timed out waiting for previous deployment, proceeding anyway..."
return 0
}

# Retry function to handle staging and deployment conflicts
cf_push_with_retry() {
local app_name="$1"
local max_retries=5
local retry_delay=90

# Acquire lock first
acquire_deploy_lock "$app_name"

# Ensure lock is released on exit
trap "release_deploy_lock '$app_name'" EXIT

# Wait for any in-progress deployment
wait_for_deployment "$app_name"

for i in $(seq 1 $max_retries); do
echo "Attempt $i of $max_retries to push $app_name..."
if cf push "$app_name" --strategy rolling; then
echo "Successfully pushed $app_name"
release_deploy_lock "$app_name"
trap - EXIT # Clear the trap
return 0
else
local exit_code=$?
if [ $i -lt $max_retries ]; then
echo "Push failed (exit code: $exit_code), waiting ${retry_delay}s before retry..."
sleep $retry_delay
# Re-check for in-progress deployments before retrying
wait_for_deployment "$app_name"
fi
fi
done

release_deploy_lock "$app_name"
trap - EXIT # Clear the trap
echo "Failed to push $app_name after $max_retries attempts"
return 1
}

if [ "${CIRCLE_BRANCH}" == "production" ]
then
echo "Logging into cloud.gov"
# Log into CF and push
cf login -a $CF_API_ENDPOINT -u $CF_PRODUCTION_SPACE_DEPLOYER_USERNAME -p $CF_PRODUCTION_SPACE_DEPLOYER_PASSWORD -o $CF_ORG -s prod
echo "PUSHING to PRODUCTION..."
cf push touchpoints-production-sidekiq-worker --strategy rolling
cf_push_with_retry touchpoints-production-sidekiq-worker
echo "Push to Production Complete."
else
echo "Not on the production branch."
Expand All @@ -22,7 +144,7 @@ then
# Log into CF and push
cf login -a $CF_API_ENDPOINT -u $CF_USERNAME -p $CF_PASSWORD -o $CF_ORG -s $CF_SPACE
echo "Pushing to Demo..."
cf push touchpoints-demo-sidekiq-worker --strategy rolling
cf_push_with_retry touchpoints-demo-sidekiq-worker
echo "Push to Demo Complete."
else
echo "Not on the main branch."
Expand All @@ -34,7 +156,7 @@ then
# Log into CF and push
cf login -a $CF_API_ENDPOINT -u $CF_USERNAME -p $CF_PASSWORD -o $CF_ORG -s $CF_SPACE
echo "Pushing to Staging..."
cf push touchpoints-staging-sidekiq-worker --strategy rolling
cf_push_with_retry touchpoints-staging-sidekiq-worker
echo "Push to Staging Complete."
else
echo "Not on the develop branch."
Expand Down
Loading
Loading