Implement creator studio and upload updates
This commit is contained in:
272
scripts/deploy-production.sh
Normal file
272
scripts/deploy-production.sh
Normal file
@@ -0,0 +1,272 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
root_dir="$(cd -- "$script_dir/.." && pwd)"
|
||||
|
||||
local_folder="${LOCAL_FOLDER:-$root_dir}"
|
||||
remote_folder="${REMOTE_FOLDER:-/opt/www/virtual/SkinbaseNova}"
|
||||
remote_server="${REMOTE_SERVER:-klevze@server3.klevze.si}"
|
||||
php_bin="${PHP_BIN:-php}"
|
||||
composer_bin="${COMPOSER_BIN:-composer}"
|
||||
ssh_bin="${SSH_BIN:-ssh}"
|
||||
rsync_bin="${RSYNC_BIN:-rsync}"
|
||||
local_build_command="${LOCAL_BUILD_COMMAND:-}"
|
||||
|
||||
run_local_build=1
|
||||
run_remote_migrations=1
|
||||
run_db_sync=0
|
||||
db_sync_source=""
|
||||
legacy_db_sync_mode=0
|
||||
force_db_sync=0
|
||||
skip_maintenance=0
|
||||
db_sync_confirm_target="${DB_SYNC_CONFIRM_TARGET:-}"
|
||||
db_sync_confirm_phrase="${DB_SYNC_CONFIRM_PHRASE:-}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: bash sync.sh [options]
|
||||
|
||||
Options:
|
||||
--skip-build Skip local npm build before rsync.
|
||||
--skip-migrate Skip php artisan migrate on the server.
|
||||
--with-db-from=local Replace the production database with a dump from the local database.
|
||||
--confirm-db-sync-target HOST
|
||||
Must match the remote server name when running non-interactively.
|
||||
--confirm-db-sync-phrase TEXT
|
||||
Must equal 'replace production db from local' when running non-interactively.
|
||||
--with-db Legacy alias for --with-db-from=local.
|
||||
--force-db-sync Legacy extra confirmation flag for --with-db.
|
||||
--no-maintenance Skip php artisan down/up during deploy.
|
||||
--help Show this help.
|
||||
|
||||
Environment overrides:
|
||||
LOCAL_FOLDER, REMOTE_FOLDER, REMOTE_SERVER, PHP_BIN, COMPOSER_BIN, SSH_BIN, RSYNC_BIN,
|
||||
LOCAL_BUILD_COMMAND, DB_SYNC_CONFIRM_TARGET, DB_SYNC_CONFIRM_PHRASE
|
||||
EOF
|
||||
}
|
||||
|
||||
confirm_database_replacement() {
|
||||
local expected_phrase="replace production db from local"
|
||||
local typed_target=""
|
||||
local typed_phrase=""
|
||||
|
||||
if [[ "$run_db_sync" -ne 1 || "$db_sync_source" != "local" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo "WARNING: this will overwrite the production database on $remote_server using your local database dump."
|
||||
|
||||
if [[ -n "$db_sync_confirm_target" || -n "$db_sync_confirm_phrase" ]]; then
|
||||
if [[ "$db_sync_confirm_target" != "$remote_server" ]]; then
|
||||
echo "Refusing DB sync: --confirm-db-sync-target must exactly match $remote_server." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$db_sync_confirm_phrase" != "$expected_phrase" ]]; then
|
||||
echo "Refusing DB sync: --confirm-db-sync-phrase must exactly equal '$expected_phrase'." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ ! -t 0 ]]; then
|
||||
echo "Refusing DB sync in non-interactive mode without --confirm-db-sync-target \"$remote_server\" and --confirm-db-sync-phrase \"$expected_phrase\"." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
read -r -p "Type the remote server to confirm DB replacement [$remote_server]: " typed_target
|
||||
if [[ "$typed_target" != "$remote_server" ]]; then
|
||||
echo "Refusing DB sync: remote server confirmation did not match." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
read -r -p "Type '$expected_phrase' to continue: " typed_phrase
|
||||
if [[ "$typed_phrase" != "$expected_phrase" ]]; then
|
||||
echo "Refusing DB sync: confirmation phrase did not match." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
is_wsl() {
|
||||
[[ -n "${WSL_DISTRO_NAME:-}" || -n "${WSL_INTEROP:-}" ]]
|
||||
}
|
||||
|
||||
run_frontend_build() {
|
||||
if [[ -n "$local_build_command" ]]; then
|
||||
(
|
||||
cd "$local_folder"
|
||||
eval "$local_build_command"
|
||||
)
|
||||
return
|
||||
fi
|
||||
|
||||
if is_wsl && command -v wslpath >/dev/null 2>&1 && command -v powershell.exe >/dev/null 2>&1; then
|
||||
local windows_local_folder
|
||||
windows_local_folder="$(wslpath -w "$local_folder")"
|
||||
|
||||
echo "Detected WSL checkout; running frontend build with Windows npm.cmd to match local node_modules..."
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \
|
||||
"Set-Location -LiteralPath '$windows_local_folder'; npm.cmd run build"
|
||||
return
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$local_folder"
|
||||
npm run build
|
||||
)
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--skip-build)
|
||||
run_local_build=0
|
||||
;;
|
||||
--skip-migrate)
|
||||
run_remote_migrations=0
|
||||
;;
|
||||
--with-db-from=local)
|
||||
run_db_sync=1
|
||||
db_sync_source="local"
|
||||
;;
|
||||
--with-db-from=*)
|
||||
echo "Unsupported DB sync source in option: $1" >&2
|
||||
echo "Only --with-db-from=local is supported." >&2
|
||||
exit 1
|
||||
;;
|
||||
--with-db)
|
||||
run_db_sync=1
|
||||
db_sync_source="local"
|
||||
legacy_db_sync_mode=1
|
||||
;;
|
||||
--force-db-sync)
|
||||
force_db_sync=1
|
||||
;;
|
||||
--confirm-db-sync-target)
|
||||
shift
|
||||
db_sync_confirm_target="${1:?Missing value for --confirm-db-sync-target}"
|
||||
;;
|
||||
--confirm-db-sync-target=*)
|
||||
db_sync_confirm_target="${1#*=}"
|
||||
;;
|
||||
--confirm-db-sync-phrase)
|
||||
shift
|
||||
db_sync_confirm_phrase="${1:?Missing value for --confirm-db-sync-phrase}"
|
||||
;;
|
||||
--confirm-db-sync-phrase=*)
|
||||
db_sync_confirm_phrase="${1#*=}"
|
||||
;;
|
||||
--no-maintenance)
|
||||
skip_maintenance=1
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [[ "$run_db_sync" -eq 1 && "$db_sync_source" != "local" ]]; then
|
||||
echo "Refusing DB sync without an explicit source. Use --with-db-from=local." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$run_db_sync" -eq 1 ]] && { [[ -z "$db_sync_confirm_target" && -n "$db_sync_confirm_phrase" ]] || [[ -n "$db_sync_confirm_target" && -z "$db_sync_confirm_phrase" ]]; }; then
|
||||
echo "Refusing DB sync: both --confirm-db-sync-target and --confirm-db-sync-phrase are required together when provided." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$legacy_db_sync_mode" -eq 1 && "$force_db_sync" -ne 1 ]]; then
|
||||
echo "Refusing legacy --with-db without --force-db-sync. Prefer --with-db-from=local instead." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$run_db_sync" -eq 1 ]]; then
|
||||
confirm_database_replacement
|
||||
fi
|
||||
|
||||
if [[ "$run_local_build" -eq 1 ]]; then
|
||||
echo "Building frontend assets locally..."
|
||||
run_frontend_build
|
||||
fi
|
||||
|
||||
echo "Syncing application files to $remote_server..."
|
||||
"$rsync_bin" -avz \
|
||||
--delete \
|
||||
--delete-delay \
|
||||
--chmod=D755,F644 \
|
||||
--exclude ".phpintel/" \
|
||||
--exclude "bootstrap/cache/" \
|
||||
--exclude ".env" \
|
||||
--exclude "public/hot" \
|
||||
--exclude "node_modules" \
|
||||
--exclude "public/files/" \
|
||||
--exclude "resources/lang/" \
|
||||
--exclude "storage/" \
|
||||
--exclude ".git/" \
|
||||
--exclude ".cursor/" \
|
||||
--exclude ".venv/" \
|
||||
--exclude "/oldSite" \
|
||||
--exclude "/vendor" \
|
||||
-e ssh \
|
||||
"$local_folder/" \
|
||||
"$remote_server:$remote_folder/"
|
||||
|
||||
if [[ "$run_db_sync" -eq 1 ]]; then
|
||||
echo "Replacing the production database from the local dump..."
|
||||
"$script_dir/push-db-to-prod.sh" \
|
||||
--force \
|
||||
--remote-server "$remote_server" \
|
||||
--remote-folder "$remote_folder" \
|
||||
$( [[ "$run_remote_migrations" -eq 0 ]] && printf '%s' '--skip-migrate' )
|
||||
fi
|
||||
|
||||
echo "Running remote Composer and Artisan steps..."
|
||||
"$ssh_bin" "$remote_server" \
|
||||
REMOTE_FOLDER="$(printf '%q' "$remote_folder")" \
|
||||
PHP_BIN="$(printf '%q' "$php_bin")" \
|
||||
COMPOSER_BIN="$(printf '%q' "$composer_bin")" \
|
||||
RUN_REMOTE_MIGRATIONS="$run_remote_migrations" \
|
||||
SKIP_MAINTENANCE="$skip_maintenance" \
|
||||
'bash -s' <<'EOF'
|
||||
set -euo pipefail
|
||||
|
||||
cd "$REMOTE_FOLDER"
|
||||
|
||||
bring_app_up() {
|
||||
if [[ "$SKIP_MAINTENANCE" -eq 0 ]]; then
|
||||
"$PHP_BIN" artisan up >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
trap bring_app_up EXIT
|
||||
|
||||
if [[ "$SKIP_MAINTENANCE" -eq 0 ]]; then
|
||||
"$PHP_BIN" artisan down --retry=60 || true
|
||||
fi
|
||||
|
||||
"$COMPOSER_BIN" install --no-dev --prefer-dist --optimize-autoloader --no-interaction
|
||||
|
||||
if [[ "$RUN_REMOTE_MIGRATIONS" -eq 1 ]]; then
|
||||
"$PHP_BIN" artisan migrate --force
|
||||
fi
|
||||
|
||||
"$PHP_BIN" artisan optimize:clear
|
||||
"$PHP_BIN" artisan config:cache
|
||||
"$PHP_BIN" artisan view:cache
|
||||
"$PHP_BIN" artisan queue:restart || true
|
||||
|
||||
if [[ "$SKIP_MAINTENANCE" -eq 0 ]]; then
|
||||
"$PHP_BIN" artisan up
|
||||
trap - EXIT
|
||||
fi
|
||||
EOF
|
||||
|
||||
echo "Deployment complete."
|
||||
284
scripts/push-db-to-prod.sh
Normal file
284
scripts/push-db-to-prod.sh
Normal file
@@ -0,0 +1,284 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
root_dir="$(cd -- "$script_dir/.." && pwd)"
|
||||
|
||||
local_folder="${LOCAL_FOLDER:-$root_dir}"
|
||||
local_env_file="${LOCAL_ENV_FILE:-$local_folder/.env}"
|
||||
remote_folder="${REMOTE_FOLDER:-/opt/www/virtual/SkinbaseNova}"
|
||||
remote_server="${REMOTE_SERVER:-klevze@server3.klevze.si}"
|
||||
remote_env_file="${REMOTE_ENV_FILE:-$remote_folder/.env}"
|
||||
php_bin="${PHP_BIN:-php}"
|
||||
ssh_bin="${SSH_BIN:-ssh}"
|
||||
scp_bin="${SCP_BIN:-scp}"
|
||||
local_mysqldump_command="${LOCAL_MYSQLDUMP_COMMAND:-}"
|
||||
|
||||
force_replace=0
|
||||
skip_remote_backup=0
|
||||
run_remote_migrate=1
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: bash scripts/push-db-to-prod.sh --force [options]
|
||||
|
||||
Options:
|
||||
--force Required. Confirms that production DB replacement is intentional.
|
||||
--skip-remote-backup Skip taking a production backup before import.
|
||||
--skip-migrate Skip php artisan migrate after import.
|
||||
--remote-server HOST Override the SSH destination.
|
||||
--remote-folder PATH Override the remote app path.
|
||||
--local-env-file PATH Override the local Laravel .env path.
|
||||
--remote-env-file PATH Override the remote Laravel .env path.
|
||||
--help Show this help.
|
||||
|
||||
This script replaces the remote application database with a dump created from the local database.
|
||||
EOF
|
||||
}
|
||||
|
||||
is_wsl() {
|
||||
[[ -n "${WSL_DISTRO_NAME:-}" || -n "${WSL_INTEROP:-}" ]]
|
||||
}
|
||||
|
||||
use_windows_local_mysql_client() {
|
||||
if [[ -n "$local_mysqldump_command" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! is_wsl; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$local_db_host" != "127.0.0.1" && "$local_db_host" != "localhost" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
command -v powershell.exe >/dev/null 2>&1 && command -v wslpath >/dev/null 2>&1
|
||||
}
|
||||
|
||||
run_local_dump() {
|
||||
local uncompressed_dump_file="${local_dump_file%.gz}"
|
||||
|
||||
if [[ -n "$local_mysqldump_command" ]]; then
|
||||
echo "Creating local database dump with LOCAL_MYSQLDUMP_COMMAND..."
|
||||
(
|
||||
cd "$local_folder"
|
||||
eval "$local_mysqldump_command"
|
||||
) | gzip > "$local_dump_file"
|
||||
return
|
||||
fi
|
||||
|
||||
if use_windows_local_mysql_client; then
|
||||
local windows_dump_file
|
||||
local escaped_password
|
||||
local escaped_host
|
||||
local escaped_port
|
||||
local escaped_user
|
||||
local escaped_name
|
||||
|
||||
windows_dump_file="$(wslpath -w "$uncompressed_dump_file")"
|
||||
escaped_password="${local_db_password//\'/\'\'}"
|
||||
escaped_host="${local_db_host//\'/\'\'}"
|
||||
escaped_port="${local_db_port//\'/\'\'}"
|
||||
escaped_user="${local_db_user//\'/\'\'}"
|
||||
escaped_name="${local_db_name//\'/\'\'}"
|
||||
|
||||
echo "Detected WSL with a Windows-local MySQL host; running local dump with Windows mysqldump.exe..."
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \
|
||||
"\$ErrorActionPreference = 'Stop'; \
|
||||
\$mysqldump = (Get-Command mysqldump.exe).Source; \
|
||||
if (-not \$mysqldump) { throw 'mysqldump.exe not found on Windows PATH.' } \
|
||||
\$env:MYSQL_PWD = '$escaped_password'; \
|
||||
\$arguments = @('--host=$escaped_host', '--port=$escaped_port', '--user=$escaped_user', '--single-transaction', '--quick', '--routines', '--triggers', '--hex-blob', '--no-tablespaces', '--default-character-set=utf8mb4', '$escaped_name'); \
|
||||
\$process = Start-Process -FilePath \$mysqldump -ArgumentList \$arguments -NoNewWindow -RedirectStandardOutput '$windows_dump_file' -Wait -PassThru; \
|
||||
if (\$process.ExitCode -ne 0) { exit \$process.ExitCode }"
|
||||
|
||||
gzip -f "$uncompressed_dump_file"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Creating local database dump from $local_db_name..."
|
||||
MYSQL_PWD="$local_db_password" mysqldump \
|
||||
--host="$local_db_host" \
|
||||
--port="$local_db_port" \
|
||||
--user="$local_db_user" \
|
||||
--single-transaction \
|
||||
--quick \
|
||||
--routines \
|
||||
--triggers \
|
||||
--hex-blob \
|
||||
--no-tablespaces \
|
||||
--default-character-set=utf8mb4 \
|
||||
"$local_db_name" | gzip > "$local_dump_file"
|
||||
}
|
||||
|
||||
read_env_value() {
|
||||
local key="$1"
|
||||
local file="$2"
|
||||
local line
|
||||
|
||||
line="$(grep -E "^${key}=" "$file" | tail -n 1 || true)"
|
||||
line="${line#*=}"
|
||||
line="${line%$'\r'}"
|
||||
line="${line#\"}"
|
||||
line="${line%\"}"
|
||||
line="${line#\'}"
|
||||
line="${line%\'}"
|
||||
printf '%s' "$line"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--force)
|
||||
force_replace=1
|
||||
;;
|
||||
--skip-remote-backup)
|
||||
skip_remote_backup=1
|
||||
;;
|
||||
--skip-migrate)
|
||||
run_remote_migrate=0
|
||||
;;
|
||||
--remote-server)
|
||||
shift
|
||||
remote_server="${1:?Missing value for --remote-server}"
|
||||
;;
|
||||
--remote-folder)
|
||||
shift
|
||||
remote_folder="${1:?Missing value for --remote-folder}"
|
||||
remote_env_file="$remote_folder/.env"
|
||||
;;
|
||||
--local-env-file)
|
||||
shift
|
||||
local_env_file="${1:?Missing value for --local-env-file}"
|
||||
;;
|
||||
--remote-env-file)
|
||||
shift
|
||||
remote_env_file="${1:?Missing value for --remote-env-file}"
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [[ "$force_replace" -ne 1 ]]; then
|
||||
echo "Refusing to replace the production database without --force." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$local_env_file" ]]; then
|
||||
echo "Local env file not found: $local_env_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local_db_host="$(read_env_value DB_HOST "$local_env_file")"
|
||||
local_db_port="$(read_env_value DB_PORT "$local_env_file")"
|
||||
local_db_name="$(read_env_value DB_DATABASE "$local_env_file")"
|
||||
local_db_user="$(read_env_value DB_USERNAME "$local_env_file")"
|
||||
local_db_password="$(read_env_value DB_PASSWORD "$local_env_file")"
|
||||
|
||||
if [[ -z "$local_db_host" || -z "$local_db_port" || -z "$local_db_name" || -z "$local_db_user" ]]; then
|
||||
echo "Local database settings are incomplete in $local_env_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local_dump_file="$(mktemp "${TMPDIR:-/tmp}/skinbase-prod-sync-XXXXXX.sql.gz")"
|
||||
remote_dump_file="/tmp/$(basename "$local_dump_file")"
|
||||
|
||||
cleanup() {
|
||||
rm -f "$local_dump_file"
|
||||
rm -f "${local_dump_file%.gz}"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
run_local_dump
|
||||
|
||||
echo "Uploading dump to $remote_server..."
|
||||
"$scp_bin" "$local_dump_file" "$remote_server:$remote_dump_file"
|
||||
|
||||
echo "Importing dump on the production server..."
|
||||
"$ssh_bin" "$remote_server" \
|
||||
REMOTE_FOLDER="$(printf '%q' "$remote_folder")" \
|
||||
REMOTE_ENV_FILE="$(printf '%q' "$remote_env_file")" \
|
||||
REMOTE_DUMP_FILE="$(printf '%q' "$remote_dump_file")" \
|
||||
PHP_BIN="$(printf '%q' "$php_bin")" \
|
||||
SKIP_REMOTE_BACKUP="$skip_remote_backup" \
|
||||
RUN_REMOTE_MIGRATE="$run_remote_migrate" \
|
||||
'bash -s' <<'EOF'
|
||||
set -euo pipefail
|
||||
|
||||
read_env_value() {
|
||||
local key="$1"
|
||||
local file="$2"
|
||||
local line
|
||||
|
||||
line="$(grep -E "^${key}=" "$file" | tail -n 1 || true)"
|
||||
line="${line#*=}"
|
||||
line="${line%$'\r'}"
|
||||
line="${line#\"}"
|
||||
line="${line%\"}"
|
||||
line="${line#\'}"
|
||||
line="${line%\'}"
|
||||
printf '%s' "$line"
|
||||
}
|
||||
|
||||
if [[ ! -f "$REMOTE_ENV_FILE" ]]; then
|
||||
echo "Remote env file not found: $REMOTE_ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
db_host="$(read_env_value DB_HOST "$REMOTE_ENV_FILE")"
|
||||
db_port="$(read_env_value DB_PORT "$REMOTE_ENV_FILE")"
|
||||
db_name="$(read_env_value DB_DATABASE "$REMOTE_ENV_FILE")"
|
||||
db_user="$(read_env_value DB_USERNAME "$REMOTE_ENV_FILE")"
|
||||
db_password="$(read_env_value DB_PASSWORD "$REMOTE_ENV_FILE")"
|
||||
|
||||
if [[ -z "$db_host" || -z "$db_port" || -z "$db_name" || -z "$db_user" ]]; then
|
||||
echo "Remote database settings are incomplete in $REMOTE_ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_REMOTE_BACKUP" -eq 0 ]]; then
|
||||
backup_dir="$REMOTE_FOLDER/storage/app/deploy-backups"
|
||||
backup_file="$backup_dir/prod-before-sync-$(date +%Y%m%d-%H%M%S).sql.gz"
|
||||
mkdir -p "$backup_dir"
|
||||
|
||||
echo "Creating production backup at $backup_file..."
|
||||
MYSQL_PWD="$db_password" mysqldump \
|
||||
--host="$db_host" \
|
||||
--port="$db_port" \
|
||||
--user="$db_user" \
|
||||
--single-transaction \
|
||||
--quick \
|
||||
--routines \
|
||||
--triggers \
|
||||
--hex-blob \
|
||||
--no-tablespaces \
|
||||
--default-character-set=utf8mb4 \
|
||||
"$db_name" | gzip > "$backup_file"
|
||||
fi
|
||||
|
||||
echo "Replacing production database $db_name..."
|
||||
gunzip -c "$REMOTE_DUMP_FILE" | MYSQL_PWD="$db_password" mysql \
|
||||
--host="$db_host" \
|
||||
--port="$db_port" \
|
||||
--user="$db_user" \
|
||||
"$db_name"
|
||||
|
||||
rm -f "$REMOTE_DUMP_FILE"
|
||||
|
||||
if [[ "$RUN_REMOTE_MIGRATE" -eq 1 ]]; then
|
||||
cd "$REMOTE_FOLDER"
|
||||
"$PHP_BIN" artisan migrate --force
|
||||
fi
|
||||
EOF
|
||||
|
||||
echo "Production database sync complete."
|
||||
96
scripts/render-nova-card.cjs
Normal file
96
scripts/render-nova-card.cjs
Normal file
@@ -0,0 +1,96 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* render-nova-card.cjs
|
||||
*
|
||||
* Headless Playwright screenshot renderer for Nova Cards.
|
||||
* Visits the signed render-frame URL, waits for React + fonts to finish,
|
||||
* then screenshots the [data-card-canvas] element and writes a PNG to disk.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/render-nova-card.cjs \
|
||||
* --url=<signed-render-frame-url> \
|
||||
* --out=<absolute-path-to-output.png> \
|
||||
* [--width=1080] [--height=1080]
|
||||
*
|
||||
* Exit codes: 0 = success, 1 = error (message on stderr).
|
||||
* On success writes JSON to stdout: { "success": true, "out": "...", "width": N, "height": N }
|
||||
*/
|
||||
|
||||
const { chromium } = require('@playwright/test')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
// ── Parse CLI args ────────────────────────────────────────────────────────────
|
||||
const argv = Object.fromEntries(
|
||||
process.argv.slice(2)
|
||||
.filter((a) => a.startsWith('--'))
|
||||
.map((a) => {
|
||||
const eq = a.indexOf('=')
|
||||
return eq === -1 ? [a.slice(2), true] : [a.slice(2, eq), a.slice(eq + 1)]
|
||||
}),
|
||||
)
|
||||
|
||||
const { url, out } = argv
|
||||
const width = parseInt(argv.width || '1080', 10)
|
||||
const height = parseInt(argv.height || '1080', 10)
|
||||
|
||||
if (!url || !out) {
|
||||
process.stderr.write(
|
||||
'Usage: render-nova-card.cjs --url=<url> --out=<png-path> [--width=1080] [--height=1080]\n',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// ── Render ────────────────────────────────────────────────────────────────────
|
||||
;(async () => {
|
||||
const browser = await chromium.launch({ headless: true })
|
||||
|
||||
try {
|
||||
const page = await browser.newPage({
|
||||
viewport: { width, height },
|
||||
deviceScaleFactor: 1,
|
||||
})
|
||||
|
||||
// Navigate and wait for network to settle (background images, fonts).
|
||||
await page.goto(url, { waitUntil: 'networkidle', timeout: 30_000 })
|
||||
|
||||
// Wait for React to finish painting the canvas element.
|
||||
const canvas = page.locator('[data-card-canvas]').first()
|
||||
await canvas.waitFor({ state: 'visible', timeout: 15_000 })
|
||||
|
||||
// Wait for all web fonts to finish loading before we capture layout.
|
||||
await page.evaluate(async () => {
|
||||
const requiredFonts = [
|
||||
'Anton',
|
||||
'Caveat',
|
||||
'Cormorant Garamond',
|
||||
'Inter',
|
||||
'Libre Franklin',
|
||||
'Playfair Display',
|
||||
]
|
||||
|
||||
await Promise.all(requiredFonts.map((family) => document.fonts?.load?.(`16px "${family}"`) || Promise.resolve()))
|
||||
|
||||
if (document.fonts?.ready) {
|
||||
await document.fonts.ready
|
||||
}
|
||||
})
|
||||
|
||||
// One extra frame so CSS transforms / container queries resolve fully.
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Screenshot only the canvas element; Playwright clips to the element bounding box.
|
||||
const png = await canvas.screenshot({ type: 'png' })
|
||||
|
||||
fs.mkdirSync(path.dirname(out), { recursive: true })
|
||||
fs.writeFileSync(out, png)
|
||||
|
||||
process.stdout.write(JSON.stringify({ success: true, out, width, height }) + '\n')
|
||||
} finally {
|
||||
await browser.close()
|
||||
}
|
||||
})().catch((err) => {
|
||||
process.stderr.write((err?.message ?? String(err)) + '\n')
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user