ssh-release 0.6.0 → 0.8.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 +12 -0
- package/README.md +14 -2
- package/dist/cli.js +6 -0
- package/dist/release.js +63 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 - 2026-06-25
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- 增加 GitHub Actions 发布模板文档,覆盖 Pull Request 预检、生产发布、`--json --progress` 输出、环境保护和 Secrets 使用方式。
|
|
8
|
+
|
|
9
|
+
## 0.7.0 - 2026-06-25
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- 增加发布后远端状态校验,`deploy` 成功后会确认目标目录、`current` 指向和远端锁清理状态,并在结果中输出 `verified` 与 `verification`。
|
|
14
|
+
|
|
3
15
|
## 0.6.0 - 2026-06-25
|
|
4
16
|
|
|
5
17
|
### Added
|
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
- `ssh-release unlock`:查看远端锁,并在显式确认锁路径后删除锁。
|
|
19
19
|
- `--json`:输出单行 JSON,便于 CI/CD 解析。
|
|
20
20
|
- `deploy --json --progress`:发布时输出 NDJSON 阶段进度,便于 CI/CD 展示实时状态。
|
|
21
|
+
- 发布后远端校验:确认版本目录或目标目录存在、`current` 已指向新版本、远端锁已清理。
|
|
21
22
|
- `release` 模式:上传压缩包、远端解压、切换 `current`、清理旧版本。
|
|
22
23
|
- `overwrite` 模式:直接覆盖发布到目标目录。
|
|
23
24
|
- 远端 `tar` 解压失败时回退逐文件上传。
|
|
@@ -188,6 +189,8 @@ ssh-release deploy
|
|
|
188
189
|
|
|
189
190
|
`release` 模式发布成功后会输出版本号、版本目录和 `current` 软链接路径。只有压缩包上传和解压完成后才切换 `current`。
|
|
190
191
|
|
|
192
|
+
发布命令返回成功前会执行远端状态校验。`release` 模式会确认新版本目录存在、`current` 已指向新版本、远端锁已清理;`overwrite` 模式会确认目标目录存在、远端锁已清理。校验失败时发布命令返回失败,不会把结果标记为成功。
|
|
193
|
+
|
|
191
194
|
如果远端已有 `.ssh-release.lock`,说明同一目标目录可能正在发布或回滚,工具会停止在修改远端状态之前。
|
|
192
195
|
|
|
193
196
|
查看锁状态:
|
|
@@ -242,7 +245,7 @@ ssh-release unlock --json
|
|
|
242
245
|
成功时输出单行 JSON:
|
|
243
246
|
|
|
244
247
|
```json
|
|
245
|
-
{"ok":true,"command":"deploy","result":{"
|
|
248
|
+
{"ok":true,"command":"deploy","result":{"mode":"release","version":"20260625-153000","verified":true}}
|
|
246
249
|
```
|
|
247
250
|
|
|
248
251
|
命令执行失败时输出:
|
|
@@ -262,11 +265,17 @@ ssh-release deploy --json --progress
|
|
|
262
265
|
```json
|
|
263
266
|
{"ok":true,"command":"deploy","event":"progress","stage":"package","status":"start"}
|
|
264
267
|
{"ok":true,"command":"deploy","event":"progress","stage":"package","status":"success"}
|
|
265
|
-
{"ok":true,"command":"deploy","result":{"mode":"release","version":"20260625-153000"}}
|
|
268
|
+
{"ok":true,"command":"deploy","result":{"mode":"release","version":"20260625-153000","verified":true}}
|
|
266
269
|
```
|
|
267
270
|
|
|
268
271
|
`stage` 可能是 `source`、`lock`、`package`、`publish`、`cleanup`,`status` 可能是 `start`、`success`、`fail`。失败事件会包含 `error` 字段。
|
|
269
272
|
|
|
273
|
+
发布结果中的 `verification` 会列出已通过的远端校验项:
|
|
274
|
+
|
|
275
|
+
```json
|
|
276
|
+
{"verified":true,"verification":[{"name":"当前版本","status":"pass","message":"current 已指向新版本"}]}
|
|
277
|
+
```
|
|
278
|
+
|
|
270
279
|
`doctor` 检查失败、`unlock` 发现锁但未删除时会返回非零退出码,并在 JSON 的 `result` 中保留检查结果。
|
|
271
280
|
|
|
272
281
|
## 安全边界
|
|
@@ -354,6 +363,8 @@ npm install -g "$PACK_DIR"/ssh-release-"$VERSION".tgz --prefix "$PACK_DIR/prefix
|
|
|
354
363
|
|
|
355
364
|
完整发布步骤见 [docs/release-checklist.md](https://github.com/JackEngineer/ssh-release/blob/main/docs/release-checklist.md)。
|
|
356
365
|
|
|
366
|
+
GitHub Actions 发布模板见 [docs/github-actions.md](https://github.com/JackEngineer/ssh-release/blob/main/docs/github-actions.md)。
|
|
367
|
+
|
|
357
368
|
版本变更见 [CHANGELOG.md](https://github.com/JackEngineer/ssh-release/blob/main/CHANGELOG.md)。
|
|
358
369
|
|
|
359
370
|
## 项目结构
|
|
@@ -394,6 +405,7 @@ tests/
|
|
|
394
405
|
└── validate.test.ts
|
|
395
406
|
|
|
396
407
|
docs/
|
|
408
|
+
├── github-actions.md
|
|
397
409
|
├── release-checklist.md
|
|
398
410
|
└── superpowers/specs/2026-06-25-ssh-release-design.md
|
|
399
411
|
```
|
package/dist/cli.js
CHANGED
|
@@ -268,6 +268,12 @@ function printDeployResult(result, io) {
|
|
|
268
268
|
if (result.usedFallback) {
|
|
269
269
|
io.log('远端 tar 不可用或解压失败,已使用逐文件上传');
|
|
270
270
|
}
|
|
271
|
+
if (result.verified) {
|
|
272
|
+
io.log('远端校验通过');
|
|
273
|
+
for (const check of result.verification ?? []) {
|
|
274
|
+
io.log(`校验: ${check.name} - ${check.message}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
271
277
|
for (const warning of result.warnings) {
|
|
272
278
|
io.log(`警告: ${warning}`);
|
|
273
279
|
}
|
package/dist/release.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { access } from 'node:fs/promises';
|
|
2
2
|
import { createReleasePackage, } from './package.js';
|
|
3
|
-
import { acquireRemoteLock } from './lock.js';
|
|
3
|
+
import { acquireRemoteLock, readRemoteLockStatus } from './lock.js';
|
|
4
4
|
import { readRemoteReleaseNames, remoteJoin, shellQuote, } from './remote.js';
|
|
5
5
|
export function createVersionName(date = new Date()) {
|
|
6
6
|
return [
|
|
@@ -75,6 +75,10 @@ export async function deploy(config, client, options = {}) {
|
|
|
75
75
|
cleanupError = error;
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
+
if (result && !deployError && !cleanupError) {
|
|
79
|
+
result.verification = await verifyDeployResult(config, client, result);
|
|
80
|
+
result.verified = true;
|
|
81
|
+
}
|
|
78
82
|
if (cleanupError && !deployError) {
|
|
79
83
|
throw cleanupError;
|
|
80
84
|
}
|
|
@@ -182,6 +186,64 @@ async function cleanupOldReleases(config, client, releasesPath, currentVersion,
|
|
|
182
186
|
warnings.push(`旧版本清理失败: ${formatError(error)}`);
|
|
183
187
|
}
|
|
184
188
|
}
|
|
189
|
+
async function verifyDeployResult(config, client, result) {
|
|
190
|
+
if (result.mode === 'overwrite') {
|
|
191
|
+
return [
|
|
192
|
+
await verifyRemoteDirectory(client, result.targetPath, '目标目录', '远端目标目录存在'),
|
|
193
|
+
await verifyRemoteLockReleased(config, client),
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
if (!result.version || !result.currentSymlink) {
|
|
197
|
+
throw new Error('release 发布结果缺少版本号或 current 路径,无法校验远端状态');
|
|
198
|
+
}
|
|
199
|
+
return [
|
|
200
|
+
await verifyRemoteDirectory(client, result.targetPath, '版本目录', '远端版本目录存在'),
|
|
201
|
+
await verifyCurrentSymlink(client, result.currentSymlink, remoteJoin(config.target.releasesDir, result.version)),
|
|
202
|
+
await verifyRemoteLockReleased(config, client),
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
async function verifyRemoteDirectory(client, remotePath, name, successMessage) {
|
|
206
|
+
try {
|
|
207
|
+
await client.exec(`test -d ${shellQuote(remotePath)}`);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
throw new Error(`${name}校验失败: ${formatError(error)}`);
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
name,
|
|
214
|
+
status: 'pass',
|
|
215
|
+
message: successMessage,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
async function verifyCurrentSymlink(client, currentSymlinkPath, expectedTarget) {
|
|
219
|
+
let actualTarget;
|
|
220
|
+
try {
|
|
221
|
+
const result = await client.exec(`readlink ${shellQuote(currentSymlinkPath)}`);
|
|
222
|
+
actualTarget = result.stdout.trim();
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
throw new Error(`current 校验失败: ${formatError(error)}`);
|
|
226
|
+
}
|
|
227
|
+
if (actualTarget !== expectedTarget) {
|
|
228
|
+
throw new Error(`current 未指向新版本: 期望 ${expectedTarget},实际 ${actualTarget || '空'}`);
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
name: '当前版本',
|
|
232
|
+
status: 'pass',
|
|
233
|
+
message: 'current 已指向新版本',
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
async function verifyRemoteLockReleased(config, client) {
|
|
237
|
+
const lockStatus = await readRemoteLockStatus(config, client);
|
|
238
|
+
if (lockStatus.locked) {
|
|
239
|
+
throw new Error(`发布锁未清理: ${lockStatus.lockPath}`);
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
name: '远端锁',
|
|
243
|
+
status: 'pass',
|
|
244
|
+
message: '发布锁已清理',
|
|
245
|
+
};
|
|
246
|
+
}
|
|
185
247
|
async function ensureSourceExists(sourcePath) {
|
|
186
248
|
try {
|
|
187
249
|
await access(sourcePath);
|