ssh-release 1.1.0 → 1.2.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 +10 -0
- package/README.md +18 -3
- package/dist/cli.js +14 -1
- package/dist/doctor.js +47 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.0 - 2026-06-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- 增加首次发布指南,覆盖初始化、配置、预检、发布、列表和回滚流程。
|
|
8
|
+
- 增加平台依赖说明,补充 macOS、Ubuntu/Debian、Windows 和 CI 中的 `sshpass`、OpenSSH、`tar`、远端 hash 命令要求。
|
|
9
|
+
- `ssh-release init` 成功后输出下一步操作提示。
|
|
10
|
+
- 配置文件缺失和 `sshpass` 缺失时输出更具体的下一步提示。
|
|
11
|
+
- `ssh-release doctor` 增加本地 `tar`、`ssh`、`scp`、`sshpass` 和远端 hash 命令检查。
|
|
12
|
+
|
|
3
13
|
## 1.1.0 - 2026-06-26
|
|
4
14
|
|
|
5
15
|
### Added
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
已实现:
|
|
12
12
|
|
|
13
13
|
- `ssh-release init`:生成 `ssh-release.config.ts` 配置模板。
|
|
14
|
-
- `ssh-release doctor
|
|
14
|
+
- `ssh-release doctor`:检查配置、本地源路径、本地命令、SSH 连接、远程目录、远端 hash、远端锁和远端 `tar`。
|
|
15
15
|
- `ssh-release deploy`:发布本地文件或目录。
|
|
16
16
|
- `ssh-release list`:查看远程版本和当前版本。
|
|
17
17
|
- `ssh-release rollback [version]`:回滚到上一个版本或指定版本。
|
|
@@ -55,6 +55,9 @@ ssh-release --help
|
|
|
55
55
|
ssh-release --version
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
首次接入步骤见 [docs/quick-start.md](https://github.com/JackEngineer/ssh-release/blob/main/docs/quick-start.md)。
|
|
59
|
+
本机和 CI 依赖见 [docs/platform-requirements.md](https://github.com/JackEngineer/ssh-release/blob/main/docs/platform-requirements.md)。
|
|
60
|
+
|
|
58
61
|
## 本地开发
|
|
59
62
|
|
|
60
63
|
```bash
|
|
@@ -177,7 +180,9 @@ source files -> package -> upload -> release -> activate -> rollback
|
|
|
177
180
|
ssh-release doctor
|
|
178
181
|
```
|
|
179
182
|
|
|
180
|
-
`doctor`
|
|
183
|
+
`doctor` 会先检查本地 `tar`、`ssh`、`scp`,使用密码登录时还会检查 `sshpass`。本地检查失败时不会继续连接远端。
|
|
184
|
+
|
|
185
|
+
`doctor` 也会检查 `.ssh-release.lock`。没有锁时正常通过;如果发现锁,会以 `warn` 显示锁路径、pid、创建时间和安全清理提示。
|
|
181
186
|
|
|
182
187
|
需要先查看发布计划但不修改服务器时:
|
|
183
188
|
|
|
@@ -359,13 +364,16 @@ export SSH_RELEASE_PASSWORD='your-password'
|
|
|
359
364
|
- `tar`:本地打包发布内容。
|
|
360
365
|
- `ssh`:执行远端目录、解压、软链接、列表和检查命令。
|
|
361
366
|
- `scp`:上传压缩包和逐文件回退上传。
|
|
362
|
-
- `sha256sum` 或 `shasum`:发布后校验远端 `manifest.json` hash。
|
|
363
367
|
- `sshpass`:仅密码登录时需要;私钥登录不需要。
|
|
364
368
|
|
|
365
369
|
在 macOS 上打包时会禁用 AppleDouble 和扩展属性元数据,避免把 `._*` 文件发布到 Linux 服务器。
|
|
366
370
|
|
|
371
|
+
远端需要可运行 `sha256sum` 或 `shasum`,用于校验 `manifest.json` hash。
|
|
372
|
+
|
|
367
373
|
远端 `tar` 是可选能力,不可用时会按配置回退逐文件上传。
|
|
368
374
|
|
|
375
|
+
Windows、macOS、Linux 和 CI 的依赖差异见 [docs/platform-requirements.md](https://github.com/JackEngineer/ssh-release/blob/main/docs/platform-requirements.md)。
|
|
376
|
+
|
|
369
377
|
## 开发命令
|
|
370
378
|
|
|
371
379
|
```bash
|
|
@@ -412,6 +420,10 @@ GitHub Actions 发布模板见 [docs/github-actions.md](https://github.com/JackE
|
|
|
412
420
|
|
|
413
421
|
失败恢复指南见 [docs/recovery.md](https://github.com/JackEngineer/ssh-release/blob/main/docs/recovery.md)。
|
|
414
422
|
|
|
423
|
+
首次发布指南见 [docs/quick-start.md](https://github.com/JackEngineer/ssh-release/blob/main/docs/quick-start.md)。
|
|
424
|
+
|
|
425
|
+
平台依赖说明见 [docs/platform-requirements.md](https://github.com/JackEngineer/ssh-release/blob/main/docs/platform-requirements.md)。
|
|
426
|
+
|
|
415
427
|
版本变更见 [CHANGELOG.md](https://github.com/JackEngineer/ssh-release/blob/main/CHANGELOG.md)。
|
|
416
428
|
|
|
417
429
|
## 项目结构
|
|
@@ -458,6 +470,8 @@ tests/
|
|
|
458
470
|
docs/
|
|
459
471
|
├── contracts.md
|
|
460
472
|
├── github-actions.md
|
|
473
|
+
├── platform-requirements.md
|
|
474
|
+
├── quick-start.md
|
|
461
475
|
├── recovery.md
|
|
462
476
|
├── release-checklist.md
|
|
463
477
|
└── superpowers/specs/2026-06-25-ssh-release-design.md
|
|
@@ -480,6 +494,7 @@ docs/
|
|
|
480
494
|
- 发布和回滚锁获取、释放和锁冲突拦截。
|
|
481
495
|
- 发布 manifest 生成、上传和远端 hash 校验。
|
|
482
496
|
- `doctor` 远端锁状态检查和安全清理提示。
|
|
497
|
+
- `doctor` 本地命令和远端 hash 检查。
|
|
483
498
|
- `unlock` 显式确认路径后删除远端锁。
|
|
484
499
|
- 远端 `tar` 失败后的逐文件上传回退。
|
|
485
500
|
- 远程版本列表读取和当前版本标记。
|
package/dist/cli.js
CHANGED
|
@@ -54,6 +54,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
54
54
|
return 0;
|
|
55
55
|
}
|
|
56
56
|
io.log(`已创建 ${parsed.configPath}`);
|
|
57
|
+
printInitNextSteps(parsed.configPath, io);
|
|
57
58
|
return 0;
|
|
58
59
|
}
|
|
59
60
|
if (command === 'deploy') {
|
|
@@ -353,6 +354,13 @@ function printUnlockResult(result, io) {
|
|
|
353
354
|
io.log('确认没有发布或回滚任务后再执行:');
|
|
354
355
|
io.log(`ssh-release unlock --confirm ${result.lockPath}`);
|
|
355
356
|
}
|
|
357
|
+
function printInitNextSteps(configPath, io) {
|
|
358
|
+
io.log('下一步:');
|
|
359
|
+
io.log('1. 设置 SSH_RELEASE_HOST、SSH_RELEASE_USER,并选择密码或私钥认证。');
|
|
360
|
+
io.log('2. 确认 source.path 和 target.path 指向要发布的本地目录和远端目录。');
|
|
361
|
+
io.log(`3. 运行 ssh-release doctor --config ${configPath} 检查配置和服务器连接。`);
|
|
362
|
+
io.log(`4. 运行 ssh-release deploy --plan --config ${configPath} 预览发布计划。`);
|
|
363
|
+
}
|
|
356
364
|
function printJsonResult(command, result, exitCode, io) {
|
|
357
365
|
io.log(JSON.stringify({
|
|
358
366
|
ok: exitCode === 0,
|
|
@@ -415,6 +423,9 @@ function formatError(error) {
|
|
|
415
423
|
return error instanceof Error ? error.message : String(error);
|
|
416
424
|
}
|
|
417
425
|
function createErrorHint(command, error) {
|
|
426
|
+
if (error.includes('配置文件不存在')) {
|
|
427
|
+
return '先运行 ssh-release init 生成配置文件,再填写 source.path、server 和 target.path 后执行 ssh-release doctor。';
|
|
428
|
+
}
|
|
418
429
|
if (error.includes('远程已有发布任务正在运行') || error.includes('.ssh-release.lock')) {
|
|
419
430
|
return '先运行 ssh-release unlock 查看远端锁,确认没有发布或回滚任务后再按提示删除锁。';
|
|
420
431
|
}
|
|
@@ -435,12 +446,14 @@ function createErrorHint(command, error) {
|
|
|
435
446
|
|| error.includes('manifest.json hash'))) {
|
|
436
447
|
return '先运行 ssh-release list --json 和 ssh-release doctor --json,确认 current、版本目录、manifest 和远端锁状态。';
|
|
437
448
|
}
|
|
449
|
+
if (error.includes('sshpass')) {
|
|
450
|
+
return '当前配置使用密码登录,本机需要安装 sshpass;macOS 可运行 brew install hudochenkov/sshpass/sshpass,Ubuntu/Debian 可运行 sudo apt-get install sshpass,Windows 和 CI 推荐改用私钥登录。';
|
|
451
|
+
}
|
|
438
452
|
if (command
|
|
439
453
|
&& command !== 'init'
|
|
440
454
|
&& (error.includes('Permission denied')
|
|
441
455
|
|| error.includes('Connection timed out')
|
|
442
456
|
|| error.includes('Could not resolve hostname')
|
|
443
|
-
|| error.includes('sshpass')
|
|
444
457
|
|| error.includes('SSH'))) {
|
|
445
458
|
return '先运行 ssh-release doctor 检查 SSH 连接、认证信息和远端目录权限。';
|
|
446
459
|
}
|
package/dist/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { access } from 'node:fs/promises';
|
|
2
2
|
import { loadConfigFile } from './config.js';
|
|
3
3
|
import { readRemoteLockStatus } from './lock.js';
|
|
4
|
+
import { runProcess } from './process.js';
|
|
4
5
|
import { shellQuote } from './remote.js';
|
|
5
6
|
export async function runDoctorFromFile(configPath, createClient) {
|
|
6
7
|
let config;
|
|
@@ -32,8 +33,25 @@ export async function runDoctor(config, client, options = {}) {
|
|
|
32
33
|
message: '配置字段有效',
|
|
33
34
|
});
|
|
34
35
|
await addSourcePathCheck(checks, config.source.path);
|
|
36
|
+
await addLocalDependencyChecks(checks, config, options.localCommandExists ?? defaultLocalCommandExists);
|
|
37
|
+
if (checks.some((check) => check.status === 'fail')) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
checks,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
35
43
|
await addRemoteCheck(checks, 'SSH 连接', () => client.exec('true'), 'SSH 可连接');
|
|
36
44
|
await addRemoteCheck(checks, '远程目录', () => client.exec(`mkdir -p ${shellQuote(config.target.path)} && test -w ${shellQuote(config.target.path)}`), '远程目录可创建且可写');
|
|
45
|
+
await addRemoteCheck(checks, '远端 hash', () => client.exec([
|
|
46
|
+
'if command -v sha256sum >/dev/null 2>&1; then',
|
|
47
|
+
'command -v sha256sum;',
|
|
48
|
+
'elif command -v shasum >/dev/null 2>&1; then',
|
|
49
|
+
'command -v shasum;',
|
|
50
|
+
'else',
|
|
51
|
+
'echo "远端缺少 sha256sum 或 shasum" >&2;',
|
|
52
|
+
'exit 1;',
|
|
53
|
+
'fi',
|
|
54
|
+
].join(' ')), '远端 sha256sum 或 shasum 可用');
|
|
37
55
|
await addRemoteLockCheck(checks, config, client);
|
|
38
56
|
try {
|
|
39
57
|
await client.exec('command -v tar');
|
|
@@ -127,6 +145,35 @@ async function addSourcePathCheck(checks, sourcePath) {
|
|
|
127
145
|
});
|
|
128
146
|
}
|
|
129
147
|
}
|
|
148
|
+
async function addLocalDependencyChecks(checks, config, localCommandExists) {
|
|
149
|
+
await addLocalCommandCheck(checks, 'tar', '本地 tar', localCommandExists);
|
|
150
|
+
await addLocalCommandCheck(checks, 'ssh', '本地 ssh', localCommandExists);
|
|
151
|
+
await addLocalCommandCheck(checks, 'scp', '本地 scp', localCommandExists);
|
|
152
|
+
if (config.server.password) {
|
|
153
|
+
await addLocalCommandCheck(checks, 'sshpass', '本地 sshpass', localCommandExists, '本地 sshpass 不可用;密码登录需要安装 sshpass,或改用私钥登录');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function addLocalCommandCheck(checks, command, name, localCommandExists, missingMessage = `${name} 不可用;请安装后重试`) {
|
|
157
|
+
const exists = await localCommandExists(command);
|
|
158
|
+
checks.push({
|
|
159
|
+
name,
|
|
160
|
+
status: exists ? 'pass' : 'fail',
|
|
161
|
+
message: exists ? `${name} 可用` : missingMessage,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
async function defaultLocalCommandExists(command) {
|
|
165
|
+
try {
|
|
166
|
+
if (process.platform === 'win32') {
|
|
167
|
+
await runProcess('where.exe', [command]);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
await runProcess('sh', ['-c', `command -v ${shellQuote(command)} >/dev/null 2>&1`]);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
130
177
|
async function addRemoteCheck(checks, name, run, successMessage) {
|
|
131
178
|
try {
|
|
132
179
|
await run();
|