openxiangda 1.0.231 → 1.0.232

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.
@@ -0,0 +1,955 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const {
5
+ compileAppPackage,
6
+ sha256,
7
+ sha256Canonical,
8
+ } = require('./delivery-v2-package');
9
+ const {
10
+ buildWorkspaceReleaseSteps,
11
+ commandFromArgs,
12
+ } = require('./release-plan');
13
+
14
+ const CONFIG_TARGET_BY_RESOURCE_TYPE = Object.freeze({
15
+ role: 'roles',
16
+ connector: 'connectors',
17
+ notification: 'notifications',
18
+ menu: 'menus',
19
+ 'data-view': 'dataViews',
20
+ storage: 'storageConfigs',
21
+ 'auth-config': 'authConfigs',
22
+ route: 'routes',
23
+ 'public-access': 'publicAccessPolicies',
24
+ 'page-permission-group': 'pagePermissionGroups',
25
+ 'form-permission-group': 'formPermissionGroups',
26
+ 'scope-dimension': 'scopeDimensions',
27
+ 'scope-grant-source': 'scopeGrantSources',
28
+ 'data-scope-policy': 'dataScopePolicies',
29
+ });
30
+
31
+ function createDeliveryV2Executor(dependencies) {
32
+ const deps = assertDependencies(dependencies);
33
+
34
+ async function inspect(input) {
35
+ const previous = await latestSuccessfulRun(input.config, input.target);
36
+ const compiled = await compileAppPackage({
37
+ workspaceRoot: input.workspaceRoot,
38
+ previousManifest: previous?.packageManifest || null,
39
+ skipBuild: input.skipBuild === true,
40
+ buildCommand: input.buildCommand,
41
+ distDir: input.distDir,
42
+ onBuildOutput: input.onBuildOutput,
43
+ });
44
+ return {
45
+ compiled,
46
+ previous,
47
+ targets: releaseTargets(
48
+ compiled.packageManifest,
49
+ previous?.packageManifest || null
50
+ ),
51
+ };
52
+ }
53
+
54
+ async function deploy(input) {
55
+ const previous = await latestSuccessfulRun(input.config, input.target);
56
+ let compiled;
57
+ let executionRoot = path.resolve(input.workspaceRoot || process.cwd());
58
+ let materialized = false;
59
+
60
+ if (input.packageDigest) {
61
+ const packageManifest = await downloadPackageManifest(
62
+ input.config,
63
+ input.target,
64
+ input.packageDigest
65
+ );
66
+ executionRoot = await materializePackageWorkspace({
67
+ config: input.config,
68
+ target: input.target,
69
+ packageDigest: input.packageDigest,
70
+ packageManifest,
71
+ sourceWorkspaceRoot: executionRoot,
72
+ });
73
+ compiled = {
74
+ workspaceRoot: executionRoot,
75
+ packageDigest: input.packageDigest,
76
+ packageManifest,
77
+ artifacts: [],
78
+ summary: packageSummary(packageManifest, previous?.packageManifest),
79
+ };
80
+ materialized = true;
81
+ } else {
82
+ compiled = await compileAppPackage({
83
+ workspaceRoot: executionRoot,
84
+ previousManifest: previous?.packageManifest || null,
85
+ buildCommand: input.buildCommand,
86
+ distDir: input.distDir,
87
+ onBuildOutput: input.onBuildOutput,
88
+ });
89
+ await uploadArtifacts(input.config, input.target, compiled.artifacts);
90
+ }
91
+
92
+ const targets = releaseTargets(
93
+ compiled.packageManifest,
94
+ previous?.packageManifest || null,
95
+ { forceAll: input.forceAll === true }
96
+ );
97
+ const run = await createRun({
98
+ config: input.config,
99
+ target: input.target,
100
+ packageDigest: compiled.packageDigest,
101
+ packageManifest: compiled.packageManifest,
102
+ kind: input.kind || 'deploy',
103
+ idempotencyKey:
104
+ input.idempotencyKey ||
105
+ defaultIdempotencyKey(
106
+ input.kind || 'deploy',
107
+ input.target,
108
+ compiled.packageDigest
109
+ ),
110
+ clientSessionId: input.clientSessionId,
111
+ });
112
+ if (run.status === 'succeeded') {
113
+ return {
114
+ run,
115
+ packageDigest: compiled.packageDigest,
116
+ packageManifest: compiled.packageManifest,
117
+ summary: compiled.summary,
118
+ targets,
119
+ materialized,
120
+ idempotent: true,
121
+ };
122
+ }
123
+
124
+ const result = await executeRun({
125
+ config: input.config,
126
+ target: input.target,
127
+ run,
128
+ packageDigest: compiled.packageDigest,
129
+ packageManifest: compiled.packageManifest,
130
+ targets,
131
+ executionRoot,
132
+ skipCompleted: true,
133
+ });
134
+ return {
135
+ ...result,
136
+ summary: compiled.summary,
137
+ targets,
138
+ materialized,
139
+ idempotent: false,
140
+ };
141
+ }
142
+
143
+ async function retry(input) {
144
+ const current = await runDetail(
145
+ input.config,
146
+ input.target,
147
+ input.runId
148
+ );
149
+ if (current.status === 'succeeded') {
150
+ return { run: current, idempotent: true };
151
+ }
152
+ const run = await deps.requestWithAuth(
153
+ input.config,
154
+ input.target.profileName,
155
+ runPath(input.target, `/${encodeURIComponent(input.runId)}/retry`),
156
+ { method: 'POST', retryTransient: true }
157
+ );
158
+ if (run.status !== 'packaged') {
159
+ throw deliveryError(
160
+ 'DELIVERY_RUN_NOT_EXECUTABLE',
161
+ `ReleaseRun ${run.id} 当前状态为 ${run.status}`,
162
+ false
163
+ );
164
+ }
165
+ const executionRoot = await materializePackageWorkspace({
166
+ config: input.config,
167
+ target: input.target,
168
+ packageDigest: run.packageDigest,
169
+ packageManifest: run.packageManifest,
170
+ sourceWorkspaceRoot: input.workspaceRoot,
171
+ });
172
+ const previous = await latestSuccessfulRun(input.config, input.target, {
173
+ excludeRunId: run.id,
174
+ });
175
+ const targets = releaseTargets(
176
+ run.packageManifest,
177
+ previous?.packageManifest || null
178
+ );
179
+ return await executeRun({
180
+ config: input.config,
181
+ target: input.target,
182
+ run,
183
+ packageDigest: run.packageDigest,
184
+ packageManifest: run.packageManifest,
185
+ targets,
186
+ executionRoot,
187
+ skipCompleted: true,
188
+ });
189
+ }
190
+
191
+ async function rollback(input) {
192
+ const targetRun = await findSuccessfulRunByRelease(
193
+ input.config,
194
+ input.target,
195
+ input.releaseId
196
+ );
197
+ if (!targetRun) {
198
+ throw deliveryError(
199
+ 'DELIVERY_ROLLBACK_RELEASE_NOT_FOUND',
200
+ `未找到 App Release ${input.releaseId} 对应的成功 Delivery V2 记录`,
201
+ false
202
+ );
203
+ }
204
+ return await deploy({
205
+ ...input,
206
+ kind: 'rollback',
207
+ packageDigest: targetRun.packageDigest,
208
+ forceAll: true,
209
+ idempotencyKey:
210
+ input.idempotencyKey ||
211
+ `rollback:${targetRun.id}:${Date.now().toString(36)}`,
212
+ });
213
+ }
214
+
215
+ async function status(input) {
216
+ return await runDetail(input.config, input.target, input.runId);
217
+ }
218
+
219
+ async function executeRun(input) {
220
+ const originalCwd = process.cwd();
221
+ const executionTarget = {
222
+ ...input.target,
223
+ deploymentId: input.target.deploymentId || null,
224
+ };
225
+ let activeRun = input.run;
226
+ let currentStep = 'preparing';
227
+ let currentComponent = 'delivery-v2';
228
+ try {
229
+ process.chdir(input.executionRoot);
230
+ deps.installDeliveryContext(
231
+ executionTarget,
232
+ activeRun,
233
+ input.targets
234
+ );
235
+ activeRun = await checkpoint(
236
+ input.config,
237
+ executionTarget,
238
+ activeRun.id,
239
+ {
240
+ stage: lifecycleStage('preparing', activeRun.attempt),
241
+ status: 'preparing',
242
+ component: 'package',
243
+ progress: {
244
+ packageDigest: input.packageDigest,
245
+ },
246
+ }
247
+ );
248
+ const steps = decorateSteps(
249
+ buildWorkspaceReleaseSteps(
250
+ input.targets,
251
+ input.packageManifest.runtimeMode,
252
+ executionTarget.profileName,
253
+ activeRun.control?.changeId
254
+ ),
255
+ executionTarget,
256
+ activeRun,
257
+ input.packageManifest,
258
+ input.executionRoot
259
+ );
260
+
261
+ if (steps.some(step => step.id === 'app-finalize')) {
262
+ activeRun = await checkpoint(
263
+ input.config,
264
+ executionTarget,
265
+ activeRun.id,
266
+ {
267
+ stage: lifecycleStage('activating', activeRun.attempt),
268
+ status: 'activating',
269
+ component: 'app-release',
270
+ progress: { stepCount: steps.length },
271
+ }
272
+ );
273
+ }
274
+
275
+ const stepResults = {};
276
+ for (const [index, step] of steps.entries()) {
277
+ const doneStage = checkpointName('done', step.id);
278
+ if (input.skipCompleted && activeRun.checkpoints?.[doneStage]) {
279
+ stepResults[step.id] =
280
+ activeRun.result?.[doneStage] || { resumed: true };
281
+ continue;
282
+ }
283
+ currentStep = step.id;
284
+ currentComponent = step.stagedKind || step.id;
285
+ const result = await deps.runOpenXiangdaInProcess(step.args, {
286
+ quiet: true,
287
+ });
288
+ stepResults[step.id] = result;
289
+ activeRun = await checkpoint(
290
+ input.config,
291
+ executionTarget,
292
+ activeRun.id,
293
+ {
294
+ stage: doneStage,
295
+ component: currentComponent,
296
+ progress: {
297
+ completed: index + 1,
298
+ total: steps.length,
299
+ },
300
+ result: compactStepResult(step.id, result),
301
+ }
302
+ );
303
+ }
304
+
305
+ if (activeRun.status !== 'verifying') {
306
+ if (activeRun.status !== 'activating') {
307
+ activeRun = await checkpoint(
308
+ input.config,
309
+ executionTarget,
310
+ activeRun.id,
311
+ {
312
+ stage: lifecycleStage('activating', activeRun.attempt),
313
+ status: 'activating',
314
+ component: 'activation',
315
+ }
316
+ );
317
+ }
318
+ activeRun = await checkpoint(
319
+ input.config,
320
+ executionTarget,
321
+ activeRun.id,
322
+ {
323
+ stage: lifecycleStage('verifying', activeRun.attempt),
324
+ status: 'verifying',
325
+ component: 'readback',
326
+ }
327
+ );
328
+ }
329
+ const verification = await verifyActivation(
330
+ input.config,
331
+ executionTarget,
332
+ stepResults
333
+ );
334
+ activeRun = await checkpoint(
335
+ input.config,
336
+ executionTarget,
337
+ activeRun.id,
338
+ {
339
+ stage: 'succeeded',
340
+ status: 'succeeded',
341
+ component: 'delivery-v2',
342
+ progress: {
343
+ completed: steps.length,
344
+ total: steps.length,
345
+ },
346
+ result: {
347
+ packageDigest: input.packageDigest,
348
+ appReleaseId: verification.appReleaseId,
349
+ runtimeReleaseId: verification.runtimeReleaseId,
350
+ },
351
+ }
352
+ );
353
+ return {
354
+ run: activeRun,
355
+ packageDigest: input.packageDigest,
356
+ packageManifest: input.packageManifest,
357
+ steps: steps.map(step => ({
358
+ id: step.id,
359
+ command: step.command,
360
+ })),
361
+ verification,
362
+ };
363
+ } catch (error) {
364
+ const failure = structuredFailure(
365
+ error,
366
+ activeRun?.id,
367
+ currentStep,
368
+ currentComponent
369
+ );
370
+ if (activeRun?.id) {
371
+ try {
372
+ await deps.requestWithAuth(
373
+ input.config,
374
+ executionTarget.profileName,
375
+ runPath(
376
+ executionTarget,
377
+ `/${encodeURIComponent(activeRun.id)}/fail`
378
+ ),
379
+ {
380
+ method: 'POST',
381
+ retryTransient: true,
382
+ body: {
383
+ stage: failure.stage,
384
+ component: failure.component,
385
+ code: failure.code,
386
+ message: failure.message,
387
+ retryable: failure.retryable,
388
+ details: {
389
+ progressPreserved: true,
390
+ },
391
+ },
392
+ }
393
+ );
394
+ } catch {
395
+ // The original execution failure remains authoritative.
396
+ }
397
+ }
398
+ error.deliveryFailure = failure;
399
+ error.data = failure;
400
+ error.code = failure.code;
401
+ throw error;
402
+ } finally {
403
+ try {
404
+ deps.clearDeliveryContext(executionTarget, activeRun);
405
+ } finally {
406
+ process.chdir(originalCwd);
407
+ }
408
+ }
409
+ }
410
+
411
+ async function uploadArtifacts(config, target, artifacts) {
412
+ for (const artifact of artifacts || []) {
413
+ if (await artifactExists(config, target, artifact.digest)) continue;
414
+ const query = new URLSearchParams({
415
+ kind: artifact.kind,
416
+ contentType: artifact.contentType || 'application/octet-stream',
417
+ metadata: JSON.stringify(cleanObject(artifact.metadata)),
418
+ });
419
+ await deps.requestFormWithAuth(
420
+ config,
421
+ target.profileName,
422
+ `${artifactPath(target, artifact.digest)}?${query.toString()}`,
423
+ () => {
424
+ const form = new FormData();
425
+ form.append(
426
+ 'file',
427
+ new Blob([artifact.buffer], {
428
+ type: artifact.contentType || 'application/octet-stream',
429
+ }),
430
+ artifact.metadata?.path || artifact.digest
431
+ );
432
+ return form;
433
+ },
434
+ { retryTransient: true, retries: 2 }
435
+ );
436
+ }
437
+ }
438
+
439
+ async function artifactExists(config, target, digest) {
440
+ try {
441
+ await deps.requestWithAuth(
442
+ config,
443
+ target.profileName,
444
+ artifactPath(target, digest),
445
+ { retryTransient: true }
446
+ );
447
+ return true;
448
+ } catch (error) {
449
+ if (isNotFound(error)) return false;
450
+ throw error;
451
+ }
452
+ }
453
+
454
+ async function downloadPackageManifest(config, target, digestInput) {
455
+ const digest = normalizeDigest(digestInput);
456
+ const downloaded = await deps.downloadWithAuth(
457
+ config,
458
+ target.profileName,
459
+ `${artifactPath(target, digest)}/content`,
460
+ { retries: 2 }
461
+ );
462
+ if (sha256(downloaded.buffer) !== digest) {
463
+ throw deliveryError(
464
+ 'DELIVERY_PACKAGE_DOWNLOAD_DIGEST_MISMATCH',
465
+ `下载的 package manifest 与 ${digest} 不一致`,
466
+ true
467
+ );
468
+ }
469
+ let manifest;
470
+ try {
471
+ manifest = JSON.parse(downloaded.buffer.toString('utf8'));
472
+ } catch {
473
+ throw deliveryError(
474
+ 'DELIVERY_PACKAGE_MANIFEST_INVALID',
475
+ 'package manifest 不是合法 JSON',
476
+ false
477
+ );
478
+ }
479
+ if (
480
+ manifest.schemaVersion !== 'openxiangda-app-package-v2' ||
481
+ sha256Canonical(manifest) !== digest
482
+ ) {
483
+ throw deliveryError(
484
+ 'DELIVERY_PACKAGE_MANIFEST_INVALID',
485
+ 'package manifest schema 或摘要无效',
486
+ false
487
+ );
488
+ }
489
+ return manifest;
490
+ }
491
+
492
+ async function materializePackageWorkspace(input) {
493
+ const root = path.join(
494
+ path.resolve(input.sourceWorkspaceRoot || process.cwd()),
495
+ '.openxiangda',
496
+ 'delivery-v2',
497
+ 'packages',
498
+ normalizeDigest(input.packageDigest)
499
+ );
500
+ fs.mkdirSync(root, { recursive: true });
501
+ const sourceLayer = layerByKind(input.packageManifest, 'source');
502
+ if (!sourceLayer) {
503
+ throw deliveryError(
504
+ 'DELIVERY_PACKAGE_SOURCE_LAYER_MISSING',
505
+ 'package manifest 缺少 source layer',
506
+ false
507
+ );
508
+ }
509
+ await materializeFiles(
510
+ input.config,
511
+ input.target,
512
+ root,
513
+ sourceLayer.files || []
514
+ );
515
+ const stateSource = path.join(
516
+ path.resolve(input.sourceWorkspaceRoot || process.cwd()),
517
+ '.openxiangda',
518
+ 'state.json'
519
+ );
520
+ const stateTarget = path.join(root, '.openxiangda', 'state.json');
521
+ if (fs.existsSync(stateSource)) {
522
+ fs.mkdirSync(path.dirname(stateTarget), { recursive: true });
523
+ fs.copyFileSync(stateSource, stateTarget);
524
+ }
525
+ const nodeModulesSource = path.join(
526
+ path.resolve(input.sourceWorkspaceRoot || process.cwd()),
527
+ 'node_modules'
528
+ );
529
+ const nodeModulesTarget = path.join(root, 'node_modules');
530
+ if (
531
+ fs.existsSync(nodeModulesSource) &&
532
+ !fs.existsSync(nodeModulesTarget)
533
+ ) {
534
+ fs.symlinkSync(nodeModulesSource, nodeModulesTarget, 'dir');
535
+ }
536
+ const runtimeLayer = layerByKind(input.packageManifest, 'runtime');
537
+ if (runtimeLayer) {
538
+ await materializeFiles(
539
+ input.config,
540
+ input.target,
541
+ path.join(root, 'dist'),
542
+ runtimeLayer.files || []
543
+ );
544
+ }
545
+ return root;
546
+ }
547
+
548
+ async function materializeFiles(config, target, root, files) {
549
+ for (const descriptor of files) {
550
+ const relative = safeRelativePath(descriptor.path);
551
+ const destination = path.join(root, relative);
552
+ if (fs.existsSync(destination)) {
553
+ const existing = fs.readFileSync(destination);
554
+ if (sha256(existing) === descriptor.digest) continue;
555
+ }
556
+ const downloaded = await deps.downloadWithAuth(
557
+ config,
558
+ target.profileName,
559
+ `${artifactPath(target, descriptor.digest)}/content`,
560
+ { retries: 2 }
561
+ );
562
+ if (sha256(downloaded.buffer) !== descriptor.digest) {
563
+ throw deliveryError(
564
+ 'DELIVERY_ARTIFACT_DOWNLOAD_DIGEST_MISMATCH',
565
+ `制品 ${descriptor.digest} 下载校验失败`,
566
+ true
567
+ );
568
+ }
569
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
570
+ const temporary = `${destination}.${process.pid}.tmp`;
571
+ fs.writeFileSync(temporary, downloaded.buffer, { mode: 0o600 });
572
+ fs.renameSync(temporary, destination);
573
+ }
574
+ }
575
+
576
+ async function latestSuccessfulRun(config, target, options = {}) {
577
+ const list = await deps.requestWithAuth(
578
+ config,
579
+ target.profileName,
580
+ `${runPath(target)}?limit=100`,
581
+ { retryTransient: true }
582
+ );
583
+ const candidate = (list?.items || []).find(
584
+ item =>
585
+ item.status === 'succeeded' &&
586
+ item.id !== options.excludeRunId &&
587
+ sameEnvironment(item, target)
588
+ );
589
+ return candidate
590
+ ? await runDetail(config, target, candidate.id)
591
+ : null;
592
+ }
593
+
594
+ async function findSuccessfulRunByRelease(config, target, releaseId) {
595
+ const list = await deps.requestWithAuth(
596
+ config,
597
+ target.profileName,
598
+ `${runPath(target)}?limit=100`,
599
+ { retryTransient: true }
600
+ );
601
+ for (const item of list?.items || []) {
602
+ if (item.status !== 'succeeded' || !sameEnvironment(item, target)) {
603
+ continue;
604
+ }
605
+ const detail = await runDetail(config, target, item.id);
606
+ if (
607
+ detail.result?.succeeded?.appReleaseId === releaseId ||
608
+ detail.result?.activation?.appReleaseId === releaseId
609
+ ) {
610
+ return detail;
611
+ }
612
+ }
613
+ return null;
614
+ }
615
+
616
+ async function runDetail(config, target, runId) {
617
+ return await deps.requestWithAuth(
618
+ config,
619
+ target.profileName,
620
+ runPath(target, `/${encodeURIComponent(runId)}`),
621
+ { retryTransient: true }
622
+ );
623
+ }
624
+
625
+ async function createRun(input) {
626
+ return await deps.requestWithAuth(
627
+ input.config,
628
+ input.target.profileName,
629
+ runPath(input.target),
630
+ {
631
+ method: 'POST',
632
+ retryTransient: true,
633
+ retries: 2,
634
+ body: {
635
+ environmentId: input.target.environmentId || null,
636
+ environmentKind: input.target.environmentKind || null,
637
+ kind: input.kind,
638
+ packageDigest: input.packageDigest,
639
+ packageManifest: input.packageManifest,
640
+ idempotencyKey: input.idempotencyKey,
641
+ clientSessionId: input.clientSessionId,
642
+ },
643
+ }
644
+ );
645
+ }
646
+
647
+ async function checkpoint(config, target, runId, body) {
648
+ return await deps.requestWithAuth(
649
+ config,
650
+ target.profileName,
651
+ runPath(target, `/${encodeURIComponent(runId)}/checkpoints`),
652
+ {
653
+ method: 'POST',
654
+ retryTransient: true,
655
+ retries: 2,
656
+ body,
657
+ }
658
+ );
659
+ }
660
+
661
+ async function verifyActivation(config, target, stepResults) {
662
+ const [appHead, runtimeHead] = await Promise.all([
663
+ deps.requestWithAuth(
664
+ config,
665
+ target.profileName,
666
+ appApiPath(target, '/app-releases/head'),
667
+ { retryTransient: true }
668
+ ),
669
+ deps.requestWithAuth(
670
+ config,
671
+ target.profileName,
672
+ appApiPath(target, '/runtime/releases/head'),
673
+ { retryTransient: true }
674
+ ),
675
+ ]);
676
+ const finalized = stepResults['app-finalize'];
677
+ const expectedAppReleaseId =
678
+ finalized?.activated?.id ||
679
+ finalized?.verified?.id ||
680
+ finalized?.prepared?.id ||
681
+ null;
682
+ const activeAppReleaseId =
683
+ appHead?.activeAppReleaseId || appHead?.release?.id || null;
684
+ if (
685
+ expectedAppReleaseId &&
686
+ activeAppReleaseId &&
687
+ expectedAppReleaseId !== activeAppReleaseId
688
+ ) {
689
+ throw deliveryError(
690
+ 'DELIVERY_ACTIVATION_READBACK_MISMATCH',
691
+ `App Release 激活读回不一致: expected=${expectedAppReleaseId} actual=${activeAppReleaseId}`,
692
+ true
693
+ );
694
+ }
695
+ return {
696
+ appReleaseId: activeAppReleaseId || expectedAppReleaseId,
697
+ runtimeReleaseId: runtimeHead?.activeRuntimeReleaseId || null,
698
+ runtimeBuildId: runtimeHead?.activeRuntimeBuildId || null,
699
+ };
700
+ }
701
+
702
+ return {
703
+ deploy,
704
+ inspect,
705
+ retry,
706
+ rollback,
707
+ status,
708
+ };
709
+ }
710
+
711
+ function releaseTargets(manifest, previousManifest, options = {}) {
712
+ const changed = kind =>
713
+ options.forceAll === true ||
714
+ layerByKind(manifest, kind)?.digest !==
715
+ layerByKind(previousManifest, kind)?.digest;
716
+ const resources = manifest?.resources || {};
717
+ const resourceSelectors = {};
718
+ if (changed('configuration')) {
719
+ for (const [type, values] of Object.entries(
720
+ resources.configuration || {}
721
+ )) {
722
+ const key = CONFIG_TARGET_BY_RESOURCE_TYPE[type];
723
+ if (key && Array.isArray(values) && values.length > 0) {
724
+ resourceSelectors[key] = [...new Set(values.map(String))].sort();
725
+ }
726
+ }
727
+ }
728
+ return {
729
+ forms: changed('form') ? resources.forms || [] : [],
730
+ pages: [],
731
+ functions: changed('backend') ? resources.functions || [] : [],
732
+ automations: changed('backend') ? resources.automations || [] : [],
733
+ workflows: changed('workflow') ? resources.workflows || [] : [],
734
+ jsCodeNodes: [],
735
+ resources: false,
736
+ resourceSelectors,
737
+ resourceDeletes: {},
738
+ runtime: Boolean(layerByKind(manifest, 'runtime') && changed('runtime')),
739
+ other: [],
740
+ };
741
+ }
742
+
743
+ function decorateSteps(
744
+ steps,
745
+ target,
746
+ run,
747
+ packageManifest,
748
+ executionRoot
749
+ ) {
750
+ const result = [];
751
+ for (const original of steps) {
752
+ const step = {
753
+ ...original,
754
+ args: [...original.args],
755
+ };
756
+ if (target.targetName) {
757
+ step.args.push('--environment', target.targetName);
758
+ }
759
+ if (target.deploymentId) {
760
+ step.args.push('--deployment-id', target.deploymentId);
761
+ }
762
+ if (step.id === 'runtime-stage') {
763
+ const runtime = layerByKind(packageManifest, 'runtime');
764
+ step.args.push(
765
+ '--no-build',
766
+ '--dist',
767
+ path.join(executionRoot, 'dist'),
768
+ '--build-id',
769
+ `pkg-${runtime.digest.slice(0, 20)}`,
770
+ '--upload-mode',
771
+ 'staged'
772
+ );
773
+ }
774
+ if (step.id === 'app-finalize') {
775
+ if (target.environmentId) {
776
+ step.args.push('--environment-id', target.environmentId);
777
+ }
778
+ step.args.push('--delivery-run-id', run.id);
779
+ }
780
+ step.command = commandFromArgs(step.args);
781
+ result.push(step);
782
+ }
783
+ return result;
784
+ }
785
+
786
+ function packageSummary(manifest, previousManifest) {
787
+ return {
788
+ sourceFileCount: layerByKind(manifest, 'source')?.fileCount || 0,
789
+ artifactUploadCount: 0,
790
+ layers: (manifest.layers || []).map(layer => ({
791
+ kind: layer.kind,
792
+ digest: layer.digest,
793
+ sourceHash: layer.sourceHash,
794
+ reused:
795
+ layer.digest === layerByKind(previousManifest, layer.kind)?.digest,
796
+ built: false,
797
+ fileCount: layer.fileCount,
798
+ size: layer.size,
799
+ })),
800
+ };
801
+ }
802
+
803
+ function compactStepResult(stepId, result) {
804
+ if (!result || typeof result !== 'object') return { ok: true };
805
+ if (stepId === 'app-finalize') {
806
+ return {
807
+ appReleaseId:
808
+ result.activated?.id ||
809
+ result.verified?.id ||
810
+ result.prepared?.id ||
811
+ null,
812
+ };
813
+ }
814
+ return {
815
+ ok: true,
816
+ id: result.id || result.release?.id || null,
817
+ };
818
+ }
819
+
820
+ function structuredFailure(error, runId, stage, component) {
821
+ const code =
822
+ error?.code ||
823
+ error?.payload?.errorCode ||
824
+ error?.payload?.data?.code ||
825
+ 'DELIVERY_EXECUTION_FAILED';
826
+ return {
827
+ runId: runId || null,
828
+ stage: stage || 'unknown',
829
+ component: component || 'unknown',
830
+ code: String(code),
831
+ message: String(error?.message || 'Delivery V2 执行失败'),
832
+ retryable: error?.retryable !== false,
833
+ progressPreserved: true,
834
+ };
835
+ }
836
+
837
+ function layerByKind(manifest, kind) {
838
+ return (manifest?.layers || []).find(layer => layer.kind === kind) || null;
839
+ }
840
+
841
+ function runPath(target, suffix = '') {
842
+ return `${appApiPath(target, '/delivery/runs')}${suffix}`;
843
+ }
844
+
845
+ function artifactPath(target, digest) {
846
+ return appApiPath(
847
+ target,
848
+ `/delivery/artifacts/${encodeURIComponent(digest)}`
849
+ );
850
+ }
851
+
852
+ function appApiPath(target, suffix) {
853
+ return `/openxiangda-api/v1/apps/${encodeURIComponent(
854
+ target.appType
855
+ )}${suffix}`;
856
+ }
857
+
858
+ function checkpointName(prefix, value) {
859
+ return `${prefix}-${String(value || 'step')
860
+ .replace(/[^a-z0-9-]+/gi, '-')
861
+ .toLowerCase()}`.slice(0, 64);
862
+ }
863
+
864
+ function lifecycleStage(name, attempt) {
865
+ return `${name}-attempt-${Math.max(Number(attempt) || 1, 1)}`;
866
+ }
867
+
868
+ function sameEnvironment(run, target) {
869
+ return String(run.environmentId || '') === String(target.environmentId || '');
870
+ }
871
+
872
+ function defaultIdempotencyKey(kind, target, digest) {
873
+ return [
874
+ 'delivery-v2',
875
+ kind,
876
+ target.appType,
877
+ target.environmentId || 'direct',
878
+ digest,
879
+ ].join(':');
880
+ }
881
+
882
+ function normalizeDigest(value) {
883
+ const digest = String(value || '')
884
+ .trim()
885
+ .toLowerCase();
886
+ if (!/^[a-f0-9]{64}$/.test(digest)) {
887
+ throw deliveryError(
888
+ 'DELIVERY_PACKAGE_DIGEST_INVALID',
889
+ 'package digest 必须是 64 位 SHA-256',
890
+ false
891
+ );
892
+ }
893
+ return digest;
894
+ }
895
+
896
+ function safeRelativePath(value) {
897
+ const normalized = String(value || '').replace(/\\/g, '/');
898
+ if (
899
+ !normalized ||
900
+ normalized.startsWith('/') ||
901
+ normalized.includes('../') ||
902
+ normalized === '..'
903
+ ) {
904
+ throw deliveryError(
905
+ 'DELIVERY_ARTIFACT_PATH_INVALID',
906
+ `制品路径无效: ${value}`,
907
+ false
908
+ );
909
+ }
910
+ return normalized;
911
+ }
912
+
913
+ function isNotFound(error) {
914
+ return (
915
+ Number(error?.status || error?.statusCode) === 404 ||
916
+ /\bHTTP 404\b|NOT_FOUND/i.test(String(error?.message || ''))
917
+ );
918
+ }
919
+
920
+ function cleanObject(value) {
921
+ return Object.fromEntries(
922
+ Object.entries(value || {}).filter(([, item]) => item !== undefined)
923
+ );
924
+ }
925
+
926
+ function deliveryError(code, message, retryable) {
927
+ const error = new Error(`${code}: ${message}`);
928
+ error.code = code;
929
+ error.retryable = retryable;
930
+ return error;
931
+ }
932
+
933
+ function assertDependencies(value) {
934
+ const required = [
935
+ 'clearDeliveryContext',
936
+ 'downloadWithAuth',
937
+ 'installDeliveryContext',
938
+ 'requestFormWithAuth',
939
+ 'requestWithAuth',
940
+ 'runOpenXiangdaInProcess',
941
+ ];
942
+ for (const key of required) {
943
+ if (typeof value?.[key] !== 'function') {
944
+ throw new Error(`Delivery V2 executor dependency 缺失: ${key}`);
945
+ }
946
+ }
947
+ return value;
948
+ }
949
+
950
+ module.exports = {
951
+ CONFIG_TARGET_BY_RESOURCE_TYPE,
952
+ createDeliveryV2Executor,
953
+ releaseTargets,
954
+ structuredFailure,
955
+ };