openxiangda-devkit-core 2.0.0-alpha.114 → 2.0.0-alpha.117
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 +3 -9
- package/dist/app-spec.d.ts +23 -11
- package/dist/app-spec.d.ts.map +1 -1
- package/dist/app-spec.js +171 -52
- package/dist/app-spec.js.map +1 -1
- package/dist/application-services.d.ts +344 -31
- package/dist/application-services.d.ts.map +1 -1
- package/dist/application-services.js +239 -80
- package/dist/application-services.js.map +1 -1
- package/dist/backend-image-build.js +6 -11
- package/dist/backend-image-build.js.map +1 -1
- package/dist/command-process.d.ts +14 -0
- package/dist/command-process.d.ts.map +1 -0
- package/dist/command-process.js +51 -0
- package/dist/command-process.js.map +1 -0
- package/dist/command-registry.d.ts +22 -2
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +7 -5
- package/dist/command-registry.js.map +1 -1
- package/dist/compiler/permission-review.d.ts.map +1 -1
- package/dist/configuration-preflight.d.ts +7 -0
- package/dist/configuration-preflight.d.ts.map +1 -0
- package/dist/configuration-preflight.js +13 -0
- package/dist/configuration-preflight.js.map +1 -0
- package/dist/control-plane-client.d.ts +8 -1
- package/dist/control-plane-client.d.ts.map +1 -1
- package/dist/control-plane-client.js +13 -0
- package/dist/control-plane-client.js.map +1 -1
- package/dist/delivery-cache.d.ts +34 -0
- package/dist/delivery-cache.d.ts.map +1 -0
- package/dist/delivery-cache.js +186 -0
- package/dist/delivery-cache.js.map +1 -0
- package/dist/delivery-source.d.ts +17 -0
- package/dist/delivery-source.d.ts.map +1 -0
- package/dist/delivery-source.js +85 -0
- package/dist/delivery-source.js.map +1 -0
- package/dist/deployment-observer.d.ts +14 -0
- package/dist/deployment-observer.d.ts.map +1 -0
- package/dist/deployment-observer.js +68 -0
- package/dist/deployment-observer.js.map +1 -0
- package/dist/deployment.d.ts +13 -0
- package/dist/deployment.d.ts.map +1 -1
- package/dist/deployment.js +34 -7
- package/dist/deployment.js.map +1 -1
- package/dist/developer-errors.d.ts +11 -0
- package/dist/developer-errors.d.ts.map +1 -0
- package/dist/developer-errors.js +16 -0
- package/dist/developer-errors.js.map +1 -0
- package/dist/developer-operations.d.ts +168 -0
- package/dist/developer-operations.d.ts.map +1 -0
- package/dist/developer-operations.js +58 -0
- package/dist/developer-operations.js.map +1 -0
- package/dist/development-lifecycle.d.ts +48 -0
- package/dist/development-lifecycle.d.ts.map +1 -0
- package/dist/development-lifecycle.js +180 -0
- package/dist/development-lifecycle.js.map +1 -0
- package/dist/documentation.d.ts +103 -0
- package/dist/documentation.d.ts.map +1 -0
- package/dist/documentation.js +111 -0
- package/dist/documentation.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/operation-progress.d.ts +26 -0
- package/dist/operation-progress.d.ts.map +1 -0
- package/dist/operation-progress.js +92 -0
- package/dist/operation-progress.js.map +1 -0
- package/dist/workspace-operation.d.ts +3 -0
- package/dist/workspace-operation.d.ts.map +1 -0
- package/dist/workspace-operation.js +45 -0
- package/dist/workspace-operation.js.map +1 -0
- package/package.json +3 -3
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { publishedDeliverySource, assertDeliverySourceUnchanged, DeliverySourceError } from './delivery-source.js';
|
|
3
|
+
import { operationStage, skippedOperationStage, updateOperationStage } from './operation-progress.js';
|
|
4
|
+
import { deliveryInputDigest, reusableValidation, recordValidation, recordSealedCandidate, reusableSealedCandidate } from './delivery-cache.js';
|
|
5
|
+
import { developmentLifecycle, summarizeLifecycle, lifecycleAtCommit, verifyBusinessAcceptance } from './development-lifecycle.js';
|
|
6
|
+
import { runCommandProcess } from './command-process.js';
|
|
7
|
+
import { compileLocalConfiguration } from './configuration-preflight.js';
|
|
8
|
+
import { NativeConfigurationCompilerError, NATIVE_CONFIGURATION_VALIDATOR_DIGEST } from 'openxiangda-contracts/native-compiler';
|
|
3
9
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from "node:fs";
|
|
4
10
|
import { dirname, join, relative, resolve } from "node:path";
|
|
5
11
|
import { CONFIGURATION_COMPATIBILITY_CAPABILITY, CURRENT_APPLICATION_CONTRACT, OPENXIANGDA_CONTRACT_VERSION, SCHEMA_VERSIONS, canonicalJson, sha256Digest, } from "openxiangda-contracts";
|
|
@@ -24,7 +30,15 @@ export class OpenXiangdaApplicationServices {
|
|
|
24
30
|
}
|
|
25
31
|
async workspaceContext(root) {
|
|
26
32
|
const workspace = await this.workspace(root);
|
|
27
|
-
|
|
33
|
+
const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true));
|
|
34
|
+
return this.ok("workspace.context", workspace.context.workspace, { ...workspace.context, development: {
|
|
35
|
+
...summarizeLifecycle(lifecycle),
|
|
36
|
+
application: lifecycle.context.application ? { path: lifecycle.context.application.path, content: lifecycle.context.application.content } : null,
|
|
37
|
+
records: [
|
|
38
|
+
...lifecycle.context.index.capabilities, ...lifecycle.context.index.activeChanges,
|
|
39
|
+
...lifecycle.context.index.decisions, ...lifecycle.context.index.history,
|
|
40
|
+
].map(({ id, title, status, path, kind }) => ({ id, title, status, path, kind })),
|
|
41
|
+
} });
|
|
28
42
|
}
|
|
29
43
|
async appSpecInit(root) {
|
|
30
44
|
const workspace = await this.workspace(root);
|
|
@@ -77,15 +91,16 @@ export class OpenXiangdaApplicationServices {
|
|
|
77
91
|
},
|
|
78
92
|
]);
|
|
79
93
|
}
|
|
80
|
-
async appSpecContext(root, selector) {
|
|
94
|
+
async appSpecContext(root, selector, historyOffset = 0) {
|
|
81
95
|
const workspace = await this.workspace(root);
|
|
82
|
-
const context = inspectAppSpec(workspace.root, this.appSpecContractIndex(workspace, true), selector);
|
|
83
|
-
return this.result("spec.context", workspace.context.workspace, context, advisoryAppSpecDiagnostics(context.diagnostics));
|
|
96
|
+
const context = inspectAppSpec(workspace.root, this.appSpecContractIndex(workspace, true), selector, historyOffset);
|
|
97
|
+
return this.result("spec.context", workspace.context.workspace, { ...context, lifecycle: summarizeLifecycle(developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true))) }, advisoryAppSpecDiagnostics(context.diagnostics));
|
|
84
98
|
}
|
|
85
99
|
async appSpecCheck(root) {
|
|
86
100
|
const workspace = await this.workspace(root);
|
|
87
101
|
const context = inspectAppSpec(workspace.root, this.appSpecContractIndex(workspace, true));
|
|
88
|
-
|
|
102
|
+
const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true));
|
|
103
|
+
return this.result("spec.check", workspace.context.workspace, { ...context, lifecycle: summarizeLifecycle(lifecycle) }, lifecycle.diagnostics, [
|
|
89
104
|
{
|
|
90
105
|
code: "check",
|
|
91
106
|
label: "继续运行应用检查",
|
|
@@ -93,6 +108,13 @@ export class OpenXiangdaApplicationServices {
|
|
|
93
108
|
},
|
|
94
109
|
]);
|
|
95
110
|
}
|
|
111
|
+
async appSpecVerify(root, deploymentId, evidencePath) {
|
|
112
|
+
const workspace = await this.workspace(root);
|
|
113
|
+
const run = await (await this.client(workspace.root)).deployment(workspace.config.app.code, deploymentId);
|
|
114
|
+
const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true));
|
|
115
|
+
const verification = verifyBusinessAcceptance(workspace.root, run, lifecycle, evidencePath);
|
|
116
|
+
return this.result('spec.verify', workspace.context.workspace, verification, verification.diagnostics);
|
|
117
|
+
}
|
|
96
118
|
async appSpecClose(input) {
|
|
97
119
|
const workspace = await this.workspace(input.root);
|
|
98
120
|
const outcome = closeAppSpecChange({
|
|
@@ -180,6 +202,51 @@ export class OpenXiangdaApplicationServices {
|
|
|
180
202
|
},
|
|
181
203
|
});
|
|
182
204
|
}
|
|
205
|
+
async contractContext(root, input = {}) {
|
|
206
|
+
const result = await this.contractDescribe(root);
|
|
207
|
+
const data = result.data;
|
|
208
|
+
const groups = {
|
|
209
|
+
resource: data.contract.resources,
|
|
210
|
+
operation: data.contract.operations,
|
|
211
|
+
workflow: data.contract.workflows,
|
|
212
|
+
route: data.contract.routes,
|
|
213
|
+
capability: data.aiCatalog.capabilities,
|
|
214
|
+
};
|
|
215
|
+
const entries = Object.entries(groups).flatMap(([kind, values]) => values.map(value => ({
|
|
216
|
+
selector: `${kind}:${String(value.code || value.resourceCode || value.id)}`,
|
|
217
|
+
kind, code: String(value.code || value.resourceCode || value.id),
|
|
218
|
+
title: String(value.name || value.label || value.title || value.code || value.resourceCode || value.id),
|
|
219
|
+
value,
|
|
220
|
+
})));
|
|
221
|
+
const selector = input.selector || 'index';
|
|
222
|
+
const offset = input.offset ?? 0;
|
|
223
|
+
const limit = input.limit ?? 30;
|
|
224
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
225
|
+
throw new Error('CONTRACT_SELECTION_INVALID: offset 必须为非负整数,limit 必须在 1 到 100 之间');
|
|
226
|
+
}
|
|
227
|
+
let selection;
|
|
228
|
+
if (selector === 'index')
|
|
229
|
+
selection = {
|
|
230
|
+
total: entries.length, offset, limit, nextOffset: offset + limit < entries.length ? offset + limit : null,
|
|
231
|
+
sections: ['navigation', 'permissions', 'all'],
|
|
232
|
+
entries: entries.slice(offset, offset + limit).map(({ value: _, ...entry }) => entry),
|
|
233
|
+
};
|
|
234
|
+
else if (selector === 'navigation')
|
|
235
|
+
selection = { adminNavigationAuthoring: data.adminNavigationAuthoring };
|
|
236
|
+
else if (selector === 'permissions')
|
|
237
|
+
selection = { permissionReview: data.permissionReview };
|
|
238
|
+
else if (selector === 'all')
|
|
239
|
+
selection = data;
|
|
240
|
+
else {
|
|
241
|
+
const entry = entries.find(item => item.selector === selector);
|
|
242
|
+
if (!entry)
|
|
243
|
+
throw new Error(`CONTRACT_SELECTION_NOT_FOUND: 未知契约选择器 ${selector},先读取 index`);
|
|
244
|
+
selection = entry.value;
|
|
245
|
+
}
|
|
246
|
+
if (Buffer.byteLength(JSON.stringify(selection)) > 256 * 1024)
|
|
247
|
+
throw new Error('CONTRACT_SELECTION_TOO_LARGE: 选择结果超过 256 KiB,请按模型或能力读取');
|
|
248
|
+
return { ...result, data: { configDigest: data.configDigest, contractDigest: data.contractDigest, aiCatalogDigest: data.aiCatalogDigest, counts: data.counts, selector, selection } };
|
|
249
|
+
}
|
|
183
250
|
async platformCapabilities(root) {
|
|
184
251
|
const workspace = await this.workspace(root);
|
|
185
252
|
const capabilities = await (await this.client(workspace.root)).capabilities();
|
|
@@ -530,12 +597,14 @@ export class OpenXiangdaApplicationServices {
|
|
|
530
597
|
async check(root, environmentKey) {
|
|
531
598
|
const workspace = await this.workspace(root);
|
|
532
599
|
const appSpec = inspectAppSpec(workspace.root, this.appSpecContractIndex(workspace, false));
|
|
600
|
+
const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, false));
|
|
533
601
|
const diagnostics = [
|
|
534
602
|
...validateAppConfig(workspace.config),
|
|
535
603
|
...this.versionTrainDiagnostics(workspace),
|
|
536
604
|
...validateNestInjectionContract(workspace.root),
|
|
537
605
|
...validateApplicationUiContract(workspace.root, workspace.config.frontend.root),
|
|
538
606
|
...advisoryAppSpecDiagnostics(appSpec.diagnostics),
|
|
607
|
+
...advisoryAppSpecDiagnostics(lifecycle.diagnostics),
|
|
539
608
|
];
|
|
540
609
|
if (diagnostics.some(item => item.severity === "error")) {
|
|
541
610
|
const sealedArtifact = this.writeCheckSealedArtifactStatus(workspace, false);
|
|
@@ -544,10 +613,28 @@ export class OpenXiangdaApplicationServices {
|
|
|
544
613
|
compatibility: null,
|
|
545
614
|
stages: [],
|
|
546
615
|
sealedArtifact,
|
|
547
|
-
appSpec: summarizeAppSpecContext(appSpec),
|
|
616
|
+
appSpec: { ...summarizeAppSpecContext(appSpec), lifecycle: summarizeLifecycle(lifecycle) },
|
|
548
617
|
}, diagnostics);
|
|
549
618
|
}
|
|
550
|
-
|
|
619
|
+
let compatibility = null;
|
|
620
|
+
try {
|
|
621
|
+
const local = await operationStage('local-configuration', '本地完整配置校验', async () => compileLocalConfiguration(workspace.config, this.toolchainVersion));
|
|
622
|
+
if (environmentKey) {
|
|
623
|
+
compatibility = await operationStage('target-preflight', '目标平台只读预检', () => this.validateTargetConfigurationCompatibility(workspace, environmentKey, local));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
diagnostics.push(this.configurationCompatibilityDiagnostic(error));
|
|
628
|
+
const sealedArtifact = this.writeCheckSealedArtifactStatus(workspace, false);
|
|
629
|
+
return this.result("check", workspace.context.workspace, {
|
|
630
|
+
generated: undefined,
|
|
631
|
+
compatibility: null,
|
|
632
|
+
stages: [],
|
|
633
|
+
sealedArtifact,
|
|
634
|
+
appSpec: { ...summarizeAppSpecContext(appSpec), lifecycle: summarizeLifecycle(lifecycle) },
|
|
635
|
+
}, diagnostics);
|
|
636
|
+
}
|
|
637
|
+
const generated = await operationStage('generate', '生成应用契约', () => this.generate({ root: workspace.root }));
|
|
551
638
|
diagnostics.push(...generated.diagnostics);
|
|
552
639
|
if (!generated.ok) {
|
|
553
640
|
const sealedArtifact = this.writeCheckSealedArtifactStatus(workspace, false);
|
|
@@ -556,38 +643,45 @@ export class OpenXiangdaApplicationServices {
|
|
|
556
643
|
compatibility: null,
|
|
557
644
|
stages: [],
|
|
558
645
|
sealedArtifact,
|
|
559
|
-
appSpec: summarizeAppSpecContext(appSpec),
|
|
646
|
+
appSpec: { ...summarizeAppSpecContext(appSpec), lifecycle: summarizeLifecycle(lifecycle) },
|
|
560
647
|
}, diagnostics, generated.nextActions);
|
|
561
648
|
}
|
|
562
649
|
const backendDiagnostics = this.initializeBackend(workspace);
|
|
563
650
|
if (backendDiagnostics.length) {
|
|
564
651
|
return this.result("check", workspace.context.workspace, undefined, backendDiagnostics);
|
|
565
652
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
653
|
+
const stages = [];
|
|
654
|
+
const cache = this.deliveryCacheContext(workspace);
|
|
655
|
+
const inputDigest = deliveryInputDigest(cache);
|
|
656
|
+
const reusable = reusableValidation(cache, inputDigest);
|
|
657
|
+
for (const name of ["check", "test", "build"]) {
|
|
658
|
+
if (reusable) {
|
|
659
|
+
await operationStage(name, `${this.packageScriptLabel(name)}(复用已验证结果)`, async () => {
|
|
660
|
+
updateOperationStage(name, `${this.packageScriptLabel(name)}(复用已验证结果)`, { reused: true, inputDigest, outputDigest: reusable.outputDigest });
|
|
661
|
+
});
|
|
662
|
+
stages.push({ name, state: 'passed', ok: true, exitCode: 0, output: '输入与构建输出摘要未变,复用已通过的本地检查', reused: true });
|
|
663
|
+
continue;
|
|
570
664
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
generated: generated.data,
|
|
576
|
-
compatibility: null,
|
|
577
|
-
stages: [],
|
|
578
|
-
sealedArtifact,
|
|
579
|
-
appSpec: summarizeAppSpecContext(appSpec),
|
|
580
|
-
}, diagnostics);
|
|
665
|
+
if (stages.some(stage => stage.state === 'failed')) {
|
|
666
|
+
stages.push({ name, state: 'skipped', ok: false, exitCode: null, output: '前置阶段失败,未执行' });
|
|
667
|
+
skippedOperationStage(name, this.packageScriptLabel(name));
|
|
668
|
+
continue;
|
|
581
669
|
}
|
|
670
|
+
const result = await this.runPackageScript(workspace.root, name);
|
|
671
|
+
stages.push({ name, ...result, state: result.ok ? 'passed' : 'failed' });
|
|
582
672
|
}
|
|
583
|
-
const
|
|
584
|
-
name,
|
|
585
|
-
...this.runPackageScript(workspace.root, name),
|
|
586
|
-
}));
|
|
587
|
-
for (const stage of stages.filter(item => !item.ok)) {
|
|
673
|
+
for (const stage of stages.filter(item => item.state === 'failed')) {
|
|
588
674
|
const code = `WORKSPACE_${stage.name.toUpperCase()}_FAILED`;
|
|
589
675
|
diagnostics.push(this.diagnostic(code, `工作区 ${stage.name} 脚本失败`, `package.json#scripts.${stage.name}`, "修复失败后重新运行 openxiangda check", { output: stage.output }));
|
|
590
676
|
}
|
|
677
|
+
if (!reusable && stages.every(stage => stage.state === 'passed')) {
|
|
678
|
+
try {
|
|
679
|
+
recordValidation(cache, inputDigest);
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
diagnostics.push(this.diagnostic('OPENXIANGDA_VALIDATION_INPUT_CHANGED', error.message, 'workspace', '等待源码修改结束后重新运行 openxiangda check'));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
591
685
|
const passed = !diagnostics.some(item => item.severity === "error");
|
|
592
686
|
const sealedArtifact = this.writeCheckSealedArtifactStatus(workspace, passed);
|
|
593
687
|
return this.result("check", workspace.context.workspace, {
|
|
@@ -595,7 +689,7 @@ export class OpenXiangdaApplicationServices {
|
|
|
595
689
|
compatibility,
|
|
596
690
|
stages,
|
|
597
691
|
sealedArtifact,
|
|
598
|
-
appSpec: summarizeAppSpecContext(appSpec),
|
|
692
|
+
appSpec: { ...summarizeAppSpecContext(appSpec), lifecycle: summarizeLifecycle(lifecycle) },
|
|
599
693
|
}, diagnostics, passed
|
|
600
694
|
? [
|
|
601
695
|
{
|
|
@@ -612,7 +706,7 @@ export class OpenXiangdaApplicationServices {
|
|
|
612
706
|
if (diagnostics.length > 0) {
|
|
613
707
|
return this.result("test", workspace.context.workspace, undefined, diagnostics);
|
|
614
708
|
}
|
|
615
|
-
const command = this.runPackageScript(workspace.root, "test");
|
|
709
|
+
const command = await this.runPackageScript(workspace.root, "test");
|
|
616
710
|
const commandDiagnostics = command.ok
|
|
617
711
|
? []
|
|
618
712
|
: [
|
|
@@ -662,7 +756,7 @@ export class OpenXiangdaApplicationServices {
|
|
|
662
756
|
if (backendDiagnostics.length) {
|
|
663
757
|
return this.result("build", workspace.context.workspace, undefined, backendDiagnostics);
|
|
664
758
|
}
|
|
665
|
-
const command = this.runPackageScript(workspace.root, "build");
|
|
759
|
+
const command = await this.runPackageScript(workspace.root, "build");
|
|
666
760
|
if (!command.ok) {
|
|
667
761
|
diagnostics.push(this.diagnostic("WORKSPACE_BUILD_FAILED", "前后端生产构建失败", "package.json#scripts.build", "修复构建错误后重试", { output: command.output }));
|
|
668
762
|
return this.result("build", workspace.context.workspace, undefined, diagnostics);
|
|
@@ -782,6 +876,10 @@ export class OpenXiangdaApplicationServices {
|
|
|
782
876
|
if (diagnostics.length > 0) {
|
|
783
877
|
return this.result("deployment.plan", workspace.context.workspace, undefined, diagnostics);
|
|
784
878
|
}
|
|
879
|
+
const source = await operationStage('source', '核对远端主线与源码', () => publishedDeliverySource(workspace.root));
|
|
880
|
+
const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true));
|
|
881
|
+
if (!lifecycle.readyForTest)
|
|
882
|
+
return this.result('deployment.plan', workspace.context.workspace, { lifecycle: summarizeLifecycle(lifecycle) }, lifecycle.diagnostics);
|
|
785
883
|
const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
|
|
786
884
|
return this.ok("deployment.plan", workspace.context.workspace, {
|
|
787
885
|
environment: input.environment,
|
|
@@ -789,6 +887,7 @@ export class OpenXiangdaApplicationServices {
|
|
|
789
887
|
sealed: false,
|
|
790
888
|
packageDigest: null,
|
|
791
889
|
buildOwner: "openxiangda deploy",
|
|
890
|
+
source,
|
|
792
891
|
frontendRoot: workspace.config.frontend.root,
|
|
793
892
|
backendRoot: workspace.config.backend.root,
|
|
794
893
|
configDigest: sources.config.digest,
|
|
@@ -806,6 +905,7 @@ export class OpenXiangdaApplicationServices {
|
|
|
806
905
|
this.diagnostic("DEPLOY_PREPRODUCTION_ONLY", "Native 2.0 只允许直接部署到预发环境", String(input.environment), "使用 openxiangda deploy;生产环境必须显式复用成功的测试 DeploymentRun"),
|
|
807
906
|
]);
|
|
808
907
|
}
|
|
908
|
+
const source = await operationStage('source', '核对远端主线与源码', () => publishedDeliverySource(workspace.root));
|
|
809
909
|
const client = await this.client(workspace.root);
|
|
810
910
|
const capabilities = await client.capabilities();
|
|
811
911
|
const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
|
|
@@ -818,32 +918,59 @@ export class OpenXiangdaApplicationServices {
|
|
|
818
918
|
});
|
|
819
919
|
assertRequiredCapabilitiesAvailable(capabilities, requiredPlatformCapabilities(workspace.config));
|
|
820
920
|
const hasBackend = backendRuntimeRequired(workspace.config);
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
921
|
+
const generated = await this.generate({ root: workspace.root, check: true });
|
|
922
|
+
if (!generated.ok)
|
|
923
|
+
return { ...generated, operation: 'deploy' };
|
|
924
|
+
const lifecycle = await operationStage('development-records', '核对需求、架构与测试验收计划', async () => {
|
|
925
|
+
const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true));
|
|
926
|
+
return { ...lifecycle, ok: lifecycle.readyForTest };
|
|
927
|
+
});
|
|
928
|
+
if (!lifecycle.readyForTest)
|
|
929
|
+
return this.result('deploy', workspace.context.workspace, { lifecycle: summarizeLifecycle(lifecycle) }, lifecycle.diagnostics);
|
|
824
930
|
const checked = await this.check(workspace.root, input.environment);
|
|
825
931
|
if (!checked.ok)
|
|
826
932
|
return { ...checked, operation: "deploy" };
|
|
827
|
-
|
|
933
|
+
await assertDeliverySourceUnchanged(workspace.root, source);
|
|
934
|
+
const cache = this.deliveryCacheContext(workspace);
|
|
935
|
+
const preparedInputDigest = deliveryInputDigest(cache);
|
|
936
|
+
const reused = await operationStage('candidate', '核对可复用的不可变制品', async () => {
|
|
937
|
+
const candidate = await reusableSealedCandidate(cache, source, input.backendImage);
|
|
938
|
+
updateOperationStage('candidate', candidate ? '复用已验证的不可变制品' : '准备构建新制品', { reused: !!candidate, ...(candidate ? { packageDigest: candidate.package.digest } : {}) });
|
|
939
|
+
return candidate;
|
|
940
|
+
});
|
|
941
|
+
const backendImage = !reused && hasBackend
|
|
828
942
|
? input.backendImage
|
|
829
943
|
? input.backendImage
|
|
830
|
-
: (await publishBackendImage({
|
|
944
|
+
: (await operationStage('backend-image', '构建并推送后端镜像', () => publishBackendImage({
|
|
831
945
|
root: workspace.root,
|
|
832
946
|
backendRoot: workspace.config.backend.root,
|
|
833
|
-
target:
|
|
947
|
+
target: backendImageBuildTarget(capabilities, workspace.config.app.code),
|
|
834
948
|
...(workspace.context.workspace.revision
|
|
835
949
|
? { sourceRevision: workspace.context.workspace.revision }
|
|
836
950
|
: {}),
|
|
837
|
-
})).reference
|
|
951
|
+
}))).reference
|
|
838
952
|
: undefined;
|
|
839
|
-
|
|
953
|
+
await assertDeliverySourceUnchanged(workspace.root, source);
|
|
954
|
+
const built = reused ? this.ok('build', workspace.context.workspace, {
|
|
955
|
+
...reused, sealedArtifact: this.sealedArtifactStatus(workspace, reused.package.digest, true),
|
|
956
|
+
}) : await operationStage('seal', '校验并冻结不可变制品', () => this.buildSealed({
|
|
840
957
|
...input,
|
|
841
958
|
root: workspace.root,
|
|
842
959
|
...(backendImage ? { backendImage } : {}),
|
|
843
960
|
skipWorkspaceBuild: true,
|
|
844
|
-
});
|
|
961
|
+
}));
|
|
845
962
|
if (!built.ok || !built.data)
|
|
846
963
|
return built;
|
|
964
|
+
if (preparedInputDigest && preparedInputDigest !== deliveryInputDigest(cache)) {
|
|
965
|
+
throw new DeliverySourceError('OPENXIANGDA_VALIDATION_INPUT_CHANGED', '密封期间源码、依赖或构建环境发生变化,已停止上传', '等待修改结束后重新运行 openxiangda deploy');
|
|
966
|
+
}
|
|
967
|
+
if (!reused)
|
|
968
|
+
recordSealedCandidate(cache, built.data.package);
|
|
969
|
+
const retained = await publishedDeliverySource(workspace.root, source.commit);
|
|
970
|
+
if (retained.repository !== source.repository) {
|
|
971
|
+
throw new DeliverySourceError('DELIVERY_REPOSITORY_CHANGED', '发布准备期间绑定远端发生变化', '恢复并核对原来源仓库,再发布同一候选');
|
|
972
|
+
}
|
|
973
|
+
await assertDeliverySourceUnchanged(workspace.root, source);
|
|
847
974
|
const deployment = await (await import("./deployment.js")).submitAppPackage({
|
|
848
975
|
client,
|
|
849
976
|
capabilities,
|
|
@@ -972,6 +1099,20 @@ export class OpenXiangdaApplicationServices {
|
|
|
972
1099
|
: []),
|
|
973
1100
|
];
|
|
974
1101
|
}
|
|
1102
|
+
async productionDeploymentPlan(root, deploymentId) {
|
|
1103
|
+
const workspace = await this.workspace(root);
|
|
1104
|
+
const client = await this.client(workspace.root);
|
|
1105
|
+
const source = await client.deployment(workspace.config.app.code, deploymentId);
|
|
1106
|
+
const appVersionId = String(source.result?.applicationVersionId || '');
|
|
1107
|
+
if (!appVersionId)
|
|
1108
|
+
throw new Error('DEPLOYMENT_APP_VERSION_NOT_AVAILABLE: 测试运行尚无可晋级版本');
|
|
1109
|
+
const diagnostics = await this.productionPromotionDiagnostics(workspace, client, source, appVersionId);
|
|
1110
|
+
return this.result('deployment.plan', workspace.context.workspace, {
|
|
1111
|
+
environment: 'production', sourceDeploymentId: deploymentId, appVersionId,
|
|
1112
|
+
packageDigest: source.packageDigest, sealed: true, submitted: false,
|
|
1113
|
+
rebuild: false, businessAcceptance: '需要核对该测试版本的业务验收记录',
|
|
1114
|
+
}, diagnostics);
|
|
1115
|
+
}
|
|
975
1116
|
async deployProduction(root, deploymentId) {
|
|
976
1117
|
const workspace = await this.workspace(root);
|
|
977
1118
|
const capsuleDiagnostics = this.toolchainDiagnostics(workspace);
|
|
@@ -988,12 +1129,12 @@ export class OpenXiangdaApplicationServices {
|
|
|
988
1129
|
if (diagnostics.length > 0) {
|
|
989
1130
|
return this.result("deploy", workspace.context.workspace, undefined, diagnostics);
|
|
990
1131
|
}
|
|
991
|
-
const deployment = await client.promote({
|
|
1132
|
+
const deployment = await operationStage('submit', '提交生产晋级', () => client.promote({
|
|
992
1133
|
appCode: workspace.config.app.code,
|
|
993
1134
|
appVersionId,
|
|
994
1135
|
environmentKind: environment,
|
|
995
1136
|
idempotencyKey: `promote:${source.packageDigest}:${environment}`,
|
|
996
|
-
});
|
|
1137
|
+
}));
|
|
997
1138
|
return this.ok("deploy", workspace.context.workspace, deployment, [
|
|
998
1139
|
{ code: "status", label: "查看生产部署状态", command: `openxiangda status ${deployment.id}` },
|
|
999
1140
|
]);
|
|
@@ -1005,15 +1146,27 @@ export class OpenXiangdaApplicationServices {
|
|
|
1005
1146
|
diagnostics.push(this.diagnostic("PRODUCTION_PREPRODUCTION_RUN_REQUIRED", "生产晋级来源必须是成功的预发 DeploymentRun", "deploymentId", "等待预发部署成功,并使用该 DeploymentRun ID 晋级", { status: source.status, environment: source.environment.kind }));
|
|
1006
1147
|
return diagnostics;
|
|
1007
1148
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1149
|
+
let version;
|
|
1150
|
+
try {
|
|
1151
|
+
const capabilities = await client.capabilities();
|
|
1152
|
+
const { assertApplicationContractCompatible, assertProductionPromotionPreflight } = await import('./deployment.js');
|
|
1153
|
+
assertApplicationContractCompatible(capabilities, CURRENT_APPLICATION_CONTRACT);
|
|
1154
|
+
version = await operationStage('promotion-preflight', '按原测试制品预检生产环境', () => client.productionPromotionPreflight(workspace.config.app.code, source.id));
|
|
1155
|
+
assertProductionPromotionPreflight(version, {
|
|
1156
|
+
appCode: workspace.config.app.code, sourceDeploymentId: source.id, appVersionId, packageDigest: source.packageDigest,
|
|
1157
|
+
});
|
|
1158
|
+
const testedLifecycle = lifecycleAtCommit(workspace.root, version.source.commit, this.appSpecContractIndex(workspace, true));
|
|
1159
|
+
if (!testedLifecycle.changeId || !testedLifecycle.acceptanceIds.length) {
|
|
1160
|
+
diagnostics.push(this.diagnostic('APPSPEC_TESTED_PLAN_REQUIRED', '指定测试版本缺少可追溯的需求与验收计划', 'appspec', '补齐真实记录并发布新的测试候选,再执行验收和生产晋级'));
|
|
1161
|
+
}
|
|
1162
|
+
const acceptance = await operationStage('business-acceptance', '核对原测试版本的业务验收报告', async () => verifyBusinessAcceptance(workspace.root, source, testedLifecycle));
|
|
1163
|
+
diagnostics.push(...acceptance.diagnostics);
|
|
1164
|
+
}
|
|
1165
|
+
catch (error) {
|
|
1166
|
+
return [this.configurationCompatibilityDiagnostic(error)];
|
|
1014
1167
|
}
|
|
1015
1168
|
if (workspace.context.workspace.dirty || version.source.dirty) {
|
|
1016
|
-
diagnostics.push(this.diagnostic("PRODUCTION_SOURCE_DIRTY", "生产晋级只接受来源干净且当前工作区无未提交变更的 AppVersion", "git", "
|
|
1169
|
+
diagnostics.push(this.diagnostic("PRODUCTION_SOURCE_DIRTY", "生产晋级只接受来源干净且当前工作区无未提交变更的 AppVersion", "git", "提交并推送当前改动,或在干净主线目录中晋级该测试版本"));
|
|
1017
1170
|
}
|
|
1018
1171
|
const repository = git(workspace.root, ["config", "--get", "remote.origin.url"]);
|
|
1019
1172
|
const currentRepository = this.repositoryIdentity(repository);
|
|
@@ -1023,25 +1176,15 @@ export class OpenXiangdaApplicationServices {
|
|
|
1023
1176
|
currentRepository !== sourceRepository) {
|
|
1024
1177
|
diagnostics.push(this.diagnostic("PRODUCTION_SOURCE_REPOSITORY_MISMATCH", "当前工作区仓库与 AppVersion 来源仓库不一致", "git.remote.origin.url", "在构建该 AppVersion 的同一仓库中执行晋级", { current: repository || null, source: version.source.repository }));
|
|
1025
1178
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
"@{upstream}",
|
|
1031
|
-
]);
|
|
1032
|
-
if (!upstream) {
|
|
1033
|
-
diagnostics.push(this.diagnostic("PRODUCTION_UPSTREAM_MISSING", "当前 Git 分支没有 upstream,无法证明 AppVersion 来源已进入远端主线", "git", "为当前分支配置 upstream 并推送来源 commit"));
|
|
1179
|
+
if (diagnostics.length)
|
|
1180
|
+
return diagnostics;
|
|
1181
|
+
try {
|
|
1182
|
+
await operationStage('promotion-source', '核对测试制品的主线来源', () => publishedDeliverySource(workspace.root, version.source.commit));
|
|
1034
1183
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
return diagnostics;
|
|
1040
|
-
}
|
|
1041
|
-
const contained = spawnSync("git", ["-C", workspace.root, "merge-base", "--is-ancestor", version.source.commit, upstream], { encoding: "utf8" });
|
|
1042
|
-
if (contained.status !== 0) {
|
|
1043
|
-
diagnostics.push(this.diagnostic("PRODUCTION_SOURCE_NOT_IN_UPSTREAM", "AppVersion 来源 commit 尚未包含在当前 upstream 中", "git", "推送来源 commit,确认 upstream 更新后再晋级", { commit: version.source.commit, upstream }));
|
|
1044
|
-
}
|
|
1184
|
+
catch (error) {
|
|
1185
|
+
if (!(error instanceof DeliverySourceError))
|
|
1186
|
+
throw error;
|
|
1187
|
+
diagnostics.push(this.diagnostic(error.code, error.message, 'git', String(error.data.remediation), error.data));
|
|
1045
1188
|
}
|
|
1046
1189
|
return diagnostics;
|
|
1047
1190
|
}
|
|
@@ -1231,10 +1374,10 @@ export class OpenXiangdaApplicationServices {
|
|
|
1231
1374
|
: {}),
|
|
1232
1375
|
};
|
|
1233
1376
|
}
|
|
1234
|
-
async validateTargetConfigurationCompatibility(workspace, environmentKey) {
|
|
1377
|
+
async validateTargetConfigurationCompatibility(workspace, environmentKey, local) {
|
|
1235
1378
|
const client = await this.client(workspace.root);
|
|
1236
1379
|
const capabilities = await client.capabilities();
|
|
1237
|
-
const sources =
|
|
1380
|
+
const { sources } = local;
|
|
1238
1381
|
const required = {
|
|
1239
1382
|
...CURRENT_APPLICATION_CONTRACT,
|
|
1240
1383
|
configurationBundleSchemaVersion: sources.config.value.schemaVersion,
|
|
@@ -1264,9 +1407,21 @@ export class OpenXiangdaApplicationServices {
|
|
|
1264
1407
|
configurationDigest: sources.config.digest,
|
|
1265
1408
|
contractDigest: sources.contracts.digest,
|
|
1266
1409
|
}, requiredPlatformCapabilities(workspace.config));
|
|
1410
|
+
if (result.projectionDigest !== local.compiled.aggregateDigest) {
|
|
1411
|
+
throw new ControlPlaneError(409, 'OPENXIANGDA_CONFIGURATION_PROJECTION_MISMATCH', '相同校验规则与输入生成了不同投影,已停止构建,请检查平台配套版本', {
|
|
1412
|
+
pointer: '/projectionDigest', expectedProjectionDigest: local.compiled.aggregateDigest,
|
|
1413
|
+
actualProjectionDigest: result.projectionDigest, validatorDigest: NATIVE_CONFIGURATION_VALIDATOR_DIGEST,
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1267
1416
|
return result;
|
|
1268
1417
|
}
|
|
1269
1418
|
configurationCompatibilityDiagnostic(error) {
|
|
1419
|
+
if (error instanceof NativeConfigurationCompilerError) {
|
|
1420
|
+
return this.diagnostic(error.code, '应用声明未通过本地完整配置校验', error.pointer, '按错误指针修正应用声明,运行 openxiangda check 复核', {
|
|
1421
|
+
pointer: error.pointer, identifiers: error.identifiers, validatorDigest: NATIVE_CONFIGURATION_VALIDATOR_DIGEST,
|
|
1422
|
+
validationOwner: 'local-shared-compiler',
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1270
1425
|
const data = error instanceof ControlPlaneError &&
|
|
1271
1426
|
error.data &&
|
|
1272
1427
|
typeof error.data === "object" &&
|
|
@@ -1337,6 +1492,13 @@ export class OpenXiangdaApplicationServices {
|
|
|
1337
1492
|
get toolchainVersion() {
|
|
1338
1493
|
return this.options.toolchainVersion || OPENXIANGDA_TOOLCHAIN_VERSION;
|
|
1339
1494
|
}
|
|
1495
|
+
deliveryCacheContext(workspace) {
|
|
1496
|
+
return {
|
|
1497
|
+
root: workspace.root, frontendRoot: workspace.config.frontend.root,
|
|
1498
|
+
...(backendRuntimeRequired(workspace.config) ? { backendRoot: workspace.config.backend.root } : {}),
|
|
1499
|
+
toolchain: { version: this.toolchainVersion, capsule: this.toolchainCapsule, validatorDigest: NATIVE_CONFIGURATION_VALIDATOR_DIGEST },
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1340
1502
|
get toolchainCapsule() {
|
|
1341
1503
|
return this.options.toolchainCapsule || defaultDevkitCoreToolchainCapsule();
|
|
1342
1504
|
}
|
|
@@ -1356,17 +1518,14 @@ export class OpenXiangdaApplicationServices {
|
|
|
1356
1518
|
}];
|
|
1357
1519
|
}
|
|
1358
1520
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1521
|
+
packageScriptLabel(script) {
|
|
1522
|
+
return { check: '类型与规范检查', test: '应用测试', build: '前后端构建' }[script] || script;
|
|
1523
|
+
}
|
|
1524
|
+
async runPackageScript(root, script) {
|
|
1525
|
+
return operationStage(script, this.packageScriptLabel(script), async () => {
|
|
1526
|
+
const result = await runCommandProcess('pnpm', ['run', script], { cwd: root });
|
|
1527
|
+
return { ok: result.ok, exitCode: result.status ?? 1, output: result.output.trim(), outputTruncated: result.outputTruncated };
|
|
1364
1528
|
});
|
|
1365
|
-
return {
|
|
1366
|
-
ok: result.status === 0,
|
|
1367
|
-
exitCode: result.status ?? 1,
|
|
1368
|
-
output: `${result.stdout || ""}${result.stderr || ""}`.trim(),
|
|
1369
|
-
};
|
|
1370
1529
|
}
|
|
1371
1530
|
repositoryIdentity(value) {
|
|
1372
1531
|
const normalized = String(value || "").trim();
|
|
@@ -1642,7 +1801,7 @@ export class OpenXiangdaApplicationServices {
|
|
|
1642
1801
|
operation,
|
|
1643
1802
|
workspace,
|
|
1644
1803
|
...(data === undefined ? {} : { data }),
|
|
1645
|
-
diagnostics,
|
|
1804
|
+
diagnostics: [...diagnostics].sort((left, right) => ({ error: 0, warning: 1, info: 2 })[left.severity] - ({ error: 0, warning: 1, info: 2 })[right.severity]),
|
|
1646
1805
|
nextActions,
|
|
1647
1806
|
};
|
|
1648
1807
|
}
|