@kne/fastify-app-manager 0.1.14 → 0.1.15
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 +34 -3
- package/index.js +6 -0
- package/libs/controllers/app.js +55 -3
- package/libs/models/appDeploy.js +71 -0
- package/libs/services/app.js +180 -18
- package/libs/services/bootstrap.js +15 -2
- package/libs/utils/deployProgress.js +99 -9
- package/libs/utils/version.js +7 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,6 +126,7 @@ deploy(versionId)
|
|
|
126
126
|
| 版本上传 | Zip Slip 防护、条目数/体积上限、解压丢弃任意层级 `node_modules`、Fullstack 包格式校验、server 生产依赖安装 |
|
|
127
127
|
| 短部署 | `deploy` / `start` / `restart` 立即返回 `deploying`,后台按 PM2 真实状态 + healthCheck 写回 `running`/`error`/`stopped` |
|
|
128
128
|
| 部署进度 | `deploy` 按「准备部署包 → 校验环境变量 → SQL 迁移 → 启动进程 → 等待就绪」上报阶段,经 `GET .../app/deploy/stream` SSE 推送(含下载 / 解压 / 安装依赖等子阶段) |
|
|
129
|
+
| 部署记录 | 每次部署(含父应用启动时补回部署目录)落库:状态 `running`/`success`/`failed`、阶段事件、部署日志(依赖安装输出、就绪失败时进程 `err.log` 末尾);父应用重启后未结束的标为中断;`GET .../app/deploy/list`、`.../app/deploy/detail` 查询 |
|
|
129
130
|
| 生命周期 | `start` / `stop` / `restart` / `remove`;父应用重启后异步 `syncAllStatuses` 对齐 PM2 |
|
|
130
131
|
| 双通道访问 | 自定义域名透明反代 + `/app/{name}` 剥前缀反代 |
|
|
131
132
|
| 日志 | PM2 落盘 + 历史分页(`beforeLine` 上滚、每页最多 100 行)+ SSE(含回放行与心跳) |
|
|
@@ -213,6 +214,9 @@ await fastify.listen({ port: 3000 });
|
|
|
213
214
|
| `maxZipSize` | number | 否 | `209715200` | zip 最大字节(200MB);同步 multipart 限制 |
|
|
214
215
|
| `maxZipEntries` | number | 否 | `20000` | zip 最大条目数(不含被丢弃的 `node_modules` 条目) |
|
|
215
216
|
| `npmInstallTimeoutMs` | number | 否 | `600000` | `server` 目录 `npm install` 超时 |
|
|
217
|
+
| `deployRecordKeep` | number | 否 | `20` | 每个应用保留的部署记录条数 |
|
|
218
|
+
| `deployLogMaxLines` | number | 否 | `2000` | 单次部署日志最多保留行数(超出丢弃最早的) |
|
|
219
|
+
| `deployErrorLogTailLines` | number | 否 | `50` | 就绪失败时附加到部署日志的进程 `err.log` 末尾行数 |
|
|
216
220
|
| `sseReplayLines` | number | 否 | `100` | SSE 连接时回放最近行数 |
|
|
217
221
|
| `sseHeartbeatMs` | number | 否 | `15000` | SSE 心跳间隔 |
|
|
218
222
|
| `logMaxSize` | number | 否 | `52428800` | 单日志文件上限(50MB),达到即切分;`0` 关闭按大小切分 |
|
|
@@ -459,7 +463,7 @@ await fastify.listen({ port: 3000 });
|
|
|
459
463
|
|
|
460
464
|
##### GET `{prefix}/app/deploy/stream`
|
|
461
465
|
|
|
462
|
-
SSE 部署进度。连接时先推送 `snapshot
|
|
466
|
+
SSE 部署进度。连接时先推送 `snapshot`(该次部署已有事件,便于断线重连;内存中没有时从部署记录回放,`deploy` 为该部署记录含最近 `sseReplayLines` 行日志,无记录为 `null`),之后每个阶段变化推送 `progress`,每行部署日志推送 `log`(`{ progressId, line }`)。
|
|
463
467
|
|
|
464
468
|
| 参数名 | 类型 | 必填 | 说明 |
|
|
465
469
|
|--------|------|------|------|
|
|
@@ -480,7 +484,7 @@ SSE 部署进度。连接时先推送 `snapshot`(该应用最近一次部署
|
|
|
480
484
|
|
|
481
485
|
```text
|
|
482
486
|
event: snapshot
|
|
483
|
-
data: {"steps":["package","env","migrate","start","ready"],"events":[]}
|
|
487
|
+
data: {"steps":["package","env","migrate","start","ready"],"events":[],"deploy":null}
|
|
484
488
|
|
|
485
489
|
event: progress
|
|
486
490
|
data: {"progressId":"p-1","step":"package","status":"running","detail":"download","message":null,"at":"2026-10-10T10:00:00.000Z"}
|
|
@@ -489,6 +493,27 @@ event: progress
|
|
|
489
493
|
data: {"progressId":"p-1","step":"ready","status":"done","detail":null,"message":null,"at":"2026-10-10T10:00:05.000Z"}
|
|
490
494
|
```
|
|
491
495
|
|
|
496
|
+
##### 部署记录
|
|
497
|
+
|
|
498
|
+
每次部署(含父应用启动时补回部署目录,`source: 'restore'`)都会落库,每个应用保留最近 `deployRecordKeep` 条。父应用重启时,未结束的记录标为 `failed`(`message: deploy interrupted: app manager restarted`)。应用详情返回 `deployment`(进行中的部署,无则 `null`)与 `lastDeployment`(最近一次部署记录摘要)。
|
|
499
|
+
|
|
500
|
+
| 方法 | 路径 | 参数 | 说明 |
|
|
501
|
+
|------|------|------|------|
|
|
502
|
+
| GET | `{prefix}/app/deploy/list` | `name`, `perPage?`, `currentPage?` | 部署记录分页(不含日志与事件),按开始时间倒序 |
|
|
503
|
+
| GET | `{prefix}/app/deploy/detail` | `name`, `progressId?`, `logLines?` | 部署记录详情;`progressId` 为空取最近一次;`logLines` 只取最后 N 行(`0` 全部) |
|
|
504
|
+
|
|
505
|
+
部署记录字段:
|
|
506
|
+
|
|
507
|
+
| 字段 | 说明 |
|
|
508
|
+
|------|------|
|
|
509
|
+
| `progressId` / `versionId` / `version` | 部署标识与版本 |
|
|
510
|
+
| `source` | `deploy` 手动部署 / `restore` 启动时补回部署目录 |
|
|
511
|
+
| `status` | `running` / `success` / `failed` |
|
|
512
|
+
| `step` / `message` | 最后到达的阶段 / 失败原因 |
|
|
513
|
+
| `startedAt` / `finishedAt` | 起止时间 |
|
|
514
|
+
| `events` | 阶段事件(仅详情) |
|
|
515
|
+
| `log` / `logTotalLines` | 部署日志:各阶段结果、依赖安装输出、就绪失败时进程 `err.log` 末尾(仅详情) |
|
|
516
|
+
|
|
492
517
|
##### 版本迁移脚本管理
|
|
493
518
|
|
|
494
519
|
管理某版本的 `.sql` 文件。脚本内容存于版本记录(数据库),版本目录不在磁盘时也可查看和编辑;目录在磁盘上(当前版本)时同步写入 `server/{migrationPath}`,部署时以数据库为准覆盖。执行状态读写应用库的 `_fs_sql_migrations`(与 `@kne/fastify-sequelize` 共用,按文件名记账;共享库中不同应用的同名脚本会互相影响)。文件名须匹配 `[A-Za-z0-9_-.]+\.sql`。
|
|
@@ -547,6 +572,8 @@ data: {"progressId":"p-1","step":"ready","status":"done","detail":null,"message"
|
|
|
547
572
|
|--------|------|------|------|
|
|
548
573
|
| name | string | 是 | 应用 slug |
|
|
549
574
|
|
|
575
|
+
返回脱敏后的应用对象,另带 `deployment`:最近一次部署尚未结束(未出错、就绪阶段未完成)时为 `{ progressId, versionId, version, startedAt, step, detail }`,否则为 `null`。准备部署包阶段应用仍是旧的 `status` 与 `currentVersionId`,用它判断哪个版本正在部署。`app/list` 的每个元素同样带该字段。
|
|
576
|
+
|
|
550
577
|
##### POST `{prefix}/app/start` | `stop` | `restart` | `remove`
|
|
551
578
|
|
|
552
579
|
| 参数名 | 类型 | 必填 | 说明 |
|
|
@@ -731,7 +758,11 @@ data: {"name":"demo","ts":1791456608533,...}
|
|
|
731
758
|
| `restoreMissingArtifacts()` | 当前版本目录(`artifactPath/server`)缺失时,按版本 `fileId` 经 `resolveVersionFile` 取回 zip 重建并恢复 `current` 链接(不跑 SQL 迁移);返回 `[{ name, version, action: 'restored' \| 'skipped' \| 'failed', reason? }]`,无 `fileId` 的记 `skipped` |
|
|
732
759
|
| `listVersions({ name, perPage, currentPage })` | 版本列表 |
|
|
733
760
|
| `deploy({ name, versionId, version, runMigration, migrations?, progressId? })` | 部署;版本目录缺失时先从宿主文件重建,启动后清理其它版本目录;立即 `deploying`,后台 `syncRealStatus`;各阶段进度写入 `getDeployProgress()` |
|
|
734
|
-
| `getDeployProgress()` | 部署进度中心:`emitter`(事件名 `deploy:{name}`)、`snapshot(name)` 返回最近一次部署 `{ progressId, events }` |
|
|
761
|
+
| `getDeployProgress()` | 部署进度中心:`emitter`(事件名 `deploy:{name}`、日志 `deploy-log:{name}`)、`snapshot(name)` 返回最近一次部署 `{ progressId, status, events, ... }`、`active(name)` 进行中的部署 |
|
|
762
|
+
| `deployList({ name, perPage?, currentPage? })` | 同 `GET .../app/deploy/list` |
|
|
763
|
+
| `deployDetail({ name, progressId?, logLines? })` | 同 `GET .../app/deploy/detail` |
|
|
764
|
+
| `lastDeployment(name)` | 最近一次部署记录摘要(无日志),无记录为 `null` |
|
|
765
|
+
| `markInterruptedDeploys()` | 把库里仍 `running`、但本进程未在跟踪的部署记录标为 `failed`(启动时自动调用),返回条数 |
|
|
735
766
|
| `waitDeployProgress({ name, progressId, timeoutMs? })` | 等到该次部署出错或 `ready` 结束(默认超时 120000ms),返回 `{ progressId, ok, timedOut, events }`;供 MCP 等同步调用方使用 |
|
|
736
767
|
| `getEnvDeclaration({ name, versionId? })` | 同 `GET .../app/env/schema` |
|
|
737
768
|
| `listInjectedEnvKeys(app)` | 应用中心注入(或在其它页签维护)的变量名 `Set`,环境变量声明中不展示 |
|
package/index.js
CHANGED
|
@@ -23,6 +23,12 @@ module.exports = fp(
|
|
|
23
23
|
maxZipSize: 200 * 1024 * 1024,
|
|
24
24
|
maxZipEntries: 20000,
|
|
25
25
|
npmInstallTimeoutMs: 10 * 60 * 1000,
|
|
26
|
+
/** 每个应用保留的部署记录条数 */
|
|
27
|
+
deployRecordKeep: 20,
|
|
28
|
+
/** 单次部署日志最多保留的行数(超出丢弃最早的) */
|
|
29
|
+
deployLogMaxLines: 2000,
|
|
30
|
+
/** 就绪失败时附加到部署日志的进程错误日志行数 */
|
|
31
|
+
deployErrorLogTailLines: 50,
|
|
26
32
|
logRetentionMaxRows: 10000,
|
|
27
33
|
sseReplayLines: 100,
|
|
28
34
|
sseHeartbeatMs: 15000,
|
package/libs/controllers/app.js
CHANGED
|
@@ -277,25 +277,77 @@ module.exports = fp(async (fastify, options) => {
|
|
|
277
277
|
async (request, reply) => {
|
|
278
278
|
const { name, progressId } = request.query;
|
|
279
279
|
await services.app.detail({ name });
|
|
280
|
-
const sse = createSseReply(reply, { heartbeatMs: options.sseHeartbeatMs });
|
|
281
280
|
const progress = services.app.getDeployProgress();
|
|
282
281
|
const matches = event => !progressId || event.progressId === progressId;
|
|
283
|
-
const
|
|
282
|
+
const memory = progress.snapshot(name);
|
|
283
|
+
let record = null;
|
|
284
|
+
if (progressId && (!memory || memory.progressId !== progressId)) {
|
|
285
|
+
// 不在本进程内(已结束被新部署覆盖或主进程重启过):从部署记录回放
|
|
286
|
+
record = await services.app.deployDetail({ name, progressId, logLines: options.sseReplayLines }).catch(() => null);
|
|
287
|
+
} else if (memory) {
|
|
288
|
+
record = await services.app.deployDetail({ name, progressId: memory.progressId, logLines: options.sseReplayLines }).catch(() => null);
|
|
289
|
+
}
|
|
290
|
+
const sse = createSseReply(reply, { heartbeatMs: options.sseHeartbeatMs });
|
|
284
291
|
sse.writeEvent('snapshot', {
|
|
285
292
|
steps: DEPLOY_STEPS,
|
|
286
|
-
events:
|
|
293
|
+
events: record ? (record.events || []).filter(matches) : memory ? memory.events.filter(matches) : [],
|
|
294
|
+
deploy: record ? Object.assign({}, record, { events: undefined }) : null
|
|
287
295
|
});
|
|
288
296
|
|
|
289
297
|
const onProgress = event => matches(event) && sse.writeEvent('progress', event);
|
|
298
|
+
const onLog = event => matches(event) && sse.writeEvent('log', event);
|
|
290
299
|
const eventName = `deploy:${name}`;
|
|
300
|
+
const logEventName = `deploy-log:${name}`;
|
|
291
301
|
progress.emitter.on(eventName, onProgress);
|
|
302
|
+
progress.emitter.on(logEventName, onLog);
|
|
292
303
|
request.raw.on('close', () => {
|
|
293
304
|
progress.emitter.off(eventName, onProgress);
|
|
305
|
+
progress.emitter.off(logEventName, onLog);
|
|
294
306
|
sse.close();
|
|
295
307
|
});
|
|
296
308
|
}
|
|
297
309
|
);
|
|
298
310
|
|
|
311
|
+
fastify.get(
|
|
312
|
+
`${options.prefix}/app/deploy/list`,
|
|
313
|
+
{
|
|
314
|
+
onRequest: auth(),
|
|
315
|
+
schema: {
|
|
316
|
+
summary: '部署记录列表(不含日志)',
|
|
317
|
+
query: {
|
|
318
|
+
type: 'object',
|
|
319
|
+
required: ['name'],
|
|
320
|
+
properties: {
|
|
321
|
+
name: { type: 'string' },
|
|
322
|
+
perPage: { type: 'number', default: 20 },
|
|
323
|
+
currentPage: { type: 'number', default: 1 }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
async request => services.app.deployList(request.query)
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
fastify.get(
|
|
332
|
+
`${options.prefix}/app/deploy/detail`,
|
|
333
|
+
{
|
|
334
|
+
onRequest: auth(),
|
|
335
|
+
schema: {
|
|
336
|
+
summary: '部署记录详情(含阶段事件与部署日志);progressId 为空取最近一次',
|
|
337
|
+
query: {
|
|
338
|
+
type: 'object',
|
|
339
|
+
required: ['name'],
|
|
340
|
+
properties: {
|
|
341
|
+
name: { type: 'string' },
|
|
342
|
+
progressId: { type: 'string' },
|
|
343
|
+
logLines: { type: 'number', default: 0, description: '只返回最后 N 行日志;0 返回全部' }
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
async request => services.app.deployDetail(request.query)
|
|
349
|
+
);
|
|
350
|
+
|
|
299
351
|
fastify.get(
|
|
300
352
|
`${options.prefix}/app/list`,
|
|
301
353
|
{
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module.exports = ({ DataTypes }) => {
|
|
2
|
+
return {
|
|
3
|
+
name: 'appDeploy',
|
|
4
|
+
model: {
|
|
5
|
+
appName: {
|
|
6
|
+
type: DataTypes.STRING,
|
|
7
|
+
allowNull: false,
|
|
8
|
+
comment: '所属应用 name'
|
|
9
|
+
},
|
|
10
|
+
progressId: {
|
|
11
|
+
type: DataTypes.STRING,
|
|
12
|
+
allowNull: false,
|
|
13
|
+
comment: '部署进度 id,与 SSE 进度订阅一致'
|
|
14
|
+
},
|
|
15
|
+
versionId: {
|
|
16
|
+
type: DataTypes.STRING,
|
|
17
|
+
allowNull: true,
|
|
18
|
+
comment: '部署的版本 id'
|
|
19
|
+
},
|
|
20
|
+
version: {
|
|
21
|
+
type: DataTypes.STRING,
|
|
22
|
+
allowNull: true,
|
|
23
|
+
comment: '部署的版本号'
|
|
24
|
+
},
|
|
25
|
+
source: {
|
|
26
|
+
type: DataTypes.STRING,
|
|
27
|
+
allowNull: false,
|
|
28
|
+
defaultValue: 'deploy',
|
|
29
|
+
comment: 'deploy 手动部署 | restore 启动时补回部署目录'
|
|
30
|
+
},
|
|
31
|
+
status: {
|
|
32
|
+
type: DataTypes.STRING,
|
|
33
|
+
allowNull: false,
|
|
34
|
+
defaultValue: 'running',
|
|
35
|
+
comment: 'running | success | failed'
|
|
36
|
+
},
|
|
37
|
+
step: {
|
|
38
|
+
type: DataTypes.STRING,
|
|
39
|
+
allowNull: true,
|
|
40
|
+
comment: '最后到达的阶段'
|
|
41
|
+
},
|
|
42
|
+
message: {
|
|
43
|
+
type: DataTypes.TEXT,
|
|
44
|
+
allowNull: true,
|
|
45
|
+
comment: '失败原因'
|
|
46
|
+
},
|
|
47
|
+
events: {
|
|
48
|
+
type: DataTypes.JSON,
|
|
49
|
+
allowNull: true,
|
|
50
|
+
comment: '进度事件 [{ step, status, detail, message, at }]'
|
|
51
|
+
},
|
|
52
|
+
log: {
|
|
53
|
+
type: DataTypes.TEXT,
|
|
54
|
+
allowNull: true,
|
|
55
|
+
comment: '部署日志(依赖安装输出、各阶段结果、启动失败时的进程错误日志)'
|
|
56
|
+
},
|
|
57
|
+
startedAt: {
|
|
58
|
+
type: DataTypes.DATE,
|
|
59
|
+
allowNull: true
|
|
60
|
+
},
|
|
61
|
+
finishedAt: {
|
|
62
|
+
type: DataTypes.DATE,
|
|
63
|
+
allowNull: true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
options: {
|
|
67
|
+
comment: '应用部署记录',
|
|
68
|
+
indexes: [{ fields: ['app_name'] }, { unique: true, fields: ['progress_id'] }]
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
};
|
package/libs/services/app.js
CHANGED
|
@@ -265,7 +265,7 @@ module.exports = fp(async (fastify, options) => {
|
|
|
265
265
|
|
|
266
266
|
const detail = async ({ name }) => {
|
|
267
267
|
const app = await getByName(name);
|
|
268
|
-
return Object.assign(toPublicApp(app), { deployment: deployProgress.active(app.name) });
|
|
268
|
+
return Object.assign(toPublicApp(app), { deployment: deployProgress.active(app.name), lastDeployment: await lastDeployment(app.name) });
|
|
269
269
|
};
|
|
270
270
|
|
|
271
271
|
const versionSqlDir = ver => path.join(ver.artifactPath, 'server', ver.migrationPath || options.sqlPath || 'sql');
|
|
@@ -486,7 +486,150 @@ module.exports = fp(async (fastify, options) => {
|
|
|
486
486
|
|
|
487
487
|
/** 同一 app 并发同步合并为一次,避免 restart/reconcile 竞态 */
|
|
488
488
|
const statusSyncInflight = new Map();
|
|
489
|
-
|
|
489
|
+
|
|
490
|
+
const pruneDeployRecords = async appName => {
|
|
491
|
+
const stale = await models.appDeploy.findAll({
|
|
492
|
+
where: { appName },
|
|
493
|
+
attributes: ['id'],
|
|
494
|
+
order: [['createdAt', 'DESC']],
|
|
495
|
+
offset: options.deployRecordKeep
|
|
496
|
+
});
|
|
497
|
+
if (stale.length) {
|
|
498
|
+
await models.appDeploy.destroy({ where: { id: stale.map(row => row.id) } });
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const persistDeploy = async state => {
|
|
503
|
+
const values = {
|
|
504
|
+
appName: state.name,
|
|
505
|
+
progressId: state.progressId,
|
|
506
|
+
versionId: state.versionId,
|
|
507
|
+
version: state.version,
|
|
508
|
+
source: state.source,
|
|
509
|
+
status: state.status,
|
|
510
|
+
step: state.step,
|
|
511
|
+
message: state.message,
|
|
512
|
+
events: state.events,
|
|
513
|
+
log: state.logs.join('\n'),
|
|
514
|
+
startedAt: state.startedAt,
|
|
515
|
+
finishedAt: state.finishedAt
|
|
516
|
+
};
|
|
517
|
+
const record = await models.appDeploy.findOne({ where: { progressId: state.progressId } });
|
|
518
|
+
if (record) {
|
|
519
|
+
await record.update(values);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
await models.appDeploy.create(values);
|
|
523
|
+
await pruneDeployRecords(state.name);
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const deployProgress = createDeployProgress({
|
|
527
|
+
persist: persistDeploy,
|
|
528
|
+
logMaxLines: options.deployLogMaxLines,
|
|
529
|
+
onPersistError: (err, state) => fastify.log.warn({ err, app: state.name, progressId: state.progressId }, 'persist deploy record failed')
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const tailLines = (lines, n) => (n > 0 && lines.length > n ? lines.slice(-n) : lines);
|
|
533
|
+
|
|
534
|
+
/** 部署记录:进程内仍在跟踪的以内存为准(日志更新),否则读库 */
|
|
535
|
+
const toDeployRecord = (row, { withLog = false, logLines = 0 } = {}) => {
|
|
536
|
+
const memory = deployProgress.snapshot(row.appName);
|
|
537
|
+
const live = memory && memory.progressId === row.progressId ? memory : null;
|
|
538
|
+
const base = live || {
|
|
539
|
+
name: row.appName,
|
|
540
|
+
progressId: row.progressId,
|
|
541
|
+
versionId: row.versionId,
|
|
542
|
+
version: row.version,
|
|
543
|
+
source: row.source,
|
|
544
|
+
status: row.status,
|
|
545
|
+
step: row.step,
|
|
546
|
+
message: row.message,
|
|
547
|
+
startedAt: row.startedAt ? new Date(row.startedAt).toISOString() : null,
|
|
548
|
+
finishedAt: row.finishedAt ? new Date(row.finishedAt).toISOString() : null,
|
|
549
|
+
events: Array.isArray(row.events) ? row.events : []
|
|
550
|
+
};
|
|
551
|
+
const record = Object.assign({}, base, { name: row.appName });
|
|
552
|
+
if (!withLog) {
|
|
553
|
+
delete record.events;
|
|
554
|
+
return record;
|
|
555
|
+
}
|
|
556
|
+
const lines = live ? deployProgress.logsOf(row.appName, row.progressId) || [] : row.log ? row.log.split('\n') : [];
|
|
557
|
+
return Object.assign(record, { logTotalLines: lines.length, log: tailLines(lines, logLines).join('\n') });
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const deployList = async ({ name, perPage = 20, currentPage = 1 } = {}) => {
|
|
561
|
+
const app = await getByName(name);
|
|
562
|
+
const { rows, count } = await models.appDeploy.findAndCountAll({
|
|
563
|
+
where: { appName: app.name },
|
|
564
|
+
attributes: { exclude: ['log', 'events'] },
|
|
565
|
+
offset: perPage * (currentPage - 1),
|
|
566
|
+
limit: perPage,
|
|
567
|
+
order: [['createdAt', 'DESC']]
|
|
568
|
+
});
|
|
569
|
+
return { pageData: rows.map(row => toDeployRecord(row)), totalCount: count };
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
/** progressId 为空取最近一次;logLines 为 0 返回全部已保存日志 */
|
|
573
|
+
const deployDetail = async ({ name, progressId, logLines = 0 } = {}) => {
|
|
574
|
+
const app = await getByName(name);
|
|
575
|
+
const where = { appName: app.name };
|
|
576
|
+
if (progressId) {
|
|
577
|
+
where.progressId = progressId;
|
|
578
|
+
}
|
|
579
|
+
let row = await models.appDeploy.findOne({ where, order: [['createdAt', 'DESC']] });
|
|
580
|
+
const memory = deployProgress.snapshot(app.name);
|
|
581
|
+
// 刚开始的部署可能还没落库
|
|
582
|
+
if (memory && (progressId ? memory.progressId === progressId : !row || row.progressId !== memory.progressId)) {
|
|
583
|
+
row = { appName: app.name, progressId: memory.progressId };
|
|
584
|
+
}
|
|
585
|
+
if (!row) {
|
|
586
|
+
httpError(404, 'deploy record not found');
|
|
587
|
+
}
|
|
588
|
+
return toDeployRecord(row, { withLog: true, logLines: Number(logLines) || 0 });
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const lastDeployment = async name => {
|
|
592
|
+
const row = await models.appDeploy.findOne({ where: { appName: name }, attributes: { exclude: ['log', 'events'] }, order: [['createdAt', 'DESC']] });
|
|
593
|
+
return row ? toDeployRecord(row) : null;
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
/** 父进程重启后,内存里的部署进度已丢失:库里仍 running 的记录无法再结束,标记为中断 */
|
|
597
|
+
const markInterruptedDeploys = async () => {
|
|
598
|
+
const rows = await models.appDeploy.findAll({ where: { status: 'running' } });
|
|
599
|
+
for (const row of rows) {
|
|
600
|
+
if (deployProgress.active(row.appName)?.progressId === row.progressId) {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
const at = new Date();
|
|
604
|
+
const message = 'deploy interrupted: app manager restarted';
|
|
605
|
+
await row.update({
|
|
606
|
+
status: 'failed',
|
|
607
|
+
message,
|
|
608
|
+
finishedAt: at,
|
|
609
|
+
events: (Array.isArray(row.events) ? row.events : []).concat({ progressId: row.progressId, step: row.step || 'package', status: 'error', detail: null, message, at: at.toISOString() }),
|
|
610
|
+
log: [row.log, `${at.toISOString()} ${message}`].filter(Boolean).join('\n')
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
return rows.length;
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
/** 就绪阶段收尾:失败时把进程错误日志末尾写进部署日志,便于定位启动失败 */
|
|
617
|
+
const settleDeployReady = async (app, progress, result) => {
|
|
618
|
+
if (result?.status === 'running') {
|
|
619
|
+
progress.report('ready', 'done');
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
const lines = await readLastLines(currentLogFile(app, 'err'), options.deployErrorLogTailLines, { timezone: options.logTimezone });
|
|
624
|
+
if (lines.length) {
|
|
625
|
+
progress.log(`---- ${app.name} err.log (last ${lines.length} lines) ----`);
|
|
626
|
+
lines.forEach(item => progress.log(item.content));
|
|
627
|
+
}
|
|
628
|
+
} catch (e) {
|
|
629
|
+
progress.log(`read err.log failed: ${e.message}`);
|
|
630
|
+
}
|
|
631
|
+
progress.report('ready', 'error', { message: result?.message || result?.status || null });
|
|
632
|
+
};
|
|
490
633
|
|
|
491
634
|
/**
|
|
492
635
|
* 根据 PM2 真实进程状态 + HTTP 健康检查,异步写回 DB status。
|
|
@@ -840,7 +983,7 @@ module.exports = fp(async (fastify, options) => {
|
|
|
840
983
|
const canResolveFile = ver => !!ver.fileId && typeof options.resolveVersionFile === 'function';
|
|
841
984
|
|
|
842
985
|
/** 版本目录不在磁盘时按 fileId 取回 zip 解压并装依赖;再以数据库中的 SQL 覆盖目录内 .sql */
|
|
843
|
-
const materializeArtifact = async (app, ver, { onProgress } = {}) => {
|
|
986
|
+
const materializeArtifact = async (app, ver, { onProgress, onLog } = {}) => {
|
|
844
987
|
if (await hasArtifact(ver)) {
|
|
845
988
|
onProgress && onProgress('reuse');
|
|
846
989
|
} else {
|
|
@@ -860,7 +1003,8 @@ module.exports = fp(async (fastify, options) => {
|
|
|
860
1003
|
sqlPath: options.sqlPath,
|
|
861
1004
|
// 老版本可能没有 env.schema.json:照常还原目录,由启动前的环境变量校验给出提示
|
|
862
1005
|
requireEnvSchema: false,
|
|
863
|
-
onProgress
|
|
1006
|
+
onProgress,
|
|
1007
|
+
onLog
|
|
864
1008
|
});
|
|
865
1009
|
await fs.move(stagingPath, ver.artifactPath, { overwrite: true });
|
|
866
1010
|
} catch (e) {
|
|
@@ -901,20 +1045,27 @@ module.exports = fp(async (fastify, options) => {
|
|
|
901
1045
|
return results;
|
|
902
1046
|
};
|
|
903
1047
|
|
|
904
|
-
/** 补回当前版本目录并恢复 current 链接;不跑 SQL
|
|
905
|
-
const restoreArtifact = async ({ app, ver }) => {
|
|
1048
|
+
/** 补回当前版本目录并恢复 current 链接;不跑 SQL 迁移。progress 存在时记录到部署记录 */
|
|
1049
|
+
const restoreArtifact = async ({ app, ver, progress }) => {
|
|
906
1050
|
if (!canResolveFile(ver)) {
|
|
907
1051
|
fastify.log.warn({ app: app.name, version: ver.version }, 'current version artifact missing and cannot be restored without fileId');
|
|
1052
|
+
progress && progress.report('package', 'error', { message: 'current version artifact missing and cannot be restored without fileId' });
|
|
908
1053
|
return { name: app.name, version: ver.version, action: 'skipped', reason: 'no fileId' };
|
|
909
1054
|
}
|
|
910
1055
|
try {
|
|
911
|
-
|
|
1056
|
+
progress && progress.report('package', 'running');
|
|
1057
|
+
await materializeArtifact(app, ver, {
|
|
1058
|
+
onProgress: progress ? detail => progress.report('package', 'running', { detail }) : undefined,
|
|
1059
|
+
onLog: progress ? progress.log : undefined
|
|
1060
|
+
});
|
|
912
1061
|
await linkCurrent(app, ver.artifactPath);
|
|
1062
|
+
progress && progress.report('package', 'done');
|
|
913
1063
|
fastify.log.info({ app: app.name, version: ver.version }, 'current version artifact restored from file');
|
|
914
|
-
return { name: app.name, version: ver.version, action: 'restored' };
|
|
1064
|
+
return Object.assign({ name: app.name, version: ver.version, action: 'restored' }, progress ? { progressId: progress.progressId } : {});
|
|
915
1065
|
} catch (e) {
|
|
916
1066
|
fastify.log.warn({ err: e, app: app.name, version: ver.version }, 'restore current version artifact failed');
|
|
917
|
-
|
|
1067
|
+
progress && progress.report('package', 'error', { message: e.message });
|
|
1068
|
+
return Object.assign({ name: app.name, version: ver.version, action: 'failed', reason: e.message }, progress ? { progressId: progress.progressId } : {});
|
|
918
1069
|
}
|
|
919
1070
|
};
|
|
920
1071
|
|
|
@@ -942,7 +1093,7 @@ module.exports = fp(async (fastify, options) => {
|
|
|
942
1093
|
let step = 'package';
|
|
943
1094
|
progress.report(step, 'running');
|
|
944
1095
|
try {
|
|
945
|
-
await materializeArtifact(app, ver, { onProgress: detail => progress.report('package', 'running', { detail }) });
|
|
1096
|
+
await materializeArtifact(app, ver, { onProgress: detail => progress.report('package', 'running', { detail }), onLog: progress.log });
|
|
946
1097
|
} catch (e) {
|
|
947
1098
|
progress.report(step, 'error', { message: e.message });
|
|
948
1099
|
httpError(400, e.message);
|
|
@@ -962,15 +1113,15 @@ module.exports = fp(async (fastify, options) => {
|
|
|
962
1113
|
}
|
|
963
1114
|
progress.report(step, 'done');
|
|
964
1115
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1116
|
+
try {
|
|
1117
|
+
await app.update({
|
|
1118
|
+
status: 'deploying',
|
|
1119
|
+
currentVersionId: String(ver.id),
|
|
1120
|
+
message: null
|
|
1121
|
+
});
|
|
970
1122
|
|
|
971
|
-
|
|
1123
|
+
await linkCurrent(app, ver.artifactPath);
|
|
972
1124
|
|
|
973
|
-
try {
|
|
974
1125
|
step = 'migrate';
|
|
975
1126
|
if (runMigration && options.migrateBeforeStart && ver.hasMigration) {
|
|
976
1127
|
progress.report(step, 'running');
|
|
@@ -1027,7 +1178,13 @@ module.exports = fp(async (fastify, options) => {
|
|
|
1027
1178
|
progress.report(step, 'running');
|
|
1028
1179
|
scheduleStatusSync(app.id, {
|
|
1029
1180
|
beforeTables: beforeForReady,
|
|
1030
|
-
onSettled: result =>
|
|
1181
|
+
onSettled: result =>
|
|
1182
|
+
settleDeployReady(app, progress, result).catch(err => {
|
|
1183
|
+
fastify.log.warn({ err, app: app.name }, 'settle deploy ready failed');
|
|
1184
|
+
if (!progress.finished()) {
|
|
1185
|
+
progress.report('ready', 'error', { message: err.message });
|
|
1186
|
+
}
|
|
1187
|
+
})
|
|
1031
1188
|
});
|
|
1032
1189
|
await pruneVersionArtifacts(app).catch(err => fastify.log.warn({ err, app: app.name }, 'prune version artifacts failed'));
|
|
1033
1190
|
return {
|
|
@@ -1300,6 +1457,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
1300
1457
|
getLoadStore,
|
|
1301
1458
|
getDeployProgress: () => deployProgress,
|
|
1302
1459
|
waitDeployProgress: ({ name, progressId, timeoutMs }) => deployProgress.wait(name, progressId, { timeoutMs }),
|
|
1460
|
+
deployList,
|
|
1461
|
+
deployDetail,
|
|
1462
|
+
lastDeployment,
|
|
1463
|
+
markInterruptedDeploys,
|
|
1464
|
+
settleDeployReady,
|
|
1303
1465
|
getEnvDeclaration,
|
|
1304
1466
|
listInjectedEnvKeys,
|
|
1305
1467
|
toPublicApp,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fp = require('fastify-plugin');
|
|
2
2
|
const path = require('node:path');
|
|
3
|
+
const crypto = require('node:crypto');
|
|
3
4
|
const fs = require('fs-extra');
|
|
4
5
|
const { EventEmitter } = require('node:events');
|
|
5
6
|
const pm2Util = require('../utils/pm2');
|
|
@@ -136,17 +137,28 @@ module.exports = fp(async (fastify, options) => {
|
|
|
136
137
|
const { services } = fastify[options.name];
|
|
137
138
|
const results = [];
|
|
138
139
|
for (const item of missing) {
|
|
139
|
-
const
|
|
140
|
+
const { app, ver } = item;
|
|
141
|
+
const progress = services.app.getDeployProgress().begin(app.name, crypto.randomUUID(), { versionId: String(ver.id), version: ver.version, source: 'restore' });
|
|
142
|
+
const result = await services.app.restoreArtifact(Object.assign({}, item, { progress }));
|
|
140
143
|
results.push(result);
|
|
141
|
-
const { app } = item;
|
|
142
144
|
const wasActive = app.status === 'running' || app.status === 'deploying';
|
|
143
145
|
try {
|
|
144
146
|
const synced = await services.app.syncStatus({ name: app.name, recoverIfMissing: result.action === 'restored' && wasActive });
|
|
145
147
|
if (result.action !== 'restored' && wasActive && synced?.status !== 'running') {
|
|
146
148
|
await app.update({ status: 'error', message: `current version artifact missing: ${result.reason}` });
|
|
147
149
|
}
|
|
150
|
+
if (!progress.finished()) {
|
|
151
|
+
if (!wasActive) {
|
|
152
|
+
progress.report('ready', 'skipped');
|
|
153
|
+
} else {
|
|
154
|
+
await services.app.settleDeployReady(app, progress, synced);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
148
157
|
} catch (e) {
|
|
149
158
|
fastify.log.warn({ err: e, app: app.name }, 'sync status after artifact restore failed');
|
|
159
|
+
if (!progress.finished()) {
|
|
160
|
+
progress.report('ready', 'error', { message: e.message });
|
|
161
|
+
}
|
|
150
162
|
}
|
|
151
163
|
}
|
|
152
164
|
return results;
|
|
@@ -154,6 +166,7 @@ module.exports = fp(async (fastify, options) => {
|
|
|
154
166
|
|
|
155
167
|
const recoverOnStartup = async () => {
|
|
156
168
|
const { services } = fastify[options.name];
|
|
169
|
+
await services.app.markInterruptedDeploys().catch(err => fastify.log.warn({ err }, 'mark interrupted deploys failed'));
|
|
157
170
|
const missing = await services.app.listMissingArtifacts();
|
|
158
171
|
// 两路并行:部署目录完好的应用不必等其它应用重装依赖
|
|
159
172
|
const [reconciled, restored] = await Promise.all([
|
|
@@ -3,37 +3,125 @@ const { EventEmitter } = require('node:events');
|
|
|
3
3
|
/** 部署阶段,按顺序推进:准备部署包 → 校验环境变量 → SQL 迁移 → 启动进程 → 等待就绪 */
|
|
4
4
|
const DEPLOY_STEPS = ['package', 'env', 'migrate', 'start', 'ready'];
|
|
5
5
|
|
|
6
|
+
const isFinished = event => event.status === 'error' || (event.step === 'ready' && event.status !== 'running');
|
|
7
|
+
|
|
8
|
+
const toPublicState = state => ({
|
|
9
|
+
name: state.name,
|
|
10
|
+
progressId: state.progressId,
|
|
11
|
+
versionId: state.versionId,
|
|
12
|
+
version: state.version,
|
|
13
|
+
source: state.source,
|
|
14
|
+
status: state.status,
|
|
15
|
+
step: state.step,
|
|
16
|
+
message: state.message,
|
|
17
|
+
startedAt: state.startedAt,
|
|
18
|
+
finishedAt: state.finishedAt,
|
|
19
|
+
events: state.events.slice()
|
|
20
|
+
});
|
|
21
|
+
|
|
6
22
|
/**
|
|
7
23
|
* 每个应用只保留最近一次部署的进度事件,供 SSE 订阅与断线重连回放。
|
|
8
24
|
* 事件:{ progressId, step, status: 'running' | 'done' | 'skipped' | 'error', detail?, message?, at }
|
|
25
|
+
* persist(state):每次状态变化后按 progressId 串行调用,state 含 logs(日志行数组);日志变化合并 flushDelayMs 再写
|
|
9
26
|
*/
|
|
10
|
-
const createDeployProgress = () => {
|
|
27
|
+
const createDeployProgress = ({ persist = null, logMaxLines = 2000, flushDelayMs = 1000, onPersistError = null } = {}) => {
|
|
11
28
|
const emitter = new EventEmitter();
|
|
12
29
|
emitter.setMaxListeners(0);
|
|
13
30
|
const latest = new Map();
|
|
14
31
|
|
|
15
|
-
const begin = (name, progressId, { versionId = null, version = null } = {}) => {
|
|
16
|
-
const state = {
|
|
32
|
+
const begin = (name, progressId, { versionId = null, version = null, source = 'deploy' } = {}) => {
|
|
33
|
+
const state = {
|
|
34
|
+
name,
|
|
35
|
+
progressId,
|
|
36
|
+
versionId,
|
|
37
|
+
version,
|
|
38
|
+
source,
|
|
39
|
+
status: 'running',
|
|
40
|
+
step: null,
|
|
41
|
+
message: null,
|
|
42
|
+
startedAt: new Date().toISOString(),
|
|
43
|
+
finishedAt: null,
|
|
44
|
+
events: [],
|
|
45
|
+
logs: []
|
|
46
|
+
};
|
|
17
47
|
latest.set(name, state);
|
|
48
|
+
|
|
49
|
+
let chain = Promise.resolve();
|
|
50
|
+
let timer = null;
|
|
51
|
+
const flush = () => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
timer = null;
|
|
54
|
+
if (!persist) {
|
|
55
|
+
return chain;
|
|
56
|
+
}
|
|
57
|
+
const data = Object.assign(toPublicState(state), { logs: state.logs.slice() });
|
|
58
|
+
chain = chain.then(() => persist(data)).catch(err => onPersistError && onPersistError(err, data));
|
|
59
|
+
return chain;
|
|
60
|
+
};
|
|
61
|
+
const scheduleFlush = () => {
|
|
62
|
+
if (timer || !persist) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
timer = setTimeout(flush, flushDelayMs);
|
|
66
|
+
timer.unref && timer.unref();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const log = text => {
|
|
70
|
+
if (text === undefined || text === null) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const at = new Date().toISOString();
|
|
74
|
+
String(text)
|
|
75
|
+
.split(/\r?\n/)
|
|
76
|
+
.filter(line => line.trim())
|
|
77
|
+
.forEach(line => {
|
|
78
|
+
state.logs.push(`${at} ${line}`);
|
|
79
|
+
emitter.emit(`deploy-log:${name}`, { progressId, line: `${at} ${line}` });
|
|
80
|
+
});
|
|
81
|
+
if (state.logs.length > logMaxLines) {
|
|
82
|
+
state.logs.splice(0, state.logs.length - logMaxLines);
|
|
83
|
+
}
|
|
84
|
+
scheduleFlush();
|
|
85
|
+
};
|
|
86
|
+
|
|
18
87
|
const report = (step, status, { detail = null, message = null } = {}) => {
|
|
19
88
|
const event = { progressId, step, status, detail, message, at: new Date().toISOString() };
|
|
20
89
|
state.events.push(event);
|
|
90
|
+
state.step = step;
|
|
91
|
+
log(`[${step}] ${status}${detail ? ` ${detail}` : ''}${message ? `: ${message}` : ''}`);
|
|
92
|
+
if (state.status === 'running' && isFinished(event)) {
|
|
93
|
+
state.status = event.status === 'error' ? 'failed' : 'success';
|
|
94
|
+
state.message = message;
|
|
95
|
+
state.finishedAt = event.at;
|
|
96
|
+
}
|
|
21
97
|
emitter.emit(`deploy:${name}`, event);
|
|
98
|
+
flush();
|
|
22
99
|
return event;
|
|
23
100
|
};
|
|
24
|
-
|
|
101
|
+
|
|
102
|
+
const finished = () => state.status !== 'running';
|
|
103
|
+
|
|
104
|
+
flush();
|
|
105
|
+
return { progressId, report, log, finished, flush };
|
|
25
106
|
};
|
|
26
107
|
|
|
27
|
-
const snapshot = name =>
|
|
108
|
+
const snapshot = name => {
|
|
109
|
+
const state = latest.get(name);
|
|
110
|
+
return state ? toPublicState(state) : null;
|
|
111
|
+
};
|
|
28
112
|
|
|
29
|
-
|
|
113
|
+
/** 当前进程内某次部署的日志行(含时间戳前缀) */
|
|
114
|
+
const logsOf = (name, progressId) => {
|
|
115
|
+
const state = latest.get(name);
|
|
116
|
+
return state && state.progressId === progressId ? state.logs.slice() : null;
|
|
117
|
+
};
|
|
30
118
|
|
|
31
|
-
const
|
|
119
|
+
const clear = name => latest.delete(name);
|
|
32
120
|
|
|
33
121
|
/** 尚未结束的最近一次部署;准备部署包阶段应用仍是旧状态与旧版本,需靠它判断哪个版本正在部署 */
|
|
34
122
|
const active = name => {
|
|
35
123
|
const state = latest.get(name);
|
|
36
|
-
if (!state || state.
|
|
124
|
+
if (!state || state.status !== 'running') {
|
|
37
125
|
return null;
|
|
38
126
|
}
|
|
39
127
|
const last = state.events[state.events.length - 1];
|
|
@@ -41,6 +129,7 @@ const createDeployProgress = () => {
|
|
|
41
129
|
progressId: state.progressId,
|
|
42
130
|
versionId: state.versionId,
|
|
43
131
|
version: state.version,
|
|
132
|
+
source: state.source,
|
|
44
133
|
startedAt: state.startedAt,
|
|
45
134
|
step: last ? last.step : null,
|
|
46
135
|
detail: last ? last.detail : null
|
|
@@ -74,10 +163,11 @@ const createDeployProgress = () => {
|
|
|
74
163
|
timer = setTimeout(() => finish(true), timeoutMs);
|
|
75
164
|
});
|
|
76
165
|
|
|
77
|
-
return { emitter, begin, snapshot, active, clear, wait };
|
|
166
|
+
return { emitter, begin, snapshot, logsOf, active, clear, wait };
|
|
78
167
|
};
|
|
79
168
|
|
|
80
169
|
module.exports = {
|
|
81
170
|
DEPLOY_STEPS,
|
|
171
|
+
isFinished,
|
|
82
172
|
createDeployProgress
|
|
83
173
|
};
|
package/libs/utils/version.js
CHANGED
|
@@ -4,7 +4,7 @@ const { spawn } = require('node:child_process');
|
|
|
4
4
|
const { extractZipSafe, resolvePackageRoot } = require('./zip');
|
|
5
5
|
const { validatePackageRoot, listSqlFiles } = require('./validatePackage');
|
|
6
6
|
|
|
7
|
-
const runNpmInstall = (cwd, timeoutMs) =>
|
|
7
|
+
const runNpmInstall = (cwd, timeoutMs, onLog) =>
|
|
8
8
|
new Promise((resolve, reject) => {
|
|
9
9
|
const child = spawn('npm', ['install', '--production', '--no-fund', '--no-audit'], {
|
|
10
10
|
cwd,
|
|
@@ -12,8 +12,12 @@ const runNpmInstall = (cwd, timeoutMs) =>
|
|
|
12
12
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
13
13
|
});
|
|
14
14
|
let stderr = '';
|
|
15
|
+
child.stdout?.on('data', chunk => {
|
|
16
|
+
onLog && onLog(chunk.toString());
|
|
17
|
+
});
|
|
15
18
|
child.stderr.on('data', chunk => {
|
|
16
19
|
stderr += chunk.toString();
|
|
20
|
+
onLog && onLog(chunk.toString());
|
|
17
21
|
});
|
|
18
22
|
const timer = setTimeout(() => {
|
|
19
23
|
child.kill('SIGKILL');
|
|
@@ -34,7 +38,7 @@ const runNpmInstall = (cwd, timeoutMs) =>
|
|
|
34
38
|
});
|
|
35
39
|
|
|
36
40
|
/** install=false 时只解压校验,不装依赖(用于不常驻磁盘的版本上传) */
|
|
37
|
-
const prepareVersionArtifact = async ({ zipBuffer, artifactPath, maxZipSize, maxZipEntries, npmInstallTimeoutMs, sqlPath, install = true, requireEnvSchema = true, onProgress }) => {
|
|
41
|
+
const prepareVersionArtifact = async ({ zipBuffer, artifactPath, maxZipSize, maxZipEntries, npmInstallTimeoutMs, sqlPath, install = true, requireEnvSchema = true, onProgress, onLog }) => {
|
|
38
42
|
await fs.emptyDir(artifactPath);
|
|
39
43
|
onProgress && onProgress('extract');
|
|
40
44
|
const extractDir = path.join(artifactPath, '_extract');
|
|
@@ -64,7 +68,7 @@ const prepareVersionArtifact = async ({ zipBuffer, artifactPath, maxZipSize, max
|
|
|
64
68
|
const serverDir = path.join(artifactPath, 'server');
|
|
65
69
|
if (install) {
|
|
66
70
|
onProgress && onProgress('install');
|
|
67
|
-
await runNpmInstall(serverDir, npmInstallTimeoutMs);
|
|
71
|
+
await runNpmInstall(serverDir, npmInstallTimeoutMs, onLog);
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
const sqlDir = path.join(serverDir, sqlPath || 'sql');
|