@cloudbase/manager-node 5.6.8 → 5.7.0-beta.1
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 +6 -0
- package/lib/cloudApp/index.js +12 -3
- package/lib/deploy/DatabaseDeployer.js +238 -0
- package/lib/deploy/DeployOrchestrator.js +564 -0
- package/lib/deploy/FunctionDeployer.js +611 -0
- package/lib/deploy/GatewayDeployer.js +522 -0
- package/lib/deploy/StateStore.js +327 -0
- package/lib/deploy/StaticDeployer.js +289 -0
- package/lib/deploy/domain.js +39 -0
- package/lib/deploy/framework.js +105 -0
- package/lib/deploy/function/artifact.js +207 -0
- package/lib/deploy/function/builders/cloud.js +454 -0
- package/lib/deploy/function/cam-preflight.js +157 -0
- package/lib/deploy/function/cloud-build-service.js +170 -0
- package/lib/deploy/function/config-guard.js +588 -0
- package/lib/deploy/function/docker-preflight.js +87 -0
- package/lib/deploy/function/image-preflight.js +164 -0
- package/lib/deploy/function/local-builder.js +110 -0
- package/lib/deploy/function/planner.js +282 -0
- package/lib/deploy/function/preflight.js +264 -0
- package/lib/deploy/types.js +99 -0
- package/lib/environment.js +11 -0
- package/lib/hosting/index.js +4 -1
- package/lib/index.js +31 -0
- package/lib/projectValidator/index.js +375 -0
- package/lib/projectValidator/types.js +2 -0
- package/lib/storage/index.js +6 -7
- package/lib/utils/index.js +41 -1
- package/package.json +2 -1
- package/types/cloudApp/index.d.ts +4 -0
- package/types/cloudApp/types.d.ts +87 -6
- package/types/deploy/DatabaseDeployer.d.ts +70 -0
- package/types/deploy/DeployOrchestrator.d.ts +150 -0
- package/types/deploy/FunctionDeployer.d.ts +158 -0
- package/types/deploy/GatewayDeployer.d.ts +175 -0
- package/types/deploy/StateStore.d.ts +170 -0
- package/types/deploy/StaticDeployer.d.ts +104 -0
- package/types/deploy/domain.d.ts +17 -0
- package/types/deploy/framework.d.ts +23 -0
- package/types/deploy/function/artifact.d.ts +40 -0
- package/types/deploy/function/builders/cloud.d.ts +110 -0
- package/types/deploy/function/cam-preflight.d.ts +51 -0
- package/types/deploy/function/cloud-build-service.d.ts +10 -0
- package/types/deploy/function/config-guard.d.ts +41 -0
- package/types/deploy/function/docker-preflight.d.ts +17 -0
- package/types/deploy/function/image-preflight.d.ts +21 -0
- package/types/deploy/function/local-builder.d.ts +26 -0
- package/types/deploy/function/planner.d.ts +15 -0
- package/types/deploy/function/preflight.d.ts +9 -0
- package/types/deploy/types.d.ts +429 -0
- package/types/env/type.d.ts +4 -4
- package/types/environment.d.ts +7 -0
- package/types/function/types.d.ts +15 -0
- package/types/hosting/index.d.ts +6 -0
- package/types/index.d.ts +18 -0
- package/types/interfaces/function.interface.d.ts +1 -1
- package/types/projectValidator/index.d.ts +24 -0
- package/types/projectValidator/types.d.ts +26 -0
- package/types/storage/index.d.ts +6 -0
- package/types/utils/index.d.ts +13 -0
|
@@ -0,0 +1,564 @@
|
|
|
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.DeployOrchestrator = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const error_1 = require("../error");
|
|
9
|
+
const DatabaseDeployer_1 = require("./DatabaseDeployer");
|
|
10
|
+
const FunctionDeployer_1 = require("./FunctionDeployer");
|
|
11
|
+
const StaticDeployer_1 = require("./StaticDeployer");
|
|
12
|
+
const GatewayDeployer_1 = require("./GatewayDeployer");
|
|
13
|
+
const StateStore_1 = require("./StateStore");
|
|
14
|
+
const framework_1 = require("./framework");
|
|
15
|
+
/** 部署顺序(按依赖:database 最先,函数依赖新 Schema;gateway 最后,依赖函数/hosting) */
|
|
16
|
+
const DEPLOY_ORDER = ['database', 'functions', 'app', 'hosting', 'gateway'];
|
|
17
|
+
/**
|
|
18
|
+
* 声明式部署编排器
|
|
19
|
+
* 顺序:database → functions → app → hosting → gateway
|
|
20
|
+
* 支持 dry-run / only / skip / 幂等重试(database 失败/冲突会中断后续步骤)
|
|
21
|
+
*/
|
|
22
|
+
class DeployOrchestrator {
|
|
23
|
+
constructor(environment) {
|
|
24
|
+
this.environment = environment;
|
|
25
|
+
this.databaseDeployer = new DatabaseDeployer_1.DatabaseDeployer(environment);
|
|
26
|
+
this.functionDeployer = new FunctionDeployer_1.FunctionDeployer(environment);
|
|
27
|
+
this.staticDeployer = new StaticDeployer_1.StaticDeployer(environment);
|
|
28
|
+
this.gatewayDeployer = new GatewayDeployer_1.GatewayDeployer(environment);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 计算部署计划(dry-run 输出)
|
|
32
|
+
*/
|
|
33
|
+
async deployPlan(options) {
|
|
34
|
+
return this.buildPlan(options.config, options);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 执行部署
|
|
38
|
+
*/
|
|
39
|
+
async deploy(options) {
|
|
40
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
41
|
+
const { config, envId, dryRun, cwd = process.cwd(), log } = options;
|
|
42
|
+
// 1. 轻量 inline 守卫(validateProject 未接入时的兜底)
|
|
43
|
+
this.guardConfig(config);
|
|
44
|
+
const plan = await this.buildPlan(config, options);
|
|
45
|
+
if (dryRun) {
|
|
46
|
+
(_a = log === null || log === void 0 ? void 0 : log.info) === null || _a === void 0 ? void 0 : _a.call(log, 'dry-run 模式,仅输出部署计划');
|
|
47
|
+
return { plan, results: [] };
|
|
48
|
+
}
|
|
49
|
+
const results = [];
|
|
50
|
+
for (const item of plan) {
|
|
51
|
+
// skip:产物/配置未变更,静默跳过(不执行、不询问)
|
|
52
|
+
if (item.status === 'skip') {
|
|
53
|
+
(_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}`);
|
|
54
|
+
// reason: 'skip' 标记,供展示层区分「成功」与「跳过」
|
|
55
|
+
results.push({ type: item.type, name: item.name, ok: true, reason: 'skip' });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
(_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}`);
|
|
59
|
+
// conflict:数据库迁移冲突 → 中断整个部署(后续资源可能依赖新 Schema)
|
|
60
|
+
if (item.status === 'conflict') {
|
|
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') {
|
|
65
|
+
return { plan, results };
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// 覆盖确认:仅函数已存在(update)时触发,对齐 tcb fn deploy 的 FunctionConflictChecker 语义
|
|
70
|
+
// yes=true 直接放行;注入 confirmUpdate 按回调决定;两者都无 → 保守跳过(no-confirm)
|
|
71
|
+
if (item.type === 'functions' && item.status === 'update') {
|
|
72
|
+
let confirmed = false;
|
|
73
|
+
if (options.yes) {
|
|
74
|
+
confirmed = true;
|
|
75
|
+
}
|
|
76
|
+
else if (options.confirmUpdate) {
|
|
77
|
+
confirmed = await options.confirmUpdate(item);
|
|
78
|
+
}
|
|
79
|
+
if (!confirmed) {
|
|
80
|
+
const reason = options.confirmUpdate
|
|
81
|
+
? 'skipped-by-user'
|
|
82
|
+
: 'no-confirm';
|
|
83
|
+
(_e = log === null || log === void 0 ? void 0 : log.warn) === null || _e === void 0 ? void 0 : _e.call(log, `[${item.type}] ${item.name} 跳过覆盖更新(${reason})`);
|
|
84
|
+
results.push({ type: item.type, name: item.name, ok: false, reason });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
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, '数据库迁移失败,中断部署');
|
|
99
|
+
return { plan, results };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// 部署成功后写回本地 state(真增量基准;失败资源不写入,保留旧快照)
|
|
104
|
+
if (!dryRun) {
|
|
105
|
+
await this.persistState({ config, plan, results, cwd });
|
|
106
|
+
}
|
|
107
|
+
return { plan, results };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 部署成功后写回本地 state 快照
|
|
111
|
+
*
|
|
112
|
+
* - hosting:成功部署/未变更(skip)的项重算产物指纹;失败项保留旧快照
|
|
113
|
+
* - app:成功部署写 buildAppConfig 快照;skip 复用旧快照
|
|
114
|
+
* - gateway:保留旧快照(本轮不做路由级实时快照)
|
|
115
|
+
*/
|
|
116
|
+
async persistState(options) {
|
|
117
|
+
var _a, _b, _c, _d, _e, _f;
|
|
118
|
+
const { config, plan, results, cwd } = options;
|
|
119
|
+
const stateStore = new StateStore_1.StateStore({ cwd });
|
|
120
|
+
const prev = stateStore.read();
|
|
121
|
+
// hosting
|
|
122
|
+
const hostingEntries = [];
|
|
123
|
+
for (const h of config.hosting || []) {
|
|
124
|
+
const name = h.name || 'default';
|
|
125
|
+
const planItem = plan.find(p => p.type === 'hosting' && p.name === name);
|
|
126
|
+
const result = results.find(r => r.type === 'hosting' && r.name === name);
|
|
127
|
+
if (result && !result.ok) {
|
|
128
|
+
// 部署失败:保留旧快照(下次 diff 仍会检测到差异)
|
|
129
|
+
const prevEntry = (_b = (_a = prev === null || prev === void 0 ? void 0 : prev.resources) === null || _a === void 0 ? void 0 : _a.hosting) === null || _b === void 0 ? void 0 : _b.find(s => s.name === name);
|
|
130
|
+
if (prevEntry) {
|
|
131
|
+
hostingEntries.push(prevEntry);
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if ((planItem === null || planItem === void 0 ? void 0 : planItem.status) === 'skip') {
|
|
136
|
+
const prevEntry = (_d = (_c = prev === null || prev === void 0 ? void 0 : prev.resources) === null || _c === void 0 ? void 0 : _c.hosting) === null || _d === void 0 ? void 0 : _d.find(s => s.name === name);
|
|
137
|
+
if (prevEntry) {
|
|
138
|
+
hostingEntries.push(prevEntry);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// 成功部署:重算当前指纹(含 size,供下次 diff 做 size 预判)
|
|
143
|
+
// 注意:deployHosting 可能执行本地构建,产物已更新,必须独立重算而非复用 plan 指纹
|
|
144
|
+
const root = path_1.default.resolve(cwd, h.root || '.');
|
|
145
|
+
const buildCommand = (0, framework_1.resolveBuildCommand)(h, root);
|
|
146
|
+
const outputDir = h.outputDir
|
|
147
|
+
? path_1.default.resolve(root, h.outputDir)
|
|
148
|
+
: buildCommand
|
|
149
|
+
? path_1.default.resolve(root, 'dist')
|
|
150
|
+
: root;
|
|
151
|
+
try {
|
|
152
|
+
const files = await stateStore.computeHostingFingerprints(outputDir, h.ignore || []);
|
|
153
|
+
hostingEntries.push({ name, deployPath: h.deployPath, files });
|
|
154
|
+
}
|
|
155
|
+
catch (_g) {
|
|
156
|
+
// 目录缺失 / 文件数超限:不写入(下次 diff 走云端兜底或全量部署)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// app
|
|
160
|
+
let appEntry;
|
|
161
|
+
if (config.app) {
|
|
162
|
+
const appResult = results.find(r => r.type === 'app');
|
|
163
|
+
if (appResult === null || appResult === void 0 ? void 0 : appResult.ok) {
|
|
164
|
+
appEntry = stateStore.buildAppConfig(config.app);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
appEntry = (_e = prev === null || prev === void 0 ? void 0 : prev.resources) === null || _e === void 0 ? void 0 : _e.app;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const resources = {};
|
|
171
|
+
if (hostingEntries.length) {
|
|
172
|
+
resources.hosting = hostingEntries;
|
|
173
|
+
}
|
|
174
|
+
if (appEntry) {
|
|
175
|
+
resources.app = appEntry;
|
|
176
|
+
}
|
|
177
|
+
if ((_f = prev === null || prev === void 0 ? void 0 : prev.resources) === null || _f === void 0 ? void 0 : _f.gateway) {
|
|
178
|
+
resources.gateway = prev.resources.gateway;
|
|
179
|
+
}
|
|
180
|
+
if (Object.keys(resources).length) {
|
|
181
|
+
stateStore.write({ resources });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* 构建完整部署计划(按依赖顺序 + only/skip 过滤)
|
|
186
|
+
*
|
|
187
|
+
* 通过查询云端状态判定每个资源的动作:
|
|
188
|
+
* - database:先按迁移文件做 preview,再聚合成单条资源级计划项(pending→create,存在 conflict→conflict,全部 applied→不出现在 plan)
|
|
189
|
+
* - functions:ListFunctions 判断是否已存在 → create / update(不做本地 hash / 字段 diff)
|
|
190
|
+
* - app:describeAppInfo 判断是否已存在 → create / update;本地 state 配置一致 → skip
|
|
191
|
+
* - hosting:本地 state 指纹 diff → 一致 skip(不重新上传)/ 有差异 deploy(带文件级明细)
|
|
192
|
+
* - gateway:planRoutes 对比云端路由 → create / update / skip(含变更字段明细)
|
|
193
|
+
* 云端查询失败时降级为保守判定(全 create),不影响 dry-run 展示。
|
|
194
|
+
*/
|
|
195
|
+
async buildPlan(config, options) {
|
|
196
|
+
const { only, skip, refresh } = options;
|
|
197
|
+
const cwd = options.cwd || process.cwd();
|
|
198
|
+
const envId = options.envId;
|
|
199
|
+
const plan = [];
|
|
200
|
+
const isEnabled = (type) => {
|
|
201
|
+
if ((only === null || only === void 0 ? void 0 : only.length) && !only.includes(type))
|
|
202
|
+
return false;
|
|
203
|
+
if (skip === null || skip === void 0 ? void 0 : skip.includes(type))
|
|
204
|
+
return false;
|
|
205
|
+
return true;
|
|
206
|
+
};
|
|
207
|
+
// 本地 state 快照(diff 基准):--refresh 时忽略本地 skip 判定
|
|
208
|
+
const stateStore = new StateStore_1.StateStore({ cwd });
|
|
209
|
+
const state = refresh ? null : stateStore.read();
|
|
210
|
+
const planCtx = { config, envId, stateStore, state, refresh, cwd };
|
|
211
|
+
for (const type of DEPLOY_ORDER) {
|
|
212
|
+
if (!isEnabled(type))
|
|
213
|
+
continue;
|
|
214
|
+
const typePlans = await this.buildPlanForType(type, planCtx);
|
|
215
|
+
plan.push(...typePlans);
|
|
216
|
+
}
|
|
217
|
+
return plan;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 按资源类型构建计划(拆分自 buildPlan,控制圈复杂度)
|
|
221
|
+
*/
|
|
222
|
+
async buildPlanForType(type, ctx) {
|
|
223
|
+
const { config, envId, stateStore, state, refresh, cwd } = ctx;
|
|
224
|
+
if (type === 'database') {
|
|
225
|
+
return this.buildDatabasePlan(config, cwd, envId);
|
|
226
|
+
}
|
|
227
|
+
if (type === 'functions') {
|
|
228
|
+
const existingFunctions = await this.listExistingFunctionNames();
|
|
229
|
+
return (config.functions || []).map((fn) => {
|
|
230
|
+
const exists = existingFunctions.has(fn.name);
|
|
231
|
+
const details = [];
|
|
232
|
+
const runtime = fn.runtime || 'Nodejs20.19';
|
|
233
|
+
const memorySize = fn.memorySize || 256;
|
|
234
|
+
details.push({ label: '运行时', value: `${runtime}, ${memorySize}MB` });
|
|
235
|
+
if (fn.type === 'HTTP') {
|
|
236
|
+
const strategy = fn.buildStrategy || 'zip';
|
|
237
|
+
details.push({ label: '构建', value: strategy });
|
|
238
|
+
if (fn.public) {
|
|
239
|
+
details.push({ label: '访问', value: 'public: true(自动配置匿名访问)' });
|
|
240
|
+
}
|
|
241
|
+
if (fn.gatewayPath) {
|
|
242
|
+
details.push({ label: '网关路径', value: fn.gatewayPath });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
const install = fn.installDependency === false ? '本地安装依赖打包' : '云端安装依赖';
|
|
247
|
+
details.push({ label: '依赖', value: install });
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
type,
|
|
251
|
+
name: fn.name,
|
|
252
|
+
status: exists ? 'update' : 'create',
|
|
253
|
+
action: exists
|
|
254
|
+
? `覆盖更新已存在函数 ${fn.name}`
|
|
255
|
+
: `新建函数 ${fn.name}`,
|
|
256
|
+
details
|
|
257
|
+
};
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
if (type === 'app') {
|
|
261
|
+
return this.buildAppPlan({ config, envId, stateStore, state, refresh });
|
|
262
|
+
}
|
|
263
|
+
if (type === 'hosting') {
|
|
264
|
+
return this.buildHostingPlans(config, ctx);
|
|
265
|
+
}
|
|
266
|
+
return this.buildGatewayPlans(config, envId);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* database 计划:preview 聚合成单条资源级计划项
|
|
270
|
+
*/
|
|
271
|
+
async buildDatabasePlan(config, cwd, envId) {
|
|
272
|
+
if (!config.database)
|
|
273
|
+
return [];
|
|
274
|
+
const dbConfig = config.database;
|
|
275
|
+
const migrationPlans = await this.databaseDeployer.plan(dbConfig, cwd, envId);
|
|
276
|
+
const pending = migrationPlans.filter(item => item.status === 'pending');
|
|
277
|
+
const conflicts = migrationPlans.filter(item => item.status === 'conflict');
|
|
278
|
+
// database 是资源级步骤:无论有多少 migration,plan 中最多只保留一条 database 记录
|
|
279
|
+
const details = migrationPlans.map(item => ({
|
|
280
|
+
label: `${item.version}_${item.name || 'unknown'}`,
|
|
281
|
+
value: item.status === 'pending'
|
|
282
|
+
? '待应用'
|
|
283
|
+
: item.status === 'conflict'
|
|
284
|
+
? `冲突:${item.reason || 'checksum mismatch'}`
|
|
285
|
+
: '已应用,跳过'
|
|
286
|
+
}));
|
|
287
|
+
if (conflicts.length > 0) {
|
|
288
|
+
return [
|
|
289
|
+
{
|
|
290
|
+
type: 'database',
|
|
291
|
+
name: 'postgresql',
|
|
292
|
+
status: 'conflict',
|
|
293
|
+
action: `数据库迁移存在 ${conflicts.length} 个冲突,执行时将中断部署`,
|
|
294
|
+
changes: conflicts.map(item => ({
|
|
295
|
+
field: `conflict.${item.version}_${item.name || 'unknown'}`,
|
|
296
|
+
to: item.reason || 'checksum mismatch'
|
|
297
|
+
})),
|
|
298
|
+
details
|
|
299
|
+
}
|
|
300
|
+
];
|
|
301
|
+
}
|
|
302
|
+
if (pending.length > 0) {
|
|
303
|
+
return [
|
|
304
|
+
{
|
|
305
|
+
type: 'database',
|
|
306
|
+
name: 'postgresql',
|
|
307
|
+
status: 'create',
|
|
308
|
+
action: `将执行 ${pending.length} 条数据库迁移`,
|
|
309
|
+
changes: pending.map(item => ({
|
|
310
|
+
field: 'migration',
|
|
311
|
+
to: `${item.version}_${item.name}`
|
|
312
|
+
})),
|
|
313
|
+
details
|
|
314
|
+
}
|
|
315
|
+
];
|
|
316
|
+
}
|
|
317
|
+
// 全部 applied 时不产生 database 计划项
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* app 计划:本地 state 配置一致 → skip;否则按云端存在性 → create / update
|
|
322
|
+
*/
|
|
323
|
+
async buildAppPlan(options) {
|
|
324
|
+
var _a;
|
|
325
|
+
const { config, envId, stateStore, state, refresh } = options;
|
|
326
|
+
if (!config.app)
|
|
327
|
+
return [];
|
|
328
|
+
const name = config.app.serviceName || 'app';
|
|
329
|
+
// 本地 state 配置一致 → skip(真增量,不重复构建版本)
|
|
330
|
+
const appEntry = stateStore.buildAppConfig(config.app);
|
|
331
|
+
const stateApp = (_a = state === null || state === void 0 ? void 0 : state.resources) === null || _a === void 0 ? void 0 : _a.app;
|
|
332
|
+
const appUnchanged = !refresh &&
|
|
333
|
+
stateApp !== undefined &&
|
|
334
|
+
appEntry !== undefined &&
|
|
335
|
+
this.deepEqual(stateApp, appEntry);
|
|
336
|
+
if (appUnchanged) {
|
|
337
|
+
return [
|
|
338
|
+
{
|
|
339
|
+
type: 'app',
|
|
340
|
+
name,
|
|
341
|
+
status: 'skip',
|
|
342
|
+
action: `云应用 ${name} 配置未变更,跳过`
|
|
343
|
+
}
|
|
344
|
+
];
|
|
345
|
+
}
|
|
346
|
+
const appExists = await this.appExists(name);
|
|
347
|
+
const changes = this.appConfigChanges(stateApp, appEntry);
|
|
348
|
+
return [
|
|
349
|
+
Object.assign({ type: 'app', name, status: appExists ? 'update' : 'create', action: appExists
|
|
350
|
+
? `覆盖更新云应用 ${name}`
|
|
351
|
+
: `新建云应用 ${name}` }, (changes.length ? { changes } : {}))
|
|
352
|
+
];
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* hosting 计划:本地 state 指纹 diff → 一致 skip / 有差异 deploy(带文件级明细)
|
|
356
|
+
*/
|
|
357
|
+
async buildHostingPlans(config, ctx) {
|
|
358
|
+
var _a, _b;
|
|
359
|
+
const { stateStore, state, refresh, cwd } = ctx;
|
|
360
|
+
const plans = [];
|
|
361
|
+
for (const h of config.hosting || []) {
|
|
362
|
+
const name = h.name || 'default';
|
|
363
|
+
const root = path_1.default.resolve(cwd, h.root || '.');
|
|
364
|
+
// 与 StaticDeployer.deployHosting 同规则解析 buildCommand / outputDir
|
|
365
|
+
const buildCommand = (0, framework_1.resolveBuildCommand)(h, root);
|
|
366
|
+
const outputDir = h.outputDir
|
|
367
|
+
? path_1.default.resolve(root, h.outputDir)
|
|
368
|
+
: buildCommand
|
|
369
|
+
? path_1.default.resolve(root, 'dist')
|
|
370
|
+
: root;
|
|
371
|
+
const stateHosting = (_b = (_a = state === null || state === void 0 ? void 0 : state.resources) === null || _a === void 0 ? void 0 : _a.hosting) === null || _b === void 0 ? void 0 : _b.find(s => s.name === name);
|
|
372
|
+
// 懒加载 diff:size 预判 + 按需 hash(目录缺失 / 文件数超限 → 保守部署)
|
|
373
|
+
let fileDiff = null;
|
|
374
|
+
try {
|
|
375
|
+
fileDiff = await stateStore.diffHostingFiles(outputDir, stateHosting === null || stateHosting === void 0 ? void 0 : stateHosting.files, {
|
|
376
|
+
ignore: h.ignore || []
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
catch (_c) {
|
|
380
|
+
fileDiff = null;
|
|
381
|
+
}
|
|
382
|
+
const unchanged = !refresh && fileDiff !== null && fileDiff.unchanged;
|
|
383
|
+
if (unchanged) {
|
|
384
|
+
plans.push({
|
|
385
|
+
type: 'hosting',
|
|
386
|
+
name,
|
|
387
|
+
status: 'skip',
|
|
388
|
+
action: `静态托管 ${name} 产物未变更,跳过`
|
|
389
|
+
});
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const hasBuild = !!h.buildCommand ||
|
|
393
|
+
(!!h.framework && h.framework !== 'static' && h.framework !== 'custom');
|
|
394
|
+
const details = [
|
|
395
|
+
{ label: '框架', value: h.framework || 'static' },
|
|
396
|
+
{
|
|
397
|
+
label: '构建',
|
|
398
|
+
value: hasBuild
|
|
399
|
+
? `本地构建(${buildCommand || 'npm run build'})→ 上传 ${outputDir}`
|
|
400
|
+
: '无构建,直接上传'
|
|
401
|
+
},
|
|
402
|
+
{ label: '部署路径', value: h.deployPath || '/' }
|
|
403
|
+
];
|
|
404
|
+
if (fileDiff === null || fileDiff === void 0 ? void 0 : fileDiff.overLimit) {
|
|
405
|
+
details.push({ label: '指纹', value: '文件数超上限,降级为全量部署' });
|
|
406
|
+
}
|
|
407
|
+
plans.push(Object.assign({ type: 'hosting', name, status: 'deploy', action: hasBuild
|
|
408
|
+
? `本地构建后直传静态托管 ${name}`
|
|
409
|
+
: `直传覆盖静态托管 ${name}`, details }, (fileDiff && !fileDiff.overLimit
|
|
410
|
+
? {
|
|
411
|
+
fileDiff: {
|
|
412
|
+
added: fileDiff.added,
|
|
413
|
+
modified: fileDiff.modified,
|
|
414
|
+
deleted: fileDiff.deleted,
|
|
415
|
+
totalChanged: fileDiff.totalChanged
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
: {})));
|
|
419
|
+
}
|
|
420
|
+
return plans;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* gateway 计划:planRoutes 对比云端路由 → create / update / skip(含变更字段明细)
|
|
424
|
+
*/
|
|
425
|
+
async buildGatewayPlans(config, envId) {
|
|
426
|
+
var _a;
|
|
427
|
+
const routes = ((_a = config.gateway) === null || _a === void 0 ? void 0 : _a.routes) || [];
|
|
428
|
+
if (routes.length === 0)
|
|
429
|
+
return [];
|
|
430
|
+
const routePlans = await this.gatewayDeployer.planRoutes({
|
|
431
|
+
routes,
|
|
432
|
+
envId,
|
|
433
|
+
hostings: config.hosting || []
|
|
434
|
+
});
|
|
435
|
+
const routeConfigMap = new Map(routes.map(r => [r.path, r]));
|
|
436
|
+
return routePlans.map(rp => {
|
|
437
|
+
const routeCfg = routeConfigMap.get(rp.path);
|
|
438
|
+
const details = [];
|
|
439
|
+
if (routeCfg === null || routeCfg === void 0 ? void 0 : routeCfg.target) {
|
|
440
|
+
details.push({ label: '目标', value: routeCfg.target });
|
|
441
|
+
}
|
|
442
|
+
if (routeCfg === null || routeCfg === void 0 ? void 0 : routeCfg.domain) {
|
|
443
|
+
details.push({ label: '域名', value: routeCfg.domain });
|
|
444
|
+
}
|
|
445
|
+
return Object.assign({ type: 'gateway', name: rp.path, status: rp.status, action: rp.status === 'create'
|
|
446
|
+
? `新建路由 ${rp.path}`
|
|
447
|
+
: rp.status === 'update'
|
|
448
|
+
? `更新路由 ${rp.path}`
|
|
449
|
+
: `路由 ${rp.path} 配置一致,跳过`, changes: rp.changes }, (details.length ? { details } : {}));
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* 查询环境中已存在的函数名集合(查询失败返回空集,降级为全新建判定)
|
|
454
|
+
*/
|
|
455
|
+
async listExistingFunctionNames() {
|
|
456
|
+
try {
|
|
457
|
+
const list = await this.environment.getFunctionService().listFunctions(100);
|
|
458
|
+
// listFunctions 返回字段名为 FunctionName(SCF ListFunctions API),兜底兼容 Name/name
|
|
459
|
+
return new Set((list || []).map((f) => f.FunctionName || f.Name || f.name));
|
|
460
|
+
}
|
|
461
|
+
catch (_a) {
|
|
462
|
+
return new Set();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* 判断云应用是否已存在(查询失败返回 false,降级为新建判定)
|
|
467
|
+
*/
|
|
468
|
+
async appExists(name) {
|
|
469
|
+
try {
|
|
470
|
+
await this.environment.getCloudAppService().describeAppInfo({
|
|
471
|
+
deployType: 'static-hosting',
|
|
472
|
+
serviceName: name
|
|
473
|
+
});
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
catch (_a) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* 执行单个计划步骤
|
|
482
|
+
*/
|
|
483
|
+
async executeStep(options) {
|
|
484
|
+
const { item, config, envId, cwd } = options;
|
|
485
|
+
switch (item.type) {
|
|
486
|
+
case 'database': {
|
|
487
|
+
const dbConfig = config.database;
|
|
488
|
+
const result = await this.databaseDeployer.deploy(dbConfig, cwd, envId);
|
|
489
|
+
return result.taskId ? `task:${result.taskId}` : undefined;
|
|
490
|
+
}
|
|
491
|
+
case 'functions': {
|
|
492
|
+
const fn = (config.functions || []).find((f) => f.name === item.name);
|
|
493
|
+
if (!fn)
|
|
494
|
+
return undefined;
|
|
495
|
+
// 函数代码目录解析(对齐 CLI FunctionPathResolver 的 dir 语义):
|
|
496
|
+
// - 显式配置 dir → path.resolve(cwd, dir)(独立于 functionRoot,不拼接)
|
|
497
|
+
// - 未配置 dir → path.join(functionRootPath, name) = {cwd}/{functionRoot}/{name}
|
|
498
|
+
const functionRoot = config.functionRoot || 'functions';
|
|
499
|
+
const functionRootPath = path_1.default.resolve(cwd, functionRoot);
|
|
500
|
+
const functionPath = fn.dir
|
|
501
|
+
? path_1.default.resolve(cwd, fn.dir)
|
|
502
|
+
: path_1.default.join(functionRootPath, fn.name);
|
|
503
|
+
// 声明式部署幂等语义:函数已存在则覆盖更新(force=true),支持重跑续传
|
|
504
|
+
const result = await this.functionDeployer.deploy({
|
|
505
|
+
func: fn,
|
|
506
|
+
functionRootPath,
|
|
507
|
+
functionPath,
|
|
508
|
+
force: true
|
|
509
|
+
});
|
|
510
|
+
return result.url;
|
|
511
|
+
}
|
|
512
|
+
case 'app': {
|
|
513
|
+
const result = await this.staticDeployer.deployApp({ config: config.app, cwd });
|
|
514
|
+
return result.url;
|
|
515
|
+
}
|
|
516
|
+
case 'hosting': {
|
|
517
|
+
const h = (config.hosting || []).find((x) => (x.name || 'default') === item.name);
|
|
518
|
+
const result = await this.staticDeployer.deployHosting({ config: h, cwd });
|
|
519
|
+
return result.url;
|
|
520
|
+
}
|
|
521
|
+
case 'gateway': {
|
|
522
|
+
const result = await this.gatewayDeployer.deploy({
|
|
523
|
+
routes: config.gateway.routes,
|
|
524
|
+
envId,
|
|
525
|
+
// 传入 hosting 配置:hosting 路由自动生成 PathRewrite.Prefix = deployPath
|
|
526
|
+
hostings: config.hosting || []
|
|
527
|
+
});
|
|
528
|
+
return result.domain;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/** 深比较(JSON 快照语义):仅用于 app 配置快照等简单对象 */
|
|
533
|
+
deepEqual(a, b) {
|
|
534
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
535
|
+
}
|
|
536
|
+
/** app 配置差异明细(from → to) */
|
|
537
|
+
appConfigChanges(prev, curr) {
|
|
538
|
+
const changes = [];
|
|
539
|
+
const keys = new Set([
|
|
540
|
+
...Object.keys(prev || {}),
|
|
541
|
+
...Object.keys(curr || {})
|
|
542
|
+
]);
|
|
543
|
+
for (const key of keys) {
|
|
544
|
+
const from = prev === null || prev === void 0 ? void 0 : prev[key];
|
|
545
|
+
const to = curr === null || curr === void 0 ? void 0 : curr[key];
|
|
546
|
+
if (from !== to) {
|
|
547
|
+
changes.push({ field: String(key), from, to });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return changes;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* 轻量 inline 守卫:配置合法 + 目录存在(validateProject 接线点)
|
|
554
|
+
*/
|
|
555
|
+
guardConfig(config) {
|
|
556
|
+
if (!config || typeof config !== 'object') {
|
|
557
|
+
throw new error_1.CloudBaseError('无效的配置:config 必须为对象');
|
|
558
|
+
}
|
|
559
|
+
if (!config.envId) {
|
|
560
|
+
throw new error_1.CloudBaseError('无效的配置:缺少 envId');
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
exports.DeployOrchestrator = DeployOrchestrator;
|