openxiangda-devkit-core 2.0.0-alpha.100
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 +13 -0
- package/dist/app-spec.d.ts +210 -0
- package/dist/app-spec.d.ts.map +1 -0
- package/dist/app-spec.js +1262 -0
- package/dist/app-spec.js.map +1 -0
- package/dist/application-services.d.ts +600 -0
- package/dist/application-services.d.ts.map +1 -0
- package/dist/application-services.js +1603 -0
- package/dist/application-services.js.map +1 -0
- package/dist/backend-image-build.d.ts +23 -0
- package/dist/backend-image-build.d.ts.map +1 -0
- package/dist/backend-image-build.js +209 -0
- package/dist/backend-image-build.js.map +1 -0
- package/dist/command-registry.d.ts +177 -0
- package/dist/command-registry.d.ts.map +1 -0
- package/dist/command-registry.js +27 -0
- package/dist/command-registry.js.map +1 -0
- package/dist/compiler/ai-catalog.d.ts +4 -0
- package/dist/compiler/ai-catalog.d.ts.map +1 -0
- package/dist/compiler/ai-catalog.js +353 -0
- package/dist/compiler/ai-catalog.js.map +1 -0
- package/dist/compiler/authorization-transition.d.ts +7 -0
- package/dist/compiler/authorization-transition.d.ts.map +1 -0
- package/dist/compiler/authorization-transition.js +27 -0
- package/dist/compiler/authorization-transition.js.map +1 -0
- package/dist/compiler/bundle.d.ts +34 -0
- package/dist/compiler/bundle.d.ts.map +1 -0
- package/dist/compiler/bundle.js +1865 -0
- package/dist/compiler/bundle.js.map +1 -0
- package/dist/compiler/config.d.ts +857 -0
- package/dist/compiler/config.d.ts.map +1 -0
- package/dist/compiler/config.js +3848 -0
- package/dist/compiler/config.js.map +1 -0
- package/dist/compiler/data-policy-expression.d.ts +12 -0
- package/dist/compiler/data-policy-expression.d.ts.map +1 -0
- package/dist/compiler/data-policy-expression.js +43 -0
- package/dist/compiler/data-policy-expression.js.map +1 -0
- package/dist/compiler/field-codec.d.ts +14 -0
- package/dist/compiler/field-codec.d.ts.map +1 -0
- package/dist/compiler/field-codec.js +131 -0
- package/dist/compiler/field-codec.js.map +1 -0
- package/dist/compiler/field-physical-plan.d.ts +10 -0
- package/dist/compiler/field-physical-plan.d.ts.map +1 -0
- package/dist/compiler/field-physical-plan.js +66 -0
- package/dist/compiler/field-physical-plan.js.map +1 -0
- package/dist/compiler/field-policy-path.d.ts +10 -0
- package/dist/compiler/field-policy-path.d.ts.map +1 -0
- package/dist/compiler/field-policy-path.js +283 -0
- package/dist/compiler/field-policy-path.js.map +1 -0
- package/dist/compiler/field-query-plan.d.ts +13 -0
- package/dist/compiler/field-query-plan.d.ts.map +1 -0
- package/dist/compiler/field-query-plan.js +122 -0
- package/dist/compiler/field-query-plan.js.map +1 -0
- package/dist/compiler/field-surface.d.ts +15 -0
- package/dist/compiler/field-surface.d.ts.map +1 -0
- package/dist/compiler/field-surface.js +51 -0
- package/dist/compiler/field-surface.js.map +1 -0
- package/dist/compiler/index.d.ts +6 -0
- package/dist/compiler/index.d.ts.map +1 -0
- package/dist/compiler/index.js +6 -0
- package/dist/compiler/index.js.map +1 -0
- package/dist/compiler/package-compiler.d.ts +22 -0
- package/dist/compiler/package-compiler.d.ts.map +1 -0
- package/dist/compiler/package-compiler.js +246 -0
- package/dist/compiler/package-compiler.js.map +1 -0
- package/dist/compiler/schema-composition.d.ts +21 -0
- package/dist/compiler/schema-composition.d.ts.map +1 -0
- package/dist/compiler/schema-composition.js +193 -0
- package/dist/compiler/schema-composition.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -0
- package/dist/connected-development.d.ts +62 -0
- package/dist/connected-development.d.ts.map +1 -0
- package/dist/connected-development.js +393 -0
- package/dist/connected-development.js.map +1 -0
- package/dist/control-plane-client.d.ts +684 -0
- package/dist/control-plane-client.d.ts.map +1 -0
- package/dist/control-plane-client.js +860 -0
- package/dist/control-plane-client.js.map +1 -0
- package/dist/deployment.d.ts +67 -0
- package/dist/deployment.d.ts.map +1 -0
- package/dist/deployment.js +311 -0
- package/dist/deployment.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/workflow.d.ts +83 -0
- package/dist/internal/workflow.d.ts.map +1 -0
- package/dist/internal/workflow.js +419 -0
- package/dist/internal/workflow.js.map +1 -0
- package/dist/nest-injection-contract.d.ts +4 -0
- package/dist/nest-injection-contract.d.ts.map +1 -0
- package/dist/nest-injection-contract.js +116 -0
- package/dist/nest-injection-contract.js.map +1 -0
- package/dist/package-compiler.d.ts +2 -0
- package/dist/package-compiler.d.ts.map +1 -0
- package/dist/package-compiler.js +2 -0
- package/dist/package-compiler.js.map +1 -0
- package/dist/session.d.ts +143 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +351 -0
- package/dist/session.js.map +1 -0
- package/dist/testing.d.ts +77 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +213 -0
- package/dist/testing.js.map +1 -0
- package/dist/toolchain-capsule.d.ts +45 -0
- package/dist/toolchain-capsule.d.ts.map +1 -0
- package/dist/toolchain-capsule.js +165 -0
- package/dist/toolchain-capsule.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +11 -0
- package/dist/version.js.map +1 -0
- package/dist/workspace-loader.d.ts +17 -0
- package/dist/workspace-loader.d.ts.map +1 -0
- package/dist/workspace-loader.js +140 -0
- package/dist/workspace-loader.js.map +1 -0
- package/dist/workspace.d.ts +15 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +69 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,1603 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
5
|
+
import { CONFIGURATION_COMPATIBILITY_CAPABILITY, CURRENT_APPLICATION_CONTRACT, OPENXIANGDA_CONTRACT_VERSION, SCHEMA_VERSIONS, canonicalJson, sha256Digest, } from "openxiangda-contracts";
|
|
6
|
+
import { compileApplicationSources, compileAppPackage, requiredPlatformCapabilities, backendRuntimeRequired, renderAdminNavigationSuggestion, sha256Bytes, validateAppConfig, } from "./compiler/index.js";
|
|
7
|
+
import { OpenXiangdaControlPlaneClient, ControlPlaneError, } from "./control-plane-client.js";
|
|
8
|
+
import { normalizePlatformBaseUrl, OpenXiangdaDeveloperSession, } from "./session.js";
|
|
9
|
+
import { runConnectedDevelopment, selectConnectedDevelopmentEnvironment, } from "./connected-development.js";
|
|
10
|
+
import { backendImageBuildTarget, publishBackendImage, } from "./backend-image-build.js";
|
|
11
|
+
import { OPENXIANGDA_TOOLCHAIN_VERSION } from "./version.js";
|
|
12
|
+
import { git, loadWorkspace, } from "./workspace-loader.js";
|
|
13
|
+
import { validateNestInjectionContract } from "./nest-injection-contract.js";
|
|
14
|
+
import { collectWorkspaceToolchainDependencies, defaultDevkitCoreToolchainCapsule, ToolchainCapsuleScanLimitError, toolchainCapsuleDiagnostic, } from "./toolchain-capsule.js";
|
|
15
|
+
import { advisoryAppSpecDiagnostics, closeAppSpecChange, createAppSpecCapability, createAppSpecChange, initializeAppSpec, inspectAppSpec, summarizeAppSpecContext, } from "./app-spec.js";
|
|
16
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
17
|
+
export class OpenXiangdaApplicationServices {
|
|
18
|
+
options;
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.options = options;
|
|
21
|
+
}
|
|
22
|
+
async workspaceContext(root) {
|
|
23
|
+
const workspace = await this.workspace(root);
|
|
24
|
+
return this.ok("workspace.context", workspace.context.workspace, workspace.context);
|
|
25
|
+
}
|
|
26
|
+
async appSpecInit(root) {
|
|
27
|
+
const workspace = await this.workspace(root);
|
|
28
|
+
const data = initializeAppSpec({
|
|
29
|
+
root: workspace.root,
|
|
30
|
+
appCode: workspace.config.app.code,
|
|
31
|
+
appName: workspace.config.app.name,
|
|
32
|
+
});
|
|
33
|
+
return this.ok("spec.init", workspace.context.workspace, data, [
|
|
34
|
+
{
|
|
35
|
+
code: "spec.context",
|
|
36
|
+
label: "读取当前 AppSpec 上下文",
|
|
37
|
+
command: "openxiangda spec context --json",
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
}
|
|
41
|
+
async appSpecAddCapability(input) {
|
|
42
|
+
const workspace = await this.workspace(input.root);
|
|
43
|
+
const data = createAppSpecCapability({
|
|
44
|
+
root: workspace.root,
|
|
45
|
+
appCode: workspace.config.app.code,
|
|
46
|
+
appName: workspace.config.app.name,
|
|
47
|
+
id: input.id,
|
|
48
|
+
title: input.title,
|
|
49
|
+
...(input.resources ? { resources: input.resources } : {}),
|
|
50
|
+
...(input.actions ? { actions: input.actions } : {}),
|
|
51
|
+
});
|
|
52
|
+
return this.ok("spec.add-capability", workspace.context.workspace, data);
|
|
53
|
+
}
|
|
54
|
+
async appSpecNew(input) {
|
|
55
|
+
const workspace = await this.workspace(input.root);
|
|
56
|
+
const data = createAppSpecChange({
|
|
57
|
+
root: workspace.root,
|
|
58
|
+
appCode: workspace.config.app.code,
|
|
59
|
+
appName: workspace.config.app.name,
|
|
60
|
+
id: input.id,
|
|
61
|
+
title: input.title,
|
|
62
|
+
...(input.risk ? { risk: input.risk } : {}),
|
|
63
|
+
...(input.summary ? { summary: input.summary } : {}),
|
|
64
|
+
...(input.capabilities ? { capabilities: input.capabilities } : {}),
|
|
65
|
+
...(input.requirements ? { requirements: input.requirements } : {}),
|
|
66
|
+
...(input.resources ? { resources: input.resources } : {}),
|
|
67
|
+
...(input.actions ? { actions: input.actions } : {}),
|
|
68
|
+
});
|
|
69
|
+
return this.ok("spec.new", workspace.context.workspace, data, [
|
|
70
|
+
{
|
|
71
|
+
code: "spec.context",
|
|
72
|
+
label: "读取本次变更相关上下文",
|
|
73
|
+
command: `openxiangda spec context ${input.id} --json`,
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
async appSpecContext(root, selector) {
|
|
78
|
+
const workspace = await this.workspace(root);
|
|
79
|
+
const context = inspectAppSpec(workspace.root, this.appSpecContractIndex(workspace, true), selector);
|
|
80
|
+
return this.result("spec.context", workspace.context.workspace, context, advisoryAppSpecDiagnostics(context.diagnostics));
|
|
81
|
+
}
|
|
82
|
+
async appSpecCheck(root) {
|
|
83
|
+
const workspace = await this.workspace(root);
|
|
84
|
+
const context = inspectAppSpec(workspace.root, this.appSpecContractIndex(workspace, true));
|
|
85
|
+
return this.result("spec.check", workspace.context.workspace, context, context.diagnostics, [
|
|
86
|
+
{
|
|
87
|
+
code: "check",
|
|
88
|
+
label: "继续运行应用检查",
|
|
89
|
+
command: "openxiangda check",
|
|
90
|
+
},
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
async appSpecClose(input) {
|
|
94
|
+
const workspace = await this.workspace(input.root);
|
|
95
|
+
const outcome = closeAppSpecChange({
|
|
96
|
+
root: workspace.root,
|
|
97
|
+
id: input.id,
|
|
98
|
+
contract: this.appSpecContractIndex(workspace, true),
|
|
99
|
+
...(input.summary ? { summary: input.summary } : {}),
|
|
100
|
+
...(input.currentSpec ? { currentSpec: input.currentSpec } : {}),
|
|
101
|
+
});
|
|
102
|
+
const { diagnostics, ...data } = outcome;
|
|
103
|
+
return this.result("spec.close", workspace.context.workspace, data, diagnostics, data.converged
|
|
104
|
+
? []
|
|
105
|
+
: [
|
|
106
|
+
{
|
|
107
|
+
code: "spec.context",
|
|
108
|
+
label: "检查当前规格与变更差异",
|
|
109
|
+
command: `openxiangda spec context ${input.id} --json`,
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
async accept(input) {
|
|
114
|
+
const workspace = await this.workspace(input.root);
|
|
115
|
+
const diagnostics = this.toolchainDiagnostics(workspace);
|
|
116
|
+
if (diagnostics.length > 0) {
|
|
117
|
+
return this.result("accept", workspace.context.workspace, undefined, diagnostics);
|
|
118
|
+
}
|
|
119
|
+
const planPath = resolve(workspace.root, String(input.planPath || ""));
|
|
120
|
+
if (!existsSync(planPath)) {
|
|
121
|
+
throw new Error(`OPENXIANGDA_ACCEPTANCE_PLAN_NOT_FOUND: ${planPath}`);
|
|
122
|
+
}
|
|
123
|
+
let plan;
|
|
124
|
+
try {
|
|
125
|
+
plan = JSON.parse(readFileSync(planPath, "utf8"));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
throw new Error(`OPENXIANGDA_ACCEPTANCE_PLAN_INVALID_JSON: ${planPath}`);
|
|
129
|
+
}
|
|
130
|
+
if (plan?.schemaVersion !==
|
|
131
|
+
"openxiangda.preproduction-acceptance-plan/v2" ||
|
|
132
|
+
plan.environmentKey !== "preproduction" ||
|
|
133
|
+
!Array.isArray(plan.actors)) {
|
|
134
|
+
throw new Error("OPENXIANGDA_ACCEPTANCE_PLAN_INVALID: 计划必须是 preproduction v2 身份矩阵");
|
|
135
|
+
}
|
|
136
|
+
const client = await this.client(workspace.root);
|
|
137
|
+
const prepared = await client.preparePreproductionAcceptanceIdentities(workspace.config.app.code, plan);
|
|
138
|
+
return this.ok("accept", workspace.context.workspace, {
|
|
139
|
+
manual: true,
|
|
140
|
+
releaseGate: false,
|
|
141
|
+
planPath,
|
|
142
|
+
...prepared,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
async contractDescribe(root) {
|
|
146
|
+
const workspace = await this.workspace(root);
|
|
147
|
+
const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
|
|
148
|
+
const adminNavigationSuggestion = renderAdminNavigationSuggestion(workspace.config);
|
|
149
|
+
return this.ok("contract.describe", workspace.context.workspace, {
|
|
150
|
+
configDigest: sources.config.digest,
|
|
151
|
+
contractDigest: sources.contracts.digest,
|
|
152
|
+
aiCatalogDigest: sources.aiCatalog.digest,
|
|
153
|
+
contract: sources.contracts.value,
|
|
154
|
+
aiCatalog: sources.aiCatalog.value,
|
|
155
|
+
adminNavigationAuthoring: {
|
|
156
|
+
schemaVersion: "openxiangda.admin-navigation-authoring/v1",
|
|
157
|
+
owner: "application",
|
|
158
|
+
target: "frontend.admin.navigation",
|
|
159
|
+
configured: sources.config.value.frontend.admin.navigation.length > 0,
|
|
160
|
+
applyMode: "copy-once",
|
|
161
|
+
automaticRuntimeDiscovery: false,
|
|
162
|
+
suggestion: adminNavigationSuggestion,
|
|
163
|
+
},
|
|
164
|
+
counts: {
|
|
165
|
+
backendSecrets: sources.config.value.backend.secrets.length,
|
|
166
|
+
backendOperations: sources.config.value.backend.operations.length,
|
|
167
|
+
frontendRoutes: sources.config.value.frontend.routes.length,
|
|
168
|
+
anonymousPublicPolicies: sources.config.value.frontend.publicAccess?.policies.length || 0,
|
|
169
|
+
dataResources: sources.config.value.data.resources.length,
|
|
170
|
+
aiCapabilities: sources.aiCatalog.value.capabilities.length,
|
|
171
|
+
eventSubscriptions: sources.config.value.events.subscriptions.length,
|
|
172
|
+
timerSubscriptions: sources.config.value.events.timers.length,
|
|
173
|
+
workflowDefinitions: sources.config.value.workflows.definitions.length,
|
|
174
|
+
workflowBindings: sources.config.value.workflows.bindings.length,
|
|
175
|
+
workflowProviders: sources.config.value.workflows.providers.length,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async platformCapabilities(root) {
|
|
180
|
+
const workspace = await this.workspace(root);
|
|
181
|
+
const capabilities = await (await this.client(workspace.root)).capabilities();
|
|
182
|
+
return this.ok("platform.capabilities", workspace.context.workspace, capabilities);
|
|
183
|
+
}
|
|
184
|
+
async provisionApplication(root) {
|
|
185
|
+
const workspace = await this.workspace(root);
|
|
186
|
+
const capsuleDiagnostics = this.toolchainDiagnostics(workspace);
|
|
187
|
+
if (capsuleDiagnostics.length > 0) {
|
|
188
|
+
return this.result("app.provision", workspace.context.workspace, undefined, capsuleDiagnostics);
|
|
189
|
+
}
|
|
190
|
+
const application = await (await this.client(workspace.root)).provisionApplication({
|
|
191
|
+
appCode: workspace.config.app.code,
|
|
192
|
+
name: workspace.config.app.name,
|
|
193
|
+
});
|
|
194
|
+
return this.ok("app.provision", workspace.context.workspace, application, [
|
|
195
|
+
{ code: "check", label: "检查应用", command: "openxiangda check" },
|
|
196
|
+
]);
|
|
197
|
+
}
|
|
198
|
+
async oauthClients(root) {
|
|
199
|
+
const workspace = await this.workspace(root);
|
|
200
|
+
const data = await (await this.client(workspace.root)).oauthClients(workspace.config.app.code);
|
|
201
|
+
return this.ok("oauth.client.list", workspace.context.workspace, data);
|
|
202
|
+
}
|
|
203
|
+
async runtimeOAuthCredentialStatus(root, environmentKey) {
|
|
204
|
+
const workspace = await this.workspace(root);
|
|
205
|
+
const environment = this.deploymentEnvironment(environmentKey);
|
|
206
|
+
const data = await (await this.client(workspace.root)).runtimeOAuthCredentialStatus(workspace.config.app.code, environment);
|
|
207
|
+
return this.ok("oauth.runtime.status", workspace.context.workspace, data);
|
|
208
|
+
}
|
|
209
|
+
async rotateRuntimeOAuthCredential(root, input) {
|
|
210
|
+
const workspace = await this.workspace(root);
|
|
211
|
+
const client = await this.client(workspace.root);
|
|
212
|
+
const environment = this.deploymentEnvironment(input.environmentKey);
|
|
213
|
+
const status = await client.runtimeOAuthCredentialStatus(workspace.config.app.code, environment);
|
|
214
|
+
if (!status.configured || !status.client) {
|
|
215
|
+
throw new Error("OAUTH2_RUNTIME_CLIENT_NOT_PROVISIONED");
|
|
216
|
+
}
|
|
217
|
+
const rotation = await client.stageRuntimeOAuthCredentialRotation(workspace.config.app.code, environment, {
|
|
218
|
+
expectedCredentialVersion: status.client.credentialVersion,
|
|
219
|
+
...(input.gracePeriodSeconds === undefined
|
|
220
|
+
? {}
|
|
221
|
+
: { gracePeriodSeconds: input.gracePeriodSeconds }),
|
|
222
|
+
idempotencyKey: input.idempotencyKey,
|
|
223
|
+
});
|
|
224
|
+
const head = await client.environmentHead(workspace.config.app.code, environment);
|
|
225
|
+
if (!head.activeAppVersionId) {
|
|
226
|
+
throw new Error("OAUTH2_RUNTIME_ROTATION_ACTIVE_VERSION_REQUIRED");
|
|
227
|
+
}
|
|
228
|
+
const deployment = await client.redeploy({
|
|
229
|
+
appCode: workspace.config.app.code,
|
|
230
|
+
appVersionId: head.activeAppVersionId,
|
|
231
|
+
...(head.environmentId ? { environmentId: head.environmentId } : {}),
|
|
232
|
+
environmentKind: head.environmentKind,
|
|
233
|
+
idempotencyKey: `oauth-runtime-rotation:${input.idempotencyKey}`,
|
|
234
|
+
requestId: input.idempotencyKey,
|
|
235
|
+
});
|
|
236
|
+
return this.ok("oauth.runtime.rotate", workspace.context.workspace, {
|
|
237
|
+
rotation,
|
|
238
|
+
deployment,
|
|
239
|
+
next: {
|
|
240
|
+
command: `openxiangda status ${deployment.id}`,
|
|
241
|
+
credentialActivatesDuringDeployment: true,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
async createOAuthClient(root, input) {
|
|
246
|
+
const workspace = await this.workspace(root);
|
|
247
|
+
const environmentKey = this.deploymentEnvironment(input.environmentKey);
|
|
248
|
+
const data = await (await this.client(workspace.root)).createOAuthClient(workspace.config.app.code, {
|
|
249
|
+
...input,
|
|
250
|
+
environmentKey,
|
|
251
|
+
});
|
|
252
|
+
return this.ok("oauth.client.create", workspace.context.workspace, data);
|
|
253
|
+
}
|
|
254
|
+
async updateOAuthClient(root, clientId, input) {
|
|
255
|
+
const workspace = await this.workspace(root);
|
|
256
|
+
const environmentKey = input.environmentKey
|
|
257
|
+
? this.deploymentEnvironment(input.environmentKey)
|
|
258
|
+
: undefined;
|
|
259
|
+
const data = await (await this.client(workspace.root)).updateOAuthClient(workspace.config.app.code, clientId, {
|
|
260
|
+
...input,
|
|
261
|
+
...(environmentKey ? { environmentKey } : {}),
|
|
262
|
+
});
|
|
263
|
+
return this.ok("oauth.client.update", workspace.context.workspace, data);
|
|
264
|
+
}
|
|
265
|
+
async rotateOAuthClientSecret(root, clientId, gracePeriodSeconds = 600) {
|
|
266
|
+
const workspace = await this.workspace(root);
|
|
267
|
+
const data = await (await this.client(workspace.root)).rotateOAuthClientSecret(workspace.config.app.code, clientId, gracePeriodSeconds);
|
|
268
|
+
return this.ok("oauth.client.rotate", workspace.context.workspace, data);
|
|
269
|
+
}
|
|
270
|
+
async revokeOAuthClient(root, clientId) {
|
|
271
|
+
const workspace = await this.workspace(root);
|
|
272
|
+
const data = await (await this.client(workspace.root)).revokeOAuthClient(workspace.config.app.code, clientId);
|
|
273
|
+
return this.ok("oauth.client.revoke", workspace.context.workspace, data);
|
|
274
|
+
}
|
|
275
|
+
async oauthAuditEvents(root, input = {}) {
|
|
276
|
+
const workspace = await this.workspace(root);
|
|
277
|
+
const environmentKey = input.environmentKey
|
|
278
|
+
? this.deploymentEnvironment(input.environmentKey)
|
|
279
|
+
: undefined;
|
|
280
|
+
const data = await (await this.client(workspace.root)).oauthAuditEvents(workspace.config.app.code, {
|
|
281
|
+
...input,
|
|
282
|
+
...(environmentKey ? { environmentKey } : {}),
|
|
283
|
+
});
|
|
284
|
+
return this.ok("oauth.audit.list", workspace.context.workspace, data);
|
|
285
|
+
}
|
|
286
|
+
async applicationSecrets(root, environmentKey) {
|
|
287
|
+
const workspace = await this.workspace(root);
|
|
288
|
+
const environment = this.deploymentEnvironment(environmentKey);
|
|
289
|
+
const data = await (await this.client(workspace.root)).applicationSecrets(workspace.config.app.code, environment);
|
|
290
|
+
return this.ok("secret.list", workspace.context.workspace, data);
|
|
291
|
+
}
|
|
292
|
+
async createApplicationSecret(root, environmentKey, input) {
|
|
293
|
+
const workspace = await this.workspace(root);
|
|
294
|
+
const environment = this.deploymentEnvironment(environmentKey);
|
|
295
|
+
const data = await (await this.client(workspace.root)).createApplicationSecret(workspace.config.app.code, environment, input);
|
|
296
|
+
return this.ok("secret.create", workspace.context.workspace, data);
|
|
297
|
+
}
|
|
298
|
+
async updateApplicationSecret(root, environmentKey, name, input) {
|
|
299
|
+
const workspace = await this.workspace(root);
|
|
300
|
+
const environment = this.deploymentEnvironment(environmentKey);
|
|
301
|
+
const data = await (await this.client(workspace.root)).updateApplicationSecret(workspace.config.app.code, environment, name, input);
|
|
302
|
+
return this.ok("secret.update", workspace.context.workspace, data);
|
|
303
|
+
}
|
|
304
|
+
async rotateApplicationSecret(root, environmentKey, name, input) {
|
|
305
|
+
const workspace = await this.workspace(root);
|
|
306
|
+
const environment = this.deploymentEnvironment(environmentKey);
|
|
307
|
+
const data = await (await this.client(workspace.root)).rotateApplicationSecret(workspace.config.app.code, environment, name, input);
|
|
308
|
+
return this.ok("secret.rotate", workspace.context.workspace, data);
|
|
309
|
+
}
|
|
310
|
+
async deleteApplicationSecret(root, environmentKey, name, input) {
|
|
311
|
+
const workspace = await this.workspace(root);
|
|
312
|
+
const environment = this.deploymentEnvironment(environmentKey);
|
|
313
|
+
const data = await (await this.client(workspace.root)).deleteApplicationSecret(workspace.config.app.code, environment, name, input);
|
|
314
|
+
return this.ok("secret.delete", workspace.context.workspace, data);
|
|
315
|
+
}
|
|
316
|
+
async applicationSecretAuditEvents(root, environmentKey, name, limit = 100) {
|
|
317
|
+
const workspace = await this.workspace(root);
|
|
318
|
+
const environment = this.deploymentEnvironment(environmentKey);
|
|
319
|
+
const data = await (await this.client(workspace.root)).applicationSecretAuditEvents(workspace.config.app.code, environment, name, limit);
|
|
320
|
+
return this.ok("secret.audit.list", workspace.context.workspace, data);
|
|
321
|
+
}
|
|
322
|
+
async nativeRoleMemberships(root, environmentKey, input = {}) {
|
|
323
|
+
const workspace = await this.workspace(root);
|
|
324
|
+
const environment = this.deploymentEnvironment(environmentKey);
|
|
325
|
+
const client = await this.client(workspace.root);
|
|
326
|
+
const data = await client.nativeRoleMemberships(workspace.config.app.code, { ...input, environmentKey: environment });
|
|
327
|
+
return this.ok("authz.membership.list", workspace.context.workspace, data);
|
|
328
|
+
}
|
|
329
|
+
async createNativeRoleMembership(root, input) {
|
|
330
|
+
const workspace = await this.workspace(root);
|
|
331
|
+
const environment = this.deploymentEnvironment(input.environmentKey);
|
|
332
|
+
const client = await this.client(workspace.root);
|
|
333
|
+
const data = await client.createNativeRoleMembership(workspace.config.app.code, { ...input, environmentKey: environment });
|
|
334
|
+
return this.ok("authz.membership.create", workspace.context.workspace, data);
|
|
335
|
+
}
|
|
336
|
+
async updateNativeRoleMembership(root, membershipId, input) {
|
|
337
|
+
const workspace = await this.workspace(root);
|
|
338
|
+
const environment = this.deploymentEnvironment(input.environmentKey);
|
|
339
|
+
const client = await this.client(workspace.root);
|
|
340
|
+
const data = await client.updateNativeRoleMembership(workspace.config.app.code, membershipId, { ...input, environmentKey: environment });
|
|
341
|
+
return this.ok("authz.membership.update", workspace.context.workspace, data);
|
|
342
|
+
}
|
|
343
|
+
async revokeNativeRoleMembership(root, membershipId, input) {
|
|
344
|
+
const workspace = await this.workspace(root);
|
|
345
|
+
const environment = this.deploymentEnvironment(input.environmentKey);
|
|
346
|
+
const client = await this.client(workspace.root);
|
|
347
|
+
const data = await client.revokeNativeRoleMembership(workspace.config.app.code, membershipId, { ...input, environmentKey: environment });
|
|
348
|
+
return this.ok("authz.membership.revoke", workspace.context.workspace, data);
|
|
349
|
+
}
|
|
350
|
+
async nativeSuperAdmins(root, environmentKey, input = {}) {
|
|
351
|
+
const workspace = await this.workspace(root);
|
|
352
|
+
const environment = this.deploymentEnvironment(environmentKey);
|
|
353
|
+
const client = await this.client(workspace.root);
|
|
354
|
+
const data = await client.nativeSuperAdmins(workspace.config.app.code, { ...input, environmentKey: environment });
|
|
355
|
+
return this.ok("authz.super-admin.list", workspace.context.workspace, data);
|
|
356
|
+
}
|
|
357
|
+
async grantNativeSuperAdmin(root, environmentKey, input) {
|
|
358
|
+
const workspace = await this.workspace(root);
|
|
359
|
+
const environment = this.deploymentEnvironment(environmentKey);
|
|
360
|
+
const client = await this.client(workspace.root);
|
|
361
|
+
const data = await client.grantNativeSuperAdmin(workspace.config.app.code, { ...input, environmentKey: environment });
|
|
362
|
+
return this.ok("authz.super-admin.grant", workspace.context.workspace, data);
|
|
363
|
+
}
|
|
364
|
+
async revokeNativeSuperAdmin(root, environmentKey, grantId, input) {
|
|
365
|
+
const workspace = await this.workspace(root);
|
|
366
|
+
const environment = this.deploymentEnvironment(environmentKey);
|
|
367
|
+
const client = await this.client(workspace.root);
|
|
368
|
+
const data = await client.revokeNativeSuperAdmin(workspace.config.app.code, grantId, { ...input, environmentKey: environment });
|
|
369
|
+
return this.ok("authz.super-admin.revoke", workspace.context.workspace, data);
|
|
370
|
+
}
|
|
371
|
+
async nativeRelationshipGrants(root, environmentKey, input = {}) {
|
|
372
|
+
const workspace = await this.workspace(root);
|
|
373
|
+
const environment = this.deploymentEnvironment(environmentKey);
|
|
374
|
+
const client = await this.client(workspace.root);
|
|
375
|
+
const data = await client.nativeRelationshipGrants(workspace.config.app.code, { ...input, environmentKey: environment });
|
|
376
|
+
return this.ok("authz.relationship.list", workspace.context.workspace, data);
|
|
377
|
+
}
|
|
378
|
+
async createNativeRelationshipGrant(root, input) {
|
|
379
|
+
const workspace = await this.workspace(root);
|
|
380
|
+
const environment = this.deploymentEnvironment(input.environmentKey);
|
|
381
|
+
const client = await this.client(workspace.root);
|
|
382
|
+
const data = await client.createNativeRelationshipGrant(workspace.config.app.code, { ...input, environmentKey: environment });
|
|
383
|
+
return this.ok("authz.relationship.create", workspace.context.workspace, data);
|
|
384
|
+
}
|
|
385
|
+
async updateNativeRelationshipGrant(root, grantId, input) {
|
|
386
|
+
const workspace = await this.workspace(root);
|
|
387
|
+
const environment = this.deploymentEnvironment(input.environmentKey);
|
|
388
|
+
const client = await this.client(workspace.root);
|
|
389
|
+
const data = await client.updateNativeRelationshipGrant(workspace.config.app.code, grantId, { ...input, environmentKey: environment });
|
|
390
|
+
return this.ok("authz.relationship.update", workspace.context.workspace, data);
|
|
391
|
+
}
|
|
392
|
+
async revokeNativeRelationshipGrant(root, grantId, input) {
|
|
393
|
+
const workspace = await this.workspace(root);
|
|
394
|
+
const environment = this.deploymentEnvironment(input.environmentKey);
|
|
395
|
+
const client = await this.client(workspace.root);
|
|
396
|
+
const data = await client.revokeNativeRelationshipGrant(workspace.config.app.code, grantId, { ...input, environmentKey: environment });
|
|
397
|
+
return this.ok("authz.relationship.revoke", workspace.context.workspace, data);
|
|
398
|
+
}
|
|
399
|
+
async nativeAuthorizationProjectionHealth(root, environmentKey) {
|
|
400
|
+
const workspace = await this.workspace(root);
|
|
401
|
+
const environment = this.deploymentEnvironment(environmentKey);
|
|
402
|
+
const data = await (await this.client(workspace.root)).nativeAuthorizationProjectionHealth(workspace.config.app.code, environment);
|
|
403
|
+
return this.ok("authz.projection.health", workspace.context.workspace, data);
|
|
404
|
+
}
|
|
405
|
+
async rebuildNativeAuthorizationProjections(root, input) {
|
|
406
|
+
const workspace = await this.workspace(root);
|
|
407
|
+
const environment = this.deploymentEnvironment(input.environmentKey);
|
|
408
|
+
const data = await (await this.client(workspace.root)).rebuildNativeAuthorizationProjections(workspace.config.app.code, {
|
|
409
|
+
...input,
|
|
410
|
+
environmentKey: environment,
|
|
411
|
+
});
|
|
412
|
+
return this.ok("authz.projection.rebuild", workspace.context.workspace, data);
|
|
413
|
+
}
|
|
414
|
+
async recoverNativeAuthorizationProjection(root, jobId, input) {
|
|
415
|
+
const workspace = await this.workspace(root);
|
|
416
|
+
const environment = this.deploymentEnvironment(input.environmentKey);
|
|
417
|
+
const data = await (await this.client(workspace.root)).recoverNativeAuthorizationProjection(workspace.config.app.code, jobId, { ...input, environmentKey: environment });
|
|
418
|
+
return this.ok("authz.projection.recover", workspace.context.workspace, data);
|
|
419
|
+
}
|
|
420
|
+
async eventSubscriptions(root) {
|
|
421
|
+
const workspace = await this.workspace(root);
|
|
422
|
+
const data = await (await this.client(workspace.root)).eventSubscriptions(workspace.config.app.code);
|
|
423
|
+
return this.ok("event.subscription.list", workspace.context.workspace, data);
|
|
424
|
+
}
|
|
425
|
+
async setEventSubscriptionStatus(root, subscriptionId, input) {
|
|
426
|
+
const workspace = await this.workspace(root);
|
|
427
|
+
const data = await (await this.client(workspace.root)).setEventSubscriptionStatus(workspace.config.app.code, subscriptionId, input);
|
|
428
|
+
return this.ok("event.subscription.status", workspace.context.workspace, data);
|
|
429
|
+
}
|
|
430
|
+
async eventDeliveries(root, limit = 100) {
|
|
431
|
+
const workspace = await this.workspace(root);
|
|
432
|
+
const data = await (await this.client(workspace.root)).eventDeliveries(workspace.config.app.code, limit);
|
|
433
|
+
return this.ok("event.delivery.list", workspace.context.workspace, data);
|
|
434
|
+
}
|
|
435
|
+
async replayEventDelivery(root, deliveryId, idempotencyKey) {
|
|
436
|
+
const workspace = await this.workspace(root);
|
|
437
|
+
const data = await (await this.client(workspace.root)).replayEventDelivery(workspace.config.app.code, deliveryId, idempotencyKey);
|
|
438
|
+
return this.ok("event.delivery.replay", workspace.context.workspace, data);
|
|
439
|
+
}
|
|
440
|
+
async timerSubscriptions(root) {
|
|
441
|
+
const workspace = await this.workspace(root);
|
|
442
|
+
const data = await (await this.client(workspace.root)).timerSubscriptions(workspace.config.app.code);
|
|
443
|
+
return this.ok("event.timer.list", workspace.context.workspace, data);
|
|
444
|
+
}
|
|
445
|
+
async setTimerSubscriptionStatus(root, timerId, input) {
|
|
446
|
+
const workspace = await this.workspace(root);
|
|
447
|
+
const data = await (await this.client(workspace.root)).setTimerSubscriptionStatus(workspace.config.app.code, timerId, input);
|
|
448
|
+
return this.ok("event.timer.status", workspace.context.workspace, data);
|
|
449
|
+
}
|
|
450
|
+
async workflowAssigneeProviders(root, environmentKey) {
|
|
451
|
+
const workspace = await this.workspace(root);
|
|
452
|
+
const environment = this.deploymentEnvironment(environmentKey);
|
|
453
|
+
const data = await (await this.client(workspace.root)).workflowAssigneeProviders(workspace.config.app.code, environment);
|
|
454
|
+
return this.ok("workflow.provider.list", workspace.context.workspace, data);
|
|
455
|
+
}
|
|
456
|
+
async rotateWorkflowAssigneeProviderSecret(root, providerCode, input) {
|
|
457
|
+
const workspace = await this.workspace(root);
|
|
458
|
+
const environmentKey = this.deploymentEnvironment(input.environmentKey);
|
|
459
|
+
const data = await (await this.client(workspace.root)).rotateWorkflowAssigneeProviderSecret(workspace.config.app.code, providerCode, { ...input, environmentKey });
|
|
460
|
+
return this.ok("workflow.provider.rotate", workspace.context.workspace, data);
|
|
461
|
+
}
|
|
462
|
+
async linkApplication(root, input) {
|
|
463
|
+
const workspace = await this.workspace(root);
|
|
464
|
+
const path = join(workspace.root, ".openxiangda", "link.json");
|
|
465
|
+
const environments = (input.environments || []).map((environment) => ({
|
|
466
|
+
...environment,
|
|
467
|
+
kind: this.deploymentEnvironment(environment.kind),
|
|
468
|
+
}));
|
|
469
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
470
|
+
const value = {
|
|
471
|
+
schemaVersion: 2,
|
|
472
|
+
appCode: workspace.config.app.code,
|
|
473
|
+
baseUrl: normalizePlatformBaseUrl(input.baseUrl),
|
|
474
|
+
environments,
|
|
475
|
+
};
|
|
476
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
477
|
+
return this.ok("app.link", workspace.context.workspace, { path, ...value });
|
|
478
|
+
}
|
|
479
|
+
async generate(input = {}) {
|
|
480
|
+
const workspace = await this.workspace(input.root);
|
|
481
|
+
const capsuleDiagnostics = this.toolchainDiagnostics(workspace);
|
|
482
|
+
if (capsuleDiagnostics.length > 0) {
|
|
483
|
+
return this.result("generate", workspace.context.workspace, undefined, capsuleDiagnostics);
|
|
484
|
+
}
|
|
485
|
+
const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
|
|
486
|
+
const output = join(workspace.root, "packages/contracts/src/generated.ts");
|
|
487
|
+
const current = existsSync(output) ? readFileSync(output, "utf8") : "";
|
|
488
|
+
const changed = current !== sources.contracts.typescript;
|
|
489
|
+
const diagnostics = [];
|
|
490
|
+
if (input.check && changed) {
|
|
491
|
+
diagnostics.push(this.diagnostic("GENERATED_CONTRACTS_OUTDATED", "生成契约与应用声明不一致", relative(workspace.root, output), "运行 openxiangda check 并提交生成结果"));
|
|
492
|
+
}
|
|
493
|
+
else if (changed) {
|
|
494
|
+
mkdirSync(dirname(output), { recursive: true });
|
|
495
|
+
writeFileSync(output, sources.contracts.typescript, "utf8");
|
|
496
|
+
}
|
|
497
|
+
return this.result("generate", workspace.context.workspace, {
|
|
498
|
+
output,
|
|
499
|
+
changed,
|
|
500
|
+
contractDigest: sources.contracts.digest,
|
|
501
|
+
aiCatalogDigest: sources.aiCatalog.digest,
|
|
502
|
+
}, diagnostics, changed && input.check
|
|
503
|
+
? [
|
|
504
|
+
{
|
|
505
|
+
code: "generate",
|
|
506
|
+
label: "更新生成契约",
|
|
507
|
+
command: "openxiangda check",
|
|
508
|
+
},
|
|
509
|
+
]
|
|
510
|
+
: []);
|
|
511
|
+
}
|
|
512
|
+
async check(root, environmentKey) {
|
|
513
|
+
const workspace = await this.workspace(root);
|
|
514
|
+
const appSpec = inspectAppSpec(workspace.root, this.appSpecContractIndex(workspace, false));
|
|
515
|
+
const diagnostics = [
|
|
516
|
+
...validateAppConfig(workspace.config),
|
|
517
|
+
...this.versionTrainDiagnostics(workspace),
|
|
518
|
+
...validateNestInjectionContract(workspace.root),
|
|
519
|
+
...advisoryAppSpecDiagnostics(appSpec.diagnostics),
|
|
520
|
+
];
|
|
521
|
+
if (diagnostics.some(item => item.severity === "error")) {
|
|
522
|
+
const sealedArtifact = this.writeCheckSealedArtifactStatus(workspace, false);
|
|
523
|
+
return this.result("check", workspace.context.workspace, {
|
|
524
|
+
generated: undefined,
|
|
525
|
+
compatibility: null,
|
|
526
|
+
stages: [],
|
|
527
|
+
sealedArtifact,
|
|
528
|
+
appSpec: summarizeAppSpecContext(appSpec),
|
|
529
|
+
}, diagnostics);
|
|
530
|
+
}
|
|
531
|
+
const generated = await this.generate({ root: workspace.root });
|
|
532
|
+
diagnostics.push(...generated.diagnostics);
|
|
533
|
+
if (!generated.ok) {
|
|
534
|
+
const sealedArtifact = this.writeCheckSealedArtifactStatus(workspace, false);
|
|
535
|
+
return this.result("check", workspace.context.workspace, {
|
|
536
|
+
generated: generated.data,
|
|
537
|
+
compatibility: null,
|
|
538
|
+
stages: [],
|
|
539
|
+
sealedArtifact,
|
|
540
|
+
appSpec: summarizeAppSpecContext(appSpec),
|
|
541
|
+
}, diagnostics, generated.nextActions);
|
|
542
|
+
}
|
|
543
|
+
let compatibility = null;
|
|
544
|
+
if (environmentKey) {
|
|
545
|
+
try {
|
|
546
|
+
compatibility = await this.validateTargetConfigurationCompatibility(workspace, environmentKey);
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
diagnostics.push(this.configurationCompatibilityDiagnostic(error));
|
|
550
|
+
const sealedArtifact = this.writeCheckSealedArtifactStatus(workspace, false);
|
|
551
|
+
return this.result("check", workspace.context.workspace, {
|
|
552
|
+
generated: generated.data,
|
|
553
|
+
compatibility: null,
|
|
554
|
+
stages: [],
|
|
555
|
+
sealedArtifact,
|
|
556
|
+
appSpec: summarizeAppSpecContext(appSpec),
|
|
557
|
+
}, diagnostics);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const stages = ["check", "test", "build"].map(name => ({
|
|
561
|
+
name,
|
|
562
|
+
...this.runPackageScript(workspace.root, name),
|
|
563
|
+
}));
|
|
564
|
+
for (const stage of stages.filter(item => !item.ok)) {
|
|
565
|
+
const code = `WORKSPACE_${stage.name.toUpperCase()}_FAILED`;
|
|
566
|
+
diagnostics.push(this.diagnostic(code, `工作区 ${stage.name} 脚本失败`, `package.json#scripts.${stage.name}`, "修复失败后重新运行 openxiangda check", { output: stage.output }));
|
|
567
|
+
}
|
|
568
|
+
const passed = !diagnostics.some(item => item.severity === "error");
|
|
569
|
+
const sealedArtifact = this.writeCheckSealedArtifactStatus(workspace, passed);
|
|
570
|
+
return this.result("check", workspace.context.workspace, {
|
|
571
|
+
generated: generated.data,
|
|
572
|
+
compatibility,
|
|
573
|
+
stages,
|
|
574
|
+
sealedArtifact,
|
|
575
|
+
appSpec: summarizeAppSpecContext(appSpec),
|
|
576
|
+
}, diagnostics, passed
|
|
577
|
+
? [
|
|
578
|
+
{
|
|
579
|
+
code: "deploy",
|
|
580
|
+
label: "密封并部署到预发",
|
|
581
|
+
command: "openxiangda deploy",
|
|
582
|
+
},
|
|
583
|
+
]
|
|
584
|
+
: []);
|
|
585
|
+
}
|
|
586
|
+
async test(root) {
|
|
587
|
+
const workspace = await this.workspace(root);
|
|
588
|
+
const diagnostics = this.toolchainDiagnostics(workspace);
|
|
589
|
+
if (diagnostics.length > 0) {
|
|
590
|
+
return this.result("test", workspace.context.workspace, undefined, diagnostics);
|
|
591
|
+
}
|
|
592
|
+
const command = this.runPackageScript(workspace.root, "test");
|
|
593
|
+
const commandDiagnostics = command.ok
|
|
594
|
+
? []
|
|
595
|
+
: [
|
|
596
|
+
this.diagnostic("WORKSPACE_TEST_FAILED", "工作区测试失败", "package.json#scripts.test", "修复失败测试后重试", { output: command.output }),
|
|
597
|
+
];
|
|
598
|
+
return this.result("test", workspace.context.workspace, command, commandDiagnostics);
|
|
599
|
+
}
|
|
600
|
+
async build(input = {}) {
|
|
601
|
+
const sealed = await this.buildSealed(input);
|
|
602
|
+
if (!sealed.data)
|
|
603
|
+
return sealed;
|
|
604
|
+
return {
|
|
605
|
+
...sealed,
|
|
606
|
+
data: {
|
|
607
|
+
package: sealed.data.package,
|
|
608
|
+
sealedArtifact: sealed.data.sealedArtifact,
|
|
609
|
+
...(sealed.data.outputDirectory
|
|
610
|
+
? { outputDirectory: sealed.data.outputDirectory }
|
|
611
|
+
: {}),
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
async buildPreview(root) {
|
|
616
|
+
const checked = await this.check(root);
|
|
617
|
+
return {
|
|
618
|
+
...checked,
|
|
619
|
+
operation: "build.preview",
|
|
620
|
+
data: {
|
|
621
|
+
sealed: false,
|
|
622
|
+
packageDigest: null,
|
|
623
|
+
check: checked.data,
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
async buildSealed(input = {}) {
|
|
628
|
+
const workspace = await this.workspace(input.root);
|
|
629
|
+
const diagnostics = this.toolchainDiagnostics(workspace);
|
|
630
|
+
if (diagnostics.length > 0) {
|
|
631
|
+
return this.result("build", workspace.context.workspace, undefined, diagnostics);
|
|
632
|
+
}
|
|
633
|
+
const generated = await this.generate({ root: workspace.root });
|
|
634
|
+
if (!generated.ok) {
|
|
635
|
+
return this.result("build", workspace.context.workspace, undefined, generated.diagnostics, generated.nextActions);
|
|
636
|
+
}
|
|
637
|
+
if (!input.skipWorkspaceBuild) {
|
|
638
|
+
const command = this.runPackageScript(workspace.root, "build");
|
|
639
|
+
if (!command.ok) {
|
|
640
|
+
diagnostics.push(this.diagnostic("WORKSPACE_BUILD_FAILED", "前后端生产构建失败", "package.json#scripts.build", "修复构建错误后重试", { output: command.output }));
|
|
641
|
+
return this.result("build", workspace.context.workspace, undefined, diagnostics);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const hasBackend = backendRuntimeRequired(workspace.config);
|
|
645
|
+
const backendImage = String(input.backendImage || "").trim();
|
|
646
|
+
if (hasBackend && !backendImage) {
|
|
647
|
+
diagnostics.push(this.diagnostic("BACKEND_IMAGE_REQUIRED", "当前应用包含 NestJS 后端,密封 AppPackage 时必须提供不可变 OCI image", "backend.image", "运行 openxiangda deploy 由工具自动构建并推送官方后端镜像"));
|
|
648
|
+
return this.result("build", workspace.context.workspace, undefined, diagnostics);
|
|
649
|
+
}
|
|
650
|
+
if (hasBackend && !/@sha256:[a-f0-9]{64}$/i.test(backendImage)) {
|
|
651
|
+
diagnostics.push(this.diagnostic("BACKEND_IMAGE_NOT_IMMUTABLE", "后端镜像必须使用不可变 sha256 digest,不能使用可变 tag", "backend.image", "运行 openxiangda deploy 重新构建并取得不可变镜像摘要"));
|
|
652
|
+
return this.result("build", workspace.context.workspace, undefined, diagnostics);
|
|
653
|
+
}
|
|
654
|
+
const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
|
|
655
|
+
const frontendContent = this.directoryBundle(join(workspace.root, workspace.config.frontend.root, "dist"));
|
|
656
|
+
const frontendDigest = sha256Bytes(frontendContent);
|
|
657
|
+
const backendContent = hasBackend
|
|
658
|
+
? canonicalJson({ image: backendImage })
|
|
659
|
+
: undefined;
|
|
660
|
+
const backendDigest = backendContent
|
|
661
|
+
? sha256Bytes(backendContent)
|
|
662
|
+
: undefined;
|
|
663
|
+
const artifacts = [
|
|
664
|
+
{
|
|
665
|
+
kind: "frontend",
|
|
666
|
+
digest: frontendDigest,
|
|
667
|
+
mediaType: "application/vnd.openxiangda.frontend-bundle.v2+json",
|
|
668
|
+
size: Buffer.byteLength(frontendContent),
|
|
669
|
+
entrypoint: "index.html",
|
|
670
|
+
},
|
|
671
|
+
...(backendDigest && backendContent
|
|
672
|
+
? [
|
|
673
|
+
{
|
|
674
|
+
kind: "backend",
|
|
675
|
+
digest: backendDigest,
|
|
676
|
+
mediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
677
|
+
size: Buffer.byteLength(backendContent),
|
|
678
|
+
metadata: {
|
|
679
|
+
imageDigest: backendImage,
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
]
|
|
683
|
+
: []),
|
|
684
|
+
sources.config.artifact,
|
|
685
|
+
sources.contracts.artifact,
|
|
686
|
+
];
|
|
687
|
+
const revision = workspace.context.workspace.revision || "0".repeat(40);
|
|
688
|
+
const packageVersion = String(workspace.packageJson.version || "0.0.0");
|
|
689
|
+
const compiled = compileAppPackage({
|
|
690
|
+
config: workspace.config,
|
|
691
|
+
version: `${packageVersion}-${revision.slice(0, 12)}`,
|
|
692
|
+
createdAt: git(workspace.root, ["show", "-s", "--format=%cI", "HEAD"]) ||
|
|
693
|
+
new Date(0).toISOString(),
|
|
694
|
+
source: {
|
|
695
|
+
repository: workspace.context.workspace.repository || `local:${workspace.root}`,
|
|
696
|
+
commit: revision,
|
|
697
|
+
dirty: workspace.context.workspace.dirty === true,
|
|
698
|
+
},
|
|
699
|
+
toolchainVersion: this.toolchainVersion,
|
|
700
|
+
artifacts,
|
|
701
|
+
manifests: {
|
|
702
|
+
frontend: frontendDigest,
|
|
703
|
+
...(backendDigest ? { backend: backendDigest } : {}),
|
|
704
|
+
config: sources.config.digest,
|
|
705
|
+
dataContract: sources.contracts.digest,
|
|
706
|
+
},
|
|
707
|
+
minimumPlatformVersion: "2.0.0-alpha.5",
|
|
708
|
+
metadata: {
|
|
709
|
+
backend: hasBackend
|
|
710
|
+
? {
|
|
711
|
+
present: true,
|
|
712
|
+
isolation: workspace.config.backend.isolation || "shared",
|
|
713
|
+
resourceProfile: workspace.config.backend.resourceProfile || "light",
|
|
714
|
+
}
|
|
715
|
+
: { present: false },
|
|
716
|
+
aiCatalogDigest: sources.aiCatalog.digest,
|
|
717
|
+
aiCatalog: sources.aiCatalog.value,
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
const artifactContent = {
|
|
721
|
+
[frontendDigest]: frontendContent,
|
|
722
|
+
[sources.config.digest]: sources.config.content,
|
|
723
|
+
[sources.contracts.digest]: sources.contracts.content,
|
|
724
|
+
};
|
|
725
|
+
if (backendDigest && backendContent) {
|
|
726
|
+
artifactContent[backendDigest] = backendContent;
|
|
727
|
+
}
|
|
728
|
+
let outputDirectory;
|
|
729
|
+
const sealedArtifact = this.sealedArtifactStatus(workspace, compiled.digest, input.write !== false);
|
|
730
|
+
if (input.write !== false) {
|
|
731
|
+
outputDirectory = join(workspace.root, ".openxiangda", "build");
|
|
732
|
+
mkdirSync(join(outputDirectory, "artifacts"), { recursive: true });
|
|
733
|
+
writeFileSync(join(outputDirectory, "app-package.json"), `${JSON.stringify(compiled.manifest, null, 2)}\n`, "utf8");
|
|
734
|
+
for (const [digest, content] of Object.entries(artifactContent)) {
|
|
735
|
+
writeFileSync(join(outputDirectory, "artifacts", digest), content, "utf8");
|
|
736
|
+
}
|
|
737
|
+
this.writeSealedArtifactStatus(workspace.root, sealedArtifact);
|
|
738
|
+
}
|
|
739
|
+
return this.result("build", workspace.context.workspace, {
|
|
740
|
+
package: compiled,
|
|
741
|
+
artifactContent,
|
|
742
|
+
sealedArtifact,
|
|
743
|
+
...(outputDirectory ? { outputDirectory } : {}),
|
|
744
|
+
}, diagnostics, [
|
|
745
|
+
{
|
|
746
|
+
code: "deploy",
|
|
747
|
+
label: "部署到预发",
|
|
748
|
+
command: "openxiangda deploy",
|
|
749
|
+
},
|
|
750
|
+
]);
|
|
751
|
+
}
|
|
752
|
+
async deploymentPlan(input) {
|
|
753
|
+
const workspace = await this.workspace(input.root);
|
|
754
|
+
const diagnostics = this.toolchainDiagnostics(workspace);
|
|
755
|
+
if (diagnostics.length > 0) {
|
|
756
|
+
return this.result("deployment.plan", workspace.context.workspace, undefined, diagnostics);
|
|
757
|
+
}
|
|
758
|
+
const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
|
|
759
|
+
return this.ok("deployment.plan", workspace.context.workspace, {
|
|
760
|
+
environment: input.environment,
|
|
761
|
+
appCode: workspace.config.app.code,
|
|
762
|
+
sealed: false,
|
|
763
|
+
packageDigest: null,
|
|
764
|
+
buildOwner: "openxiangda deploy",
|
|
765
|
+
frontendRoot: workspace.config.frontend.root,
|
|
766
|
+
backendRoot: workspace.config.backend.root,
|
|
767
|
+
configDigest: sources.config.digest,
|
|
768
|
+
contractDigest: sources.contracts.digest,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
async deploy(input) {
|
|
772
|
+
const workspace = await this.workspace(input.root);
|
|
773
|
+
const capsuleDiagnostics = this.toolchainDiagnostics(workspace);
|
|
774
|
+
if (capsuleDiagnostics.length > 0) {
|
|
775
|
+
return this.result("deploy", workspace.context.workspace, undefined, capsuleDiagnostics);
|
|
776
|
+
}
|
|
777
|
+
if (String(input.environment) !== "preproduction") {
|
|
778
|
+
return this.result("deploy", workspace.context.workspace, undefined, [
|
|
779
|
+
this.diagnostic("DEPLOY_PREPRODUCTION_ONLY", "Native 2.0 只允许直接部署到预发环境", String(input.environment), "使用 openxiangda deploy;生产环境必须显式复用成功的测试 DeploymentRun"),
|
|
780
|
+
]);
|
|
781
|
+
}
|
|
782
|
+
const client = await this.client(workspace.root);
|
|
783
|
+
const capabilities = await client.capabilities();
|
|
784
|
+
const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
|
|
785
|
+
const { assertApplicationContractCompatible, assertRequiredCapabilitiesAvailable, } = await import("./deployment.js");
|
|
786
|
+
assertApplicationContractCompatible(capabilities, {
|
|
787
|
+
appPackageSchemaVersion: SCHEMA_VERSIONS.appPackage,
|
|
788
|
+
configurationBundleSchemaVersion: sources.config.value.schemaVersion,
|
|
789
|
+
contractBundleSchemaVersion: sources.contracts.value.schemaVersion,
|
|
790
|
+
compilerContractVersion: sources.config.value.compilerContractVersion,
|
|
791
|
+
});
|
|
792
|
+
assertRequiredCapabilitiesAvailable(capabilities, requiredPlatformCapabilities(workspace.config));
|
|
793
|
+
const hasBackend = backendRuntimeRequired(workspace.config);
|
|
794
|
+
const target = !hasBackend || input.backendImage
|
|
795
|
+
? undefined
|
|
796
|
+
: backendImageBuildTarget(capabilities, workspace.config.app.code);
|
|
797
|
+
const checked = await this.check(workspace.root, input.environment);
|
|
798
|
+
if (!checked.ok)
|
|
799
|
+
return { ...checked, operation: "deploy" };
|
|
800
|
+
const backendImage = hasBackend
|
|
801
|
+
? input.backendImage
|
|
802
|
+
? input.backendImage
|
|
803
|
+
: (await publishBackendImage({
|
|
804
|
+
root: workspace.root,
|
|
805
|
+
backendRoot: workspace.config.backend.root,
|
|
806
|
+
target: target,
|
|
807
|
+
...(workspace.context.workspace.revision
|
|
808
|
+
? { sourceRevision: workspace.context.workspace.revision }
|
|
809
|
+
: {}),
|
|
810
|
+
})).reference
|
|
811
|
+
: undefined;
|
|
812
|
+
const built = await this.buildSealed({
|
|
813
|
+
...input,
|
|
814
|
+
root: workspace.root,
|
|
815
|
+
...(backendImage ? { backendImage } : {}),
|
|
816
|
+
skipWorkspaceBuild: true,
|
|
817
|
+
});
|
|
818
|
+
if (!built.ok || !built.data)
|
|
819
|
+
return built;
|
|
820
|
+
const deployment = await (await import("./deployment.js")).submitAppPackage({
|
|
821
|
+
client,
|
|
822
|
+
capabilities,
|
|
823
|
+
compiledPackage: built.data.package,
|
|
824
|
+
artifactContent: built.data.artifactContent,
|
|
825
|
+
environmentKind: input.environment,
|
|
826
|
+
...(input.environmentId ? { environmentId: input.environmentId } : {}),
|
|
827
|
+
idempotencyKey: input.idempotencyKey ||
|
|
828
|
+
`deploy:${built.data.package.digest}:${input.environment}`,
|
|
829
|
+
...(input.requestId ? { requestId: input.requestId } : {}),
|
|
830
|
+
});
|
|
831
|
+
return this.ok("deploy", workspace.context.workspace, deployment, [
|
|
832
|
+
{
|
|
833
|
+
code: "status",
|
|
834
|
+
label: "查看部署状态",
|
|
835
|
+
command: `openxiangda status ${deployment.id}`,
|
|
836
|
+
},
|
|
837
|
+
]);
|
|
838
|
+
}
|
|
839
|
+
async deploymentStatus(root, deploymentId) {
|
|
840
|
+
const workspace = await this.workspace(root);
|
|
841
|
+
const client = await this.client(workspace.root);
|
|
842
|
+
let data;
|
|
843
|
+
if (deploymentId) {
|
|
844
|
+
data = await client.deployment(workspace.config.app.code, deploymentId);
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
const latest = (await client.deployments(workspace.config.app.code, 1))
|
|
848
|
+
.items[0];
|
|
849
|
+
data = latest
|
|
850
|
+
? await client.deployment(workspace.config.app.code, latest.id)
|
|
851
|
+
: undefined;
|
|
852
|
+
}
|
|
853
|
+
if (!data) {
|
|
854
|
+
return this.result("status", workspace.context.workspace, undefined, [
|
|
855
|
+
this.diagnostic("DEPLOYMENT_NOT_FOUND", "当前应用还没有部署记录", "deploymentId", "运行 openxiangda deploy 创建测试部署"),
|
|
856
|
+
], [
|
|
857
|
+
{ code: "deploy", label: "部署测试环境", command: "openxiangda deploy" },
|
|
858
|
+
]);
|
|
859
|
+
}
|
|
860
|
+
return this.ok("status", workspace.context.workspace, data, this.deploymentRecoveryActions(data));
|
|
861
|
+
}
|
|
862
|
+
async environmentStatus(root) {
|
|
863
|
+
const workspace = await this.workspace(root);
|
|
864
|
+
const data = await (await this.client(workspace.root)).applicationEnvironments(workspace.config.app.code);
|
|
865
|
+
return this.ok("environment.status", workspace.context.workspace, data);
|
|
866
|
+
}
|
|
867
|
+
async startEnvironment(root, environment, options = {}) {
|
|
868
|
+
return await this.changeEnvironmentRuntimeState(root, environment, "start", options);
|
|
869
|
+
}
|
|
870
|
+
async stopEnvironment(root, environment, options = {}) {
|
|
871
|
+
return await this.changeEnvironmentRuntimeState(root, environment, "stop", options);
|
|
872
|
+
}
|
|
873
|
+
async deploymentLogs(root, deploymentId) {
|
|
874
|
+
const status = await this.deploymentStatus(root, deploymentId);
|
|
875
|
+
if (!status.ok || !status.data)
|
|
876
|
+
return { ...status, operation: "logs" };
|
|
877
|
+
const run = status.data;
|
|
878
|
+
return {
|
|
879
|
+
...status,
|
|
880
|
+
operation: "logs",
|
|
881
|
+
data: {
|
|
882
|
+
deploymentId: run.id,
|
|
883
|
+
status: run.status,
|
|
884
|
+
stage: run.stage,
|
|
885
|
+
checkpoints: run.checkpoints,
|
|
886
|
+
failure: run.failure,
|
|
887
|
+
rootFailure: run.rootFailure,
|
|
888
|
+
latestFailure: run.latestFailure,
|
|
889
|
+
candidate: run.candidate,
|
|
890
|
+
recovery: run.recovery,
|
|
891
|
+
attempts: run.attempts,
|
|
892
|
+
traceId: run.result?.traceId,
|
|
893
|
+
},
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
async retry(root, deploymentId) {
|
|
897
|
+
const workspace = await this.workspace(root);
|
|
898
|
+
const deployment = await (await this.client(workspace.root)).retryDeployment(workspace.config.app.code, deploymentId);
|
|
899
|
+
return this.ok("retry", workspace.context.workspace, deployment, [
|
|
900
|
+
{
|
|
901
|
+
code: "status",
|
|
902
|
+
label: "查看重试状态",
|
|
903
|
+
command: `openxiangda status ${deployment.id}`,
|
|
904
|
+
},
|
|
905
|
+
]);
|
|
906
|
+
}
|
|
907
|
+
async cancel(root, deploymentId) {
|
|
908
|
+
const workspace = await this.workspace(root);
|
|
909
|
+
const deployment = await (await this.client(workspace.root)).cancelDeployment(workspace.config.app.code, deploymentId);
|
|
910
|
+
return this.ok("cancel", workspace.context.workspace, deployment, [
|
|
911
|
+
{
|
|
912
|
+
code: "status",
|
|
913
|
+
label: "查看取消结果",
|
|
914
|
+
command: `openxiangda status ${deployment.id}`,
|
|
915
|
+
},
|
|
916
|
+
]);
|
|
917
|
+
}
|
|
918
|
+
deploymentRecoveryActions(run) {
|
|
919
|
+
const recovery = run.recovery;
|
|
920
|
+
return [
|
|
921
|
+
...(recovery.nextCommand
|
|
922
|
+
? [
|
|
923
|
+
{
|
|
924
|
+
code: recovery.retryable ? "retry" : "recover",
|
|
925
|
+
label: recovery.retryable
|
|
926
|
+
? "显式重试失败部署"
|
|
927
|
+
: "执行平台指定恢复",
|
|
928
|
+
command: recovery.nextCommand,
|
|
929
|
+
},
|
|
930
|
+
]
|
|
931
|
+
: []),
|
|
932
|
+
{
|
|
933
|
+
code: "logs",
|
|
934
|
+
label: "查看部署尝试账本",
|
|
935
|
+
command: `openxiangda logs ${run.id}`,
|
|
936
|
+
},
|
|
937
|
+
...(recovery.cancelAllowed
|
|
938
|
+
? [
|
|
939
|
+
{
|
|
940
|
+
code: "cancel",
|
|
941
|
+
label: "取消未激活部署",
|
|
942
|
+
command: `openxiangda cancel ${run.id}`,
|
|
943
|
+
},
|
|
944
|
+
]
|
|
945
|
+
: []),
|
|
946
|
+
];
|
|
947
|
+
}
|
|
948
|
+
async deployProduction(root, deploymentId) {
|
|
949
|
+
const workspace = await this.workspace(root);
|
|
950
|
+
const capsuleDiagnostics = this.toolchainDiagnostics(workspace);
|
|
951
|
+
if (capsuleDiagnostics.length > 0) {
|
|
952
|
+
return this.result("deploy", workspace.context.workspace, undefined, capsuleDiagnostics);
|
|
953
|
+
}
|
|
954
|
+
const environment = "production";
|
|
955
|
+
const client = await this.client(workspace.root);
|
|
956
|
+
const source = await client.deployment(workspace.config.app.code, deploymentId);
|
|
957
|
+
const appVersionId = String(source.result?.applicationVersionId || "");
|
|
958
|
+
if (!appVersionId)
|
|
959
|
+
throw new Error("DEPLOYMENT_APP_VERSION_NOT_AVAILABLE");
|
|
960
|
+
const diagnostics = await this.productionPromotionDiagnostics(workspace, client, source, appVersionId);
|
|
961
|
+
if (diagnostics.length > 0) {
|
|
962
|
+
return this.result("deploy", workspace.context.workspace, undefined, diagnostics);
|
|
963
|
+
}
|
|
964
|
+
const deployment = await client.promote({
|
|
965
|
+
appCode: workspace.config.app.code,
|
|
966
|
+
appVersionId,
|
|
967
|
+
environmentKind: environment,
|
|
968
|
+
idempotencyKey: `promote:${source.packageDigest}:${environment}`,
|
|
969
|
+
});
|
|
970
|
+
return this.ok("deploy", workspace.context.workspace, deployment, [
|
|
971
|
+
{ code: "status", label: "查看生产部署状态", command: `openxiangda status ${deployment.id}` },
|
|
972
|
+
]);
|
|
973
|
+
}
|
|
974
|
+
async productionPromotionDiagnostics(workspace, client, source, appVersionId) {
|
|
975
|
+
const diagnostics = [];
|
|
976
|
+
if (source.status !== "succeeded" ||
|
|
977
|
+
source.environment.kind !== "preproduction") {
|
|
978
|
+
diagnostics.push(this.diagnostic("PRODUCTION_PREPRODUCTION_RUN_REQUIRED", "生产晋级来源必须是成功的预发 DeploymentRun", "deploymentId", "等待预发部署成功,并使用该 DeploymentRun ID 晋级", { status: source.status, environment: source.environment.kind }));
|
|
979
|
+
return diagnostics;
|
|
980
|
+
}
|
|
981
|
+
const versions = await client.appVersions(workspace.config.app.code, 100);
|
|
982
|
+
const version = versions.items.find(item => item.id === appVersionId);
|
|
983
|
+
if (!version) {
|
|
984
|
+
return [
|
|
985
|
+
this.diagnostic("PRODUCTION_APP_VERSION_NOT_FOUND", "预发运行引用的 AppVersion 不在当前应用版本集中", "appVersionId"),
|
|
986
|
+
];
|
|
987
|
+
}
|
|
988
|
+
if (workspace.context.workspace.dirty || version.source.dirty) {
|
|
989
|
+
diagnostics.push(this.diagnostic("PRODUCTION_SOURCE_DIRTY", "生产晋级只接受来源干净且当前工作区无未提交变更的 AppVersion", "git", "提交并推送变更后重新部署预发,再晋级该 AppVersion"));
|
|
990
|
+
}
|
|
991
|
+
const repository = git(workspace.root, ["config", "--get", "remote.origin.url"]);
|
|
992
|
+
const currentRepository = this.repositoryIdentity(repository);
|
|
993
|
+
const sourceRepository = this.repositoryIdentity(version.source.repository);
|
|
994
|
+
if (!currentRepository ||
|
|
995
|
+
!sourceRepository ||
|
|
996
|
+
currentRepository !== sourceRepository) {
|
|
997
|
+
diagnostics.push(this.diagnostic("PRODUCTION_SOURCE_REPOSITORY_MISMATCH", "当前工作区仓库与 AppVersion 来源仓库不一致", "git.remote.origin.url", "在构建该 AppVersion 的同一仓库中执行晋级", { current: repository || null, source: version.source.repository }));
|
|
998
|
+
}
|
|
999
|
+
const upstream = git(workspace.root, [
|
|
1000
|
+
"rev-parse",
|
|
1001
|
+
"--abbrev-ref",
|
|
1002
|
+
"--symbolic-full-name",
|
|
1003
|
+
"@{upstream}",
|
|
1004
|
+
]);
|
|
1005
|
+
if (!upstream) {
|
|
1006
|
+
diagnostics.push(this.diagnostic("PRODUCTION_UPSTREAM_MISSING", "当前 Git 分支没有 upstream,无法证明 AppVersion 来源已进入远端主线", "git", "为当前分支配置 upstream 并推送来源 commit"));
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
const refreshed = spawnSync("git", ["-C", workspace.root, "fetch", "--quiet"], { encoding: "utf8" });
|
|
1010
|
+
if (refreshed.status !== 0) {
|
|
1011
|
+
diagnostics.push(this.diagnostic("PRODUCTION_UPSTREAM_REFRESH_FAILED", "无法刷新远端引用,生产晋级不能依赖可能过期的本地 upstream", "git", "恢复远端访问并重新执行晋级"));
|
|
1012
|
+
return diagnostics;
|
|
1013
|
+
}
|
|
1014
|
+
const contained = spawnSync("git", ["-C", workspace.root, "merge-base", "--is-ancestor", version.source.commit, upstream], { encoding: "utf8" });
|
|
1015
|
+
if (contained.status !== 0) {
|
|
1016
|
+
diagnostics.push(this.diagnostic("PRODUCTION_SOURCE_NOT_IN_UPSTREAM", "AppVersion 来源 commit 尚未包含在当前 upstream 中", "git", "推送来源 commit,确认 upstream 更新后再晋级", { commit: version.source.commit, upstream }));
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return diagnostics;
|
|
1020
|
+
}
|
|
1021
|
+
async rollback(root, environment, appVersionId, options = {}) {
|
|
1022
|
+
const workspace = await this.workspace(root);
|
|
1023
|
+
const targetEnvironment = this.deploymentEnvironment(environment);
|
|
1024
|
+
const requestedOperationId = options.operationId?.trim();
|
|
1025
|
+
if (requestedOperationId && !UUID_PATTERN.test(requestedOperationId)) {
|
|
1026
|
+
throw new Error("OPENXIANGDA_ROLLBACK_OPERATION_ID_INVALID: --operation-id 必须是 UUID");
|
|
1027
|
+
}
|
|
1028
|
+
const operationId = requestedOperationId || randomUUID();
|
|
1029
|
+
const deployment = await (await this.client(workspace.root)).rollback({
|
|
1030
|
+
appCode: workspace.config.app.code,
|
|
1031
|
+
appVersionId,
|
|
1032
|
+
environmentKind: targetEnvironment,
|
|
1033
|
+
idempotencyKey: `rollback:${appVersionId}:${targetEnvironment}:${operationId}`,
|
|
1034
|
+
requestId: operationId,
|
|
1035
|
+
});
|
|
1036
|
+
return this.ok("rollback", workspace.context.workspace, deployment);
|
|
1037
|
+
}
|
|
1038
|
+
deploymentEnvironment(value) {
|
|
1039
|
+
const normalized = String(value || "").trim();
|
|
1040
|
+
if (normalized === "preproduction" || normalized === "production") {
|
|
1041
|
+
return normalized;
|
|
1042
|
+
}
|
|
1043
|
+
throw new Error(`OPENXIANGDA_REMOTE_ENVIRONMENT_INVALID: ${normalized || "<empty>"}`);
|
|
1044
|
+
}
|
|
1045
|
+
async changeEnvironmentRuntimeState(root, environment, action, options) {
|
|
1046
|
+
const workspace = await this.workspace(root);
|
|
1047
|
+
const environmentKey = this.deploymentEnvironment(environment);
|
|
1048
|
+
const client = await this.client(workspace.root);
|
|
1049
|
+
const environments = await client.applicationEnvironments(workspace.config.app.code);
|
|
1050
|
+
const current = environments.items.find(item => item.environmentKey === environmentKey);
|
|
1051
|
+
if (!current) {
|
|
1052
|
+
throw new Error(`OPENXIANGDA_ENVIRONMENT_NOT_PROVISIONED: ${environmentKey}`);
|
|
1053
|
+
}
|
|
1054
|
+
const input = {
|
|
1055
|
+
idempotencyKey: options.idempotencyKey ||
|
|
1056
|
+
`environment:${action}:${environmentKey}:revision:${current.revision}`,
|
|
1057
|
+
...(options.requestId ? { requestId: options.requestId } : {}),
|
|
1058
|
+
};
|
|
1059
|
+
const deployment = action === "start"
|
|
1060
|
+
? await client.startEnvironment(workspace.config.app.code, environmentKey, input)
|
|
1061
|
+
: await client.stopEnvironment(workspace.config.app.code, environmentKey, input);
|
|
1062
|
+
return this.ok(`environment.${action}`, workspace.context.workspace, deployment, [
|
|
1063
|
+
{
|
|
1064
|
+
code: "status",
|
|
1065
|
+
label: "查看环境状态",
|
|
1066
|
+
command: "openxiangda status",
|
|
1067
|
+
},
|
|
1068
|
+
]);
|
|
1069
|
+
}
|
|
1070
|
+
async dev(root, input = {}) {
|
|
1071
|
+
const workspace = await this.workspace(root);
|
|
1072
|
+
const capsuleDiagnostics = this.toolchainDiagnostics(workspace);
|
|
1073
|
+
if (capsuleDiagnostics.length > 0) {
|
|
1074
|
+
return this.result("dev", workspace.context.workspace, undefined, capsuleDiagnostics);
|
|
1075
|
+
}
|
|
1076
|
+
const injectionDiagnostics = validateNestInjectionContract(workspace.root);
|
|
1077
|
+
if (injectionDiagnostics.length) {
|
|
1078
|
+
return this.result("dev", workspace.context.workspace, undefined, injectionDiagnostics);
|
|
1079
|
+
}
|
|
1080
|
+
const generated = await this.generate({
|
|
1081
|
+
root: workspace.root,
|
|
1082
|
+
});
|
|
1083
|
+
if (!generated.ok) {
|
|
1084
|
+
return this.result("dev", workspace.context.workspace, undefined, generated.diagnostics, generated.nextActions);
|
|
1085
|
+
}
|
|
1086
|
+
try {
|
|
1087
|
+
const linkPath = join(workspace.root, ".openxiangda", "link.json");
|
|
1088
|
+
if (!existsSync(linkPath)) {
|
|
1089
|
+
throw new Error("OPENXIANGDA_CONNECTED_LINK_REQUIRED: 先运行 openxiangda create <directory>");
|
|
1090
|
+
}
|
|
1091
|
+
let link;
|
|
1092
|
+
try {
|
|
1093
|
+
link = JSON.parse(readFileSync(linkPath, "utf8"));
|
|
1094
|
+
}
|
|
1095
|
+
catch {
|
|
1096
|
+
throw new Error("OPENXIANGDA_CONNECTED_LINK_INVALID");
|
|
1097
|
+
}
|
|
1098
|
+
if (link.schemaVersion !== 2 || link.appCode !== workspace.config.app.code || !link.baseUrl) {
|
|
1099
|
+
throw new Error("OPENXIANGDA_CONNECTED_LINK_INVALID");
|
|
1100
|
+
}
|
|
1101
|
+
const developerSession = await OpenXiangdaDeveloperSession.load();
|
|
1102
|
+
if (!developerSession) {
|
|
1103
|
+
throw new Error("OPENXIANGDA_CONNECTED_LOGIN_REQUIRED: 先运行 openxiangda login");
|
|
1104
|
+
}
|
|
1105
|
+
developerSession.assertPlatform(link.baseUrl);
|
|
1106
|
+
await developerSession.whoami();
|
|
1107
|
+
const client = new OpenXiangdaControlPlaneClient({
|
|
1108
|
+
baseUrl: developerSession.baseUrl,
|
|
1109
|
+
tokenProvider: developerSession,
|
|
1110
|
+
});
|
|
1111
|
+
const environments = await client.applicationEnvironments(workspace.config.app.code);
|
|
1112
|
+
const environment = selectConnectedDevelopmentEnvironment(environments.items);
|
|
1113
|
+
if (!environment) {
|
|
1114
|
+
throw new Error("OPENXIANGDA_CONNECTED_ACTIVE_ENVIRONMENT_REQUIRED: 应用需要已发布的 test(preproduction) 或 production 环境");
|
|
1115
|
+
}
|
|
1116
|
+
const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
|
|
1117
|
+
const remoteSession = {
|
|
1118
|
+
create: async () => {
|
|
1119
|
+
const value = await client.createConnectedDevelopmentSession(workspace.config.app.code, {
|
|
1120
|
+
environmentKey: environment.environmentKey,
|
|
1121
|
+
manifestDigest: sources.config.digest,
|
|
1122
|
+
configuration: sources.config.value,
|
|
1123
|
+
});
|
|
1124
|
+
return {
|
|
1125
|
+
id: value.sessionId,
|
|
1126
|
+
token: value.sessionToken,
|
|
1127
|
+
expiresAt: value.expiresAt,
|
|
1128
|
+
mode: value.mode,
|
|
1129
|
+
manifestOverlay: value.manifestOverlay,
|
|
1130
|
+
manifestDigest: value.manifestDigest,
|
|
1131
|
+
};
|
|
1132
|
+
},
|
|
1133
|
+
current: async (token) => {
|
|
1134
|
+
const value = await client.connectedDevelopmentSession(workspace.config.app.code, token);
|
|
1135
|
+
return {
|
|
1136
|
+
id: value.sessionId,
|
|
1137
|
+
expiresAt: value.expiresAt,
|
|
1138
|
+
mode: value.mode,
|
|
1139
|
+
manifestOverlay: value.manifestOverlay,
|
|
1140
|
+
manifestDigest: value.manifestDigest,
|
|
1141
|
+
};
|
|
1142
|
+
},
|
|
1143
|
+
refresh: async (token) => {
|
|
1144
|
+
const value = await client.refreshConnectedDevelopmentSession(workspace.config.app.code, token);
|
|
1145
|
+
return {
|
|
1146
|
+
id: value.sessionId,
|
|
1147
|
+
expiresAt: value.expiresAt,
|
|
1148
|
+
mode: value.mode,
|
|
1149
|
+
manifestOverlay: value.manifestOverlay,
|
|
1150
|
+
manifestDigest: value.manifestDigest,
|
|
1151
|
+
};
|
|
1152
|
+
},
|
|
1153
|
+
revoke: async (token) => {
|
|
1154
|
+
await client.revokeConnectedDevelopmentSession(workspace.config.app.code, token);
|
|
1155
|
+
},
|
|
1156
|
+
};
|
|
1157
|
+
input.onStatus?.("connected dev 已启用增量配置:已有资源上的可空新字段无需先部署 test");
|
|
1158
|
+
const connected = await runConnectedDevelopment({
|
|
1159
|
+
root: workspace.root,
|
|
1160
|
+
appCode: workspace.config.app.code,
|
|
1161
|
+
platformBaseUrl: developerSession.baseUrl,
|
|
1162
|
+
environment,
|
|
1163
|
+
developerSession,
|
|
1164
|
+
remoteSession,
|
|
1165
|
+
...(input.noOpen === undefined ? {} : { noOpen: input.noOpen }),
|
|
1166
|
+
...(input.webPort === undefined ? {} : { webPort: input.webPort }),
|
|
1167
|
+
...(input.onStatus ? { onStatus: input.onStatus } : {}),
|
|
1168
|
+
});
|
|
1169
|
+
const diagnostics = connected.exitCode === 0
|
|
1170
|
+
? []
|
|
1171
|
+
: [
|
|
1172
|
+
this.diagnostic("DEV_PROCESS_FAILED", "连接式开发进程异常退出", workspace.root, "查看 Web 或 Nest 进程的终端输出后重试"),
|
|
1173
|
+
];
|
|
1174
|
+
return this.result("dev", workspace.context.workspace, connected, diagnostics);
|
|
1175
|
+
}
|
|
1176
|
+
catch (error) {
|
|
1177
|
+
const failure = this.connectedDevelopmentFailure(error, workspace.root);
|
|
1178
|
+
return this.result("dev", workspace.context.workspace, undefined, [
|
|
1179
|
+
this.diagnostic(failure.code, failure.message, failure.path, failure.remediation),
|
|
1180
|
+
], [{ code: failure.actionCode, label: failure.actionLabel, command: failure.command }]);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
async workspace(root) {
|
|
1184
|
+
return await loadWorkspace(root, this.toolchainVersion);
|
|
1185
|
+
}
|
|
1186
|
+
appSpecContractIndex(workspace, includeDigests) {
|
|
1187
|
+
const source = includeDigests
|
|
1188
|
+
? compileApplicationSources(workspace.config, this.toolchainVersion)
|
|
1189
|
+
: undefined;
|
|
1190
|
+
return {
|
|
1191
|
+
appCode: workspace.config.app.code,
|
|
1192
|
+
resourceCodes: (workspace.config.data?.resources || []).map(resource => resource.code),
|
|
1193
|
+
actionCodes: (workspace.config.backend?.operations || []).map(operation => operation.code),
|
|
1194
|
+
...(source
|
|
1195
|
+
? {
|
|
1196
|
+
configDigest: source.config.digest,
|
|
1197
|
+
contractDigest: source.contracts.digest,
|
|
1198
|
+
aiCatalogDigest: source.aiCatalog.digest,
|
|
1199
|
+
}
|
|
1200
|
+
: {}),
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
async validateTargetConfigurationCompatibility(workspace, environmentKey) {
|
|
1204
|
+
const client = await this.client(workspace.root);
|
|
1205
|
+
const capabilities = await client.capabilities();
|
|
1206
|
+
const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
|
|
1207
|
+
const required = {
|
|
1208
|
+
...CURRENT_APPLICATION_CONTRACT,
|
|
1209
|
+
configurationBundleSchemaVersion: sources.config.value.schemaVersion,
|
|
1210
|
+
contractBundleSchemaVersion: sources.contracts.value.schemaVersion,
|
|
1211
|
+
compilerContractVersion: sources.config.value.compilerContractVersion,
|
|
1212
|
+
};
|
|
1213
|
+
const { assertApplicationContractCompatible, assertConfigurationValidationResult, assertRequiredCapabilitiesAvailable, } = await import("./deployment.js");
|
|
1214
|
+
assertApplicationContractCompatible(capabilities, required);
|
|
1215
|
+
assertRequiredCapabilitiesAvailable(capabilities, requiredPlatformCapabilities(workspace.config));
|
|
1216
|
+
const result = await client.validateConfigurationCompatibility(workspace.config.app.code, capabilities, {
|
|
1217
|
+
schemaVersion: SCHEMA_VERSIONS.configurationValidationRequest,
|
|
1218
|
+
environmentKey,
|
|
1219
|
+
clientContractVersion: OPENXIANGDA_CONTRACT_VERSION,
|
|
1220
|
+
required,
|
|
1221
|
+
configuration: {
|
|
1222
|
+
schemaVersion: sources.config.value.schemaVersion,
|
|
1223
|
+
digest: sources.config.digest,
|
|
1224
|
+
canonical: sources.config.content,
|
|
1225
|
+
},
|
|
1226
|
+
contract: {
|
|
1227
|
+
schemaVersion: sources.contracts.value.schemaVersion,
|
|
1228
|
+
digest: sources.contracts.digest,
|
|
1229
|
+
canonical: sources.contracts.content,
|
|
1230
|
+
},
|
|
1231
|
+
});
|
|
1232
|
+
assertConfigurationValidationResult(capabilities, required, result, {
|
|
1233
|
+
configurationDigest: sources.config.digest,
|
|
1234
|
+
contractDigest: sources.contracts.digest,
|
|
1235
|
+
}, requiredPlatformCapabilities(workspace.config));
|
|
1236
|
+
return result;
|
|
1237
|
+
}
|
|
1238
|
+
configurationCompatibilityDiagnostic(error) {
|
|
1239
|
+
const data = error instanceof ControlPlaneError &&
|
|
1240
|
+
error.data &&
|
|
1241
|
+
typeof error.data === "object" &&
|
|
1242
|
+
!Array.isArray(error.data)
|
|
1243
|
+
? error.data
|
|
1244
|
+
: {};
|
|
1245
|
+
const required = { ...CURRENT_APPLICATION_CONTRACT };
|
|
1246
|
+
const pointer = String(data.pointer ||
|
|
1247
|
+
(error instanceof ControlPlaneError ? error.remote?.path : "") ||
|
|
1248
|
+
"/");
|
|
1249
|
+
const details = {
|
|
1250
|
+
pointer,
|
|
1251
|
+
clientContractVersion: OPENXIANGDA_CONTRACT_VERSION,
|
|
1252
|
+
clientSchemaVersions: {
|
|
1253
|
+
appPackage: required.appPackageSchemaVersion,
|
|
1254
|
+
configuration: required.configurationBundleSchemaVersion,
|
|
1255
|
+
contract: required.contractBundleSchemaVersion,
|
|
1256
|
+
compiler: required.compilerContractVersion,
|
|
1257
|
+
},
|
|
1258
|
+
platformVersion: "unknown",
|
|
1259
|
+
platformCapability: {
|
|
1260
|
+
code: CONFIGURATION_COMPATIBILITY_CAPABILITY,
|
|
1261
|
+
version: "unknown",
|
|
1262
|
+
status: "unavailable",
|
|
1263
|
+
},
|
|
1264
|
+
required,
|
|
1265
|
+
supported: [],
|
|
1266
|
+
...data,
|
|
1267
|
+
};
|
|
1268
|
+
const diagnostic = this.diagnostic(String(error?.code ||
|
|
1269
|
+
"OPENXIANGDA_CONFIGURATION_COMPATIBILITY_FAILED"), error instanceof Error
|
|
1270
|
+
? error.message
|
|
1271
|
+
: "目标平台配置兼容性预检失败", pointer, error instanceof ControlPlaneError
|
|
1272
|
+
? error.remote?.remediation ||
|
|
1273
|
+
(typeof data.remediation === "string"
|
|
1274
|
+
? data.remediation
|
|
1275
|
+
: "升级目标平台或修正应用声明后重试 openxiangda check")
|
|
1276
|
+
: "检查目标平台兼容性后重试 openxiangda check", details);
|
|
1277
|
+
return {
|
|
1278
|
+
...diagnostic,
|
|
1279
|
+
retryable: error instanceof ControlPlaneError
|
|
1280
|
+
? error.remote?.retryable === true || error.status >= 500
|
|
1281
|
+
: false,
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
async client(root) {
|
|
1285
|
+
if (this.options.client)
|
|
1286
|
+
return this.options.client;
|
|
1287
|
+
if (this.options.clientOptions) {
|
|
1288
|
+
return new OpenXiangdaControlPlaneClient(this.options.clientOptions);
|
|
1289
|
+
}
|
|
1290
|
+
const session = await OpenXiangdaDeveloperSession.load();
|
|
1291
|
+
if (!session)
|
|
1292
|
+
throw new Error("OPENXIANGDA_AUTH_REQUIRED");
|
|
1293
|
+
if (root) {
|
|
1294
|
+
const linkPath = join(root, ".openxiangda", "link.json");
|
|
1295
|
+
if (existsSync(linkPath)) {
|
|
1296
|
+
const link = JSON.parse(readFileSync(linkPath, "utf8"));
|
|
1297
|
+
if (link.baseUrl)
|
|
1298
|
+
session.assertPlatform(String(link.baseUrl));
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
return new OpenXiangdaControlPlaneClient({
|
|
1302
|
+
baseUrl: session.baseUrl,
|
|
1303
|
+
tokenProvider: session,
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
get toolchainVersion() {
|
|
1307
|
+
return this.options.toolchainVersion || OPENXIANGDA_TOOLCHAIN_VERSION;
|
|
1308
|
+
}
|
|
1309
|
+
get toolchainCapsule() {
|
|
1310
|
+
return this.options.toolchainCapsule || defaultDevkitCoreToolchainCapsule();
|
|
1311
|
+
}
|
|
1312
|
+
runPackageScript(root, script) {
|
|
1313
|
+
const result = spawnSync("pnpm", ["run", script], {
|
|
1314
|
+
cwd: root,
|
|
1315
|
+
encoding: "utf8",
|
|
1316
|
+
env: process.env,
|
|
1317
|
+
});
|
|
1318
|
+
return {
|
|
1319
|
+
ok: result.status === 0,
|
|
1320
|
+
exitCode: result.status ?? 1,
|
|
1321
|
+
output: `${result.stdout || ""}${result.stderr || ""}`.trim(),
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
repositoryIdentity(value) {
|
|
1325
|
+
const normalized = String(value || "").trim();
|
|
1326
|
+
if (!normalized)
|
|
1327
|
+
return "";
|
|
1328
|
+
if (normalized.startsWith("/") || normalized.startsWith("file://")) {
|
|
1329
|
+
return normalized.replace(/^file:\/\//, "").replace(/\.git\/?$/, "");
|
|
1330
|
+
}
|
|
1331
|
+
const scp = normalized.match(/^(?:[^@]+@)?([^:]+):(.+)$/);
|
|
1332
|
+
if (scp && !normalized.includes("://")) {
|
|
1333
|
+
return `${scp[1]}/${scp[2]}`.toLowerCase().replace(/\.git\/?$/, "");
|
|
1334
|
+
}
|
|
1335
|
+
try {
|
|
1336
|
+
const url = new URL(normalized);
|
|
1337
|
+
return `${url.hostname}${url.pathname}`
|
|
1338
|
+
.toLowerCase()
|
|
1339
|
+
.replace(/\.git\/?$/, "")
|
|
1340
|
+
.replace(/\/$/, "");
|
|
1341
|
+
}
|
|
1342
|
+
catch {
|
|
1343
|
+
return normalized.toLowerCase().replace(/\.git\/?$/, "");
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
directoryBundle(root) {
|
|
1347
|
+
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
1348
|
+
throw Object.assign(new Error(`前端构建目录不存在: ${root}`), {
|
|
1349
|
+
code: "FRONTEND_DIST_NOT_FOUND",
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
const files = [];
|
|
1353
|
+
const visit = (directory) => {
|
|
1354
|
+
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1355
|
+
const path = join(directory, entry.name);
|
|
1356
|
+
if (entry.isDirectory())
|
|
1357
|
+
visit(path);
|
|
1358
|
+
else if (entry.isFile()) {
|
|
1359
|
+
const content = readFileSync(path);
|
|
1360
|
+
files.push({
|
|
1361
|
+
path: relative(root, path).replaceAll("\\", "/"),
|
|
1362
|
+
sha256: sha256Bytes(content),
|
|
1363
|
+
content: content.toString("base64"),
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
visit(root);
|
|
1369
|
+
return canonicalJson({ schemaVersion: 2, files });
|
|
1370
|
+
}
|
|
1371
|
+
writeCheckSealedArtifactStatus(workspace, passed) {
|
|
1372
|
+
const previousArtifact = this.previousSealedArtifact(workspace);
|
|
1373
|
+
const status = {
|
|
1374
|
+
schemaVersion: "openxiangda.sealed-artifact-status/v2",
|
|
1375
|
+
state: "check-did-not-seal",
|
|
1376
|
+
sealed: false,
|
|
1377
|
+
usableForDeploy: false,
|
|
1378
|
+
refreshedBy: "check",
|
|
1379
|
+
persisted: true,
|
|
1380
|
+
statusPath: ".openxiangda/build/seal-status.json",
|
|
1381
|
+
manifestPath: ".openxiangda/build/app-package.json",
|
|
1382
|
+
workspaceRevision: workspace.context.workspace.revision || null,
|
|
1383
|
+
workspaceDirty: workspace.context.workspace.dirty === true,
|
|
1384
|
+
packageDigest: null,
|
|
1385
|
+
...(previousArtifact ? { previousArtifact } : {}),
|
|
1386
|
+
reasonCode: "OPENXIANGDA_CHECK_DOES_NOT_SEAL",
|
|
1387
|
+
nextCommand: passed ? "openxiangda deploy" : "openxiangda check",
|
|
1388
|
+
};
|
|
1389
|
+
this.writeSealedArtifactStatus(workspace.root, status);
|
|
1390
|
+
return status;
|
|
1391
|
+
}
|
|
1392
|
+
sealedArtifactStatus(workspace, packageDigest, persisted) {
|
|
1393
|
+
return {
|
|
1394
|
+
schemaVersion: "openxiangda.sealed-artifact-status/v2",
|
|
1395
|
+
state: "sealed",
|
|
1396
|
+
sealed: true,
|
|
1397
|
+
usableForDeploy: persisted,
|
|
1398
|
+
refreshedBy: "build",
|
|
1399
|
+
persisted,
|
|
1400
|
+
statusPath: ".openxiangda/build/seal-status.json",
|
|
1401
|
+
manifestPath: ".openxiangda/build/app-package.json",
|
|
1402
|
+
workspaceRevision: workspace.context.workspace.revision || null,
|
|
1403
|
+
workspaceDirty: workspace.context.workspace.dirty === true,
|
|
1404
|
+
packageDigest,
|
|
1405
|
+
nextCommand: "openxiangda deploy",
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
previousSealedArtifact(workspace) {
|
|
1409
|
+
const manifestPath = join(workspace.root, ".openxiangda", "build", "app-package.json");
|
|
1410
|
+
if (!existsSync(manifestPath) || !statSync(manifestPath).isFile()) {
|
|
1411
|
+
return undefined;
|
|
1412
|
+
}
|
|
1413
|
+
if (statSync(manifestPath).size > 5 * 1024 * 1024) {
|
|
1414
|
+
return {
|
|
1415
|
+
packageDigest: null,
|
|
1416
|
+
sourceRevision: null,
|
|
1417
|
+
sourceDirty: null,
|
|
1418
|
+
matchesWorkspaceSource: false,
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
try {
|
|
1422
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
1423
|
+
const sourceRevision = typeof manifest.source?.commit === "string"
|
|
1424
|
+
? manifest.source.commit
|
|
1425
|
+
: null;
|
|
1426
|
+
const sourceDirty = typeof manifest.source?.dirty === "boolean"
|
|
1427
|
+
? manifest.source.dirty
|
|
1428
|
+
: null;
|
|
1429
|
+
const workspaceRevision = workspace.context.workspace.revision || null;
|
|
1430
|
+
const workspaceDirty = workspace.context.workspace.dirty === true;
|
|
1431
|
+
return {
|
|
1432
|
+
packageDigest: sha256Digest(manifest),
|
|
1433
|
+
sourceRevision,
|
|
1434
|
+
sourceDirty,
|
|
1435
|
+
matchesWorkspaceSource: sourceRevision !== null &&
|
|
1436
|
+
sourceRevision === workspaceRevision &&
|
|
1437
|
+
sourceDirty === workspaceDirty,
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
catch {
|
|
1441
|
+
return {
|
|
1442
|
+
packageDigest: null,
|
|
1443
|
+
sourceRevision: null,
|
|
1444
|
+
sourceDirty: null,
|
|
1445
|
+
matchesWorkspaceSource: false,
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
writeSealedArtifactStatus(root, status) {
|
|
1450
|
+
const statusPath = join(root, status.statusPath);
|
|
1451
|
+
mkdirSync(dirname(statusPath), { recursive: true });
|
|
1452
|
+
writeFileSync(statusPath, `${JSON.stringify(status, null, 2)}\n`, "utf8");
|
|
1453
|
+
}
|
|
1454
|
+
versionTrainDiagnostics(workspace) {
|
|
1455
|
+
let dependencies;
|
|
1456
|
+
try {
|
|
1457
|
+
dependencies = collectWorkspaceToolchainDependencies(workspace.root);
|
|
1458
|
+
}
|
|
1459
|
+
catch (error) {
|
|
1460
|
+
if (error instanceof ToolchainCapsuleScanLimitError) {
|
|
1461
|
+
return [
|
|
1462
|
+
this.diagnostic(error.code, "工作区 package.json 扫描超过安全资源上限", "package.json", "减少工作区中纳入检查的 package.json 数量或文件大小后重试", { message: error.message }),
|
|
1463
|
+
];
|
|
1464
|
+
}
|
|
1465
|
+
throw error;
|
|
1466
|
+
}
|
|
1467
|
+
const parsed = dependencies.map(dependency => {
|
|
1468
|
+
const version = dependency.specifier.replace(/^workspace:/, "");
|
|
1469
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?(?:\+[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?$/);
|
|
1470
|
+
return {
|
|
1471
|
+
...dependency,
|
|
1472
|
+
version,
|
|
1473
|
+
major: match ? Number(match[1]) : null,
|
|
1474
|
+
exact: Boolean(match),
|
|
1475
|
+
};
|
|
1476
|
+
});
|
|
1477
|
+
const unpinned = parsed.filter(dependency => !dependency.exact);
|
|
1478
|
+
const incompatible = parsed.filter(dependency => dependency.exact && dependency.major !== 2);
|
|
1479
|
+
const diagnostics = [];
|
|
1480
|
+
if (unpinned.length > 0) {
|
|
1481
|
+
diagnostics.push(this.diagnostic("TOOLCHAIN_DEPENDENCY_NOT_PINNED", "openxiangda-* 依赖必须精确锁定,不能使用范围、标签或未定版本的 workspace 协议", "package.json", "把每个 openxiangda-* 依赖改为已验证的精确 2.x 版本,并提交 lockfile", { dependencies: unpinned }));
|
|
1482
|
+
}
|
|
1483
|
+
if (incompatible.length > 0) {
|
|
1484
|
+
diagnostics.push(this.diagnostic("TOOLCHAIN_RELEASE_TRAIN_INCOMPATIBLE", "应用包含不属于 OpenXiangda 2.x 发布列车的依赖", "package.json", "使用兼容的 OpenXiangda 2.x 精确版本;包版本可以独立递增,不要求字符串相同", { dependencies: incompatible }));
|
|
1485
|
+
}
|
|
1486
|
+
const capsuleDiagnostic = toolchainCapsuleDiagnostic(dependencies, this.toolchainCapsule);
|
|
1487
|
+
if (capsuleDiagnostic)
|
|
1488
|
+
diagnostics.push(capsuleDiagnostic);
|
|
1489
|
+
return diagnostics;
|
|
1490
|
+
}
|
|
1491
|
+
toolchainDiagnostics(workspace) {
|
|
1492
|
+
return this.versionTrainDiagnostics(workspace);
|
|
1493
|
+
}
|
|
1494
|
+
diagnostic(code, message, path, remediation, details) {
|
|
1495
|
+
return {
|
|
1496
|
+
schemaVersion: SCHEMA_VERSIONS.diagnostic,
|
|
1497
|
+
code,
|
|
1498
|
+
severity: "error",
|
|
1499
|
+
message,
|
|
1500
|
+
path,
|
|
1501
|
+
retryable: false,
|
|
1502
|
+
...(remediation ? { remediation } : {}),
|
|
1503
|
+
...(details ? { details } : {}),
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
connectedDevelopmentFailure(error, workspaceRoot) {
|
|
1507
|
+
const rawCode = String(error?.code ||
|
|
1508
|
+
(error instanceof Error ? error.message.split(":", 1)[0] : "") ||
|
|
1509
|
+
"OPENXIANGDA_CONNECTED_DEV_FAILED").trim();
|
|
1510
|
+
const known = {
|
|
1511
|
+
OPENXIANGDA_CONNECTED_LINK_REQUIRED: {
|
|
1512
|
+
message: "当前工作区尚未绑定 OpenXiangda 平台",
|
|
1513
|
+
remediation: "运行 create 幂等完成工作区绑定和平台初始化",
|
|
1514
|
+
actionCode: "create",
|
|
1515
|
+
actionLabel: "初始化应用",
|
|
1516
|
+
command: "openxiangda create <directory>",
|
|
1517
|
+
path: ".openxiangda/link.json",
|
|
1518
|
+
},
|
|
1519
|
+
OPENXIANGDA_CONNECTED_LINK_INVALID: {
|
|
1520
|
+
message: "当前工作区的平台绑定无效或属于另一个应用",
|
|
1521
|
+
remediation: "运行 create 幂等校验并重新生成当前应用绑定",
|
|
1522
|
+
actionCode: "create",
|
|
1523
|
+
actionLabel: "重新初始化应用",
|
|
1524
|
+
command: "openxiangda create <directory>",
|
|
1525
|
+
path: ".openxiangda/link.json",
|
|
1526
|
+
},
|
|
1527
|
+
OPENXIANGDA_CONNECTED_LOGIN_REQUIRED: {
|
|
1528
|
+
message: "尚未登录 OpenXiangda 平台",
|
|
1529
|
+
remediation: "登录工作区绑定的平台后重试",
|
|
1530
|
+
actionCode: "login",
|
|
1531
|
+
actionLabel: "登录平台",
|
|
1532
|
+
command: "openxiangda login --base-url <platform>",
|
|
1533
|
+
},
|
|
1534
|
+
OPENXIANGDA_AUTH_REQUIRED: {
|
|
1535
|
+
message: "OpenXiangda 登录已失效",
|
|
1536
|
+
remediation: "重新登录工作区绑定的平台后重试",
|
|
1537
|
+
actionCode: "login",
|
|
1538
|
+
actionLabel: "重新登录",
|
|
1539
|
+
command: "openxiangda login --base-url <platform>",
|
|
1540
|
+
},
|
|
1541
|
+
OPENXIANGDA_PLATFORM_SESSION_MISMATCH: {
|
|
1542
|
+
message: "工作区绑定平台与当前登录平台不一致",
|
|
1543
|
+
remediation: "切换登录平台,或明确重新绑定当前工作区",
|
|
1544
|
+
actionCode: "login",
|
|
1545
|
+
actionLabel: "切换登录平台",
|
|
1546
|
+
command: "openxiangda login --base-url <linked-platform>",
|
|
1547
|
+
path: ".openxiangda/link.json",
|
|
1548
|
+
},
|
|
1549
|
+
OPENXIANGDA_CONNECTED_ACTIVE_ENVIRONMENT_REQUIRED: {
|
|
1550
|
+
message: "应用没有可用于 connected dev 的已发布环境",
|
|
1551
|
+
remediation: "先发布 test(preproduction);只有 production 时也可以直接连接正式数据",
|
|
1552
|
+
actionCode: "deploy.test",
|
|
1553
|
+
actionLabel: "部署 test",
|
|
1554
|
+
command: "openxiangda deploy",
|
|
1555
|
+
},
|
|
1556
|
+
};
|
|
1557
|
+
const dataRemediation = error
|
|
1558
|
+
?.data?.remediation;
|
|
1559
|
+
const remoteRemediation = error instanceof ControlPlaneError
|
|
1560
|
+
? error.remote?.remediation ||
|
|
1561
|
+
(typeof dataRemediation === "string" ? dataRemediation : undefined)
|
|
1562
|
+
: undefined;
|
|
1563
|
+
const selected = known[rawCode] || {
|
|
1564
|
+
message: "连接式开发预检失败",
|
|
1565
|
+
remediation: remoteRemediation || (error instanceof Error && error.message
|
|
1566
|
+
? error.message.replace(/Bearer\s+\S+/gi, "Bearer [redacted]")
|
|
1567
|
+
: "检查平台登录、应用权限和 Dev Session 状态后重试"),
|
|
1568
|
+
actionCode: "dev.retry",
|
|
1569
|
+
actionLabel: "重试 connected dev",
|
|
1570
|
+
command: "openxiangda dev",
|
|
1571
|
+
};
|
|
1572
|
+
return {
|
|
1573
|
+
code: rawCode,
|
|
1574
|
+
message: selected.message,
|
|
1575
|
+
remediation: selected.remediation,
|
|
1576
|
+
actionCode: selected.actionCode,
|
|
1577
|
+
actionLabel: selected.actionLabel,
|
|
1578
|
+
command: selected.command,
|
|
1579
|
+
path: selected.path || workspaceRoot,
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
ok(operation, workspace, data, nextActions = []) {
|
|
1583
|
+
return {
|
|
1584
|
+
ok: true,
|
|
1585
|
+
operation,
|
|
1586
|
+
workspace,
|
|
1587
|
+
data,
|
|
1588
|
+
diagnostics: [],
|
|
1589
|
+
nextActions,
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
result(operation, workspace, data, diagnostics, nextActions = []) {
|
|
1593
|
+
return {
|
|
1594
|
+
ok: diagnostics.every((item) => item.severity !== "error"),
|
|
1595
|
+
operation,
|
|
1596
|
+
workspace,
|
|
1597
|
+
...(data === undefined ? {} : { data }),
|
|
1598
|
+
diagnostics,
|
|
1599
|
+
nextActions,
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
//# sourceMappingURL=application-services.js.map
|