ssh-release 0.4.0 → 0.5.0
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/CHANGELOG.md +6 -0
- package/README.md +28 -0
- package/dist/cli.js +94 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
- `ssh-release list`:查看远程版本和当前版本。
|
|
17
17
|
- `ssh-release rollback [version]`:回滚到上一个版本或指定版本。
|
|
18
18
|
- `ssh-release unlock`:查看远端锁,并在显式确认锁路径后删除锁。
|
|
19
|
+
- `--json`:输出单行 JSON,便于 CI/CD 解析。
|
|
19
20
|
- `release` 模式:上传压缩包、远端解压、切换 `current`、清理旧版本。
|
|
20
21
|
- `overwrite` 模式:直接覆盖发布到目标目录。
|
|
21
22
|
- 远端 `tar` 解压失败时回退逐文件上传。
|
|
@@ -224,6 +225,32 @@ ssh-release rollback 20260625-150000
|
|
|
224
225
|
|
|
225
226
|
`overwrite` 模式没有版本列表,也不支持回滚。
|
|
226
227
|
|
|
228
|
+
## JSON 输出
|
|
229
|
+
|
|
230
|
+
需要在自动化脚本中解析结果时,可以给命令增加 `--json`:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
ssh-release deploy --dry-run --json
|
|
234
|
+
ssh-release doctor --json
|
|
235
|
+
ssh-release list --json
|
|
236
|
+
ssh-release rollback --json
|
|
237
|
+
ssh-release unlock --json
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
成功时输出单行 JSON:
|
|
241
|
+
|
|
242
|
+
```json
|
|
243
|
+
{"ok":true,"command":"deploy","result":{"dryRun":true,"mode":"release"}}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
命令执行失败时输出:
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
{"ok":false,"command":"deploy","error":"错误信息"}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`doctor` 检查失败、`unlock` 发现锁但未删除时会返回非零退出码,并在 JSON 的 `result` 中保留检查结果。
|
|
253
|
+
|
|
227
254
|
## 安全边界
|
|
228
255
|
|
|
229
256
|
配置模板不包含真实 IP、密码或生产路径。
|
|
@@ -363,6 +390,7 @@ docs/
|
|
|
363
390
|
- 危险远程路径拒绝。
|
|
364
391
|
- 发布模式和压缩格式校验。
|
|
365
392
|
- CLI 命令分发。
|
|
393
|
+
- CLI JSON 输出。
|
|
366
394
|
- 本地 `.tgz` 打包和排除项。
|
|
367
395
|
- macOS AppleDouble 元数据排除。
|
|
368
396
|
- `release` 和 `overwrite` 发布流程。
|
package/dist/cli.js
CHANGED
|
@@ -19,15 +19,28 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
19
19
|
const io = options.io ?? console;
|
|
20
20
|
const parsed = parseCliArgs(argv);
|
|
21
21
|
if (parsed.error) {
|
|
22
|
+
if (parsed.json) {
|
|
23
|
+
printJsonError(parsed.command, parsed.error, io);
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
22
26
|
io.error(parsed.error);
|
|
23
27
|
return 1;
|
|
24
28
|
}
|
|
25
29
|
if (parsed.help) {
|
|
30
|
+
if (parsed.json) {
|
|
31
|
+
printJsonResult('help', { usage: createUsageText() }, 0, io);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
26
34
|
printUsage(io);
|
|
27
35
|
return 0;
|
|
28
36
|
}
|
|
29
37
|
if (parsed.version) {
|
|
30
|
-
|
|
38
|
+
const version = readPackageVersion();
|
|
39
|
+
if (parsed.json) {
|
|
40
|
+
printJsonResult('version', { version }, 0, io);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
io.log(version);
|
|
31
44
|
return 0;
|
|
32
45
|
}
|
|
33
46
|
const handlers = options.handlers ?? createDefaultHandlers(parsed.configPath);
|
|
@@ -36,35 +49,76 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
36
49
|
try {
|
|
37
50
|
if (command === 'init') {
|
|
38
51
|
await handlers.init();
|
|
52
|
+
if (parsed.json) {
|
|
53
|
+
printJsonResult('init', { configPath: parsed.configPath }, 0, io);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
39
56
|
io.log(`已创建 ${parsed.configPath}`);
|
|
40
57
|
return 0;
|
|
41
58
|
}
|
|
42
59
|
if (command === 'deploy') {
|
|
43
|
-
|
|
60
|
+
const result = await handlers.deploy({ dryRun: parsed.dryRun });
|
|
61
|
+
if (parsed.json) {
|
|
62
|
+
printJsonResult('deploy', result, 0, io);
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
printDeployResult(result, io);
|
|
44
66
|
return 0;
|
|
45
67
|
}
|
|
46
68
|
if (command === 'rollback') {
|
|
47
|
-
|
|
69
|
+
const result = await handlers.rollback(args[0]);
|
|
70
|
+
if (parsed.json) {
|
|
71
|
+
printJsonResult('rollback', result, 0, io);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
printRollbackResult(result, io);
|
|
48
75
|
return 0;
|
|
49
76
|
}
|
|
50
77
|
if (command === 'list') {
|
|
51
|
-
|
|
78
|
+
const result = await handlers.list();
|
|
79
|
+
if (parsed.json) {
|
|
80
|
+
printJsonResult('list', result, 0, io);
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
printListResult(result, io);
|
|
52
84
|
return 0;
|
|
53
85
|
}
|
|
54
86
|
if (command === 'doctor') {
|
|
55
87
|
const report = await handlers.doctor();
|
|
88
|
+
const exitCode = report.ok ? 0 : 1;
|
|
89
|
+
if (parsed.json) {
|
|
90
|
+
printJsonResult('doctor', report, exitCode, io);
|
|
91
|
+
return exitCode;
|
|
92
|
+
}
|
|
56
93
|
printDoctorReport(report, io);
|
|
57
|
-
return
|
|
94
|
+
return exitCode;
|
|
58
95
|
}
|
|
59
96
|
if (command === 'unlock') {
|
|
60
97
|
const result = await handlers.unlock({ confirmPath: parsed.confirmPath });
|
|
98
|
+
const exitCode = result.locked && !result.removed ? 1 : 0;
|
|
99
|
+
if (parsed.json) {
|
|
100
|
+
printJsonResult('unlock', result, exitCode, io);
|
|
101
|
+
return exitCode;
|
|
102
|
+
}
|
|
61
103
|
printUnlockResult(result, io);
|
|
62
|
-
return
|
|
104
|
+
return exitCode;
|
|
105
|
+
}
|
|
106
|
+
if (parsed.json) {
|
|
107
|
+
if (!command) {
|
|
108
|
+
printJsonResult('help', { usage: createUsageText() }, 0, io);
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
printJsonError(command, `未知命令: ${command}`, io);
|
|
112
|
+
return 1;
|
|
63
113
|
}
|
|
64
114
|
printUsage(io);
|
|
65
115
|
return command ? 1 : 0;
|
|
66
116
|
}
|
|
67
117
|
catch (error) {
|
|
118
|
+
if (parsed.json) {
|
|
119
|
+
printJsonError(command, formatError(error), io);
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
68
122
|
io.error(error instanceof Error ? error.message : String(error));
|
|
69
123
|
return 1;
|
|
70
124
|
}
|
|
@@ -75,6 +129,7 @@ function parseCliArgs(argv) {
|
|
|
75
129
|
let configPath = CONFIG_FILE_NAME;
|
|
76
130
|
let dryRun = false;
|
|
77
131
|
let help = false;
|
|
132
|
+
let json = false;
|
|
78
133
|
let version = false;
|
|
79
134
|
for (let index = 0; index < argv.length; index += 1) {
|
|
80
135
|
const arg = argv[index];
|
|
@@ -86,10 +141,14 @@ function parseCliArgs(argv) {
|
|
|
86
141
|
version = true;
|
|
87
142
|
continue;
|
|
88
143
|
}
|
|
144
|
+
if (arg === '--json') {
|
|
145
|
+
json = true;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
89
148
|
if (arg === '--config' || arg === '-c') {
|
|
90
149
|
const value = argv[index + 1];
|
|
91
150
|
if (!value || value.startsWith('-')) {
|
|
92
|
-
return createParsedError('--config 需要配置文件路径', configPath);
|
|
151
|
+
return createParsedError('--config 需要配置文件路径', configPath, json);
|
|
93
152
|
}
|
|
94
153
|
configPath = value;
|
|
95
154
|
index += 1;
|
|
@@ -102,14 +161,14 @@ function parseCliArgs(argv) {
|
|
|
102
161
|
if (arg === '--confirm') {
|
|
103
162
|
const value = argv[index + 1];
|
|
104
163
|
if (!value || value.startsWith('-')) {
|
|
105
|
-
return createParsedError('--confirm 需要远端锁路径', configPath);
|
|
164
|
+
return createParsedError('--confirm 需要远端锁路径', configPath, json);
|
|
106
165
|
}
|
|
107
166
|
confirmPath = value;
|
|
108
167
|
index += 1;
|
|
109
168
|
continue;
|
|
110
169
|
}
|
|
111
170
|
if (arg.startsWith('-')) {
|
|
112
|
-
return createParsedError(`未知选项: ${arg}`, configPath);
|
|
171
|
+
return createParsedError(`未知选项: ${arg}`, configPath, json);
|
|
113
172
|
}
|
|
114
173
|
args.push(arg);
|
|
115
174
|
}
|
|
@@ -120,16 +179,18 @@ function parseCliArgs(argv) {
|
|
|
120
179
|
configPath,
|
|
121
180
|
dryRun,
|
|
122
181
|
help,
|
|
182
|
+
json,
|
|
123
183
|
version,
|
|
124
184
|
};
|
|
125
185
|
}
|
|
126
|
-
function createParsedError(error, configPath) {
|
|
186
|
+
function createParsedError(error, configPath, json = false) {
|
|
127
187
|
return {
|
|
128
188
|
args: [],
|
|
129
189
|
configPath,
|
|
130
190
|
dryRun: false,
|
|
131
191
|
error,
|
|
132
192
|
help: false,
|
|
193
|
+
json,
|
|
133
194
|
version: false,
|
|
134
195
|
};
|
|
135
196
|
}
|
|
@@ -230,6 +291,20 @@ function printUnlockResult(result, io) {
|
|
|
230
291
|
io.log('确认没有发布或回滚任务后再执行:');
|
|
231
292
|
io.log(`ssh-release unlock --confirm ${result.lockPath}`);
|
|
232
293
|
}
|
|
294
|
+
function printJsonResult(command, result, exitCode, io) {
|
|
295
|
+
io.log(JSON.stringify({
|
|
296
|
+
ok: exitCode === 0,
|
|
297
|
+
command,
|
|
298
|
+
result,
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
function printJsonError(command, error, io) {
|
|
302
|
+
io.log(JSON.stringify({
|
|
303
|
+
ok: false,
|
|
304
|
+
command,
|
|
305
|
+
error,
|
|
306
|
+
}));
|
|
307
|
+
}
|
|
233
308
|
function toRealPath(filePath) {
|
|
234
309
|
try {
|
|
235
310
|
return realpathSync(filePath);
|
|
@@ -239,7 +314,10 @@ function toRealPath(filePath) {
|
|
|
239
314
|
}
|
|
240
315
|
}
|
|
241
316
|
function printUsage(io) {
|
|
242
|
-
io.log(
|
|
317
|
+
io.log(createUsageText());
|
|
318
|
+
}
|
|
319
|
+
function createUsageText() {
|
|
320
|
+
return `用法:
|
|
243
321
|
ssh-release init [--config <path>]
|
|
244
322
|
ssh-release doctor [--config <path>]
|
|
245
323
|
ssh-release deploy [--config <path>]
|
|
@@ -247,13 +325,17 @@ function printUsage(io) {
|
|
|
247
325
|
ssh-release list [--config <path>]
|
|
248
326
|
ssh-release rollback [version] [--config <path>]
|
|
249
327
|
ssh-release unlock [--confirm <lock-path>] [--config <path>]
|
|
328
|
+
ssh-release <command> --json
|
|
250
329
|
ssh-release --help
|
|
251
|
-
ssh-release --version
|
|
330
|
+
ssh-release --version`;
|
|
252
331
|
}
|
|
253
332
|
function readPackageVersion() {
|
|
254
333
|
const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
255
334
|
return packageJson.version ?? '0.0.0';
|
|
256
335
|
}
|
|
336
|
+
function formatError(error) {
|
|
337
|
+
return error instanceof Error ? error.message : String(error);
|
|
338
|
+
}
|
|
257
339
|
if (isCliEntrypoint(import.meta.url, process.argv[1])) {
|
|
258
340
|
const exitCode = await runCli();
|
|
259
341
|
process.exit(exitCode);
|