@noahyu/cd-cli 1.0.1 → 1.1.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/README.md +23 -84
- package/dist/src/index.js +96 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ export default {
|
|
|
110
110
|
/** 私钥 */
|
|
111
111
|
privateKey: '-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----',
|
|
112
112
|
/** 私钥文件路径 */
|
|
113
|
-
privateKeyPath: '
|
|
113
|
+
privateKeyPath: '/home/user/.ssh/id_rsa',
|
|
114
114
|
/** 密码 */
|
|
115
115
|
password: 'your-password',
|
|
116
116
|
/** 部署目录 */
|
|
@@ -169,16 +169,12 @@ export default {
|
|
|
169
169
|
server: {
|
|
170
170
|
host: '192.168.1.100',
|
|
171
171
|
username: 'root',
|
|
172
|
-
privateKeyPath: '
|
|
172
|
+
privateKeyPath: '/home/user/.ssh/id_rsa',
|
|
173
173
|
deployPath: '/var/www/app',
|
|
174
174
|
},
|
|
175
175
|
}
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
-
- `~/.ssh/id_rsa` - 用户主目录下的私钥
|
|
179
|
-
- `/home/user/.ssh/id_ed25519` - 绝对路径
|
|
180
|
-
- `./keys/deploy_key` - 相对路径
|
|
181
|
-
|
|
182
178
|
### 方式三:密码认证
|
|
183
179
|
|
|
184
180
|
适用于简单测试环境(生产环境不推荐):
|
|
@@ -212,23 +208,23 @@ export default {
|
|
|
212
208
|
|
|
213
209
|
## 配置选项
|
|
214
210
|
|
|
215
|
-
| 选项 | 类型 | 必填 | 说明
|
|
216
|
-
| ----------------------- | -------- | ---- |
|
|
217
|
-
| `buildCommand` | string | 否 | 构建命令,如 "npm run build"
|
|
218
|
-
| `buildDir` | string | 是 | 构建输出目录,如 ".output"
|
|
219
|
-
| `version` | string | 否 | 版本号,不指定会在部署时询问
|
|
220
|
-
| `server.host` | string | 是 | 服务器地址
|
|
221
|
-
| `server.port` | number | 否 | SSH 端口,默认 22
|
|
222
|
-
| `server.username` | string | 是 | 用户名
|
|
223
|
-
| `server.password` | string | 否 | SSH 密码
|
|
224
|
-
| `server.privateKey` | string | 否 | SSH
|
|
225
|
-
| `server.privateKeyPath` | string | 否 | SSH 私钥文件路径
|
|
226
|
-
| `server.deployPath` | string | 是 | 服务器部署路径
|
|
227
|
-
| `pm2.appName` | string | 否 | PM2 应用名称
|
|
228
|
-
| `pm2.restart` | boolean | 否 | 是否重启 PM2 应用
|
|
229
|
-
| `excludeFiles` | string[] | 否 | 排除的文件模式(相对于构建目录)
|
|
230
|
-
| `beforeDeploy` | string[] | 否 | 部署前执行的命令
|
|
231
|
-
| `afterDeploy` | string[] | 否 | 部署后执行的命令
|
|
211
|
+
| 选项 | 类型 | 必填 | 说明 |
|
|
212
|
+
| ----------------------- | -------- | ---- | -------------------------------- |
|
|
213
|
+
| `buildCommand` | string | 否 | 构建命令,如 "npm run build" |
|
|
214
|
+
| `buildDir` | string | 是 | 构建输出目录,如 ".output" |
|
|
215
|
+
| `version` | string | 否 | 版本号,不指定会在部署时询问 |
|
|
216
|
+
| `server.host` | string | 是 | 服务器地址 |
|
|
217
|
+
| `server.port` | number | 否 | SSH 端口,默认 22 |
|
|
218
|
+
| `server.username` | string | 是 | 用户名 |
|
|
219
|
+
| `server.password` | string | 否 | SSH 密码 |
|
|
220
|
+
| `server.privateKey` | string | 否 | SSH 私钥内容,优先级最高 |
|
|
221
|
+
| `server.privateKeyPath` | string | 否 | SSH 私钥文件路径 |
|
|
222
|
+
| `server.deployPath` | string | 是 | 服务器部署路径 |
|
|
223
|
+
| `pm2.appName` | string | 否 | PM2 应用名称 |
|
|
224
|
+
| `pm2.restart` | boolean | 否 | 是否重启 PM2 应用 |
|
|
225
|
+
| `excludeFiles` | string[] | 否 | 排除的文件模式(相对于构建目录) |
|
|
226
|
+
| `beforeDeploy` | string[] | 否 | 部署前执行的命令 |
|
|
227
|
+
| `afterDeploy` | string[] | 否 | 部署后执行的命令 |
|
|
232
228
|
|
|
233
229
|
## 命令详解
|
|
234
230
|
|
|
@@ -278,63 +274,6 @@ Options:
|
|
|
278
274
|
pcli-cd init
|
|
279
275
|
```
|
|
280
276
|
|
|
281
|
-
## 使用场景
|
|
282
|
-
|
|
283
|
-
### 1. Nuxt 3 项目部署
|
|
284
|
-
|
|
285
|
-
```javascript
|
|
286
|
-
// Nuxt.js 项目配置
|
|
287
|
-
export default {
|
|
288
|
-
buildCommand: 'npm run build',
|
|
289
|
-
buildDir: '.output',
|
|
290
|
-
server: {
|
|
291
|
-
host: 'your-server.com',
|
|
292
|
-
username: 'root',
|
|
293
|
-
privateKeyPath: '~/.ssh/id_rsa', // 推荐使用私钥认证
|
|
294
|
-
deployPath: '/var/www/nuxt-app',
|
|
295
|
-
},
|
|
296
|
-
pm2: {
|
|
297
|
-
appName: 'nuxt-app',
|
|
298
|
-
restart: true,
|
|
299
|
-
},
|
|
300
|
-
excludeFiles: [
|
|
301
|
-
// 注意:以下路径相对于 .output 目录
|
|
302
|
-
// 请根据实际构建产物内容调整
|
|
303
|
-
'src/**', // 如果源码被复制到输出目录
|
|
304
|
-
'**/.DS_Store', // macOS 系统文件
|
|
305
|
-
],
|
|
306
|
-
}
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### 2. Node.js API 项目部署
|
|
310
|
-
|
|
311
|
-
```javascript
|
|
312
|
-
// Node.js API 项目配置
|
|
313
|
-
export default {
|
|
314
|
-
buildCommand: 'npm run build',
|
|
315
|
-
buildDir: 'dist',
|
|
316
|
-
server: {
|
|
317
|
-
host: 'your-server.com',
|
|
318
|
-
username: 'root',
|
|
319
|
-
privateKey: '~/.ssh/id_rsa',
|
|
320
|
-
deployPath: '/var/www/api'
|
|
321
|
-
},
|
|
322
|
-
pm2: {
|
|
323
|
-
appName: 'api-server',
|
|
324
|
-
restart: true
|
|
325
|
-
},
|
|
326
|
-
excludeFiles: [
|
|
327
|
-
// 注意:以下路径相对于 dist 目录
|
|
328
|
-
// 请根据实际构建产物内容调整
|
|
329
|
-
'src/**', // 如果源码被复制到输出目录
|
|
330
|
-
'**/.DS_Store', // macOS 系统文件
|
|
331
|
-
]
|
|
332
|
-
afterDeploy: [
|
|
333
|
-
'npm install --production'
|
|
334
|
-
]
|
|
335
|
-
}
|
|
336
|
-
```
|
|
337
|
-
|
|
338
277
|
## excludeFiles 配置说明
|
|
339
278
|
|
|
340
279
|
⚠️ **重要提醒**:`excludeFiles` 作用于**构建产物目录**(如 `dist/`、`.output/` 等),不是项目根目录。
|
|
@@ -450,17 +389,17 @@ pcli-cd init
|
|
|
450
389
|
|
|
451
390
|
```bash
|
|
452
391
|
# 检查私钥文件是否存在
|
|
453
|
-
ls -la
|
|
392
|
+
ls -la /home/user/.ssh/id_rsa
|
|
454
393
|
|
|
455
394
|
# 检查私钥文件权限
|
|
456
|
-
chmod 600
|
|
395
|
+
chmod 600 /home/user/.ssh/id_rsa
|
|
457
396
|
```
|
|
458
397
|
|
|
459
398
|
**2. 私钥格式错误**
|
|
460
399
|
|
|
461
400
|
```bash
|
|
462
401
|
# 检查私钥格式(应该以此开头)
|
|
463
|
-
head -1
|
|
402
|
+
head -1 /home/user/.ssh/id_rsa
|
|
464
403
|
# RSA: -----BEGIN RSA PRIVATE KEY-----
|
|
465
404
|
# OpenSSH: -----BEGIN OPENSSH PRIVATE KEY-----
|
|
466
405
|
# ECDSA: -----BEGIN EC PRIVATE KEY-----
|
|
@@ -479,7 +418,7 @@ head -1 ~/.ssh/id_rsa
|
|
|
479
418
|
ssh -p 22 root@your-server.com
|
|
480
419
|
|
|
481
420
|
# 测试指定私钥连接
|
|
482
|
-
ssh -i
|
|
421
|
+
ssh -i /home/user/.ssh/id_rsa -p 22 root@your-server.com
|
|
483
422
|
```
|
|
484
423
|
|
|
485
424
|
## License
|
package/dist/src/index.js
CHANGED
|
@@ -73,6 +73,12 @@ async function deploy(config) {
|
|
|
73
73
|
spinner.start('正在连接服务器...');
|
|
74
74
|
const ssh = await createSSHConnection(config.server);
|
|
75
75
|
spinner.succeed('服务器连接成功');
|
|
76
|
+
await cleanTempLinks(ssh, config.server.deployPath, buildDirName);
|
|
77
|
+
spinner.start('检查部署环境...');
|
|
78
|
+
await handleExistingDeployDir(ssh, config.server.deployPath, buildDirName, spinner);
|
|
79
|
+
if (spinner.isSpinning) {
|
|
80
|
+
spinner.succeed('部署环境检查完成');
|
|
81
|
+
}
|
|
76
82
|
const versionDirName = `${buildDirName}-${version}`;
|
|
77
83
|
const versionPath = join(config.server.deployPath, versionDirName);
|
|
78
84
|
const currentLinkPath = join(config.server.deployPath, buildDirName);
|
|
@@ -104,9 +110,22 @@ async function deploy(config) {
|
|
|
104
110
|
}
|
|
105
111
|
spinner.start(`正在切换到新版本 ${version}...`);
|
|
106
112
|
const tempLinkPath = `${currentLinkPath}.tmp.${Date.now()}`;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
try {
|
|
114
|
+
const linkResult = await ssh.execCommand(`ln -sfn ${versionPath} ${tempLinkPath}`);
|
|
115
|
+
if (linkResult.code !== 0) {
|
|
116
|
+
throw new Error(`创建临时软链接失败: ${linkResult.stderr}`);
|
|
117
|
+
}
|
|
118
|
+
const moveResult = await ssh.execCommand(`mv ${tempLinkPath} ${currentLinkPath}`);
|
|
119
|
+
if (moveResult.code !== 0) {
|
|
120
|
+
await ssh.execCommand(`rm -f ${tempLinkPath}`);
|
|
121
|
+
throw new Error(`切换软链接失败: ${moveResult.stderr}`);
|
|
122
|
+
}
|
|
123
|
+
spinner.succeed(`版本切换完成: ${buildDirName} -> ${versionDirName}`);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
await ssh.execCommand(`rm -f ${tempLinkPath}`);
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
110
129
|
if (config.pm2) {
|
|
111
130
|
spinner.start('正在重启 PM2 应用...');
|
|
112
131
|
const { appName, restart = true } = config.pm2;
|
|
@@ -391,9 +410,22 @@ async function performRollback(config, targetVersion, buildDirName) {
|
|
|
391
410
|
spinner.succeed('目标版本检查通过');
|
|
392
411
|
spinner.start(`正在回滚到版本 ${targetVersion}...`);
|
|
393
412
|
const tempLinkPath = `${currentLinkPath}.tmp.${Date.now()}`;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
413
|
+
try {
|
|
414
|
+
const linkResult = await ssh.execCommand(`ln -sfn ${versionPath} ${tempLinkPath}`);
|
|
415
|
+
if (linkResult.code !== 0) {
|
|
416
|
+
throw new Error(`创建临时软链接失败: ${linkResult.stderr}`);
|
|
417
|
+
}
|
|
418
|
+
const moveResult = await ssh.execCommand(`mv ${tempLinkPath} ${currentLinkPath}`);
|
|
419
|
+
if (moveResult.code !== 0) {
|
|
420
|
+
await ssh.execCommand(`rm -f ${tempLinkPath}`);
|
|
421
|
+
throw new Error(`切换软链接失败: ${moveResult.stderr}`);
|
|
422
|
+
}
|
|
423
|
+
spinner.succeed(`回滚完成: ${buildDirName} -> ${versionDirName}`);
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
await ssh.execCommand(`rm -f ${tempLinkPath}`);
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
397
429
|
if (config.pm2) {
|
|
398
430
|
spinner.start('正在重启 PM2 应用...');
|
|
399
431
|
const { appName } = config.pm2;
|
|
@@ -448,5 +480,63 @@ async function createSSHConnection(server) {
|
|
|
448
480
|
throw new Error(`SSH 连接失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
449
481
|
}
|
|
450
482
|
}
|
|
483
|
+
async function cleanTempLinks(ssh, deployPath, buildDirName) {
|
|
484
|
+
try {
|
|
485
|
+
await ssh.execCommand(`find ${deployPath} -name "${buildDirName}.tmp.*" -type l -delete`);
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
console.warn(chalk.yellow(`⚠️ 清理临时链接时出现警告: ${error}`));
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async function handleExistingDeployDir(ssh, deployPath, buildDirName, spinner) {
|
|
492
|
+
const currentLinkPath = join(deployPath, buildDirName);
|
|
493
|
+
const checkResult = await ssh.execCommand(`test -e ${currentLinkPath}`);
|
|
494
|
+
if (checkResult.code !== 0) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const linkCheckResult = await ssh.execCommand(`test -L ${currentLinkPath}`);
|
|
498
|
+
if (linkCheckResult.code === 0) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const typeResult = await ssh.execCommand(`stat -c %F ${currentLinkPath} 2>/dev/null || file -b ${currentLinkPath}`);
|
|
502
|
+
const fileType = typeResult.stdout.trim();
|
|
503
|
+
if (fileType.includes('directory') || fileType === 'directory') {
|
|
504
|
+
const backupPath = `${currentLinkPath}.backup.${Date.now()}`;
|
|
505
|
+
spinner.stop();
|
|
506
|
+
console.log(chalk.yellow(`⚠️ 检测到已存在的目录: ${currentLinkPath}`));
|
|
507
|
+
console.log(chalk.blue(`📁 将备份到: ${backupPath}`));
|
|
508
|
+
const answers = await inquirer.prompt([
|
|
509
|
+
{
|
|
510
|
+
type: 'confirm',
|
|
511
|
+
name: 'proceed',
|
|
512
|
+
message: '是否继续部署?(已存在的目录将被备份)',
|
|
513
|
+
default: true,
|
|
514
|
+
},
|
|
515
|
+
]);
|
|
516
|
+
if (!answers.proceed) {
|
|
517
|
+
throw new Error('用户取消部署');
|
|
518
|
+
}
|
|
519
|
+
spinner.start('正在备份已存在的目录...');
|
|
520
|
+
const backupResult = await ssh.execCommand(`mv ${currentLinkPath} ${backupPath}`);
|
|
521
|
+
if (backupResult.code !== 0) {
|
|
522
|
+
throw new Error(`备份目录失败: ${backupResult.stderr}`);
|
|
523
|
+
}
|
|
524
|
+
spinner.stop();
|
|
525
|
+
console.log(chalk.green(`✅ 目录已备份到: ${backupPath}`));
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
const backupPath = `${currentLinkPath}.backup.${Date.now()}`;
|
|
529
|
+
spinner.stop();
|
|
530
|
+
console.log(chalk.yellow(`⚠️ 检测到已存在的文件: ${currentLinkPath}`));
|
|
531
|
+
console.log(chalk.blue(`📁 将备份到: ${backupPath}`));
|
|
532
|
+
spinner.start('正在备份已存在的文件...');
|
|
533
|
+
const backupResult = await ssh.execCommand(`mv ${currentLinkPath} ${backupPath}`);
|
|
534
|
+
if (backupResult.code !== 0) {
|
|
535
|
+
throw new Error(`备份文件失败: ${backupResult.stderr}`);
|
|
536
|
+
}
|
|
537
|
+
spinner.stop();
|
|
538
|
+
console.log(chalk.green(`✅ 文件已备份到: ${backupPath}`));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
451
541
|
|
|
452
542
|
export { deployCommand, initConfig, listVersions, rollbackVersion };
|