@zhengyizhao/deploy-helper 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PUBLISHING.md +86 -0
- package/README.md +506 -59
- package/package.json +7 -1
- package/src/commands/backup.js +55 -16
- package/src/commands/env.js +25 -16
- package/src/commands/init.js +444 -67
- package/src/commands/rollback.js +82 -25
- package/src/commands/status.js +149 -47
- package/src/commands/update.js +72 -37
- package/src/utils/config.js +25 -1
- package/src/utils/detect.js +144 -12
- package/src/utils/setup.js +379 -31
- package/src/utils/ssh.js +32 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhengyizhao/deploy-helper",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Interactive CLI to deploy your project to any VPS server",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
"keywords": [],
|
|
10
10
|
"author": "",
|
|
11
11
|
"license": "ISC",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/sjksndnfkwks/deploy-helper"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/sjksndnfkwks/deploy-helper#readme",
|
|
17
|
+
"bugs": "https://github.com/sjksndnfkwks/deploy-helper/issues",
|
|
12
18
|
"dependencies": {
|
|
13
19
|
"chalk": "^5.6.2",
|
|
14
20
|
"commander": "^14.0.3",
|
package/src/commands/backup.js
CHANGED
|
@@ -2,26 +2,29 @@ import chalk from 'chalk';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import inquirer from 'inquirer';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import fs from 'fs';
|
|
6
5
|
import { connectSSH, runRemoteSilent } from '../utils/ssh.js';
|
|
7
6
|
import { loadConfig, saveConfig } from '../utils/config.js';
|
|
8
7
|
|
|
9
8
|
const BACKUP_BASE = '/var/deploy-helper/db-backups';
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
|
-
*
|
|
11
|
+
* 根据数据库类型生成备份命令。
|
|
12
|
+
* passwordEnvInline=true:把密码以 ENV='xxx' 形式内联(仅用于一次性命令,命令一执行完进程就退出)
|
|
13
|
+
* passwordEnvInline=false:用 cron 脚本里 source 的 credentials 文件(推荐)
|
|
13
14
|
*/
|
|
14
|
-
function buildDumpCommand(dbConfig, outputFile) {
|
|
15
|
+
function buildDumpCommand(dbConfig, outputFile, passwordEnvInline = true) {
|
|
15
16
|
const { type, host, port, user, password, database } = dbConfig;
|
|
16
17
|
const h = host || '127.0.0.1';
|
|
17
18
|
|
|
18
19
|
if (type === 'mysql') {
|
|
19
20
|
const p = port || 3306;
|
|
20
|
-
|
|
21
|
+
const pwd = passwordEnvInline ? `MYSQL_PWD='${password}' ` : '';
|
|
22
|
+
return `${pwd}mysqldump -h ${h} -P ${p} -u ${user} ${database} > ${outputFile}`;
|
|
21
23
|
}
|
|
22
24
|
if (type === 'postgresql') {
|
|
23
25
|
const p = port || 5432;
|
|
24
|
-
|
|
26
|
+
const pwd = passwordEnvInline ? `PGPASSWORD='${password}' ` : '';
|
|
27
|
+
return `${pwd}pg_dump -h ${h} -p ${p} -U ${user} ${database} > ${outputFile}`;
|
|
25
28
|
}
|
|
26
29
|
if (type === 'mongodb') {
|
|
27
30
|
const p = port || 27017;
|
|
@@ -126,14 +129,14 @@ async function doBackup(config, silent = false) {
|
|
|
126
129
|
|
|
127
130
|
await runRemoteSilent(ssh, `mkdir -p ${backupDir}`);
|
|
128
131
|
|
|
129
|
-
//
|
|
132
|
+
// 生成备份(一次性命令:内联密码 OK,进程退出即消失)
|
|
130
133
|
const dumpSpinner = ora(`备份 ${dbConfig.type} 数据库 [${dbConfig.database}]...`).start();
|
|
131
|
-
const dumpCmd = buildDumpCommand(dbConfig, dbConfig.type === 'mongodb' ? outputFile : rawFile);
|
|
134
|
+
const dumpCmd = buildDumpCommand(dbConfig, dbConfig.type === 'mongodb' ? outputFile : rawFile, true);
|
|
132
135
|
const dumpResult = await runRemoteSilent(ssh, dumpCmd);
|
|
133
136
|
|
|
134
137
|
if (dumpResult.code !== 0) {
|
|
135
138
|
dumpSpinner.fail('备份失败');
|
|
136
|
-
console.log(chalk.red(dumpResult.stdout));
|
|
139
|
+
console.log(chalk.red(dumpResult.stderr || dumpResult.stdout));
|
|
137
140
|
ssh.dispose();
|
|
138
141
|
return null;
|
|
139
142
|
}
|
|
@@ -277,30 +280,66 @@ async function scheduleBackup(config) {
|
|
|
277
280
|
const backupDir = `${BACKUP_BASE}/${config.appName}`;
|
|
278
281
|
const ext = dbConfig.type === 'mongodb' ? '.archive.gz' : '.sql.gz';
|
|
279
282
|
|
|
280
|
-
//
|
|
283
|
+
// 凭据文件单独存放,权限 600;脚本通过 . credentials.sh 加载
|
|
284
|
+
const credPath = `/etc/deploy-helper/${config.appName}.creds`;
|
|
285
|
+
const credContent = (() => {
|
|
286
|
+
if (dbConfig.type === 'mysql') return `export MYSQL_PWD='${dbConfig.password}'\n`;
|
|
287
|
+
if (dbConfig.type === 'postgresql') return `export PGPASSWORD='${dbConfig.password}'\n`;
|
|
288
|
+
if (dbConfig.type === 'mongodb') return `export DH_MONGO_USER='${dbConfig.user}'\nexport DH_MONGO_PWD='${dbConfig.password}'\n`;
|
|
289
|
+
return '';
|
|
290
|
+
})();
|
|
291
|
+
|
|
292
|
+
await runRemoteSilent(ssh, `mkdir -p /etc/deploy-helper && chmod 700 /etc/deploy-helper`);
|
|
293
|
+
await runRemoteSilent(ssh, `cat > ${credPath} <<'DH_CRED_EOF'\n${credContent}DH_CRED_EOF`);
|
|
294
|
+
await runRemoteSilent(ssh, `chmod 600 ${credPath}`);
|
|
295
|
+
|
|
296
|
+
// 生成备份命令(不内联密码,从 cred 文件加载)
|
|
297
|
+
const dumpForScript = (() => {
|
|
298
|
+
const h = dbConfig.host || '127.0.0.1';
|
|
299
|
+
if (dbConfig.type === 'mysql') {
|
|
300
|
+
return `mysqldump -h ${h} -P ${dbConfig.port || 3306} -u ${dbConfig.user} ${dbConfig.database} > "\${OUTFILE%.gz}"`;
|
|
301
|
+
}
|
|
302
|
+
if (dbConfig.type === 'postgresql') {
|
|
303
|
+
return `pg_dump -h ${h} -p ${dbConfig.port || 5432} -U ${dbConfig.user} ${dbConfig.database} > "\${OUTFILE%.gz}"`;
|
|
304
|
+
}
|
|
305
|
+
if (dbConfig.type === 'mongodb') {
|
|
306
|
+
const auth = dbConfig.password
|
|
307
|
+
? `--username "$DH_MONGO_USER" --password "$DH_MONGO_PWD" --authenticationDatabase admin`
|
|
308
|
+
: '';
|
|
309
|
+
return `mongodump --host ${h} --port ${dbConfig.port || 27017} ${auth} --db ${dbConfig.database} --archive="$OUTFILE" --gzip`;
|
|
310
|
+
}
|
|
311
|
+
return '';
|
|
312
|
+
})();
|
|
313
|
+
|
|
314
|
+
// 生成备份脚本(heredoc 单引号 EOF:变量不展开,原样写入)
|
|
281
315
|
const scriptContent = `#!/bin/bash
|
|
316
|
+
set -e
|
|
317
|
+
. ${credPath}
|
|
282
318
|
TIMESTAMP=$(date +%Y-%m-%dT%H-%M-%S)
|
|
283
319
|
OUTFILE="${backupDir}/${dbConfig.database}_\${TIMESTAMP}${ext}"
|
|
284
320
|
mkdir -p ${backupDir}
|
|
285
|
-
${
|
|
321
|
+
${dumpForScript}
|
|
286
322
|
${dbConfig.type !== 'mongodb' ? `gzip -f "\${OUTFILE%.gz}"` : ''}
|
|
287
|
-
ls -t ${backupDir} | tail -n +11 | xargs -I{} rm -f ${backupDir}/{} 2>/dev/null
|
|
323
|
+
ls -t ${backupDir} | tail -n +11 | xargs -I{} rm -f ${backupDir}/{} 2>/dev/null || true
|
|
288
324
|
echo "[$(date)] Backup completed: \$OUTFILE" >> /var/log/deploy-helper-backup.log
|
|
289
325
|
`;
|
|
290
326
|
|
|
291
327
|
const scriptPath = `/usr/local/bin/deploy-helper-backup-${config.appName}.sh`;
|
|
292
|
-
await runRemoteSilent(ssh, `cat > ${scriptPath} <<
|
|
293
|
-
|
|
328
|
+
await runRemoteSilent(ssh, `cat > ${scriptPath} <<'DH_SCRIPT_EOF'\n${scriptContent}DH_SCRIPT_EOF`);
|
|
329
|
+
// 脚本本身含密码加载逻辑——chmod 700 仅 root 可读
|
|
330
|
+
await runRemoteSilent(ssh, `chmod 700 ${scriptPath} && chown root:root ${scriptPath}`);
|
|
294
331
|
|
|
295
|
-
// 注入 crontab
|
|
332
|
+
// 注入 crontab(root 用户的 crontab)
|
|
296
333
|
await runRemoteSilent(
|
|
297
334
|
ssh,
|
|
298
335
|
`(crontab -l 2>/dev/null | grep -v "deploy-helper-backup-${config.appName}"; echo "${cronExpr} ${scriptPath}") | crontab -`
|
|
299
336
|
);
|
|
300
337
|
|
|
301
338
|
spinner.succeed('定时备份配置完成');
|
|
302
|
-
console.log(chalk.gray(` Cron
|
|
303
|
-
console.log(chalk.gray(`
|
|
339
|
+
console.log(chalk.gray(` Cron 表达式:${cronExpr}`));
|
|
340
|
+
console.log(chalk.gray(` 备份脚本: ${scriptPath} (chmod 700)`));
|
|
341
|
+
console.log(chalk.gray(` 凭据文件: ${credPath} (chmod 600)`));
|
|
342
|
+
console.log(chalk.gray(` 执行日志: /var/log/deploy-helper-backup.log\n`));
|
|
304
343
|
ssh.dispose();
|
|
305
344
|
|
|
306
345
|
} catch (err) {
|
package/src/commands/env.js
CHANGED
|
@@ -153,24 +153,33 @@ async function pushEnv(config, envContent, vars) {
|
|
|
153
153
|
await runRemoteSilent(ssh, `chmod 600 ${config.remotePath}/.env`);
|
|
154
154
|
uploadSpinner.succeed('.env 上传完成,权限已设为 600');
|
|
155
155
|
|
|
156
|
-
//
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
156
|
+
// cron 模式不需要重启(下次定时使用新环境变量)
|
|
157
|
+
const appMode = config.appMode || 'web';
|
|
158
|
+
if (appMode === 'cron') {
|
|
159
|
+
console.log(chalk.gray(' ℹ 定时任务模式,新 .env 将在下次执行时生效'));
|
|
160
|
+
} else {
|
|
161
|
+
const { restart } = await inquirer.prompt([{
|
|
162
|
+
type: 'confirm',
|
|
163
|
+
name: 'restart',
|
|
164
|
+
message: '是否重启服务让新变量生效?',
|
|
165
|
+
default: true,
|
|
166
|
+
}]);
|
|
163
167
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
168
|
+
if (restart) {
|
|
169
|
+
const restartSpinner = ora('重启服务...').start();
|
|
170
|
+
if (config.projectType === 'nodejs') {
|
|
171
|
+
await runRemoteSilent(ssh, `pm2 restart ${config.appName}`);
|
|
172
|
+
} else if (config.projectType === 'python') {
|
|
173
|
+
await runRemoteSilent(ssh, `supervisorctl restart ${config.appName}`);
|
|
174
|
+
} else if (config.projectType === 'docker') {
|
|
175
|
+
if (config.composeFile) {
|
|
176
|
+
await runRemoteSilent(ssh, `cd ${config.remotePath} && docker compose -f ${config.composeFile} up -d`);
|
|
177
|
+
} else {
|
|
178
|
+
await runRemoteSilent(ssh, `docker restart ${config.appName}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
restartSpinner.succeed('服务已重启');
|
|
172
182
|
}
|
|
173
|
-
restartSpinner.succeed('服务已重启');
|
|
174
183
|
}
|
|
175
184
|
|
|
176
185
|
ssh.dispose();
|