'use strict'; /** * 一键部署:打包 → 上传 → 远程执行 update.sh → 校验服务已起来。 * * 用法(凭据全部走环境变量,脚本里不写死任何密钥): * * # Windows PowerShell * $env:SSH_HOST='103.236.99.177'; $env:SSH_PORT='33851' * $env:SSH_USER='root'; $env:SSH_PASS='<密码>' * node scripts/deploy.js * * # Linux / macOS * SSH_HOST=... SSH_PORT=... SSH_USER=... SSH_PASS=... node scripts/deploy.js * * 可选环境变量: * DEPLOY_APP 部署目录,默认 /opt/autostore-bot * DEPLOY_PORT 健康检查端口,默认 78 * * 打包时**排除**这些,以免覆盖服务器上的真实数据与环境: * node_modules / .git / data / *.log / .env * 服务器上 `update.sh` 同样会保留 .env、data、node_modules、runtime。 */ const { execFileSync, spawnSync } = require('child_process'); const fs = require('fs'); const os = require('os'); const path = require('path'); const HOST = process.env.SSH_HOST; const PASS = process.env.SSH_PASS; const APP = process.env.DEPLOY_APP || '/opt/autostore-bot'; const PORT = process.env.DEPLOY_PORT || '78'; if (!HOST || !PASS) { console.error('缺少 SSH_HOST / SSH_PASS 环境变量'); process.exit(2); } const ROOT = path.resolve(__dirname, '..'); const HELPER = path.join(__dirname, 'ssh-helper.js'); const TARBALL = path.join(os.tmpdir(), `autostore-deploy-${Date.now()}.tar.gz`); /** 通过 ssh-helper 执行远程命令,输出直接透传 */ function remote(command) { const r = spawnSync(process.execPath, [HELPER, 'exec', command], { stdio: ['ignore', 'pipe', 'pipe'], env: process.env }); const stdout = r.stdout ? r.stdout.toString('utf8') : ''; const stderr = r.stderr ? r.stderr.toString('utf8') : ''; if (stdout) process.stdout.write(stdout); if (stderr) process.stderr.write(stderr); return { code: r.status || 0, stdout, stderr }; } /** 通过 ssh-helper 上传文件 */ function upload(localFile, remotePath) { const r = spawnSync(process.execPath, [HELPER, 'put', localFile, remotePath], { stdio: ['ignore', 'pipe', 'pipe'], env: process.env }); const stdout = r.stdout ? r.stdout.toString('utf8') : ''; const stderr = r.stderr ? r.stderr.toString('utf8') : ''; if (stdout) process.stdout.write(stdout); if (stderr) process.stderr.write(stderr); if (r.status !== 0) throw new Error('上传失败'); } function pack() { // tar 的 --exclude 必须放在 -C 之前才能生效 const args = [ '-czf', TARBALL, '--exclude=node_modules', '--exclude=.git', '--exclude=data', '--exclude=*.log', '--exclude=.env', '-C', ROOT, '.' ]; try { execFileSync('tar', args, { stdio: 'ignore' }); } catch { console.error('打包失败。Windows 请确保 tar 可用(Win10 1803+ 自带 bsdtar);' + '或者手动压缩项目目录后通过 scripts/ssh-helper.js put 上传。'); process.exit(1); } return fs.statSync(TARBALL).size; } function main() { console.log(`===== 1/4 打包 ${ROOT} =====`); const size = pack(); console.log(` ${TARBALL} (${size} bytes)`); console.log('\n===== 2/4 上传 ====='); upload(TARBALL, `${APP}/update.tar.gz`); console.log('\n===== 3/4 远程应用更新 ====='); const applied = remote(`bash ${APP}/update.sh 2>&1 | tail -5`); if (applied.code !== 0) { console.error('update.sh 返回非零状态,请检查上面的输出'); } console.log('\n===== 4/4 校验服务 ====='); // 等 pm2 把进程拉起来 atExit(() => { const health = remote( `for i in 1 2 3 4 5 6 7 8 9 10; do ` + `if curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:${PORT}/ | grep -q 200; then ` + `echo "控制台 200 OK"; break; fi; sleep 3; done; ` + `ss -lntp | grep ':${PORT} ' || echo "警告:端口 ${PORT} 未监听"` ); if (!health.stdout.includes('200 OK')) { console.error(`\n服务未就绪,请登录服务器查看日志:\n` + ` tail -50 ${APP}/data/pm2-out.log\n` + ` tail -50 ${APP}/data/pm2-error.log`); process.exitCode = 1; } }); } /** 注册退出前执行(保证 1~3 步的输出先打完) */ const exitHooks = []; function atExit(fn) { exitHooks.push(fn); } process.on('exit', () => { for (const fn of exitHooks) fn(); }); try { main(); } catch (err) { console.error('部署失败:' + (err && err.message ? err.message : err)); process.exitCode = 1; } finally { try { fs.unlinkSync(TARBALL); } catch { // 临时文件删不掉无所谓 } }