Files
backend/scripts/websites-agent/deploy.sh
T
Alireza HassaniandCursor 40284df880 Surface the real build error when website deploy.sh fails.
Super Admin was only showing "exited with code 1" because the ERR trap never copied the last compiler or module-not-found line from the deploy log.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 17:39:03 +03:30

222 lines
6.4 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SLUG="${1:-}"
if [[ -z "$SLUG" ]]; then
echo "usage: deploy.sh <slug>" >&2
exit 1
fi
ROOT="/var/www/sites/$SLUG"
LOG="/var/log/websites/deploy-$SLUG.log"
STATUS="/var/log/websites/deploy-$SLUG.status.json"
ECOSYSTEM="/var/www/sites/ecosystem.config.cjs"
exec >>"$LOG" 2>&1
write_status() {
local status="$1"
local detail="${2:-}"
detail="${detail//\\/\\\\}"
detail="${detail//\"/\\\"}"
detail="${detail//$'\n'/ }"
detail="${detail:0:500}"
printf '{"slug":"%s","status":"%s","detail":"%s","at":"%s"}\n' \
"$SLUG" \
"$status" \
"$detail" \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
>"$STATUS"
}
fail_snippet() {
if [[ ! -f "$LOG" ]]; then
return 0
fi
local chunk snippet=""
# Only the current deploy (log is appended across runs).
chunk="$(awk 'BEGIN{s=""} /^==== / && /deploy start:/{s=""} {s=s $0 ORS} END{printf "%s", s}' "$LOG")"
snippet="$(
{ printf '%s' "$chunk" | grep -E "Error:|Type error|Module not found|ELIFECYCLE|npm ERR!" || true; } \
| sed 's/\x1b\[[0-9;]*m//g' \
| tail -n 4 \
| tr '\n' ' ' \
| sed 's/[[:space:]]\+/ /g'
)"
if [[ -z "${snippet// }" ]]; then
snippet="$(
printf '%s' "$chunk" \
| tail -n 12 \
| sed 's/\x1b\[[0-9;]*m//g' \
| tr '\n' ' ' \
| sed 's/[[:space:]]\+/ /g'
)"
fi
printf '%s' "$snippet"
}
echo "==== $(date -u +%Y-%m-%dT%H:%M:%SZ) deploy start: $SLUG ===="
write_status "started" "Deploy running"
cleanup_fail() {
local code=$?
trap - ERR
local snippet
snippet="$(fail_snippet)"
if [[ -n "${snippet// }" ]]; then
write_status "failed" "exit $code: $snippet"
else
write_status "failed" "deploy.sh exited with code $code"
fi
exit "$code"
}
trap cleanup_fail ERR
if [[ ! -d "$ROOT/.git" ]]; then
echo "missing site: $ROOT"
write_status "failed" "missing site directory"
exit 1
fi
cd "$ROOT"
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -i /root/.ssh/websites_deploy -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new}"
git fetch origin
# Prefer origin/HEAD, then main, then master (repos may use either).
BRANCH=""
if git rev-parse --verify --quiet origin/HEAD >/dev/null; then
BRANCH="$(git rev-parse --abbrev-ref origin/HEAD 2>/dev/null || true)"
BRANCH="${BRANCH#origin/}"
fi
if [[ -z "$BRANCH" || "$BRANCH" == "HEAD" ]]; then
if git rev-parse --verify --quiet origin/main >/dev/null; then
BRANCH="main"
elif git rev-parse --verify --quiet origin/master >/dev/null; then
BRANCH="master"
else
echo "could not determine default branch (tried origin/HEAD, main, master)"
write_status "failed" "no origin/main or origin/master"
exit 1
fi
fi
echo "resetting to origin/$BRANCH"
git checkout -B "$BRANCH" "origin/$BRANCH"
git reset --hard "origin/$BRANCH"
if [[ ! -f package.json ]]; then
echo "package.json missing after checkout"
write_status "failed" "package.json missing — empty or wrong branch?"
exit 1
fi
npm ci
npm run build
# Next.js standalone: copy static assets Next does not place in standalone/, then
# point PM2 at server.js and drop the full node_modules tree.
if [[ -f .next/standalone/server.js ]]; then
echo "standalone build detected — preparing runtime bundle"
mkdir -p .next/standalone/.next
rm -rf .next/standalone/.next/static
cp -a .next/static .next/standalone/.next/static
if [[ -d public ]]; then
rm -rf .next/standalone/public
# Follow symlinks (e.g. public/images -> ../src/images) so standalone
# serves real files. cp -a alone would copy a broken relative link.
mkdir -p .next/standalone/public
cp -aL public/. .next/standalone/public/
fi
SLUG="$SLUG" ROOT="$ROOT" ECOSYSTEM="$ECOSYSTEM" node <<'NODE'
const fs = require('fs');
const path = process.env.ECOSYSTEM;
const slug = process.env.SLUG;
const root = process.env.ROOT;
let cfg = { apps: [] };
try {
delete require.cache[require.resolve(path)];
cfg = require(path);
if (!Array.isArray(cfg.apps)) cfg.apps = [];
} catch (_) {
cfg = { apps: [] };
}
const app = cfg.apps.find((a) => a.name === slug);
if (!app) {
console.error('ecosystem entry missing for', slug);
process.exit(1);
}
const port =
(app.env && Number(app.env.PORT)) ||
(String(app.args || '').match(/--port\s+(\d+)/) || [])[1] ||
3005;
app.cwd = root + '/.next/standalone';
app.script = 'server.js';
app.args = '';
app.env = {
...(app.env || {}),
NODE_ENV: 'production',
PORT: String(port),
HOSTNAME: '127.0.0.1',
};
const lines = ['module.exports = {', ' apps: ['];
cfg.apps.forEach((entry, idx) => {
lines.push(' {');
lines.push(` name: ${JSON.stringify(entry.name)},`);
lines.push(` cwd: ${JSON.stringify(entry.cwd)},`);
lines.push(` script: ${JSON.stringify(entry.script)},`);
if (entry.args) {
lines.push(` args: ${JSON.stringify(entry.args)},`);
}
lines.push(' env: {');
for (const [key, value] of Object.entries(entry.env || {})) {
lines.push(` ${key}: ${JSON.stringify(String(value))},`);
}
lines.push(' },');
lines.push(` error_file: ${JSON.stringify(entry.error_file)},`);
lines.push(` out_file: ${JSON.stringify(entry.out_file)},`);
lines.push(' time: true,');
if (entry.max_memory_restart) {
lines.push(` max_memory_restart: ${JSON.stringify(entry.max_memory_restart)},`);
}
lines.push(idx === cfg.apps.length - 1 ? ' }' : ' },');
});
lines.push(' ],');
lines.push('};');
lines.push('');
fs.writeFileSync(path, lines.join('\n'));
console.log('ecosystem updated for standalone:', slug, 'port', port);
NODE
# Full delete+start so PM2 picks up new cwd/script (restart alone keeps old path).
pm2 delete "$SLUG" || true
pm2 start "$ECOSYSTEM" --only "$SLUG"
pm2 save || true
echo "removing full node_modules (standalone runtime keeps its own minimal copy)"
rm -rf node_modules
# Drop Next build leftovers not needed at runtime (cache + non-standalone trees).
# Keep .next/standalone only.
if [[ -d .next ]]; then
find .next -mindepth 1 -maxdepth 1 ! -name standalone -exec rm -rf {} +
fi
echo "==== $(date -u +%Y-%m-%dT%H:%M:%SZ) deploy ok (standalone): $SLUG ===="
write_status "success" "Deployed origin/$BRANCH (standalone)"
trap - ERR
exit 0
fi
echo "no standalone output — using next start (legacy)"
pm2 restart "$SLUG" --update-env || pm2 start "$ECOSYSTEM" --only "$SLUG"
pm2 save || true
echo "==== $(date -u +%Y-%m-%dT%H:%M:%SZ) deploy ok: $SLUG ===="
write_status "success" "Deployed origin/$BRANCH"
trap - ERR