openxiangda 1.0.241 → 1.0.243

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.
Files changed (2) hide show
  1. package/lib/cli.js +378 -30
  2. package/package.json +1 -1
package/lib/cli.js CHANGED
@@ -3698,6 +3698,29 @@ function deploymentQueueDetails(error) {
3698
3698
  return value;
3699
3699
  }
3700
3700
 
3701
+ function deploymentQueueWaitDecision(statusInput) {
3702
+ const status = String(statusInput || '').trim().toLowerCase();
3703
+ return {
3704
+ status,
3705
+ wait: status === 'running',
3706
+ evidencePending: status === 'deployed',
3707
+ };
3708
+ }
3709
+
3710
+ function deploymentEvidencePendingError(deployment = {}) {
3711
+ const deploymentId = String(deployment.deploymentId || deployment.id || '').trim();
3712
+ const error = new Error(
3713
+ `APP_DEPLOYMENT_EVIDENCE_PENDING: 目标环境部署 ${deploymentId || '-'} 已完成激活,当前为 deployed,待提交测试证据;请先执行 openxiangda release test --deployment ${deploymentId || '<id>'} --evidence-json <JSON|file>`
3714
+ );
3715
+ error.code = 'APP_DEPLOYMENT_EVIDENCE_PENDING';
3716
+ error.data = {
3717
+ deploymentId: deploymentId || null,
3718
+ status: 'deployed',
3719
+ nextAction: 'release test',
3720
+ };
3721
+ return error;
3722
+ }
3723
+
3701
3724
  async function createApplicationDeploymentWhenAvailable(
3702
3725
  config,
3703
3726
  target,
@@ -3718,9 +3741,17 @@ async function createApplicationDeploymentWhenAvailable(
3718
3741
  );
3719
3742
  } catch (error) {
3720
3743
  const queue = deploymentQueueDetails(error);
3721
- if (!queue || Date.now() >= deadline || waitSeconds === 0) throw error;
3744
+ if (!queue) throw error;
3722
3745
  const deploymentId = String(queue.deploymentId || '').trim();
3723
3746
  if (!deploymentId) throw error;
3747
+ const queuedState = deploymentQueueWaitDecision(queue.status);
3748
+ if (queuedState.evidencePending) {
3749
+ throw deploymentEvidencePendingError({
3750
+ ...queue,
3751
+ deploymentId,
3752
+ });
3753
+ }
3754
+ if (Date.now() >= deadline || waitSeconds === 0) throw error;
3724
3755
  const delaySeconds = Math.min(
3725
3756
  Math.max(Number(queue.retryAfterSeconds || 5), 1),
3726
3757
  30
@@ -3742,7 +3773,14 @@ async function createApplicationDeploymentWhenAvailable(
3742
3773
  } catch {
3743
3774
  break;
3744
3775
  }
3745
- if (!['running', 'deployed'].includes(active?.status)) break;
3776
+ const activeState = deploymentQueueWaitDecision(active?.status);
3777
+ if (activeState.evidencePending) {
3778
+ throw deploymentEvidencePendingError({
3779
+ ...active,
3780
+ deploymentId,
3781
+ });
3782
+ }
3783
+ if (!activeState.wait) break;
3746
3784
  }
3747
3785
  if (Date.now() >= deadline) {
3748
3786
  fail(
@@ -3990,6 +4028,281 @@ async function readManagedDeployment(
3990
4028
  return deployment;
3991
4029
  }
3992
4030
 
4031
+ function inspectSucceededPreproductionDeployment(
4032
+ deployment,
4033
+ context,
4034
+ now = Date.now()
4035
+ ) {
4036
+ const candidate = context?.candidate || {};
4037
+ const target = context?.target || {};
4038
+ const currentAppReleaseId = String(
4039
+ context?.currentAppReleaseId || ''
4040
+ ).trim();
4041
+ const evidence = deployment?.evidenceSummary;
4042
+ const reasons = [];
4043
+ if (deployment?.status !== 'succeeded') {
4044
+ reasons.push('status_not_succeeded');
4045
+ }
4046
+ if (deployment?.kind !== 'deploy') {
4047
+ reasons.push('kind_not_preproduction_deploy');
4048
+ }
4049
+ if (deployment?.candidateId !== candidate.id) {
4050
+ reasons.push('candidate_mismatch');
4051
+ }
4052
+ if (deployment?.targetEnvironmentId !== target.environmentId) {
4053
+ reasons.push('target_environment_mismatch');
4054
+ }
4055
+ if (!currentAppReleaseId) reasons.push('current_app_release_missing');
4056
+ if (deployment?.targetAppReleaseId !== currentAppReleaseId) {
4057
+ reasons.push('active_app_release_mismatch');
4058
+ }
4059
+ if (!deployment?.evidenceHash) reasons.push('evidence_hash_missing');
4060
+ if (!evidence || typeof evidence !== 'object' || Array.isArray(evidence)) {
4061
+ reasons.push('evidence_summary_missing');
4062
+ } else {
4063
+ if (sha256Canonical(evidence) !== deployment.evidenceHash) {
4064
+ reasons.push('evidence_hash_mismatch');
4065
+ }
4066
+ if (evidence.outcome !== 'passed') reasons.push('evidence_not_passed');
4067
+ if (evidence.deploymentId !== deployment.id) {
4068
+ reasons.push('evidence_deployment_mismatch');
4069
+ }
4070
+ if (evidence.candidateId !== candidate.id) {
4071
+ reasons.push('evidence_candidate_mismatch');
4072
+ }
4073
+ if (evidence.candidateHash !== candidate.candidateHash) {
4074
+ reasons.push('evidence_candidate_hash_mismatch');
4075
+ }
4076
+ if ((evidence.testPlanHash || null) !== (candidate.testPlanHash || null)) {
4077
+ reasons.push('evidence_test_plan_hash_mismatch');
4078
+ }
4079
+ if (evidence.environmentId !== target.environmentId) {
4080
+ reasons.push('evidence_environment_mismatch');
4081
+ }
4082
+ if (evidence.environmentKind !== 'preproduction') {
4083
+ reasons.push('evidence_environment_kind_mismatch');
4084
+ }
4085
+ if (evidence.appType !== target.appType) {
4086
+ reasons.push('evidence_app_type_mismatch');
4087
+ }
4088
+ if (evidence.appReleaseId !== deployment.targetAppReleaseId) {
4089
+ reasons.push('evidence_app_release_mismatch');
4090
+ }
4091
+ const validUntil = Date.parse(evidence.validUntil || '');
4092
+ if (!Number.isFinite(validUntil) || validUntil <= now) {
4093
+ reasons.push('evidence_expired');
4094
+ }
4095
+ }
4096
+ return { accepted: reasons.length === 0, reasons };
4097
+ }
4098
+
4099
+ function selectSucceededPreproductionDeployment(
4100
+ deployments,
4101
+ context,
4102
+ now = Date.now()
4103
+ ) {
4104
+ const ordered = [...(deployments || [])].sort((left, right) => {
4105
+ const rightCreated =
4106
+ Date.parse(right?.createdAt || right?.finishedAt || '') || 0;
4107
+ const leftCreated =
4108
+ Date.parse(left?.createdAt || left?.finishedAt || '') || 0;
4109
+ return rightCreated - leftCreated;
4110
+ });
4111
+ for (const deployment of ordered) {
4112
+ const validation = inspectSucceededPreproductionDeployment(
4113
+ deployment,
4114
+ context,
4115
+ now
4116
+ );
4117
+ if (validation.accepted) return deployment;
4118
+ }
4119
+ return null;
4120
+ }
4121
+
4122
+ function managedPreproductionEvidenceLineage(
4123
+ deployment,
4124
+ candidate,
4125
+ target
4126
+ ) {
4127
+ return {
4128
+ deploymentId: deployment.id,
4129
+ candidateId: candidate.id,
4130
+ candidateHash: candidate.candidateHash,
4131
+ testPlanHash: candidate.testPlanHash || null,
4132
+ environmentId: target.environmentId,
4133
+ environmentKind: target.environmentKind,
4134
+ appType: target.appType,
4135
+ appReleaseId: deployment.targetAppReleaseId,
4136
+ evidenceHash: deployment.evidenceHash || null,
4137
+ };
4138
+ }
4139
+
4140
+ function recoveredPreproductionShipPatch(
4141
+ resolution,
4142
+ candidate,
4143
+ target,
4144
+ recoveredAt = new Date().toISOString()
4145
+ ) {
4146
+ const deployment = resolution.deployment;
4147
+ return {
4148
+ status: 'awaiting_production_confirmation',
4149
+ preproductionDeploymentId: deployment.id,
4150
+ preproductionAppReleaseId: deployment.targetAppReleaseId,
4151
+ preproductionStatus: deployment.status,
4152
+ preproductionEvidenceHash: deployment.evidenceHash,
4153
+ preproductionEvidenceLineage: managedPreproductionEvidenceLineage(
4154
+ deployment,
4155
+ candidate,
4156
+ target
4157
+ ),
4158
+ preproductionRecovery: {
4159
+ status: 'recovered_from_candidate_history',
4160
+ replacedDeploymentId: resolution.replacedDeploymentId,
4161
+ replacedDeploymentStatus: resolution.replacedDeploymentStatus,
4162
+ selectedDeploymentId: deployment.id,
4163
+ recoveredAt,
4164
+ },
4165
+ failure: null,
4166
+ };
4167
+ }
4168
+
4169
+ async function resolvePreproductionDeploymentForAcceptance(
4170
+ config,
4171
+ target,
4172
+ logicalAppCode,
4173
+ originalDeploymentId,
4174
+ candidate
4175
+ ) {
4176
+ await assertRemoteCandidateMatches(
4177
+ config,
4178
+ target,
4179
+ logicalAppCode,
4180
+ candidate
4181
+ );
4182
+ const status = await requestWithAuth(
4183
+ config,
4184
+ target.profileName,
4185
+ environmentSetApiPath(logicalAppCode, '/status')
4186
+ );
4187
+ const targetStatus = (status?.environments || []).find(
4188
+ environment =>
4189
+ environment?.id === target.environmentId &&
4190
+ environment?.kind === 'preproduction' &&
4191
+ environment?.appType === target.appType
4192
+ );
4193
+ const currentAppReleaseId = String(
4194
+ targetStatus?.heads?.appReleaseId || ''
4195
+ ).trim();
4196
+ if (!targetStatus || !currentAppReleaseId) {
4197
+ fail(
4198
+ `PREPRODUCTION_ACTIVE_HEAD_REQUIRED: 预发环境 ${target.environmentId} 缺少可验证的当前 AppRelease head`
4199
+ );
4200
+ }
4201
+
4202
+ let originalDeployment = null;
4203
+ let originalReadError = null;
4204
+ try {
4205
+ originalDeployment = await readManagedDeployment(
4206
+ config,
4207
+ target,
4208
+ logicalAppCode,
4209
+ originalDeploymentId,
4210
+ { full: true }
4211
+ );
4212
+ } catch (error) {
4213
+ originalReadError = error;
4214
+ }
4215
+ const context = { candidate, target, currentAppReleaseId };
4216
+ if (originalDeployment?.status === 'deployed') {
4217
+ const identityMatches =
4218
+ originalDeployment.kind === 'deploy' &&
4219
+ originalDeployment.candidateId === candidate.id &&
4220
+ originalDeployment.targetEnvironmentId === target.environmentId &&
4221
+ originalDeployment.targetAppReleaseId === currentAppReleaseId;
4222
+ if (identityMatches) {
4223
+ return {
4224
+ deployment: originalDeployment,
4225
+ recovered: false,
4226
+ currentAppReleaseId,
4227
+ };
4228
+ }
4229
+ }
4230
+ if (originalDeployment?.status === 'succeeded') {
4231
+ const validation = inspectSucceededPreproductionDeployment(
4232
+ originalDeployment,
4233
+ context
4234
+ );
4235
+ if (validation.accepted) {
4236
+ return {
4237
+ deployment: originalDeployment,
4238
+ recovered: false,
4239
+ currentAppReleaseId,
4240
+ };
4241
+ }
4242
+ }
4243
+
4244
+ const history = await requestWithAuth(
4245
+ config,
4246
+ target.profileName,
4247
+ `${applicationDeploymentApiPath(logicalAppCode)}?limit=200`
4248
+ );
4249
+ const summaries = (Array.isArray(history) ? history : [])
4250
+ .filter(
4251
+ deployment =>
4252
+ deployment?.id !== originalDeploymentId &&
4253
+ deployment?.status === 'succeeded' &&
4254
+ deployment?.kind === 'deploy' &&
4255
+ deployment?.candidateId === candidate.id &&
4256
+ deployment?.targetEnvironmentId === target.environmentId &&
4257
+ deployment?.targetAppReleaseId === currentAppReleaseId
4258
+ )
4259
+ .sort((left, right) => {
4260
+ const rightCreated = Date.parse(right?.createdAt || '') || 0;
4261
+ const leftCreated = Date.parse(left?.createdAt || '') || 0;
4262
+ return rightCreated - leftCreated;
4263
+ });
4264
+ const details = [];
4265
+ for (const summary of summaries) {
4266
+ const detail = await requestWithAuth(
4267
+ config,
4268
+ target.profileName,
4269
+ `${applicationDeploymentApiPath(
4270
+ logicalAppCode,
4271
+ summary.id
4272
+ )}?full=true`
4273
+ );
4274
+ details.push(detail);
4275
+ }
4276
+ const replacement = selectSucceededPreproductionDeployment(
4277
+ details,
4278
+ context
4279
+ );
4280
+ if (!replacement) {
4281
+ const originalStatus = originalDeployment?.status || 'unreadable';
4282
+ const error = new Error(
4283
+ `PREPRODUCTION_NOT_READY_FOR_ACCEPTANCE: 预发 deployment ${originalDeploymentId} 当前状态为 ${originalStatus},且未找到同 candidate、同环境、当前 AppRelease 和有效测试证据完全一致的 succeeded replacement`
4284
+ );
4285
+ error.code = 'PREPRODUCTION_NOT_READY_FOR_ACCEPTANCE';
4286
+ error.data = {
4287
+ originalDeploymentId,
4288
+ originalStatus,
4289
+ originalReadErrorCode: originalReadError?.code || null,
4290
+ candidateId: candidate.id,
4291
+ targetEnvironmentId: target.environmentId,
4292
+ currentAppReleaseId,
4293
+ };
4294
+ throw error;
4295
+ }
4296
+ rememberTargetDeployment(target, replacement);
4297
+ return {
4298
+ deployment: replacement,
4299
+ recovered: true,
4300
+ replacedDeploymentId: originalDeploymentId,
4301
+ replacedDeploymentStatus: originalDeployment?.status || 'unreadable',
4302
+ currentAppReleaseId,
4303
+ };
4304
+ }
4305
+
3993
4306
  async function completeManagedDeployment(
3994
4307
  config,
3995
4308
  target,
@@ -4004,11 +4317,40 @@ async function completeManagedDeployment(
4004
4317
  evidence: deployment.evidenceSummary || evidenceInput || null,
4005
4318
  };
4006
4319
  }
4007
- if (deployment.status !== 'deployed') {
4320
+ if (!['deployed', 'failed'].includes(deployment.status)) {
4008
4321
  fail(
4009
4322
  `DEPLOYMENT_NOT_READY_FOR_COMPLETION: deployment ${deployment.id} 当前状态为 ${deployment.status}`
4010
4323
  );
4011
4324
  }
4325
+ const reconciled = await requestWithAuth(
4326
+ config,
4327
+ target.profileName,
4328
+ applicationDeploymentApiPath(
4329
+ logicalAppCode,
4330
+ deployment.id,
4331
+ 'reconcile'
4332
+ ),
4333
+ {
4334
+ method: 'POST',
4335
+ body: {
4336
+ reason: 'bind final active AppRelease lineage before test evidence',
4337
+ },
4338
+ }
4339
+ );
4340
+ if (reconciled.status === 'succeeded') {
4341
+ rememberTargetDeployment(target, reconciled);
4342
+ return {
4343
+ deployment: reconciled,
4344
+ evidence: reconciled.evidenceSummary || evidenceInput || null,
4345
+ };
4346
+ }
4347
+ if (reconciled.status !== 'deployed') {
4348
+ fail(
4349
+ `DEPLOYMENT_RECONCILIATION_NOT_CONFIRMED: deployment ${deployment.id} 恢复后状态为 ${reconciled.status}`
4350
+ );
4351
+ }
4352
+ deployment = reconciled;
4353
+ rememberTargetDeployment(target, deployment);
4012
4354
  const evidence = buildBoundDeploymentEvidence(
4013
4355
  evidenceInput,
4014
4356
  target,
@@ -4273,21 +4615,25 @@ async function runApplicationShipCommand(
4273
4615
  );
4274
4616
  const candidate = readCandidate(existingShip.candidateId);
4275
4617
  assertLocalCandidate(candidate);
4276
- let preproductionDeployment = await readManagedDeployment(
4618
+ const preproductionResolution = await resolvePreproductionDeploymentForAcceptance(
4277
4619
  config,
4278
4620
  preproduction,
4279
4621
  logicalApp.code,
4280
4622
  existingShip.preproductionDeploymentId,
4281
- { full: true }
4623
+ candidate
4282
4624
  );
4283
- if (
4284
- !['deployed', 'succeeded'].includes(
4285
- preproductionDeployment.status
4286
- ) ||
4287
- !preproductionDeployment.targetAppReleaseId
4288
- ) {
4289
- fail(
4290
- `PREPRODUCTION_NOT_READY_FOR_ACCEPTANCE: 预发 deployment ${preproductionDeployment.id} 当前状态为 ${preproductionDeployment.status}`
4625
+ let preproductionDeployment = preproductionResolution.deployment;
4626
+ if (preproductionResolution.recovered) {
4627
+ writeManagedShip(
4628
+ changeId,
4629
+ recoveredPreproductionShipPatch(
4630
+ preproductionResolution,
4631
+ candidate,
4632
+ preproduction
4633
+ )
4634
+ );
4635
+ warn(
4636
+ `预发验收已从同 candidate 的成功部署恢复: ${preproductionResolution.replacedDeploymentId} -> ${preproductionDeployment.id}`
4291
4637
  );
4292
4638
  }
4293
4639
 
@@ -4360,6 +4706,15 @@ async function runApplicationShipCommand(
4360
4706
  const promotionStartedAt = new Date().toISOString();
4361
4707
  writeManagedShip(changeId, {
4362
4708
  status: 'confirmed_for_production',
4709
+ preproductionDeploymentId: preproductionDeployment.id,
4710
+ preproductionAppReleaseId: preproductionDeployment.targetAppReleaseId,
4711
+ preproductionStatus: preproductionDeployment.status,
4712
+ preproductionEvidenceHash: preproductionDeployment.evidenceHash,
4713
+ preproductionEvidenceLineage: managedPreproductionEvidenceLineage(
4714
+ preproductionDeployment,
4715
+ candidate,
4716
+ preproduction
4717
+ ),
4363
4718
  productionConfirmation: {
4364
4719
  status: 'confirmed',
4365
4720
  confirmedAt,
@@ -4627,26 +4982,14 @@ async function runApplicationEnvironmentReleaseCommand(
4627
4982
  const candidate = deployment.candidateId
4628
4983
  ? readCandidate(deployment.candidateId)
4629
4984
  : null;
4630
- const evidence = buildBoundDeploymentEvidence(
4631
- readJsonInput(flags['evidence-json'], 'evidence-json'),
4985
+ return await completeManagedDeployment(
4986
+ config,
4632
4987
  target,
4988
+ logicalApp.code,
4633
4989
  deployment,
4634
- candidate
4635
- );
4636
- const completed = await requestWithAuth(
4637
- config,
4638
- target.profileName,
4639
- applicationDeploymentApiPath(logicalApp.code, deploymentId, 'complete'),
4640
- {
4641
- method: 'POST',
4642
- body: {
4643
- evidenceSummary: evidence,
4644
- evidenceHash: sha256Canonical(evidence),
4645
- },
4646
- }
4990
+ candidate,
4991
+ readJsonInput(flags['evidence-json'], 'evidence-json')
4647
4992
  );
4648
- rememberTargetDeployment(target, completed);
4649
- return { deployment: completed, evidence };
4650
4993
  }
4651
4994
 
4652
4995
  if (subcommand === 'rollback') {
@@ -30089,8 +30432,13 @@ function buildWorkspacePublishEnv(
30089
30432
  module.exports = {
30090
30433
  buildResourceManifestSddTargets,
30091
30434
  buildScopeGrantSourceSyncDecision,
30435
+ deploymentEvidencePendingError,
30436
+ deploymentQueueWaitDecision,
30437
+ inspectSucceededPreproductionDeployment,
30092
30438
  main,
30439
+ recoveredPreproductionShipPatch,
30093
30440
  resolveReleaseCommandScopedFiles,
30441
+ selectSucceededPreproductionDeployment,
30094
30442
  resolveDeliveryV2BackendManifestReplacementIntent,
30095
30443
  resolveDataViewSourceFormCodes,
30096
30444
  satisfiedFormCodesFromContext,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.241",
3
+ "version": "1.0.243",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {