openxiangda 1.0.242 → 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 +301 -10
  2. package/package.json +1 -1
package/lib/cli.js CHANGED
@@ -4028,6 +4028,281 @@ async function readManagedDeployment(
4028
4028
  return deployment;
4029
4029
  }
4030
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
+
4031
4306
  async function completeManagedDeployment(
4032
4307
  config,
4033
4308
  target,
@@ -4340,21 +4615,25 @@ async function runApplicationShipCommand(
4340
4615
  );
4341
4616
  const candidate = readCandidate(existingShip.candidateId);
4342
4617
  assertLocalCandidate(candidate);
4343
- let preproductionDeployment = await readManagedDeployment(
4618
+ const preproductionResolution = await resolvePreproductionDeploymentForAcceptance(
4344
4619
  config,
4345
4620
  preproduction,
4346
4621
  logicalApp.code,
4347
4622
  existingShip.preproductionDeploymentId,
4348
- { full: true }
4623
+ candidate
4349
4624
  );
4350
- if (
4351
- !['deployed', 'succeeded'].includes(
4352
- preproductionDeployment.status
4353
- ) ||
4354
- !preproductionDeployment.targetAppReleaseId
4355
- ) {
4356
- fail(
4357
- `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}`
4358
4637
  );
4359
4638
  }
4360
4639
 
@@ -4427,6 +4706,15 @@ async function runApplicationShipCommand(
4427
4706
  const promotionStartedAt = new Date().toISOString();
4428
4707
  writeManagedShip(changeId, {
4429
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
+ ),
4430
4718
  productionConfirmation: {
4431
4719
  status: 'confirmed',
4432
4720
  confirmedAt,
@@ -30146,8 +30434,11 @@ module.exports = {
30146
30434
  buildScopeGrantSourceSyncDecision,
30147
30435
  deploymentEvidencePendingError,
30148
30436
  deploymentQueueWaitDecision,
30437
+ inspectSucceededPreproductionDeployment,
30149
30438
  main,
30439
+ recoveredPreproductionShipPatch,
30150
30440
  resolveReleaseCommandScopedFiles,
30441
+ selectSucceededPreproductionDeployment,
30151
30442
  resolveDeliveryV2BackendManifestReplacementIntent,
30152
30443
  resolveDataViewSourceFormCodes,
30153
30444
  satisfiedFormCodesFromContext,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.242",
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": {