fix(ops): switch pm2 to standalone server + fix wholesale_settings query
Deploy to route.crispygoat.com / deploy (push) Successful in 4m14s
Deploy to route.crispygoat.com / deploy (push) Successful in 4m14s
Three problems were silently breaking the prod Tuxedo redesign and
the offline DB error swallowed all visibility into them:
1. `next start` against `output: "standalone"` is unsupported. Next.js
prints "next start does not work with output: standalone" and
silently disables the image optimizer — every `/_next/image?url=...`
returned "url parameter is not allowed". pm2 was previously
started as `pm2 start npm -- start -- -p 3100`. Switched to
`node /home/tyler/route-commerce/scripts/start-standalone.cjs`.
2. The standalone server reads `.next/static/` relative to itself, so
deploy must `cp -r .next/static .next/standalone/.next/static` on
every sync. Without this, every `_next/static/chunks/*.js` returns
404 and the page hydration dies. Added to the deploy workflow.
3. bash's `set -a; . ./.env` truncates DATABASE_URL at the `&` in
`&channel_binding=require`, so the standalone server boots with
empty DB env. `getBrandSettingsPublic` then caught the empty-pool
error and returned `{success:false, error:'Failed to fetch brand settings'}`,
so `state.heroImageUrl` stayed null and the hero poster never
rendered. The new `scripts/start-standalone.cjs` parses .env in
Node (which handles `&` correctly) and exec's the server with the
loaded env.
4. `getBrandSettingsPublic` queried `ws.wholesale_enabled`, which
no longer exists in `wholesale_settings`. The schema migration
renamed it to `online_payment_enabled`. Switched the column
reference and added a console.error log so future drift surfaces
instead of being swallowed by the empty catch.
Plus `next.config.ts` had `hostname: "s3.crispygoat.com"` added
under `images.remotePatterns` so the optimizer accepts MinIO URLs.
This commit is contained in:
@@ -204,9 +204,29 @@ jobs:
|
|||||||
|
|
||||||
# Install deps and restart on server
|
# Install deps and restart on server
|
||||||
echo "Installing deps and restarting PM2..."
|
echo "Installing deps and restarting PM2..."
|
||||||
# IMPORTANT: --update-env so PM2 re-reads .env. Without it PM2 keeps
|
# IMPORTANT: With `output: "standalone"` in next.config.ts we MUST
|
||||||
# the original env vars cached at first start and silently ignores
|
# run `node .next/standalone/server.js` (not `next start`) — `next start`
|
||||||
# newly added ones (like SMARTSHEET_*).
|
# against a standalone build is unsupported and silently disables the
|
||||||
ssh -o ConnectTimeout=60 -o StrictHostKeyChecking=no tyler@route.crispygoat.com "cd $APP_DIR && npm install --omit=dev 2>&1 | tail -5 && pm2 restart route-commerce --update-env || pm2 start npm --name route-commerce -- start -- -p 3100 && pm2 save && sleep 4 && curl -f -s http://localhost:3100/api/health/db-schema || { echo 'Health check failed after start - schema not applied (see plan)'; exit 1; }"
|
# image optimizer (every `/_next/image?url=...` returns
|
||||||
|
# `"url" parameter is not allowed`). See commit 129c9d2.
|
||||||
|
#
|
||||||
|
# We also need to (1) ship the standalone loader script
|
||||||
|
# `scripts/start-standalone.cjs`, (2) copy `.next/static/` into
|
||||||
|
# `.next/standalone/.next/static/` so the standalone server can
|
||||||
|
# serve client-side JS/CSS chunks, and (3) load `.env` via a
|
||||||
|
# Node loader — bash's `set -a; . ./.env` truncates DATABASE_URL
|
||||||
|
# at the `&` in `&channel_binding=require`.
|
||||||
|
scp -o ConnectTimeout=15 -o StrictHostKeyChecking=no scripts/start-standalone.cjs tyler@route.crispygoat.com:$APP_DIR/scripts/start-standalone.cjs 2>/dev/null || true
|
||||||
|
ssh -o ConnectTimeout=60 -o StrictHostKeyChecking=no tyler@route.crispygoat.com "set -e; cd $APP_DIR && npm install --omit=dev 2>&1 | tail -5 && \
|
||||||
|
# Standalone server reads .next/static/ relative to itself.
|
||||||
|
mkdir -p .next/standalone/.next && \
|
||||||
|
rm -rf .next/standalone/.next/static && \
|
||||||
|
cp -r .next/static .next/standalone/.next/static && \
|
||||||
|
# Start with the wrapper that loads .env and exec's the server.
|
||||||
|
# `pm2 start` is idempotent via `pm2 restart` after the first run.
|
||||||
|
pm2 delete route-commerce 2>/dev/null || true; \
|
||||||
|
PORT=3100 HOSTNAME=0.0.0.0 pm2 start scripts/start-standalone.cjs --name route-commerce --interpreter /home/tyler/.cache/act/tool_cache/node/22.22.3/x64/bin/node 2>&1 | tail -3 && \
|
||||||
|
pm2 save && sleep 4 && \
|
||||||
|
curl -fsS http://localhost:3100/api/health/db-schema >/dev/null || { echo 'Health check failed after start - schema not applied (see plan)'; exit 1; }"
|
||||||
|
|
||||||
echo "Deployed successfully"
|
echo "Deployed successfully"
|
||||||
@@ -34,6 +34,12 @@ const nextConfig: NextConfig = {
|
|||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "picsum.photos",
|
hostname: "picsum.photos",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Brand media hosted on the crispygoat MinIO bucket
|
||||||
|
// (e.g. s3.crispygoat.com/videos/wp-import/...)
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "s3.crispygoat.com",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
formats: ["image/avif", "image/webp"],
|
formats: ["image/avif", "image/webp"],
|
||||||
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
||||||
|
|||||||
Executable
+62
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Production startup wrapper for `next start` when `output: "standalone"`
|
||||||
|
* is set in next.config.ts. Loads .env from a known absolute location,
|
||||||
|
* then exec's the standalone server with the loaded env.
|
||||||
|
*
|
||||||
|
* Why this exists:
|
||||||
|
* 1. `next start` against a standalone build is unsupported by Next.js —
|
||||||
|
* it prints "next start does not work with output: standalone
|
||||||
|
* configuration. Use `node .next/standalone/server.js` instead" — and
|
||||||
|
* silently disables the image optimizer (every `/_next/image?url=...`
|
||||||
|
* returned "url parameter is not allowed"), which is why all the
|
||||||
|
* WP-imported brand imagery never rendered. See commit 129c9d2.
|
||||||
|
*
|
||||||
|
* 2. bash's `set -a; . ./.env` truncates DATABASE_URL at the `&` in
|
||||||
|
* `&channel_binding=require`, so the server boots with empty DB env
|
||||||
|
* and the public storefront falls back to dark gradient backgrounds.
|
||||||
|
* A small Node loader sidesteps the bash word-splitting.
|
||||||
|
*
|
||||||
|
* 3. The standalone server reads `.next/static/` relative to itself, so
|
||||||
|
* the deploy workflow must `cp -r .next/static .next/standalone/.next/static`
|
||||||
|
* after every sync.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* PORT=3100 HOSTNAME=0.0.0.0 \
|
||||||
|
* NODE_BIN=/home/tyler/.cache/act/tool_cache/node/22.22.3/x64/bin/node \
|
||||||
|
* node /home/tyler/route-commerce/scripts/start-standalone.cjs
|
||||||
|
*
|
||||||
|
* pm2 typically overrides interpreter with its own node and sets `cwd`
|
||||||
|
* arbitrarily, so we resolve the project root from the script's own path
|
||||||
|
* instead of `process.cwd()`.
|
||||||
|
*/
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const APP_DIR = path.resolve(__dirname, "..");
|
||||||
|
const ENV_PATH = process.env.ENV_FILE || path.join(APP_DIR, ".env");
|
||||||
|
const STANDALONE = path.join(APP_DIR, ".next", "standalone", "server.js");
|
||||||
|
|
||||||
|
if (!fs.existsSync(STANDALONE)) {
|
||||||
|
console.error(`[start-standalone] Missing standalone server at ${STANDALONE}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(ENV_PATH)) {
|
||||||
|
for (const line of fs.readFileSync(ENV_PATH, "utf8").split("\n")) {
|
||||||
|
const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
||||||
|
if (m && !process.env[m[1]]) process.env[m[1]] = m[2];
|
||||||
|
}
|
||||||
|
console.log(`[start-standalone] loaded env from ${ENV_PATH}`);
|
||||||
|
} else {
|
||||||
|
console.warn(`[start-standalone] no .env at ${ENV_PATH}; relying on existing env`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hand off to the standalone server. Re-exec keeps the process title at
|
||||||
|
// `next-server` (next-start.sh sets it) and matches `node .next/standalone/server.js`.
|
||||||
|
const { spawn } = require("child_process");
|
||||||
|
const child = spawn(process.execPath, [STANDALONE], { stdio: "inherit", env: process.env });
|
||||||
|
child.on("exit", (code) => process.exit(code ?? 0));
|
||||||
|
for (const sig of ["SIGINT", "SIGTERM"]) {
|
||||||
|
process.on(sig, () => child.kill(sig));
|
||||||
|
}
|
||||||
@@ -262,7 +262,7 @@ export async function getBrandSettingsPublic(brandSlug: string): Promise<GetBran
|
|||||||
|
|
||||||
await getSession(); try {
|
await getSession(); try {
|
||||||
const { rows } = await pool.query<BrandSettings & { wholesale_enabled?: boolean | null }>(
|
const { rows } = await pool.query<BrandSettings & { wholesale_enabled?: boolean | null }>(
|
||||||
`SELECT bs.*, b.name AS brand_name, ws.wholesale_enabled
|
`SELECT bs.*, b.name AS brand_name, ws.online_payment_enabled AS wholesale_enabled
|
||||||
FROM brands b
|
FROM brands b
|
||||||
JOIN brand_settings bs ON bs.brand_id = b.id
|
JOIN brand_settings bs ON bs.brand_id = b.id
|
||||||
LEFT JOIN wholesale_settings ws ON ws.brand_id = b.id
|
LEFT JOIN wholesale_settings ws ON ws.brand_id = b.id
|
||||||
@@ -272,6 +272,7 @@ await getSession(); try {
|
|||||||
);
|
);
|
||||||
const data = rows[0];
|
const data = rows[0];
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
console.warn(`[getBrandSettingsPublic] No row for slug=${brandSlug}`);
|
||||||
return { success: false, error: "Failed to fetch brand settings", wholesaleEnabled: undefined };
|
return { success: false, error: "Failed to fetch brand settings", wholesaleEnabled: undefined };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -279,7 +280,8 @@ await getSession(); try {
|
|||||||
settings: data,
|
settings: data,
|
||||||
wholesaleEnabled: data.wholesale_enabled,
|
wholesaleEnabled: data.wholesale_enabled,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
console.error(`[getBrandSettingsPublic] DB error for slug=${brandSlug}:`, err);
|
||||||
return { success: false, error: "Failed to fetch brand settings", wholesaleEnabled: undefined };
|
return { success: false, error: "Failed to fetch brand settings", wholesaleEnabled: undefined };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user