ssh-release 0.5.0 → 0.6.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 +18 -0
- package/dist/cli.js +33 -3
- package/dist/release.js +45 -24
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
- `ssh-release rollback [version]`:回滚到上一个版本或指定版本。
|
|
18
18
|
- `ssh-release unlock`:查看远端锁,并在显式确认锁路径后删除锁。
|
|
19
19
|
- `--json`:输出单行 JSON,便于 CI/CD 解析。
|
|
20
|
+
- `deploy --json --progress`:发布时输出 NDJSON 阶段进度,便于 CI/CD 展示实时状态。
|
|
20
21
|
- `release` 模式:上传压缩包、远端解压、切换 `current`、清理旧版本。
|
|
21
22
|
- `overwrite` 模式:直接覆盖发布到目标目录。
|
|
22
23
|
- 远端 `tar` 解压失败时回退逐文件上传。
|
|
@@ -231,6 +232,7 @@ ssh-release rollback 20260625-150000
|
|
|
231
232
|
|
|
232
233
|
```bash
|
|
233
234
|
ssh-release deploy --dry-run --json
|
|
235
|
+
ssh-release deploy --json --progress
|
|
234
236
|
ssh-release doctor --json
|
|
235
237
|
ssh-release list --json
|
|
236
238
|
ssh-release rollback --json
|
|
@@ -249,6 +251,22 @@ ssh-release unlock --json
|
|
|
249
251
|
{"ok":false,"command":"deploy","error":"错误信息"}
|
|
250
252
|
```
|
|
251
253
|
|
|
254
|
+
发布时需要持续读取阶段状态,可以使用:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
ssh-release deploy --json --progress
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
该模式会按行输出 NDJSON。进度事件先输出,最终结果最后输出:
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{"ok":true,"command":"deploy","event":"progress","stage":"package","status":"start"}
|
|
264
|
+
{"ok":true,"command":"deploy","event":"progress","stage":"package","status":"success"}
|
|
265
|
+
{"ok":true,"command":"deploy","result":{"mode":"release","version":"20260625-153000"}}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`stage` 可能是 `source`、`lock`、`package`、`publish`、`cleanup`,`status` 可能是 `start`、`success`、`fail`。失败事件会包含 `error` 字段。
|
|
269
|
+
|
|
252
270
|
`doctor` 检查失败、`unlock` 发现锁但未删除时会返回非零退出码,并在 JSON 的 `result` 中保留检查结果。
|
|
253
271
|
|
|
254
272
|
## 安全边界
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { CONFIG_FILE_NAME, loadConfigFile, writeConfigTemplate } from './config.js';
|
|
6
6
|
import { runDoctorFromFile } from './doctor.js';
|
|
7
7
|
import { listReleases } from './list.js';
|
|
8
|
-
import { createDeployPlan, deploy } from './release.js';
|
|
8
|
+
import { createDeployPlan, deploy, } from './release.js';
|
|
9
9
|
import { rollback } from './rollback.js';
|
|
10
10
|
import { createRemoteClient } from './ssh.js';
|
|
11
11
|
import { unlock } from './unlock.js';
|
|
@@ -57,7 +57,13 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
57
57
|
return 0;
|
|
58
58
|
}
|
|
59
59
|
if (command === 'deploy') {
|
|
60
|
-
const
|
|
60
|
+
const deployOptions = {
|
|
61
|
+
dryRun: parsed.dryRun,
|
|
62
|
+
};
|
|
63
|
+
if (parsed.json && parsed.progress) {
|
|
64
|
+
deployOptions.onProgress = (event) => printJsonProgress('deploy', event, io);
|
|
65
|
+
}
|
|
66
|
+
const result = await handlers.deploy(deployOptions);
|
|
61
67
|
if (parsed.json) {
|
|
62
68
|
printJsonResult('deploy', result, 0, io);
|
|
63
69
|
return 0;
|
|
@@ -130,6 +136,7 @@ function parseCliArgs(argv) {
|
|
|
130
136
|
let dryRun = false;
|
|
131
137
|
let help = false;
|
|
132
138
|
let json = false;
|
|
139
|
+
let progress = false;
|
|
133
140
|
let version = false;
|
|
134
141
|
for (let index = 0; index < argv.length; index += 1) {
|
|
135
142
|
const arg = argv[index];
|
|
@@ -145,6 +152,10 @@ function parseCliArgs(argv) {
|
|
|
145
152
|
json = true;
|
|
146
153
|
continue;
|
|
147
154
|
}
|
|
155
|
+
if (arg === '--progress') {
|
|
156
|
+
progress = true;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
148
159
|
if (arg === '--config' || arg === '-c') {
|
|
149
160
|
const value = argv[index + 1];
|
|
150
161
|
if (!value || value.startsWith('-')) {
|
|
@@ -172,6 +183,12 @@ function parseCliArgs(argv) {
|
|
|
172
183
|
}
|
|
173
184
|
args.push(arg);
|
|
174
185
|
}
|
|
186
|
+
if (progress && !json) {
|
|
187
|
+
return createParsedError('--progress 需要配合 --json 使用', configPath, json);
|
|
188
|
+
}
|
|
189
|
+
if (progress && args[0] && args[0] !== 'deploy') {
|
|
190
|
+
return createParsedError('--progress 仅支持 deploy 命令', configPath, json);
|
|
191
|
+
}
|
|
175
192
|
return {
|
|
176
193
|
args: args.slice(1),
|
|
177
194
|
command: args[0],
|
|
@@ -180,6 +197,7 @@ function parseCliArgs(argv) {
|
|
|
180
197
|
dryRun,
|
|
181
198
|
help,
|
|
182
199
|
json,
|
|
200
|
+
progress,
|
|
183
201
|
version,
|
|
184
202
|
};
|
|
185
203
|
}
|
|
@@ -191,6 +209,7 @@ function createParsedError(error, configPath, json = false) {
|
|
|
191
209
|
error,
|
|
192
210
|
help: false,
|
|
193
211
|
json,
|
|
212
|
+
progress: false,
|
|
194
213
|
version: false,
|
|
195
214
|
};
|
|
196
215
|
}
|
|
@@ -202,7 +221,9 @@ function createDefaultHandlers(configPath) {
|
|
|
202
221
|
if (options?.dryRun) {
|
|
203
222
|
return createDeployPlan(config);
|
|
204
223
|
}
|
|
205
|
-
return deploy(config, createRemoteClient(config)
|
|
224
|
+
return deploy(config, createRemoteClient(config), {
|
|
225
|
+
onProgress: options?.onProgress,
|
|
226
|
+
});
|
|
206
227
|
},
|
|
207
228
|
rollback: async (version) => {
|
|
208
229
|
const config = await loadConfigFile(configPath);
|
|
@@ -298,6 +319,14 @@ function printJsonResult(command, result, exitCode, io) {
|
|
|
298
319
|
result,
|
|
299
320
|
}));
|
|
300
321
|
}
|
|
322
|
+
function printJsonProgress(command, event, io) {
|
|
323
|
+
io.log(JSON.stringify({
|
|
324
|
+
ok: true,
|
|
325
|
+
command,
|
|
326
|
+
event: 'progress',
|
|
327
|
+
...event,
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
301
330
|
function printJsonError(command, error, io) {
|
|
302
331
|
io.log(JSON.stringify({
|
|
303
332
|
ok: false,
|
|
@@ -322,6 +351,7 @@ function createUsageText() {
|
|
|
322
351
|
ssh-release doctor [--config <path>]
|
|
323
352
|
ssh-release deploy [--config <path>]
|
|
324
353
|
ssh-release deploy --dry-run [--config <path>]
|
|
354
|
+
ssh-release deploy --json --progress [--config <path>]
|
|
325
355
|
ssh-release list [--config <path>]
|
|
326
356
|
ssh-release rollback [version] [--config <path>]
|
|
327
357
|
ssh-release unlock [--confirm <lock-path>] [--config <path>]
|
package/dist/release.js
CHANGED
|
@@ -28,25 +28,25 @@ function pad(value) {
|
|
|
28
28
|
return value.toString().padStart(2, '0');
|
|
29
29
|
}
|
|
30
30
|
export async function deploy(config, client, options = {}) {
|
|
31
|
-
await ensureSourceExists(config.source.path);
|
|
31
|
+
await withDeployProgress(options, 'source', () => ensureSourceExists(config.source.path));
|
|
32
32
|
const versionName = createVersionName(options.now ?? new Date());
|
|
33
33
|
const packageFactory = options.createPackage ?? createReleasePackage;
|
|
34
|
-
const releaseLock = await acquireRemoteLock(config, client);
|
|
34
|
+
const releaseLock = await withDeployProgress(options, 'lock', () => acquireRemoteLock(config, client));
|
|
35
35
|
let releasePackage;
|
|
36
36
|
let result;
|
|
37
37
|
let deployError;
|
|
38
38
|
let cleanupError;
|
|
39
39
|
try {
|
|
40
|
-
releasePackage = await packageFactory({
|
|
40
|
+
releasePackage = await withDeployProgress(options, 'package', () => packageFactory({
|
|
41
41
|
sourcePath: config.source.path,
|
|
42
42
|
exclude: config.source.exclude,
|
|
43
43
|
versionName,
|
|
44
|
-
});
|
|
44
|
+
}));
|
|
45
45
|
if (config.deploy.mode === 'overwrite') {
|
|
46
|
-
result = await deployOverwrite(config, client, releasePackage, versionName);
|
|
46
|
+
result = await withDeployProgress(options, 'publish', () => deployOverwrite(config, client, releasePackage, versionName));
|
|
47
47
|
return result;
|
|
48
48
|
}
|
|
49
|
-
result = await deployRelease(config, client, releasePackage, versionName);
|
|
49
|
+
result = await withDeployProgress(options, 'publish', () => deployRelease(config, client, releasePackage, versionName));
|
|
50
50
|
return result;
|
|
51
51
|
}
|
|
52
52
|
catch (error) {
|
|
@@ -54,29 +54,31 @@ export async function deploy(config, client, options = {}) {
|
|
|
54
54
|
throw error;
|
|
55
55
|
}
|
|
56
56
|
finally {
|
|
57
|
-
|
|
57
|
+
await withDeployProgress(options, 'cleanup', async () => {
|
|
58
|
+
if (releasePackage) {
|
|
59
|
+
try {
|
|
60
|
+
await releasePackage.cleanup();
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
cleanupError = error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
58
66
|
try {
|
|
59
|
-
await
|
|
67
|
+
await releaseLock();
|
|
60
68
|
}
|
|
61
69
|
catch (error) {
|
|
62
|
-
|
|
70
|
+
const message = `远程发布锁清理失败: ${formatError(error)}`;
|
|
71
|
+
if (result) {
|
|
72
|
+
result.warnings.push(message);
|
|
73
|
+
}
|
|
74
|
+
else if (!deployError && !cleanupError) {
|
|
75
|
+
cleanupError = error;
|
|
76
|
+
}
|
|
63
77
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
await releaseLock();
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
const message = `远程发布锁清理失败: ${formatError(error)}`;
|
|
70
|
-
if (result) {
|
|
71
|
-
result.warnings.push(message);
|
|
78
|
+
if (cleanupError && !deployError) {
|
|
79
|
+
throw cleanupError;
|
|
72
80
|
}
|
|
73
|
-
|
|
74
|
-
cleanupError = error;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
if (cleanupError && !deployError) {
|
|
78
|
-
throw cleanupError;
|
|
79
|
-
}
|
|
81
|
+
});
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
84
|
export async function createDeployPlan(config, options = {}) {
|
|
@@ -191,3 +193,22 @@ async function ensureSourceExists(sourcePath) {
|
|
|
191
193
|
function formatError(error) {
|
|
192
194
|
return error instanceof Error ? error.message : String(error);
|
|
193
195
|
}
|
|
196
|
+
async function withDeployProgress(options, stage, run) {
|
|
197
|
+
await emitDeployProgress(options, { stage, status: 'start' });
|
|
198
|
+
try {
|
|
199
|
+
const result = await run();
|
|
200
|
+
await emitDeployProgress(options, { stage, status: 'success' });
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
await emitDeployProgress(options, {
|
|
205
|
+
stage,
|
|
206
|
+
status: 'fail',
|
|
207
|
+
error: formatError(error),
|
|
208
|
+
});
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function emitDeployProgress(options, event) {
|
|
213
|
+
await options.onProgress?.(event);
|
|
214
|
+
}
|