Skip to content
Merged
Changes from all commits
Commits
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
25 changes: 25 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,31 @@ jobs:
eval "$MIGRATION_COMMAND"
fi

# Auto-bootstrap seed when DB is empty (prevents blank app after deploy/reset).
get_table_count() {
table="$1"
sudo docker compose -f docker-compose.prod.yml exec -T mysql \
mysql -N -s -uroot -p"$MYSQL_ROOT_PASSWORD" "$MYSQL_DATABASE" -e "SELECT COUNT(*) FROM $table;" 2>/dev/null | tr -d '\r'
Comment on lines +240 to +241
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

The MySQL root password is passed as a CLI argument (-p"$MYSQL_ROOT_PASSWORD"), which can be visible to other processes on the VM via process listings. Prefer using MYSQL_PWD (passed via docker compose exec -e MYSQL_PWD=...) or a defaults file inside the container so the password isn’t part of the command line.

Suggested change
sudo docker compose -f docker-compose.prod.yml exec -T mysql \
mysql -N -s -uroot -p"$MYSQL_ROOT_PASSWORD" "$MYSQL_DATABASE" -e "SELECT COUNT(*) FROM $table;" 2>/dev/null | tr -d '\r'
sudo docker compose -f docker-compose.prod.yml exec -T -e MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql \
mysql -N -s -uroot "$MYSQL_DATABASE" -e "SELECT COUNT(*) FROM $table;" 2>/dev/null | tr -d '\r'

Copilot uses AI. Check for mistakes.
}

PRODUCTS_COUNT="$(get_table_count products || echo 0)"
VARIANTS_COUNT="$(get_table_count product_variants || echo 0)"
STOCK_COUNT="$(get_table_count inventory_stock || echo 0)"
Comment on lines +240 to +246
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

get_table_count suppresses errors and the pipeline’s exit status will be from tr, so failures like MySQL not ready / auth errors / table missing will produce an empty string that later gets treated as 0. That can incorrectly trigger the reset+seed path. Suggest: wait for the mysql service to be healthy before counting, and make get_table_count fail loudly (e.g., add set -o pipefail, capture/validate output is a number, and retry/abort on error rather than treating it as zero).

Suggested change
sudo docker compose -f docker-compose.prod.yml exec -T mysql \
mysql -N -s -uroot -p"$MYSQL_ROOT_PASSWORD" "$MYSQL_DATABASE" -e "SELECT COUNT(*) FROM $table;" 2>/dev/null | tr -d '\r'
}
PRODUCTS_COUNT="$(get_table_count products || echo 0)"
VARIANTS_COUNT="$(get_table_count product_variants || echo 0)"
STOCK_COUNT="$(get_table_count inventory_stock || echo 0)"
# Wait for MySQL to be ready and fail loudly on errors.
local attempt=0
local max_attempts=5
local sleep_seconds=3
local count
# Ensure pipeline failures propagate (mysql errors aren't hidden by `tr`).
set -o pipefail
while [ "$attempt" -lt "$max_attempts" ]; do
if count="$(
sudo docker compose -f docker-compose.prod.yml exec -T mysql \
mysql -N -s -uroot -p"$MYSQL_ROOT_PASSWORD" "$MYSQL_DATABASE" \
-e "SELECT COUNT(*) FROM $table;" | tr -d '\r'
)"; then
if printf '%s\n' "$count" | grep -Eq '^[0-9]+$'; then
echo "$count"
return 0
else
echo "Non-numeric row count '$count' returned for table '$table'" >&2
return 1
fi
fi
attempt=$((attempt + 1))
echo "Unable to query table '$table' (attempt $attempt/$max_attempts). Waiting for MySQL to become ready..." >&2
sleep "$sleep_seconds"
done
echo "Failed to query table '$table' after $max_attempts attempts; aborting." >&2
return 1
}
if ! PRODUCTS_COUNT="$(get_table_count products)"; then
echo "Error retrieving row count for 'products' table; aborting deployment." >&2
exit 1
fi
if ! VARIANTS_COUNT="$(get_table_count product_variants)"; then
echo "Error retrieving row count for 'product_variants' table; aborting deployment." >&2
exit 1
fi
if ! STOCK_COUNT="$(get_table_count inventory_stock)"; then
echo "Error retrieving row count for 'inventory_stock' table; aborting deployment." >&2
exit 1
fi

Copilot uses AI. Check for mistakes.

echo "Post-deploy counts: products=$PRODUCTS_COUNT, variants=$VARIANTS_COUNT, inventory_stock=$STOCK_COUNT"

if [ "${PRODUCTS_COUNT:-0}" = "0" ] || [ "${VARIANTS_COUNT:-0}" = "0" ]; then
echo "Detected empty critical tables. Running reset-db-and-seed.sh..."
if [ -f "./deploy/scripts/reset-db-and-seed.sh" ]; then
sudo chmod +x ./deploy/scripts/reset-db-and-seed.sh
sudo DEPLOY_PATH="$DEPLOY_PATH" COMPOSE_FILE="$DEPLOY_PATH/docker-compose.prod.yml" ENV_FILE="$ENV_FILE" bash ./deploy/scripts/reset-db-and-seed.sh
# Ensure services are up after seed
sudo env REGISTRY="$REGISTRY" IMAGE_NAMESPACE="$IMAGE_NAMESPACE" IMAGE_TAG="$TARGET_TAG" DB_USERNAME="$DB_USERNAME" DB_PASSWORD="$DB_PASSWORD" MYSQL_DATABASE="$MYSQL_DATABASE" MYSQL_ROOT_PASSWORD="$MYSQL_ROOT_PASSWORD" docker compose -f docker-compose.prod.yml up -d --remove-orphans
Comment on lines +250 to +256
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

The condition products == 0 || variants == 0 will run reset-db-and-seed.sh, which drops and recreates the database. This is risky because a legitimately empty table (or a transient/partial seed) would wipe an otherwise valid production DB. Consider tightening the guard to only bootstrap when the database is clearly fresh (e.g., all critical tables are 0 and/or schema just created), and/or gate behind an explicit flag/secret (e.g., AUTO_BOOTSTRAP_SEED=true) so normal deploys can’t trigger a destructive reset.

Copilot uses AI. Check for mistakes.
else
echo "Seed script not found: ./deploy/scripts/reset-db-and-seed.sh"
fi
fi

MAX_RETRIES=12
RETRY_DELAY=10
ATTEMPT=1
Expand Down
Loading