@cloudbase/manager-node 5.8.2 → 5.8.4
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 +15 -0
- package/lib/database/index.js +11 -6
- package/lib/deploy/DeployOrchestrator.js +170 -278
- package/lib/deploy/FunctionDeployer.js +310 -52
- package/lib/deploy/StaticDeployer.js +7 -1
- package/lib/deploy/function/artifact.js +55 -41
- package/lib/deploy/function/builders/cloud.js +58 -359
- package/lib/deploy/function/cam-preflight.js +2 -1
- package/lib/deploy/function/cloud-build-service.js +2 -16
- package/lib/deploy/function/config-guard.js +210 -55
- package/lib/deploy/function/enterprise-tcr.js +195 -0
- package/lib/deploy/function/image-tag.js +31 -0
- package/lib/deploy/function/local-builder.js +178 -32
- package/lib/deploy/function/personal-tcr.js +153 -0
- package/lib/deploy/function/preflight.js +3 -2
- package/lib/deploy/types.js +11 -0
- package/lib/hosting/index.js +27 -0
- package/lib/projectValidator/index.js +22 -23
- package/package.json +2 -2
- package/types/database/index.d.ts +1 -0
- package/types/deploy/DeployOrchestrator.d.ts +19 -25
- package/types/deploy/FunctionDeployer.d.ts +35 -3
- package/types/deploy/StaticDeployer.d.ts +11 -0
- package/types/deploy/function/artifact.d.ts +0 -5
- package/types/deploy/function/builders/cloud.d.ts +6 -39
- package/types/deploy/function/config-guard.d.ts +18 -0
- package/types/deploy/function/enterprise-tcr.d.ts +32 -0
- package/types/deploy/function/image-tag.d.ts +10 -0
- package/types/deploy/function/local-builder.d.ts +35 -6
- package/types/deploy/function/personal-tcr.d.ts +35 -0
- package/types/deploy/types.d.ts +110 -28
- package/types/hosting/index.d.ts +13 -0
- package/types/projectValidator/index.d.ts +3 -2
- package/types/projectValidator/types.d.ts +0 -1
- package/lib/deploy/StateStore.js +0 -328
- package/types/deploy/StateStore.d.ts +0 -174
package/types/deploy/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ICloudFunctionTrigger,
|
|
1
|
+
import { ICloudFunctionTrigger, IFunctionVPC } from '../interfaces';
|
|
2
2
|
/**
|
|
3
3
|
* 镜像 HTTP 函数固定监听端口
|
|
4
4
|
* SCF HTTP 自定义镜像函数要求容器监听 9000
|
|
@@ -22,7 +22,7 @@ export type DeployActionKind = 'create' | 'update' | 'replace' | 'noop';
|
|
|
22
22
|
/** 部署计划中的细粒度操作 */
|
|
23
23
|
export type DeployOperation = 'create-function' | 'update-code' | 'update-config' | 'replace-function' | 'configure-access' | 'configure-gateway';
|
|
24
24
|
/** 统一进度阶段 */
|
|
25
|
-
export type DeployStage = 'validate' | 'preflight' | 'plan' | 'pack' | 'upload' | 'build' | 'push' | 'deploy-function' | 'wait-active' | 'configure-access' | 'configure-gateway' | 'done';
|
|
25
|
+
export type DeployStage = 'validate' | 'preflight' | 'plan' | 'pack' | 'upload' | 'login' | 'build' | 'push' | 'deploy-function' | 'wait-active' | 'configure-access' | 'configure-gateway' | 'done';
|
|
26
26
|
/** 单项检查结论 */
|
|
27
27
|
export type CheckStatus = 'pass' | 'warn' | 'fail';
|
|
28
28
|
/**
|
|
@@ -94,6 +94,17 @@ export declare const FUNCTION_DEPLOY_ERROR: {
|
|
|
94
94
|
readonly BUILD_ARG_INVALID: "BUILD_ARG_INVALID";
|
|
95
95
|
readonly CLOUD_REGISTRY_CREDENTIAL_MISSING: "CLOUD_REGISTRY_CREDENTIAL_MISSING";
|
|
96
96
|
readonly CLOUD_REGISTRY_CREDENTIAL_INVALID: "CLOUD_REGISTRY_CREDENTIAL_INVALID";
|
|
97
|
+
readonly TCR_REGION_MISSING: "TCR_REGION_MISSING";
|
|
98
|
+
readonly TCR_INSTANCE_DISCOVERY_FAILED: "TCR_INSTANCE_DISCOVERY_FAILED";
|
|
99
|
+
readonly TCR_INSTANCE_NOT_FOUND: "TCR_INSTANCE_NOT_FOUND";
|
|
100
|
+
readonly TCR_INSTANCE_AMBIGUOUS: "TCR_INSTANCE_AMBIGUOUS";
|
|
101
|
+
readonly TCR_INSTANCE_INVALID: "TCR_INSTANCE_INVALID";
|
|
102
|
+
readonly TCR_NAMESPACE_INVALID: "TCR_NAMESPACE_INVALID";
|
|
103
|
+
readonly TCR_NAMESPACE_QUERY_FAILED: "TCR_NAMESPACE_QUERY_FAILED";
|
|
104
|
+
readonly TCR_NAMESPACE_CREATE_FAILED: "TCR_NAMESPACE_CREATE_FAILED";
|
|
105
|
+
readonly TCR_TOKEN_CREATE_FAILED: "TCR_TOKEN_CREATE_FAILED";
|
|
106
|
+
readonly TCR_TOKEN_INVALID: "TCR_TOKEN_INVALID";
|
|
107
|
+
readonly TCR_DOCKER_LOGIN_FAILED: "TCR_DOCKER_LOGIN_FAILED";
|
|
97
108
|
readonly GATEWAY_PATH_INVALID: "GATEWAY_PATH_INVALID";
|
|
98
109
|
readonly LOCAL_FALLBACK_INVALID: "LOCAL_FALLBACK_INVALID";
|
|
99
110
|
readonly PREFLIGHT_CODE_PATH_MISSING: "PREFLIGHT_CODE_PATH_MISSING";
|
|
@@ -243,7 +254,7 @@ export interface IHttpZipDeployConfig extends IHttpFunctionDeployCommon, ILocalC
|
|
|
243
254
|
*/
|
|
244
255
|
installDependency?: boolean | 'TRUE' | 'FALSE';
|
|
245
256
|
}
|
|
246
|
-
/** 个人版 TCR
|
|
257
|
+
/** 个人版 TCR 推送凭证,local/cloud 均可使用。 */
|
|
247
258
|
export interface IRegistryCredential {
|
|
248
259
|
/** 腾讯云账号 ID(UIN),作为个人版 CCR 登录用户名 */
|
|
249
260
|
username: string;
|
|
@@ -258,12 +269,14 @@ export interface IImageBuildTarget {
|
|
|
258
269
|
dockerfile?: string;
|
|
259
270
|
/** 目标镜像仓库实例 ID */
|
|
260
271
|
registryId?: string;
|
|
261
|
-
/**
|
|
272
|
+
/**
|
|
273
|
+
* 目标镜像命名空间。personal local/cloud 优先使用显式配置;
|
|
274
|
+
* 缺省时使用当前 CloudBase envId。enterprise 可省略并由实例准备逻辑创建或复用。
|
|
275
|
+
*/
|
|
262
276
|
namespace?: string;
|
|
263
277
|
/**
|
|
264
|
-
* 目标镜像仓库路径。
|
|
265
|
-
*
|
|
266
|
-
* local 策略当前需提供可直接 push 的完整 registry/namespace/repository 地址。
|
|
278
|
+
* 目标镜像仓库路径。local/cloud 均可省略并默认使用函数名;
|
|
279
|
+
* manager-node 会结合目标 registry 与 namespace 组装最终推送地址。
|
|
267
280
|
*/
|
|
268
281
|
repository?: string;
|
|
269
282
|
/** local 策略的目标镜像 tag;cloud 策略由平台生成,不接受自定义 tag */
|
|
@@ -272,20 +285,39 @@ export interface IImageBuildTarget {
|
|
|
272
285
|
platform?: string;
|
|
273
286
|
/** 构建参数,不得写入敏感值 */
|
|
274
287
|
buildArgs?: Record<string, string>;
|
|
275
|
-
/**
|
|
288
|
+
/**
|
|
289
|
+
* 个人版 TCR 推送凭证。personal local/cloud 必须由调用方显式提供;
|
|
290
|
+
* manager-node 仅使用该凭证登录,不创建用户、不修改远端密码,也不持久化凭证。
|
|
291
|
+
*/
|
|
276
292
|
registryCredential?: IRegistryCredential;
|
|
277
293
|
/** 忽略同摘要镜像复用,强制重新构建 */
|
|
278
294
|
forceBuild?: boolean;
|
|
295
|
+
/** 个人版 TCR 保留的最新镜像标签数量 */
|
|
296
|
+
retainedTags?: number;
|
|
279
297
|
}
|
|
280
|
-
/**
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
298
|
+
/**
|
|
299
|
+
* 声明式镜像运行时配置
|
|
300
|
+
*
|
|
301
|
+
* 直接对齐 toolbox schema 的 ICloudBaseFunctionImageConfig:镜像相关字段全部
|
|
302
|
+
* 收敛在 imageConfig 命名空间下,构建目标进一步嵌套在 imageConfig.build。
|
|
303
|
+
* manager-node 直接消费该形状,不再做字段平铺映射。
|
|
304
|
+
*/
|
|
305
|
+
export interface IDeployImageConfig {
|
|
285
306
|
/** 镜像类型;省略时有 registryId 推断为 enterprise,否则推断为 personal */
|
|
286
307
|
imageType?: 'enterprise' | 'personal';
|
|
287
|
-
/**
|
|
308
|
+
/** 已有镜像地址,image 策略必填 */
|
|
309
|
+
imageUri?: string;
|
|
310
|
+
/** 企业版 TCR 实例 ID;个人版镜像不传 */
|
|
288
311
|
registryId?: string;
|
|
312
|
+
/** 镜像构建目标,cloud/local 策略必填 */
|
|
313
|
+
build?: IImageBuildTarget;
|
|
314
|
+
/**
|
|
315
|
+
* 本地构建环境不可用时的处理方式
|
|
316
|
+
* 默认 error,避免静默改变构建环境
|
|
317
|
+
*/
|
|
318
|
+
localFallback?: 'cloud' | 'error';
|
|
319
|
+
/** 容器监听端口,仅允许 9000 */
|
|
320
|
+
imagePort?: number;
|
|
289
321
|
entryPoint?: string;
|
|
290
322
|
command?: string;
|
|
291
323
|
args?: string;
|
|
@@ -293,29 +325,59 @@ export interface IHttpImageRuntimeCommon extends IHttpFunctionDeployCommon {
|
|
|
293
325
|
argsList?: string[];
|
|
294
326
|
containerImageAccelerate?: boolean;
|
|
295
327
|
}
|
|
328
|
+
/** HTTP 自定义镜像公共配置 */
|
|
329
|
+
export interface IHttpImageRuntimeCommon extends IHttpFunctionDeployCommon {
|
|
330
|
+
runtime?: typeof CUSTOM_IMAGE_RUNTIME;
|
|
331
|
+
/** 镜像运行时配置,对齐 toolbox schema 的嵌套形状 */
|
|
332
|
+
imageConfig?: IDeployImageConfig;
|
|
333
|
+
}
|
|
296
334
|
/** 云端构建镜像部署 */
|
|
297
335
|
export interface IHttpCloudDeployConfig extends IHttpImageRuntimeCommon {
|
|
298
336
|
buildStrategy: 'cloud';
|
|
299
|
-
|
|
337
|
+
imageConfig: IDeployImageConfig;
|
|
300
338
|
}
|
|
301
339
|
/** 本地构建镜像部署 */
|
|
302
340
|
export interface IHttpLocalDeployConfig extends IHttpImageRuntimeCommon {
|
|
303
341
|
buildStrategy: 'local';
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* 本地构建环境不可用时的处理方式
|
|
307
|
-
* 默认 error,避免静默改变构建环境
|
|
308
|
-
*/
|
|
309
|
-
localFallback?: 'cloud' | 'error';
|
|
342
|
+
imageConfig: IDeployImageConfig;
|
|
310
343
|
}
|
|
311
344
|
/** 已有镜像直接部署 */
|
|
312
345
|
export interface IHttpImageDeployConfig extends IHttpImageRuntimeCommon {
|
|
313
346
|
buildStrategy: 'image';
|
|
314
|
-
imageConfig:
|
|
347
|
+
imageConfig: IDeployImageConfig;
|
|
315
348
|
}
|
|
316
349
|
export type IHttpFunctionDeployConfig = IHttpZipDeployConfig | IHttpCloudDeployConfig | IHttpLocalDeployConfig | IHttpImageDeployConfig;
|
|
317
350
|
export type IFunctionDeployConfig = IEventFunctionDeployConfig | IHttpFunctionDeployConfig;
|
|
318
|
-
|
|
351
|
+
export interface IDeployInputField {
|
|
352
|
+
/** 稳定字段名,供 CLI/MCP 决定如何收集输入 */
|
|
353
|
+
name: 'envId' | 'imageUri' | 'registryId' | 'repository' | 'namespace' | 'registryCredential.username' | 'registryCredential.password';
|
|
354
|
+
/** 配置中的字段路径 */
|
|
355
|
+
path: string;
|
|
356
|
+
/** 是否必须补齐才能继续部署 */
|
|
357
|
+
required: boolean;
|
|
358
|
+
/** 面向调用方的原因,不包含敏感信息 */
|
|
359
|
+
reason: string;
|
|
360
|
+
}
|
|
361
|
+
/** 最小化镜像配置的默认值上下文 */
|
|
362
|
+
export interface IMinimalDeployConfigContext {
|
|
363
|
+
envId?: string;
|
|
364
|
+
functionPath?: string;
|
|
365
|
+
functionRootPath?: string;
|
|
366
|
+
/** 调用方明确提供的非交互默认值;密码等敏感值只在显式传入时使用 */
|
|
367
|
+
defaults?: {
|
|
368
|
+
imageUri?: string;
|
|
369
|
+
registryId?: string;
|
|
370
|
+
repository?: string;
|
|
371
|
+
namespace?: string;
|
|
372
|
+
registryCredential?: IRegistryCredential;
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/** 最小化配置准备结果 */
|
|
376
|
+
export interface IMinimalDeployConfigResult {
|
|
377
|
+
config: IFunctionDeployConfig;
|
|
378
|
+
missing: IDeployInputField[];
|
|
379
|
+
appliedDefaults: string[];
|
|
380
|
+
}
|
|
319
381
|
export type INormalizedEventConfig = IEventFunctionDeployConfig & {
|
|
320
382
|
type: 'Event';
|
|
321
383
|
buildStrategy: 'zip';
|
|
@@ -330,22 +392,34 @@ export type INormalizedHttpCloudConfig = IHttpCloudDeployConfig & {
|
|
|
330
392
|
type: 'HTTP';
|
|
331
393
|
buildStrategy: 'cloud';
|
|
332
394
|
runtime: typeof CUSTOM_IMAGE_RUNTIME;
|
|
333
|
-
|
|
395
|
+
imageConfig: IDeployImageConfig & {
|
|
396
|
+
imagePort: number;
|
|
397
|
+
imageType: 'enterprise' | 'personal';
|
|
398
|
+
build: IImageBuildTarget;
|
|
399
|
+
};
|
|
334
400
|
};
|
|
335
401
|
/** 规范化后的 HTTP 本地构建配置 */
|
|
336
402
|
export type INormalizedHttpLocalConfig = IHttpLocalDeployConfig & {
|
|
337
403
|
type: 'HTTP';
|
|
338
404
|
buildStrategy: 'local';
|
|
339
405
|
runtime: typeof CUSTOM_IMAGE_RUNTIME;
|
|
340
|
-
|
|
341
|
-
|
|
406
|
+
imageConfig: IDeployImageConfig & {
|
|
407
|
+
imagePort: number;
|
|
408
|
+
imageType: 'enterprise' | 'personal';
|
|
409
|
+
build: IImageBuildTarget;
|
|
410
|
+
localFallback: 'cloud' | 'error';
|
|
411
|
+
};
|
|
342
412
|
};
|
|
343
413
|
/** 规范化后的 HTTP 已有镜像配置 */
|
|
344
414
|
export type INormalizedHttpImageConfig = IHttpImageDeployConfig & {
|
|
345
415
|
type: 'HTTP';
|
|
346
416
|
buildStrategy: 'image';
|
|
347
417
|
runtime: typeof CUSTOM_IMAGE_RUNTIME;
|
|
348
|
-
|
|
418
|
+
imageConfig: IDeployImageConfig & {
|
|
419
|
+
imagePort: number;
|
|
420
|
+
imageType: 'enterprise' | 'personal';
|
|
421
|
+
imageUri: string;
|
|
422
|
+
};
|
|
349
423
|
};
|
|
350
424
|
export type INormalizedDeployConfig = INormalizedEventConfig | INormalizedHttpZipConfig | INormalizedHttpCloudConfig | INormalizedHttpLocalConfig | INormalizedHttpImageConfig;
|
|
351
425
|
/** 代码类部署物 */
|
|
@@ -413,16 +487,25 @@ export interface IFunctionDeployPlan {
|
|
|
413
487
|
/** 单步执行结果 */
|
|
414
488
|
export interface IDeployStepResult {
|
|
415
489
|
stage: DeployStage;
|
|
490
|
+
/** 同一 stage 下的具体步骤名称,例如 Docker 环境检查、镜像仓库权限检查 */
|
|
491
|
+
label?: string;
|
|
416
492
|
status: 'success' | 'skipped' | 'failed';
|
|
417
493
|
/** 毫秒 */
|
|
418
494
|
elapsedTime: number;
|
|
419
495
|
detail?: string;
|
|
496
|
+
errorCode?: string;
|
|
420
497
|
}
|
|
421
498
|
/** 进度事件 */
|
|
422
499
|
export interface IDeployProgressEvent {
|
|
423
500
|
stage: DeployStage;
|
|
501
|
+
/** 同一 stage 下的具体步骤名称,例如 Docker 环境检查、镜像仓库权限检查 */
|
|
502
|
+
label?: string;
|
|
424
503
|
status: 'start' | 'success' | 'skipped' | 'failed';
|
|
425
504
|
message?: string;
|
|
505
|
+
/** 当前阶段耗时(毫秒),仅 success/failed 时提供 */
|
|
506
|
+
elapsedTime?: number;
|
|
507
|
+
errorCode?: string;
|
|
508
|
+
requestId?: string;
|
|
426
509
|
}
|
|
427
510
|
/** 部署结果 */
|
|
428
511
|
export interface IFunctionDeployResult {
|
|
@@ -447,7 +530,6 @@ export interface IFunctionDeployResult {
|
|
|
447
530
|
warnings: string[];
|
|
448
531
|
plan: IFunctionDeployPlan;
|
|
449
532
|
}
|
|
450
|
-
/** 部署选项 */
|
|
451
533
|
export interface IFunctionDeployOptions {
|
|
452
534
|
/** 只生成计划,不执行任何变更 */
|
|
453
535
|
dryRun?: boolean;
|
package/types/hosting/index.d.ts
CHANGED
|
@@ -229,6 +229,9 @@ export declare class HostingService {
|
|
|
229
229
|
}>;
|
|
230
230
|
/**
|
|
231
231
|
* 查询域名状态信息
|
|
232
|
+
*
|
|
233
|
+
* 仅在 CDN 域名(domainType=cdn)或老环境(未返回 domainType)时调用 CDN 接口,
|
|
234
|
+
* 其他域名类型(gateway / eo 等)返回空结果
|
|
232
235
|
* @param options
|
|
233
236
|
*/
|
|
234
237
|
tcbCheckResource(options: ICheckSourceOptions): Promise<{
|
|
@@ -238,6 +241,9 @@ export declare class HostingService {
|
|
|
238
241
|
}>;
|
|
239
242
|
/**
|
|
240
243
|
* 域名配置变更
|
|
244
|
+
*
|
|
245
|
+
* 仅在 CDN 域名(domainType=cdn)或老环境(未返回 domainType)时调用 CDN 接口,
|
|
246
|
+
* 其他域名类型(gateway / eo 等)返回空结果
|
|
241
247
|
* @param options
|
|
242
248
|
*/
|
|
243
249
|
tcbModifyAttribute(options: IModifyOptions): Promise<{}>;
|
|
@@ -263,4 +269,11 @@ export declare class HostingService {
|
|
|
263
269
|
* 将 cloudPath 转换成 cloudPath/ 形式
|
|
264
270
|
*/
|
|
265
271
|
private getCloudKey;
|
|
272
|
+
/**
|
|
273
|
+
* 获取静态托管域名类型(cdn / gateway / eo)
|
|
274
|
+
*
|
|
275
|
+
* 后端通过 DescribeEnvInfo 返回 StaticStorages[0].DomainType,
|
|
276
|
+
* 仅 cdn 或空值(老环境)才允许调用 CDN 相关接口。
|
|
277
|
+
*/
|
|
278
|
+
private getStorageDomainType;
|
|
266
279
|
}
|
|
@@ -5,8 +5,9 @@ export declare class ProjectValidator {
|
|
|
5
5
|
private validateSchema;
|
|
6
6
|
private validateEnvId;
|
|
7
7
|
/**
|
|
8
|
-
* 个人版 TCR 的 username/password
|
|
9
|
-
*
|
|
8
|
+
* 个人版 TCR 的 username/password 必须在 cloudbaserc 中显式声明。
|
|
9
|
+
* namespace/repository 可省略,分别由部署器按 envId/函数名推导。
|
|
10
|
+
* ConfigParser 已先解析 {{env.*}},因此这里同时承担缺失凭证 env 值的前置校验。
|
|
10
11
|
*/
|
|
11
12
|
private validatePersonalRegistryConfig;
|
|
12
13
|
private hasPersonalCloudFunction;
|
package/lib/deploy/StateStore.js
DELETED
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.StateStore = exports.DEFAULT_HASH_LENGTH = exports.DEFAULT_MAX_HOSTING_FILES = exports.HostingFilesOverLimitError = void 0;
|
|
7
|
-
const fs_1 = __importDefault(require("fs"));
|
|
8
|
-
const path_1 = __importDefault(require("path"));
|
|
9
|
-
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
-
const child_process_1 = require("child_process");
|
|
11
|
-
const error_1 = require("../error");
|
|
12
|
-
/** hosting 指纹文件数超过上限时抛出(触发降级为全量部署) */
|
|
13
|
-
class HostingFilesOverLimitError extends Error {
|
|
14
|
-
constructor(fileCount, limit) {
|
|
15
|
-
super(`hosting 文件数 ${fileCount} 超过指纹上限 ${limit},跳过逐文件 diff,降级为全量部署`);
|
|
16
|
-
this.name = 'HostingFilesOverLimitError';
|
|
17
|
-
this.fileCount = fileCount;
|
|
18
|
-
this.limit = limit;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
exports.HostingFilesOverLimitError = HostingFilesOverLimitError;
|
|
22
|
-
/** 默认强制忽略的目录/文件(不可被调用方关闭) */
|
|
23
|
-
const DEFAULT_IGNORE = ['.DS_Store', 'node_modules/'];
|
|
24
|
-
/** 默认指纹文件数上限(防快照膨胀与 I/O 失控) */
|
|
25
|
-
exports.DEFAULT_MAX_HOSTING_FILES = 50000;
|
|
26
|
-
/** 默认 hash 截断长度(16 位 hex = 64bit,碰撞概率可忽略;快照体积减半) */
|
|
27
|
-
exports.DEFAULT_HASH_LENGTH = 16;
|
|
28
|
-
/**
|
|
29
|
-
* 部署状态快照(StateStore)
|
|
30
|
-
*
|
|
31
|
-
* 职责:读写 `.cloudbase/state.json`(默认),为 dry-run 的 hosting/app diff 提供本地基准,
|
|
32
|
-
* 避免每次 diff 都拉取云端。
|
|
33
|
-
*
|
|
34
|
-
* 指纹算法:hosting = 产物目录逐文件内容 sha256(相对路径为 key)。
|
|
35
|
-
* 只存 hash 不存内容,体积 = O(文件数);app 快照不写入 envVariables(敏感过滤)。
|
|
36
|
-
*/
|
|
37
|
-
class StateStore {
|
|
38
|
-
constructor(options = {}) {
|
|
39
|
-
this.cwd = options.cwd || process.cwd();
|
|
40
|
-
this.env = options.env;
|
|
41
|
-
this.stateFile = options.stateFile;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* 计算 state 文件路径:
|
|
45
|
-
* - 自定义 stateFile 优先
|
|
46
|
-
* - 否则 {cwd}/.cloudbase/state.json(无 env)或 state.{env}.json(多环境隔离)
|
|
47
|
-
*/
|
|
48
|
-
getFilePath() {
|
|
49
|
-
if (this.stateFile) {
|
|
50
|
-
return this.stateFile;
|
|
51
|
-
}
|
|
52
|
-
const base = this.env ? `state.${this.env}.json` : 'state.json';
|
|
53
|
-
return path_1.default.join(this.cwd, '.cloudbase', base);
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* 读取状态快照;文件不存在或 JSON 损坏返回 null(缺失降级,由调用方走云端兜底)
|
|
57
|
-
*/
|
|
58
|
-
read() {
|
|
59
|
-
const filePath = this.getFilePath();
|
|
60
|
-
if (!fs_1.default.existsSync(filePath)) {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
try {
|
|
64
|
-
return JSON.parse(fs_1.default.readFileSync(filePath, 'utf8'));
|
|
65
|
-
}
|
|
66
|
-
catch (_a) {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* 写入状态快照(自动补 version/updatedAt/gitCommit,自动创建 .cloudbase 目录)
|
|
72
|
-
*/
|
|
73
|
-
write(state) {
|
|
74
|
-
var _a;
|
|
75
|
-
const filePath = this.getFilePath();
|
|
76
|
-
fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
|
|
77
|
-
const full = {
|
|
78
|
-
version: 1,
|
|
79
|
-
updatedAt: new Date().toISOString(),
|
|
80
|
-
gitCommit: (_a = state.gitCommit) !== null && _a !== void 0 ? _a : this.resolveGitCommit(),
|
|
81
|
-
envId: state.envId,
|
|
82
|
-
resources: state.resources
|
|
83
|
-
};
|
|
84
|
-
fs_1.default.writeFileSync(filePath, JSON.stringify(full, null, 2), 'utf8');
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* 计算产物目录逐文件 sha256 指纹(仅 hash,兼容旧调用)
|
|
88
|
-
*
|
|
89
|
-
* @param projectPath 产物目录(hosting 的 outputDir / root)
|
|
90
|
-
* @param ignore 自定义忽略规则(追加到默认 node_modules / .DS_Store 之后)
|
|
91
|
-
* @param options 扫描选项(maxFiles / hashLength)
|
|
92
|
-
* @returns 相对路径(/ 分隔)→ 文件内容 sha256(默认截断 16 位)
|
|
93
|
-
*/
|
|
94
|
-
async computeHostingFiles(projectPath, ignore = [], options = {}) {
|
|
95
|
-
const fingerprints = await this.computeHostingFingerprints(projectPath, ignore, options);
|
|
96
|
-
const result = {};
|
|
97
|
-
for (const [relPath, fp] of Object.entries(fingerprints)) {
|
|
98
|
-
result[relPath] = fp.hash;
|
|
99
|
-
}
|
|
100
|
-
return result;
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* 计算产物目录逐文件指纹(hash + size)
|
|
104
|
-
*
|
|
105
|
-
* - 强制忽略 node_modules / .DS_Store(与传入 ignore 合并,不可关闭)
|
|
106
|
-
* - hash 默认截断为 16 位 hex(hashLength 可调,64 = 完整 sha256)
|
|
107
|
-
* - 文件数超过 maxFiles 抛 HostingFilesOverLimitError(防快照膨胀与 I/O 失控)
|
|
108
|
-
*
|
|
109
|
-
* @param projectPath 产物目录(hosting 的 outputDir / root)
|
|
110
|
-
* @param ignore 自定义忽略规则
|
|
111
|
-
* @param options 扫描选项(maxFiles / hashLength)
|
|
112
|
-
* @returns 相对路径(/ 分隔)→ { hash, size }
|
|
113
|
-
*/
|
|
114
|
-
async computeHostingFingerprints(projectPath, ignore = [], options = {}) {
|
|
115
|
-
if (!fs_1.default.existsSync(projectPath) || !fs_1.default.statSync(projectPath).isDirectory()) {
|
|
116
|
-
throw new error_1.CloudBaseError(`产物目录不存在或不是目录:${projectPath}`);
|
|
117
|
-
}
|
|
118
|
-
const { maxFiles = exports.DEFAULT_MAX_HOSTING_FILES, hashLength = exports.DEFAULT_HASH_LENGTH } = options;
|
|
119
|
-
const effectiveIgnore = this.mergeIgnore(ignore);
|
|
120
|
-
const files = {};
|
|
121
|
-
let count = 0;
|
|
122
|
-
const walk = (dir, rel) => {
|
|
123
|
-
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
124
|
-
const entryPath = path_1.default.join(dir, entry.name);
|
|
125
|
-
const relPath = rel ? path_1.default.join(rel, entry.name) : entry.name;
|
|
126
|
-
if (entry.isDirectory()) {
|
|
127
|
-
if (this.isIgnored(relPath, effectiveIgnore)) {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
walk(entryPath, relPath);
|
|
131
|
-
}
|
|
132
|
-
else if (entry.isFile() && !this.isIgnored(relPath, effectiveIgnore)) {
|
|
133
|
-
count++;
|
|
134
|
-
if (maxFiles > 0 && count > maxFiles) {
|
|
135
|
-
throw new HostingFilesOverLimitError(count, maxFiles);
|
|
136
|
-
}
|
|
137
|
-
const normalized = relPath.split(path_1.default.sep).join('/');
|
|
138
|
-
files[normalized] = {
|
|
139
|
-
hash: this.hashFile(entryPath, hashLength),
|
|
140
|
-
size: fs_1.default.statSync(entryPath).size
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
walk(projectPath, '');
|
|
146
|
-
return files;
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* 计算产物目录与快照的变更集(懒加载)
|
|
150
|
-
*
|
|
151
|
-
* 优化:先 stat 拿 size,size 与快照不同的文件直接判定变更(不读文件内容),
|
|
152
|
-
* 只有 size 相同(或旧快照 size 未知)的文件才读内容 hash 对比,
|
|
153
|
-
* 避免为未变文件做无谓的 I/O 与 hash。
|
|
154
|
-
* 文件数超过 maxFiles 时放弃 diff,返回 overLimit=true(调用方应降级为全量部署)。
|
|
155
|
-
*
|
|
156
|
-
* @param projectPath 产物目录(hosting 的 outputDir / root)
|
|
157
|
-
* @param previous 上次快照指纹(undefined = 首次部署,全部视为新增)
|
|
158
|
-
* @param options 选项(ignore / maxFiles / hashLength)
|
|
159
|
-
*/
|
|
160
|
-
async diffHostingFiles(projectPath, previous, options = {}) {
|
|
161
|
-
if (!fs_1.default.existsSync(projectPath) || !fs_1.default.statSync(projectPath).isDirectory()) {
|
|
162
|
-
throw new error_1.CloudBaseError(`产物目录不存在或不是目录:${projectPath}`);
|
|
163
|
-
}
|
|
164
|
-
const { ignore = [], maxFiles = exports.DEFAULT_MAX_HOSTING_FILES, hashLength = exports.DEFAULT_HASH_LENGTH } = options;
|
|
165
|
-
const effectiveIgnore = this.mergeIgnore(ignore);
|
|
166
|
-
// 归一化快照:旧格式(string)size 未知 → -1,走 hash 对比而非 size 预判
|
|
167
|
-
let prev;
|
|
168
|
-
if (previous) {
|
|
169
|
-
prev = {};
|
|
170
|
-
for (const [k, v] of Object.entries(previous)) {
|
|
171
|
-
prev[k] = this.normalizeFileValue(v);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
const localFiles = new Set();
|
|
175
|
-
const added = [];
|
|
176
|
-
const modified = [];
|
|
177
|
-
let count = 0;
|
|
178
|
-
let overLimit = false;
|
|
179
|
-
const walk = (dir, rel) => {
|
|
180
|
-
if (overLimit) {
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
184
|
-
const entryPath = path_1.default.join(dir, entry.name);
|
|
185
|
-
const relPath = rel ? path_1.default.join(rel, entry.name) : entry.name;
|
|
186
|
-
if (entry.isDirectory()) {
|
|
187
|
-
if (this.isIgnored(relPath, effectiveIgnore)) {
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
walk(entryPath, relPath);
|
|
191
|
-
}
|
|
192
|
-
else if (entry.isFile() && !this.isIgnored(relPath, effectiveIgnore)) {
|
|
193
|
-
count++;
|
|
194
|
-
if (maxFiles > 0 && count > maxFiles) {
|
|
195
|
-
overLimit = true;
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
const normalized = relPath.split(path_1.default.sep).join('/');
|
|
199
|
-
localFiles.add(normalized);
|
|
200
|
-
const prevEntry = prev === null || prev === void 0 ? void 0 : prev[normalized];
|
|
201
|
-
if (!prevEntry) {
|
|
202
|
-
added.push(normalized);
|
|
203
|
-
}
|
|
204
|
-
else if (prevEntry.size !== -1 && prevEntry.size !== fs_1.default.statSync(entryPath).size) {
|
|
205
|
-
// size 变化 → 内容必变,无需读文件(懒加载核心)
|
|
206
|
-
modified.push(normalized);
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
// size 相同或旧快照 size 未知 → 读内容 hash 确认
|
|
210
|
-
const hash = this.hashFile(entryPath, hashLength);
|
|
211
|
-
if (prevEntry.hash !== hash) {
|
|
212
|
-
modified.push(normalized);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
walk(projectPath, '');
|
|
219
|
-
if (overLimit) {
|
|
220
|
-
return {
|
|
221
|
-
added: [],
|
|
222
|
-
modified: [],
|
|
223
|
-
deleted: [],
|
|
224
|
-
totalChanged: 0,
|
|
225
|
-
unchanged: false,
|
|
226
|
-
overLimit: true
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
const deleted = prev
|
|
230
|
-
? Object.keys(prev).filter(k => !localFiles.has(k) && !this.isIgnored(k, effectiveIgnore))
|
|
231
|
-
: [];
|
|
232
|
-
const totalChanged = added.length + modified.length + deleted.length;
|
|
233
|
-
return { added, modified, deleted, totalChanged, unchanged: totalChanged === 0 };
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* 提取 app 配置快照(仅 diff 白名单字段)
|
|
237
|
-
* envVariables / ignore 等非 diff 字段与敏感信息不进入快照
|
|
238
|
-
*/
|
|
239
|
-
buildAppConfig(config) {
|
|
240
|
-
if (!config || typeof config !== 'object') {
|
|
241
|
-
return undefined;
|
|
242
|
-
}
|
|
243
|
-
const entry = {};
|
|
244
|
-
if (config.serviceName !== undefined) {
|
|
245
|
-
entry.serviceName = config.serviceName;
|
|
246
|
-
}
|
|
247
|
-
if (config.installCommand !== undefined) {
|
|
248
|
-
entry.installCommand = config.installCommand;
|
|
249
|
-
}
|
|
250
|
-
if (config.buildCommand !== undefined) {
|
|
251
|
-
entry.buildCommand = config.buildCommand;
|
|
252
|
-
}
|
|
253
|
-
if (config.outputDir !== undefined) {
|
|
254
|
-
entry.outputDir = config.outputDir;
|
|
255
|
-
}
|
|
256
|
-
if (config.deployPath !== undefined) {
|
|
257
|
-
entry.deployPath = config.deployPath;
|
|
258
|
-
}
|
|
259
|
-
return entry;
|
|
260
|
-
}
|
|
261
|
-
hashFile(filePath, hashLength = exports.DEFAULT_HASH_LENGTH) {
|
|
262
|
-
const content = fs_1.default.readFileSync(filePath);
|
|
263
|
-
return crypto_1.default.createHash('sha256').update(content).digest('hex').slice(0, hashLength);
|
|
264
|
-
}
|
|
265
|
-
/** 合并默认忽略规则(node_modules / .DS_Store 不可被调用方关闭) */
|
|
266
|
-
mergeIgnore(ignore) {
|
|
267
|
-
return Array.from(new Set([...DEFAULT_IGNORE, ...(ignore || [])]));
|
|
268
|
-
}
|
|
269
|
-
/** 归一化快照指纹值:旧格式(纯 string = 仅 hash,size 未知用 -1 表示) */
|
|
270
|
-
normalizeFileValue(value) {
|
|
271
|
-
if (typeof value === 'string') {
|
|
272
|
-
return { hash: value, size: -1 };
|
|
273
|
-
}
|
|
274
|
-
return value;
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* 是否命中忽略规则:
|
|
278
|
-
* - 目录模式(结尾 /):匹配该目录及其下所有
|
|
279
|
-
* - glob(含 **):转换为正则匹配相对路径
|
|
280
|
-
* - 其余:精确/前缀匹配
|
|
281
|
-
*/
|
|
282
|
-
isIgnored(relPath, ignore) {
|
|
283
|
-
const normalized = relPath.split(path_1.default.sep).join('/');
|
|
284
|
-
for (const pattern of ignore) {
|
|
285
|
-
if (this.matchPattern(normalized, pattern)) {
|
|
286
|
-
return true;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
return false;
|
|
290
|
-
}
|
|
291
|
-
matchPattern(relPath, pattern) {
|
|
292
|
-
const p = pattern.trim();
|
|
293
|
-
if (!p) {
|
|
294
|
-
return false;
|
|
295
|
-
}
|
|
296
|
-
// 目录模式(结尾 /):匹配目录本身及下所有内容
|
|
297
|
-
if (p.endsWith('/')) {
|
|
298
|
-
const prefix = p.slice(0, -1);
|
|
299
|
-
return relPath === prefix || relPath.startsWith(prefix + '/');
|
|
300
|
-
}
|
|
301
|
-
// glob(含 **):把 ** 转成 .*,其余转义后整体匹配
|
|
302
|
-
if (p.includes('**')) {
|
|
303
|
-
const regexStr = '^' +
|
|
304
|
-
p.split('**').map(part => this.escapeRegExp(part)).join('.*') +
|
|
305
|
-
'$';
|
|
306
|
-
return new RegExp(regexStr).test(relPath);
|
|
307
|
-
}
|
|
308
|
-
// 精确匹配或前缀匹配(目录下的文件)
|
|
309
|
-
return relPath === p || relPath.startsWith(p + '/');
|
|
310
|
-
}
|
|
311
|
-
escapeRegExp(s) {
|
|
312
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
313
|
-
}
|
|
314
|
-
/** 解析当前 git commit(非 git 仓库或失败返回 undefined,不阻断) */
|
|
315
|
-
resolveGitCommit() {
|
|
316
|
-
try {
|
|
317
|
-
const out = (0, child_process_1.execSync)('git rev-parse HEAD', {
|
|
318
|
-
cwd: this.cwd,
|
|
319
|
-
stdio: 'pipe'
|
|
320
|
-
});
|
|
321
|
-
return out.toString().trim() || undefined;
|
|
322
|
-
}
|
|
323
|
-
catch (_a) {
|
|
324
|
-
return undefined;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
exports.StateStore = StateStore;
|