@cloudbase/manager-node 5.7.0-beta.2 → 5.7.0-beta.3
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/lib/deploy/DeployOrchestrator.js +110 -42
- package/lib/deploy/StaticDeployer.js +0 -96
- package/lib/deploy/domain.js +5 -3
- package/lib/deploy/function/builders/cloud.js +94 -12
- package/lib/deploy/function/config-guard.js +5 -1
- package/lib/deploy/function/local-builder.js +21 -2
- package/lib/projectValidator/index.js +0 -39
- package/lib/utils/index.js +26 -9
- package/package.json +1 -1
- package/types/deploy/DeployOrchestrator.d.ts +18 -0
- package/types/deploy/StaticDeployer.d.ts +0 -7
- package/types/deploy/domain.d.ts +2 -2
- package/types/projectValidator/index.d.ts +0 -5
|
@@ -12,6 +12,7 @@ const StaticDeployer_1 = require("./StaticDeployer");
|
|
|
12
12
|
const GatewayDeployer_1 = require("./GatewayDeployer");
|
|
13
13
|
const StateStore_1 = require("./StateStore");
|
|
14
14
|
const framework_1 = require("./framework");
|
|
15
|
+
const parallel_1 = require("../utils/parallel");
|
|
15
16
|
/** 部署顺序(按依赖:database 最先,函数依赖新 Schema;gateway 最后,依赖函数/hosting) */
|
|
16
17
|
const DEPLOY_ORDER = ['database', 'functions', 'app', 'hosting', 'gateway'];
|
|
17
18
|
/**
|
|
@@ -35,10 +36,16 @@ class DeployOrchestrator {
|
|
|
35
36
|
}
|
|
36
37
|
/**
|
|
37
38
|
* 执行部署
|
|
39
|
+
*
|
|
40
|
+
* 编排模型:plan 按「连续相同资源类型」切成 segments,segment 之间严格按
|
|
41
|
+
* DEPLOY_ORDER 串行;segment 内部按 concurrency 并行执行。失败中断策略:
|
|
42
|
+
* - 默认 fail-fast:任一资源失败即中断后续(return)
|
|
43
|
+
* - continueOnError:非 database 资源失败仍继续,最后汇总失败数
|
|
44
|
+
* - database 失败始终强制中断(无论是否 continueOnError)
|
|
38
45
|
*/
|
|
39
46
|
async deploy(options) {
|
|
40
47
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
41
|
-
const { config, envId, dryRun, cwd = process.cwd(), log } = options;
|
|
48
|
+
const { config, envId, dryRun, cwd = process.cwd(), log, concurrency = 1, continueOnError = false } = options;
|
|
42
49
|
// 1. 轻量 inline 守卫(validateProject 未接入时的兜底)
|
|
43
50
|
this.guardConfig(config);
|
|
44
51
|
const plan = await this.buildPlan(config, options);
|
|
@@ -47,55 +54,89 @@ class DeployOrchestrator {
|
|
|
47
54
|
return { plan, results: [] };
|
|
48
55
|
}
|
|
49
56
|
const results = [];
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
// 按连续相同 type 切段:段间串行(依赖顺序),段内并行
|
|
58
|
+
const segments = this.segmentPlan(plan);
|
|
59
|
+
const limit = Math.max(1, Math.min(Number(concurrency) || 1, 20));
|
|
60
|
+
for (const segment of segments) {
|
|
61
|
+
// 段内预判定(顺序执行,保留交互/冲突中断语义)
|
|
62
|
+
const toExecute = [];
|
|
63
|
+
let segmentFailed = false;
|
|
64
|
+
let segmentDbFailed = false;
|
|
65
|
+
for (const item of segment) {
|
|
66
|
+
// skip:产物/配置未变更,静默跳过(不执行、不询问)
|
|
67
|
+
if (item.status === 'skip') {
|
|
68
|
+
(_b = log === null || log === void 0 ? void 0 : log.info) === null || _b === void 0 ? void 0 : _b.call(log, `[${item.type}] ${item.name}: ${item.action}`);
|
|
69
|
+
results.push({ type: item.type, name: item.name, ok: true, reason: 'skip' });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
(_c = log === null || log === void 0 ? void 0 : log.info) === null || _c === void 0 ? void 0 : _c.call(log, `[${item.type}] ${item.name}: ${item.action}`);
|
|
73
|
+
// conflict:数据库迁移冲突 → 中断整个部署(后续资源可能依赖新 Schema)
|
|
74
|
+
if (item.status === 'conflict') {
|
|
75
|
+
const message = '检测到冲突,已中断部署';
|
|
76
|
+
(_d = log === null || log === void 0 ? void 0 : log.error) === null || _d === void 0 ? void 0 : _d.call(log, `[${item.type}] ${item.name} ${message}`);
|
|
77
|
+
results.push({ type: item.type, name: item.name, ok: false, error: message });
|
|
78
|
+
if (item.type === 'database') {
|
|
79
|
+
return { plan, results };
|
|
80
|
+
}
|
|
81
|
+
segmentFailed = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// 覆盖确认:仅函数已存在(update)时触发,对齐 tcb fn deploy 的 FunctionConflictChecker 语义
|
|
85
|
+
// yes=true 直接放行;注入 confirmUpdate 按回调决定;两者都无 → 保守跳过(no-confirm)
|
|
86
|
+
if (item.type === 'functions' && item.status === 'update') {
|
|
87
|
+
let confirmed = false;
|
|
88
|
+
if (options.yes) {
|
|
89
|
+
confirmed = true;
|
|
90
|
+
}
|
|
91
|
+
else if (options.confirmUpdate) {
|
|
92
|
+
confirmed = await options.confirmUpdate(item);
|
|
93
|
+
}
|
|
94
|
+
if (!confirmed) {
|
|
95
|
+
const reason = options.confirmUpdate
|
|
96
|
+
? 'skipped-by-user'
|
|
97
|
+
: 'no-confirm';
|
|
98
|
+
(_e = log === null || log === void 0 ? void 0 : log.warn) === null || _e === void 0 ? void 0 : _e.call(log, `[${item.type}] ${item.name} 跳过覆盖更新(${reason})`);
|
|
99
|
+
results.push({ type: item.type, name: item.name, ok: false, reason });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
toExecute.push(item);
|
|
57
104
|
}
|
|
58
|
-
(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const message = '检测到冲突,已中断部署';
|
|
62
|
-
(_d = log === null || log === void 0 ? void 0 : log.error) === null || _d === void 0 ? void 0 : _d.call(log, `[${item.type}] ${item.name} ${message}`);
|
|
63
|
-
results.push({ type: item.type, name: item.name, ok: false, error: message });
|
|
64
|
-
if (item.type === 'database') {
|
|
105
|
+
if (toExecute.length === 0) {
|
|
106
|
+
// 段内无实际执行项:若本段已因 conflict 标记失败且 fail-fast,则中断
|
|
107
|
+
if (segmentFailed && !continueOnError) {
|
|
65
108
|
return { plan, results };
|
|
66
109
|
}
|
|
67
110
|
continue;
|
|
68
111
|
}
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
112
|
+
// 段内并行执行
|
|
113
|
+
const controller = new parallel_1.AsyncTaskParallelController(limit);
|
|
114
|
+
const tasks = toExecute.map((item) => async () => {
|
|
115
|
+
var _a;
|
|
116
|
+
try {
|
|
117
|
+
const stepResult = await this.executeStep({ item, config, envId, cwd });
|
|
118
|
+
results.push({ type: item.type, name: item.name, ok: true, url: stepResult });
|
|
75
119
|
}
|
|
76
|
-
|
|
77
|
-
|
|
120
|
+
catch (e) {
|
|
121
|
+
const message = (e === null || e === void 0 ? void 0 : e.message) || String(e);
|
|
122
|
+
(_a = log === null || log === void 0 ? void 0 : log.error) === null || _a === void 0 ? void 0 : _a.call(log, `[${item.type}] ${item.name} 部署失败: ${message}`);
|
|
123
|
+
results.push({ type: item.type, name: item.name, ok: false, error: message });
|
|
124
|
+
segmentFailed = true;
|
|
125
|
+
if (item.type === 'database') {
|
|
126
|
+
segmentDbFailed = true;
|
|
127
|
+
}
|
|
78
128
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
129
|
+
});
|
|
130
|
+
controller.loadTasks(tasks);
|
|
131
|
+
await controller.run();
|
|
132
|
+
// 段内失败中断决策
|
|
133
|
+
if (segmentFailed) {
|
|
134
|
+
if (segmentDbFailed) {
|
|
135
|
+
(_f = log === null || log === void 0 ? void 0 : log.error) === null || _f === void 0 ? void 0 : _f.call(log, '数据库迁移失败,中断部署');
|
|
136
|
+
return { plan, results };
|
|
86
137
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const stepResult = await this.executeStep({ item, config, envId, cwd });
|
|
90
|
-
results.push({ type: item.type, name: item.name, ok: true, url: stepResult });
|
|
91
|
-
}
|
|
92
|
-
catch (e) {
|
|
93
|
-
const message = (e === null || e === void 0 ? void 0 : e.message) || String(e);
|
|
94
|
-
(_f = log === null || log === void 0 ? void 0 : log.error) === null || _f === void 0 ? void 0 : _f.call(log, `[${item.type}] ${item.name} 部署失败: ${message}`);
|
|
95
|
-
results.push({ type: item.type, name: item.name, ok: false, error: message });
|
|
96
|
-
// database 失败中断整个部署:后续资源可能依赖新 Schema
|
|
97
|
-
if (item.type === 'database') {
|
|
98
|
-
(_g = log === null || log === void 0 ? void 0 : log.error) === null || _g === void 0 ? void 0 : _g.call(log, '数据库迁移失败,中断部署');
|
|
138
|
+
if (!continueOnError) {
|
|
139
|
+
(_g = log === null || log === void 0 ? void 0 : log.error) === null || _g === void 0 ? void 0 : _g.call(log, '存在资源部署失败,中断部署');
|
|
99
140
|
return { plan, results };
|
|
100
141
|
}
|
|
101
142
|
}
|
|
@@ -106,6 +147,33 @@ class DeployOrchestrator {
|
|
|
106
147
|
}
|
|
107
148
|
return { plan, results };
|
|
108
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* 将扁平 plan 按「连续相同资源类型」切成有序 segments
|
|
152
|
+
*
|
|
153
|
+
* segment 之间保留 DEPLOY_ORDER 依赖顺序(严格串行);segment 内部为同类型
|
|
154
|
+
* 多实例,可并行执行。例如 [db, fnA, fnB, hostingX, hostingY, gateway]
|
|
155
|
+
* → [[db], [fnA, fnB], [hostingX, hostingY], [gateway]]
|
|
156
|
+
*/
|
|
157
|
+
segmentPlan(plan) {
|
|
158
|
+
const segments = [];
|
|
159
|
+
let current = [];
|
|
160
|
+
let currentType;
|
|
161
|
+
for (const item of plan) {
|
|
162
|
+
if (currentType === undefined || item.type === currentType) {
|
|
163
|
+
current.push(item);
|
|
164
|
+
currentType = item.type;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
segments.push(current);
|
|
168
|
+
current = [item];
|
|
169
|
+
currentType = item.type;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (current.length > 0) {
|
|
173
|
+
segments.push(current);
|
|
174
|
+
}
|
|
175
|
+
return segments;
|
|
176
|
+
}
|
|
109
177
|
/**
|
|
110
178
|
* 部署成功后写回本地 state 快照
|
|
111
179
|
*
|
|
@@ -57,7 +57,6 @@ class StaticDeployer {
|
|
|
57
57
|
* 访问地址 = https://{CdnDomain}{deployPath}(与 tcb hosting deploy 一致)
|
|
58
58
|
*/
|
|
59
59
|
async deployHosting(options) {
|
|
60
|
-
var _a, _b;
|
|
61
60
|
const { config, cwd = process.cwd() } = options;
|
|
62
61
|
const name = config.name || 'default';
|
|
63
62
|
const root = path_1.default.resolve(cwd, config.root || '.');
|
|
@@ -80,73 +79,6 @@ class StaticDeployer {
|
|
|
80
79
|
ignore: config.ignore || ['.DS_Store']
|
|
81
80
|
};
|
|
82
81
|
await this.environment.getHostingService().uploadFiles(hostingOptions);
|
|
83
|
-
// SPA 回退:
|
|
84
|
-
// - true:404 时返回 index.html(OriginalHttpStatus=Disabled 保留 200,Charity404=Disabled 防公益页面覆盖)
|
|
85
|
-
// ErrorDocument 不带 deployPath 前缀:TCB 托管会把 ErrorDocument 相对 deployPath 解析
|
|
86
|
-
// (index.html → web-pay-0804-test-ignore8/index.html),加前缀反而双重嵌套找不到
|
|
87
|
-
// - false:显式取消,重置 ErrorDocument 为空(静态托管首页 IndexDocument 保留)
|
|
88
|
-
// - undefined:不关心,保持云端现状
|
|
89
|
-
// 注意:putBucketWebsite 是整体覆盖,设置前先读取现有配置并保留 RoutingRules + AutoAddressing,
|
|
90
|
-
// 否则会清空用户之前在控制台配置的路由规则(如路径前缀重写/错误码重定向)和忽略 .html 扩展名能力
|
|
91
|
-
// 仅移除当前 deployPath 对应的 SPA 回退规则:
|
|
92
|
-
// cloudPath='/' => index.html;cloudPath='/web' => web/index.html
|
|
93
|
-
// 不能泛化删除所有 */index.html,否则会误删用户自定义的 404 重定向规则
|
|
94
|
-
const indexKey = cloudPath === '/' ? 'index.html' : `${cloudPath.replace(/^\//, '')}/index.html`;
|
|
95
|
-
const isCurrentSpaFallbackRedirect = (r) => {
|
|
96
|
-
if (r.httpErrorCodeReturnedEquals !== '404')
|
|
97
|
-
return false;
|
|
98
|
-
return r.replaceKeyWith === indexKey;
|
|
99
|
-
};
|
|
100
|
-
// 开启 SPA 回退时,清理历史上指向 index.html 的 404 重定向(含 ReplaceKeyPrefixWith)
|
|
101
|
-
// 避免 302 到 cos-website 与 ErrorDocument= index.html 冲突
|
|
102
|
-
const isLegacySpaIndexFallbackRedirect = (r) => {
|
|
103
|
-
var _a, _b;
|
|
104
|
-
if (r.httpErrorCodeReturnedEquals !== '404')
|
|
105
|
-
return false;
|
|
106
|
-
if (r.replaceKeyWith === 'index.html')
|
|
107
|
-
return true;
|
|
108
|
-
if ((_a = r.replaceKeyWith) === null || _a === void 0 ? void 0 : _a.endsWith('/index.html'))
|
|
109
|
-
return true;
|
|
110
|
-
if (r.replaceKeyPrefixWith === 'index.html')
|
|
111
|
-
return true;
|
|
112
|
-
if ((_b = r.replaceKeyPrefixWith) === null || _b === void 0 ? void 0 : _b.endsWith('/index.html'))
|
|
113
|
-
return true;
|
|
114
|
-
return false;
|
|
115
|
-
};
|
|
116
|
-
if (config.spaFallback === true) {
|
|
117
|
-
try {
|
|
118
|
-
const preserved = await this.preserveWebsiteConfig();
|
|
119
|
-
await this.environment.getHostingService().setWebsiteDocument({
|
|
120
|
-
indexDocument: 'index.html',
|
|
121
|
-
errorDocument: 'index.html',
|
|
122
|
-
originalHttpStatus: 'Disabled',
|
|
123
|
-
charity404: 'Disabled',
|
|
124
|
-
routingRules: (preserved.routingRules || []).filter((r) => !isLegacySpaIndexFallbackRedirect(r)),
|
|
125
|
-
// 优先保留云端 AutoAddressing(避免覆盖控制台配置),缺失时再兜底 Disabled
|
|
126
|
-
autoAddressing: (_a = preserved.autoAddressing) !== null && _a !== void 0 ? _a : 'Disabled'
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
catch (e) {
|
|
130
|
-
throw new error_1.CloudBaseError(`[${name}] 设置 SPA 回退失败(spaFallback: true):${(e === null || e === void 0 ? void 0 : e.message) || String(e)}`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
else if (config.spaFallback === false) {
|
|
134
|
-
try {
|
|
135
|
-
const preserved = await this.preserveWebsiteConfig();
|
|
136
|
-
await this.environment.getHostingService().setWebsiteDocument({
|
|
137
|
-
indexDocument: 'index.html',
|
|
138
|
-
errorDocument: '',
|
|
139
|
-
// 仅移除当前 deployPath 对应的 SPA 回退规则(404 + replaceKeyWith=indexKey)
|
|
140
|
-
// 保留用户自定义 404 规则和其他路由规则
|
|
141
|
-
routingRules: (preserved.routingRules || []).filter((r) => !isCurrentSpaFallbackRedirect(r)),
|
|
142
|
-
// 优先保留云端 AutoAddressing(避免覆盖控制台配置),缺失时再兜底 Enabled
|
|
143
|
-
autoAddressing: (_b = preserved.autoAddressing) !== null && _b !== void 0 ? _b : 'Enabled'
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
catch (e) {
|
|
147
|
-
throw new error_1.CloudBaseError(`[${name}] 取消 SPA 回退失败(spaFallback: false):${(e === null || e === void 0 ? void 0 : e.message) || String(e)}`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
82
|
const url = await this.resolveHostingUrl(cloudPath);
|
|
151
83
|
return { name, url, wroteRecord: false };
|
|
152
84
|
}
|
|
@@ -234,34 +166,6 @@ class StaticDeployer {
|
|
|
234
166
|
const url = await this.resolveAppUrl(serviceName, config.deployPath || `/${serviceName}`);
|
|
235
167
|
return { name: serviceName, url, wroteRecord: true };
|
|
236
168
|
}
|
|
237
|
-
/**
|
|
238
|
-
* 读取现有静态网站配置并保留 RoutingRules + AutoAddressing
|
|
239
|
-
* (putBucketWebsite 为整体覆盖,设置 SPA 回退前必须带回已有配置,否则会被清空)
|
|
240
|
-
*/
|
|
241
|
-
async preserveWebsiteConfig() {
|
|
242
|
-
var _a;
|
|
243
|
-
try {
|
|
244
|
-
const res = await this.environment.getHostingService().getWebsiteConfig();
|
|
245
|
-
const website = (res === null || res === void 0 ? void 0 : res.WebsiteConfiguration) || {};
|
|
246
|
-
const rules = website.RoutingRules || [];
|
|
247
|
-
return {
|
|
248
|
-
routingRules: rules.map((r) => {
|
|
249
|
-
var _a, _b, _c, _d;
|
|
250
|
-
return ({
|
|
251
|
-
keyPrefixEquals: (_a = r.Condition) === null || _a === void 0 ? void 0 : _a.KeyPrefixEquals,
|
|
252
|
-
httpErrorCodeReturnedEquals: (_b = r.Condition) === null || _b === void 0 ? void 0 : _b.HttpErrorCodeReturnedEquals,
|
|
253
|
-
replaceKeyWith: (_c = r.Redirect) === null || _c === void 0 ? void 0 : _c.ReplaceKeyWith,
|
|
254
|
-
replaceKeyPrefixWith: (_d = r.Redirect) === null || _d === void 0 ? void 0 : _d.ReplaceKeyPrefixWith
|
|
255
|
-
});
|
|
256
|
-
}),
|
|
257
|
-
autoAddressing: (_a = website.AutoAddressing) === null || _a === void 0 ? void 0 : _a.Status
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
catch (_b) {
|
|
261
|
-
// 查询失败(未开启静态网站/首次部署)视为无已有配置
|
|
262
|
-
return { routingRules: [], autoAddressing: undefined };
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
169
|
/**
|
|
266
170
|
* 解析 hosting 访问地址:https://{CdnDomain}{deployPath}
|
|
267
171
|
*/
|
package/lib/deploy/domain.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resolveAccessServiceDomain = resolveAccessServiceDomain;
|
|
4
4
|
exports.getGatewayBaseUrl = getGatewayBaseUrl;
|
|
5
|
+
/** 环境 HTTP 访问服务默认域名后缀(如 `xxx-1326375956.ap-shanghai.${ACCESS_DOMAIN_SUFFIX}`) */
|
|
6
|
+
const ACCESS_DOMAIN_SUFFIX = 'app.tcloudbase.com';
|
|
5
7
|
/**
|
|
6
|
-
* 构造环境 HTTP 访问服务默认域名 `${envId}-${AppId}.${region}
|
|
8
|
+
* 构造环境 HTTP 访问服务默认域名 `${envId}-${AppId}.${region}.${ACCESS_DOMAIN_SUFFIX}`
|
|
7
9
|
*
|
|
8
10
|
* 关键:用 `UserInfo.AppId`(腾讯云 appid)而非 `UserInfo.Uin`(账号 uin)。
|
|
9
|
-
* 控制台默认域名格式是 `${envId}-${AppId}.${region}
|
|
11
|
+
* 控制台默认域名格式是 `${envId}-${AppId}.${region}.${ACCESS_DOMAIN_SUFFIX}`
|
|
10
12
|
* (如 `xxx-1326375956.ap-shanghai.app.tcloudbase.com`),与 cookie 中的 `appid` 字段对应。
|
|
11
13
|
* AppId/Region 缺失或查询失败时返回 undefined
|
|
12
14
|
*/
|
|
@@ -17,7 +19,7 @@ async function resolveAccessServiceDomain(environment, envId) {
|
|
|
17
19
|
const appId = (_a = EnvInfo === null || EnvInfo === void 0 ? void 0 : EnvInfo.UserInfo) === null || _a === void 0 ? void 0 : _a.AppId;
|
|
18
20
|
const region = EnvInfo === null || EnvInfo === void 0 ? void 0 : EnvInfo.Region;
|
|
19
21
|
if (appId && region) {
|
|
20
|
-
return `${envId}-${appId}.${region}
|
|
22
|
+
return `${envId}-${appId}.${region}.${ACCESS_DOMAIN_SUFFIX}`;
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
catch (_b) {
|
|
@@ -41,6 +41,12 @@ const POLL_MAX_TRANSIENT_ERRORS = 5;
|
|
|
41
41
|
const SAFE_REGISTRY_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._\-/]*$/;
|
|
42
42
|
/** tag 白名单:字母数字、下划线、点、中划线 */
|
|
43
43
|
const SAFE_TAG_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
|
|
44
|
+
/** Dockerfile 路径:只能是构建上下文内的安全相对路径 */
|
|
45
|
+
const SAFE_DOCKERFILE_PATTERN = /^[a-zA-Z0-9._\-/]+$/;
|
|
46
|
+
/** Docker build arg key 与环境变量同规则 */
|
|
47
|
+
const SAFE_BUILD_ARG_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
48
|
+
/** 疑似敏感构建参数名;cloud 构建会把参数写入命令/构建记录,因此直接拒绝 */
|
|
49
|
+
const SENSITIVE_BUILD_ARG_KEY_PATTERN = /(secret|token|password|passwd|credential|private[_-]?key|access[_-]?key)/i;
|
|
44
50
|
/** 校验仓库/命名空间等镜像地址片段,拒绝命令注入风险字符 */
|
|
45
51
|
function assertSafeRegistryToken(value, field) {
|
|
46
52
|
if (!SAFE_REGISTRY_PATTERN.test(value)) {
|
|
@@ -62,6 +68,40 @@ function assertSafeTag(tag) {
|
|
|
62
68
|
});
|
|
63
69
|
}
|
|
64
70
|
}
|
|
71
|
+
/** 校验 Dockerfile 路径,避免通过 shell command 逃逸构建上下文 */
|
|
72
|
+
function assertSafeDockerfile(dockerfile) {
|
|
73
|
+
const value = dockerfile.trim();
|
|
74
|
+
const segments = value.split('/');
|
|
75
|
+
if (!value ||
|
|
76
|
+
value.length > 512 ||
|
|
77
|
+
value.startsWith('/') ||
|
|
78
|
+
value.startsWith('\\') ||
|
|
79
|
+
/^[A-Za-z]:[\\/]/.test(value) ||
|
|
80
|
+
!SAFE_DOCKERFILE_PATTERN.test(value) ||
|
|
81
|
+
segments.some(segment => segment === '..')) {
|
|
82
|
+
throw new error_1.CloudBaseError('Dockerfile 路径不合法,需为构建上下文内的安全相对路径', {
|
|
83
|
+
code: types_1.FUNCTION_DEPLOY_ERROR.BUILD_DOCKERFILE_INVALID
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** 校验 cloud 构建参数;值进入 CloudApp Env,command 中只引用固定变量名 */
|
|
88
|
+
function assertSafeBuildArgs(buildArgs) {
|
|
89
|
+
for (const [key, value] of Object.entries(buildArgs)) {
|
|
90
|
+
if (!SAFE_BUILD_ARG_KEY_PATTERN.test(key)) {
|
|
91
|
+
throw new error_1.CloudBaseError(`构建参数名不合法:${key}`, {
|
|
92
|
+
code: types_1.FUNCTION_DEPLOY_ERROR.BUILD_ARG_INVALID
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (SENSITIVE_BUILD_ARG_KEY_PATTERN.test(key)) {
|
|
96
|
+
throw new error_1.CloudBaseError(`构建参数 ${key} 可能包含敏感信息;cloud 构建禁止通过 buildArgs 传递密钥`, { code: types_1.FUNCTION_DEPLOY_ERROR.BUILD_ARG_INVALID });
|
|
97
|
+
}
|
|
98
|
+
if (typeof value !== 'string' || /[\u0000]/.test(value)) {
|
|
99
|
+
throw new error_1.CloudBaseError(`构建参数 ${key} 的值必须是无 NUL 字符的字符串`, {
|
|
100
|
+
code: types_1.FUNCTION_DEPLOY_ERROR.BUILD_ARG_INVALID
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
65
105
|
/** 个人版 CCR 默认域名 */
|
|
66
106
|
const DEFAULT_PERSONAL_REGISTRY = 'ccr.ccs.tencentyun.com';
|
|
67
107
|
/** 注入到构建上下文 zip 中的推送脚本相对路径(与 CustomSteps 的 push命令一致) */
|
|
@@ -222,18 +262,50 @@ function resolveTarget(config) {
|
|
|
222
262
|
};
|
|
223
263
|
}
|
|
224
264
|
/**
|
|
225
|
-
*
|
|
265
|
+
* 解析并校验 cloud 构建执行参数。
|
|
266
|
+
* 用户值不直接拼入 shell:Dockerfile/platform/buildArgs value 均通过 CloudApp Env 注入,
|
|
267
|
+
* CustomSteps 只包含 SDK 固定命令和经过白名单校验的 build arg key。
|
|
268
|
+
*/
|
|
269
|
+
function resolveCloudBuildOptions(config) {
|
|
270
|
+
const dockerfile = config.build.dockerfile || types_1.DEFAULT_DOCKERFILE;
|
|
271
|
+
const platform = config.build.platform || types_1.DEFAULT_IMAGE_PLATFORM;
|
|
272
|
+
const buildArgs = config.build.buildArgs || {};
|
|
273
|
+
assertSafeDockerfile(dockerfile);
|
|
274
|
+
if (platform !== types_1.DEFAULT_IMAGE_PLATFORM) {
|
|
275
|
+
throw new error_1.CloudBaseError(`当前仅支持构建 ${types_1.DEFAULT_IMAGE_PLATFORM} 镜像`, {
|
|
276
|
+
code: types_1.FUNCTION_DEPLOY_ERROR.BUILD_PLATFORM_INVALID
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
assertSafeBuildArgs(buildArgs);
|
|
280
|
+
return {
|
|
281
|
+
dockerfile,
|
|
282
|
+
platform,
|
|
283
|
+
buildArgs,
|
|
284
|
+
forceBuild: config.build.forceBuild === true
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* 生成固定的 CustomSteps 模板(build + push)。
|
|
226
289
|
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* 这些变量的值已在 resolveTarget 中做白名单校验,故命令本身无注入面。
|
|
290
|
+
* Dockerfile、platform 和 build arg value 通过 CloudApp Env 注入;只有通过严格
|
|
291
|
+
* ENV key 校验的 build arg key 会进入固定 command。forceBuild 显式映射为 --no-cache。
|
|
230
292
|
*/
|
|
231
|
-
function buildCustomSteps() {
|
|
293
|
+
function buildCustomSteps(options) {
|
|
294
|
+
const buildArgs = Object.keys(options.buildArgs)
|
|
295
|
+
.sort()
|
|
296
|
+
.map(key => `--build-arg ${key}="$BUILD_ARG_${key}"`)
|
|
297
|
+
.join(' ');
|
|
298
|
+
const noCache = options.forceBuild ? ' --no-cache' : '';
|
|
299
|
+
const extraArgs = buildArgs ? ` ${buildArgs}` : '';
|
|
232
300
|
return [
|
|
233
301
|
{
|
|
234
302
|
name: 'build-image',
|
|
235
|
-
command: 'docker build
|
|
236
|
-
'$
|
|
303
|
+
command: 'docker build' +
|
|
304
|
+
' --platform "$CLOUDBASE_BUILD_PLATFORM"' +
|
|
305
|
+
' -f "$CLOUDBASE_DOCKERFILE"' +
|
|
306
|
+
noCache +
|
|
307
|
+
extraArgs +
|
|
308
|
+
' -t $TCR_REGISTRY/$TCR_NAMESPACE/$CLOUDBASE_SERVICE_NAME:$CLOUDBASE_VERSION_NAME .'
|
|
237
309
|
},
|
|
238
310
|
{
|
|
239
311
|
name: 'push-image',
|
|
@@ -249,10 +321,16 @@ function buildCustomSteps() {
|
|
|
249
321
|
* - TCR_INSTANCE_ID:仅企业版 TCR 注入,脚本据此走 CreateInstanceToken 分支;
|
|
250
322
|
* 个人版 CCR 不注入,脚本走 STS 直接 docker login 分支。
|
|
251
323
|
*/
|
|
252
|
-
function buildEnv(target, region) {
|
|
324
|
+
function buildEnv(target, buildOptions, region) {
|
|
253
325
|
const env = [
|
|
254
326
|
{ key: 'TCR_REGISTRY', value: target.registry },
|
|
255
|
-
{ key: 'TCR_NAMESPACE', value: target.namespace }
|
|
327
|
+
{ key: 'TCR_NAMESPACE', value: target.namespace },
|
|
328
|
+
{ key: 'CLOUDBASE_DOCKERFILE', value: buildOptions.dockerfile },
|
|
329
|
+
{ key: 'CLOUDBASE_BUILD_PLATFORM', value: buildOptions.platform },
|
|
330
|
+
...Object.entries(buildOptions.buildArgs).map(([key, value]) => ({
|
|
331
|
+
key: `BUILD_ARG_${key}`,
|
|
332
|
+
value
|
|
333
|
+
}))
|
|
256
334
|
];
|
|
257
335
|
if (region) {
|
|
258
336
|
env.push({ key: 'TCR_REGION', value: region });
|
|
@@ -331,7 +409,8 @@ function summarizeFailedSteps(steps) {
|
|
|
331
409
|
async function buildImageOnCloud(config, service, options = {}) {
|
|
332
410
|
const { onLog, region } = options;
|
|
333
411
|
const target = resolveTarget(config);
|
|
334
|
-
|
|
412
|
+
const buildOptions = resolveCloudBuildOptions(config);
|
|
413
|
+
// 在打包和上传前完成真实消费字段校验,避免无效配置触发远端资源操作。
|
|
335
414
|
const registryCredentials = buildRegistryCredentials(config, target);
|
|
336
415
|
const serviceName = target.serviceName;
|
|
337
416
|
const cwd = config.build.cwd || config.functionPath || process.cwd();
|
|
@@ -361,9 +440,12 @@ async function buildImageOnCloud(config, service, options = {}) {
|
|
|
361
440
|
const created = await service.createBuild({
|
|
362
441
|
serviceName,
|
|
363
442
|
cosTimestamp: cosInfo.unixTimestamp,
|
|
364
|
-
env: [
|
|
443
|
+
env: [
|
|
444
|
+
...buildEnv(target, buildOptions, region),
|
|
445
|
+
...registryCredentials.env
|
|
446
|
+
],
|
|
365
447
|
secrets: registryCredentials.secrets,
|
|
366
|
-
customSteps: buildCustomSteps()
|
|
448
|
+
customSteps: buildCustomSteps(buildOptions)
|
|
367
449
|
});
|
|
368
450
|
if (!(created === null || created === void 0 ? void 0 : created.versionName)) {
|
|
369
451
|
throw new error_1.CloudBaseError('触发云端构建失败:返回缺少 VersionName', {
|
|
@@ -449,7 +449,11 @@ function checkBuildTarget(raw, checks) {
|
|
|
449
449
|
checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_ARG_INVALID, `构建参数 ${key} 的值必须是字符串`));
|
|
450
450
|
}
|
|
451
451
|
if (SENSITIVE_KEY_PATTERN.test(key)) {
|
|
452
|
-
|
|
452
|
+
const message = `构建参数 ${key} 可能包含敏感信息`;
|
|
453
|
+
const remediation = '构建参数会写入镜像构建记录,请改用运行时环境变量或密钥管理';
|
|
454
|
+
checks.push(raw.buildStrategy === 'cloud'
|
|
455
|
+
? fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_ARG_INVALID, message, remediation)
|
|
456
|
+
: warn(types_1.FUNCTION_DEPLOY_ERROR.BUILD_ARG_INVALID, message, remediation));
|
|
453
457
|
}
|
|
454
458
|
});
|
|
455
459
|
}
|
|
@@ -64,6 +64,22 @@ function parseDigest(output) {
|
|
|
64
64
|
const match = output.match(/Digest:\s*(sha256:[0-9a-f]{64})/i);
|
|
65
65
|
return match ? match[1] : undefined;
|
|
66
66
|
}
|
|
67
|
+
/** 生成脱敏后的命令摘要,避免 buildArgs 值进入部署日志 */
|
|
68
|
+
function formatBuildCommandForLog(args) {
|
|
69
|
+
const displayArgs = [];
|
|
70
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
71
|
+
const arg = args[index];
|
|
72
|
+
displayArgs.push(arg);
|
|
73
|
+
if (arg === '--build-arg' && index + 1 < args.length) {
|
|
74
|
+
const buildArg = args[index + 1];
|
|
75
|
+
const separatorIndex = buildArg.indexOf('=');
|
|
76
|
+
const key = separatorIndex >= 0 ? buildArg.slice(0, separatorIndex) : buildArg;
|
|
77
|
+
displayArgs.push(`${key}=<redacted>`);
|
|
78
|
+
index += 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return `docker ${displayArgs.join(' ')}`;
|
|
82
|
+
}
|
|
67
83
|
/**
|
|
68
84
|
* 本地构建镜像并推送到目标仓库
|
|
69
85
|
*
|
|
@@ -90,16 +106,19 @@ async function buildAndPushImage(config, onLog, deps = defaultDeps) {
|
|
|
90
106
|
const platform = config.build.platform || types_1.DEFAULT_IMAGE_PLATFORM;
|
|
91
107
|
// 组装 buildx 参数:构建目标平台镜像并直接推送
|
|
92
108
|
const args = ['buildx', 'build', '--platform', platform, '-f', dockerfilePath, '-t', imageUri];
|
|
109
|
+
if (config.build.forceBuild) {
|
|
110
|
+
args.push('--no-cache');
|
|
111
|
+
}
|
|
93
112
|
// 追加构建参数(键值均作为独立参数,无 shell 拼接)
|
|
94
113
|
const buildArgs = config.build.buildArgs || {};
|
|
95
114
|
for (const key of Object.keys(buildArgs)) {
|
|
96
115
|
args.push('--build-arg', `${key}=${buildArgs[key]}`);
|
|
97
116
|
}
|
|
98
117
|
args.push('--push', context);
|
|
99
|
-
onLog === null || onLog === void 0 ? void 0 : onLog(`执行
|
|
118
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`执行 ${formatBuildCommandForLog(args)}`);
|
|
100
119
|
const build = await deps.runCommand('docker', args);
|
|
101
120
|
if (!build.ok) {
|
|
102
|
-
throw new error_1.CloudBaseError(
|
|
121
|
+
throw new error_1.CloudBaseError('镜像构建或推送失败,请查看 Docker 构建输出定位具体原因', {
|
|
103
122
|
code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_PUSH_FAILED
|
|
104
123
|
});
|
|
105
124
|
}
|
|
@@ -284,7 +284,6 @@ class ProjectValidator {
|
|
|
284
284
|
this.validateGatewayTargets(config, errors);
|
|
285
285
|
this.validateGatewayPathRewriteConflicts(config, warnings);
|
|
286
286
|
this.validateHostingSubPathBaseRisks(config, warnings);
|
|
287
|
-
this.validateSpaFallbackPrefixIsolation(config, warnings);
|
|
288
287
|
}
|
|
289
288
|
/**
|
|
290
289
|
* 检测 hosting 路由中 enablePathTransmission=true 与自动生成 pathRewrite 的冲突
|
|
@@ -349,44 +348,6 @@ class ProjectValidator {
|
|
|
349
348
|
});
|
|
350
349
|
});
|
|
351
350
|
}
|
|
352
|
-
/**
|
|
353
|
-
* spaFallback 为站点级回退能力,不是按网关前缀独立生效。
|
|
354
|
-
* 同一 hosting 在 spaFallback=true 下被多个网关前缀复用时,容易出现不同前缀回退串扰。
|
|
355
|
-
*/
|
|
356
|
-
validateSpaFallbackPrefixIsolation(config, warnings) {
|
|
357
|
-
var _a;
|
|
358
|
-
const routePathsByHosting = new Map();
|
|
359
|
-
(((_a = config.gateway) === null || _a === void 0 ? void 0 : _a.routes) || []).forEach((route) => {
|
|
360
|
-
var _a;
|
|
361
|
-
if (!((_a = route.target) === null || _a === void 0 ? void 0 : _a.startsWith('hosting:')))
|
|
362
|
-
return;
|
|
363
|
-
const hostingName = route.target.slice('hosting:'.length);
|
|
364
|
-
const pathValue = typeof route.path === 'string' ? route.path.trim() : '';
|
|
365
|
-
if (!pathValue)
|
|
366
|
-
return;
|
|
367
|
-
if (!routePathsByHosting.has(hostingName)) {
|
|
368
|
-
routePathsByHosting.set(hostingName, new Set());
|
|
369
|
-
}
|
|
370
|
-
routePathsByHosting.get(hostingName).add(pathValue);
|
|
371
|
-
});
|
|
372
|
-
(config.hosting || []).forEach((hosting, index) => {
|
|
373
|
-
if (hosting.spaFallback !== true)
|
|
374
|
-
return;
|
|
375
|
-
if (!hosting.name)
|
|
376
|
-
return;
|
|
377
|
-
const routePaths = [...(routePathsByHosting.get(hosting.name) || new Set())];
|
|
378
|
-
if (routePaths.length <= 1)
|
|
379
|
-
return;
|
|
380
|
-
warnings.push({
|
|
381
|
-
level: 'warning',
|
|
382
|
-
resource: `hosting[${index}]`,
|
|
383
|
-
field: 'spaFallback',
|
|
384
|
-
message: `hosting:${hosting.name} 在 spaFallback=true 下被多个网关前缀复用(${routePaths.join(', ')}),` +
|
|
385
|
-
'可能出现前缀间 404 回退串扰',
|
|
386
|
-
fix: '建议将不同前缀拆分为独立 hosting 服务,或为每个前缀显式配置独立回退策略',
|
|
387
|
-
});
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
351
|
validateUniqueNames(resource, items, errors) {
|
|
391
352
|
const names = new Map();
|
|
392
353
|
items.forEach((item, index) => {
|
package/lib/utils/index.js
CHANGED
|
@@ -58,6 +58,17 @@ async function compressToZip(option) {
|
|
|
58
58
|
fs_extra_1.default.mkdirpSync(extraTmpDir);
|
|
59
59
|
}
|
|
60
60
|
return new Promise((resolve, reject) => {
|
|
61
|
+
// 幂等清理:archiver 的 error 事件与 output.close 事件在不同代码路径上,
|
|
62
|
+
// 但 extraEntries 写入异常分支调用 archive.abort() 后的异步收尾流程
|
|
63
|
+
// 仍可能间接触发 archive.error,导致 cleanupExtra 被多次调用。
|
|
64
|
+
// 用 done 标志保证清理和 resolve/reject 仅生效一次。
|
|
65
|
+
let done = false;
|
|
66
|
+
const settle = (fn) => {
|
|
67
|
+
if (done)
|
|
68
|
+
return;
|
|
69
|
+
done = true;
|
|
70
|
+
fn();
|
|
71
|
+
};
|
|
61
72
|
const cleanupExtra = () => {
|
|
62
73
|
if (extraTmpDir) {
|
|
63
74
|
try {
|
|
@@ -71,15 +82,19 @@ async function compressToZip(option) {
|
|
|
71
82
|
const output = fs_extra_1.default.createWriteStream(outputPath);
|
|
72
83
|
const archive = (0, archiver_1.default)('zip');
|
|
73
84
|
output.on('close', function () {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
settle(() => {
|
|
86
|
+
cleanupExtra();
|
|
87
|
+
resolve({
|
|
88
|
+
zipPath: outputPath,
|
|
89
|
+
size: Math.ceil(archive.pointer() / 1024)
|
|
90
|
+
});
|
|
78
91
|
});
|
|
79
92
|
});
|
|
80
93
|
archive.on('error', function (err) {
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
settle(() => {
|
|
95
|
+
cleanupExtra();
|
|
96
|
+
reject(err);
|
|
97
|
+
});
|
|
83
98
|
});
|
|
84
99
|
archive.pipe(output);
|
|
85
100
|
// append files from a glob pattern
|
|
@@ -102,9 +117,11 @@ async function compressToZip(option) {
|
|
|
102
117
|
});
|
|
103
118
|
}
|
|
104
119
|
catch (error) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
120
|
+
settle(() => {
|
|
121
|
+
cleanupExtra();
|
|
122
|
+
archive.abort();
|
|
123
|
+
reject(error);
|
|
124
|
+
});
|
|
108
125
|
return;
|
|
109
126
|
}
|
|
110
127
|
}
|
package/package.json
CHANGED
|
@@ -20,6 +20,10 @@ export interface IDeployOptions {
|
|
|
20
20
|
confirmUpdate?: (item: IDeployPlanItem) => Promise<boolean>;
|
|
21
21
|
/** 强制云端对比(漂移检测):忽略本地 state 快照的 skip 判定,强制重新对比/部署 */
|
|
22
22
|
refresh?: boolean;
|
|
23
|
+
/** 同类型资源最大并行数,默认 1(串行)。仅作用于同类型连续实例之间,跨类型依赖顺序不变 */
|
|
24
|
+
concurrency?: number;
|
|
25
|
+
/** 失败后继续部署其余资源(database 失败仍强制中断) */
|
|
26
|
+
continueOnError?: boolean;
|
|
23
27
|
log?: {
|
|
24
28
|
info?: (msg: string) => void;
|
|
25
29
|
success?: (msg: string) => void;
|
|
@@ -85,8 +89,22 @@ export declare class DeployOrchestrator {
|
|
|
85
89
|
deployPlan(options: IDeployOptions): Promise<IDeployPlanItem[]>;
|
|
86
90
|
/**
|
|
87
91
|
* 执行部署
|
|
92
|
+
*
|
|
93
|
+
* 编排模型:plan 按「连续相同资源类型」切成 segments,segment 之间严格按
|
|
94
|
+
* DEPLOY_ORDER 串行;segment 内部按 concurrency 并行执行。失败中断策略:
|
|
95
|
+
* - 默认 fail-fast:任一资源失败即中断后续(return)
|
|
96
|
+
* - continueOnError:非 database 资源失败仍继续,最后汇总失败数
|
|
97
|
+
* - database 失败始终强制中断(无论是否 continueOnError)
|
|
88
98
|
*/
|
|
89
99
|
deploy(options: IDeployOptions): Promise<IDeployResult>;
|
|
100
|
+
/**
|
|
101
|
+
* 将扁平 plan 按「连续相同资源类型」切成有序 segments
|
|
102
|
+
*
|
|
103
|
+
* segment 之间保留 DEPLOY_ORDER 依赖顺序(严格串行);segment 内部为同类型
|
|
104
|
+
* 多实例,可并行执行。例如 [db, fnA, fnB, hostingX, hostingY, gateway]
|
|
105
|
+
* → [[db], [fnA, fnB], [hostingX, hostingY], [gateway]]
|
|
106
|
+
*/
|
|
107
|
+
private segmentPlan;
|
|
90
108
|
/**
|
|
91
109
|
* 部署成功后写回本地 state 快照
|
|
92
110
|
*
|
|
@@ -28,8 +28,6 @@ export interface IHostingDeployConfig {
|
|
|
28
28
|
ignore?: string[];
|
|
29
29
|
/** 构建时环境变量(注入本地构建进程,非敏感) */
|
|
30
30
|
envVariables?: Record<string, string | number | boolean>;
|
|
31
|
-
/** SPA 回退:true 时 404 返回 index.html */
|
|
32
|
-
spaFallback?: boolean;
|
|
33
31
|
}
|
|
34
32
|
export interface IStaticDeployResult {
|
|
35
33
|
name: string;
|
|
@@ -80,11 +78,6 @@ export declare class StaticDeployer {
|
|
|
80
78
|
* app 云端构建:上传源码 ZIP → createApp 云端构建 → 写版本记录
|
|
81
79
|
*/
|
|
82
80
|
private deployAppCloud;
|
|
83
|
-
/**
|
|
84
|
-
* 读取现有静态网站配置并保留 RoutingRules + AutoAddressing
|
|
85
|
-
* (putBucketWebsite 为整体覆盖,设置 SPA 回退前必须带回已有配置,否则会被清空)
|
|
86
|
-
*/
|
|
87
|
-
private preserveWebsiteConfig;
|
|
88
81
|
/**
|
|
89
82
|
* 解析 hosting 访问地址:https://{CdnDomain}{deployPath}
|
|
90
83
|
*/
|
package/types/deploy/domain.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Environment } from '../environment';
|
|
2
2
|
/**
|
|
3
|
-
* 构造环境 HTTP 访问服务默认域名 `${envId}-${AppId}.${region}
|
|
3
|
+
* 构造环境 HTTP 访问服务默认域名 `${envId}-${AppId}.${region}.${ACCESS_DOMAIN_SUFFIX}`
|
|
4
4
|
*
|
|
5
5
|
* 关键:用 `UserInfo.AppId`(腾讯云 appid)而非 `UserInfo.Uin`(账号 uin)。
|
|
6
|
-
* 控制台默认域名格式是 `${envId}-${AppId}.${region}
|
|
6
|
+
* 控制台默认域名格式是 `${envId}-${AppId}.${region}.${ACCESS_DOMAIN_SUFFIX}`
|
|
7
7
|
* (如 `xxx-1326375956.ap-shanghai.app.tcloudbase.com`),与 cookie 中的 `appid` 字段对应。
|
|
8
8
|
* AppId/Region 缺失或查询失败时返回 undefined
|
|
9
9
|
*/
|
|
@@ -27,11 +27,6 @@ export declare class ProjectValidator {
|
|
|
27
27
|
* 这里在“可能存在构建步骤”的 hosting 配置上给出防坑告警。
|
|
28
28
|
*/
|
|
29
29
|
private validateHostingSubPathBaseRisks;
|
|
30
|
-
/**
|
|
31
|
-
* spaFallback 为站点级回退能力,不是按网关前缀独立生效。
|
|
32
|
-
* 同一 hosting 在 spaFallback=true 下被多个网关前缀复用时,容易出现不同前缀回退串扰。
|
|
33
|
-
*/
|
|
34
|
-
private validateSpaFallbackPrefixIsolation;
|
|
35
30
|
private validateUniqueNames;
|
|
36
31
|
private validateUniqueField;
|
|
37
32
|
private validateGatewayTargets;
|