openxiangda 1.0.251 → 1.0.253
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 +18 -1
- package/lib/cli.js +231 -15
- package/lib/delivery-v2-cli.js +89 -6
- package/lib/delivery-v2-executor.js +450 -81
- package/lib/delivery-v2-package.js +445 -12
- package/lib/js-code-build.js +5 -3
- package/lib/policy.js +19 -0
- package/lib/release-plan.js +10 -3
- package/openxiangda-skills/SKILL.md +14 -2
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +12 -0
- package/package.json +2 -2
- package/policy/openxiangda-policy.json +10 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +1 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +1 -0
- package/templates/openxiangda-react-spa/AGENTS.md +2 -2
- package/templates/openxiangda-react-spa/DELIVERY.md +12 -2
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +2 -1
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +2 -1
- package/templates/sy-lowcode-app-workspace/AGENTS.md +2 -2
- package/templates/sy-lowcode-app-workspace/DELIVERY.md +12 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- OpenXiangda-Policy-Version:
|
|
1
|
+
<!-- OpenXiangda-Policy-Version: 7 -->
|
|
2
2
|
# OpenXiangda
|
|
3
3
|
|
|
4
4
|
OpenXiangda is a lightweight CLI and skill package for private low-code platforms.
|
|
@@ -28,6 +28,23 @@ checkpoints live on the platform, so a CLI/AI interruption does not discard
|
|
|
28
28
|
completed work. V2 does not require SDD, a clean/pushed Git mainline,
|
|
29
29
|
`--change`, `--only`, or candidate/ship orchestration.
|
|
30
30
|
|
|
31
|
+
`check --json` reports the exact changed resources, dependency-only Form
|
|
32
|
+
bindings, and ordered execution plan. Authored Form/Backend/Workflow code is
|
|
33
|
+
built only with the CLI-sealed toolchain; a V2 package never links the caller's
|
|
34
|
+
`node_modules`, and undeclared third-party build dependencies fail before any
|
|
35
|
+
remote write. Resource removals also fail closed until a dedicated migration
|
|
36
|
+
path is used.
|
|
37
|
+
|
|
38
|
+
`status` and `retry` locate the run across preproduction and production when
|
|
39
|
+
`--environment` is omitted. Retry resumes only unfinished checkpoints, safely
|
|
40
|
+
replays local Form bindings on another machine, and uses a server-side attempt
|
|
41
|
+
fence so an older executor cannot continue writing after takeover. Runtime
|
|
42
|
+
build IDs include both the Runtime layer and sealed package digests, so a newly
|
|
43
|
+
sealed package cannot collide with an older package that happens to contain the
|
|
44
|
+
same Runtime bytes. An exact same-package retry may reuse an `uploaded` release
|
|
45
|
+
only after verifying its content hash, source revision, and Runtime parent;
|
|
46
|
+
immutable storage objects are never overwritten.
|
|
47
|
+
|
|
31
48
|
The lower-level commands later in this README remain available for V1
|
|
32
49
|
compatibility and diagnostics; do not use them to assemble a normal V2 release.
|
|
33
50
|
|
package/lib/cli.js
CHANGED
|
@@ -10656,6 +10656,7 @@ async function form(args) {
|
|
|
10656
10656
|
fail('用法: openxiangda form ensure --only <formCode1,formCode2>');
|
|
10657
10657
|
}
|
|
10658
10658
|
const target = getWorkspaceTarget(config, profileName, flags);
|
|
10659
|
+
const replayLocal = Boolean(flags['replay-local']);
|
|
10659
10660
|
await ensureDirectMutationPublishContext(
|
|
10660
10661
|
config,
|
|
10661
10662
|
target,
|
|
@@ -10669,22 +10670,30 @@ async function form(args) {
|
|
|
10669
10670
|
target.bound.resources?.formSettings?.[formCode],
|
|
10670
10671
|
].find(entry => String(entry?.formUuid || '').trim());
|
|
10671
10672
|
if (existing?.formUuid) {
|
|
10673
|
+
if (replayLocal) {
|
|
10674
|
+
try {
|
|
10675
|
+
await requestWithAuth(
|
|
10676
|
+
config,
|
|
10677
|
+
target.profileName,
|
|
10678
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(existing.formUuid)}`
|
|
10679
|
+
);
|
|
10680
|
+
} catch (error) {
|
|
10681
|
+
if (!isHttpNotFound(error)) throw error;
|
|
10682
|
+
const missing = new Error(
|
|
10683
|
+
`FORM_ENSURE_REPLAY_TARGET_MISSING: ${formCode} 的已绑定表单 ${existing.formUuid} 在目标环境不存在,不能以 replay-local 补写远端资源`
|
|
10684
|
+
);
|
|
10685
|
+
missing.code = 'FORM_ENSURE_REPLAY_TARGET_MISSING';
|
|
10686
|
+
missing.retryable = false;
|
|
10687
|
+
throw missing;
|
|
10688
|
+
}
|
|
10689
|
+
}
|
|
10672
10690
|
results.push({
|
|
10673
10691
|
code: formCode,
|
|
10674
10692
|
formUuid: existing.formUuid,
|
|
10675
|
-
action: 'noop',
|
|
10693
|
+
action: replayLocal ? 'verify-local' : 'noop',
|
|
10676
10694
|
});
|
|
10677
10695
|
continue;
|
|
10678
10696
|
}
|
|
10679
|
-
const localSchema = await loadLocalFormSchema(
|
|
10680
|
-
process.cwd(),
|
|
10681
|
-
formCode
|
|
10682
|
-
);
|
|
10683
|
-
if (!localSchema) {
|
|
10684
|
-
fail(
|
|
10685
|
-
`FORM_ENSURE_SCHEMA_REQUIRED: ${formCode} 缺少 src/forms/${formCode}/schema.ts`
|
|
10686
|
-
);
|
|
10687
|
-
}
|
|
10688
10697
|
const formUuid = managedFormUuidForCode(
|
|
10689
10698
|
target.appType,
|
|
10690
10699
|
formCode
|
|
@@ -10699,6 +10708,22 @@ async function form(args) {
|
|
|
10699
10708
|
} catch (error) {
|
|
10700
10709
|
if (!isHttpNotFound(error)) throw error;
|
|
10701
10710
|
}
|
|
10711
|
+
if (!existingRemote && replayLocal) {
|
|
10712
|
+
const missing = new Error(
|
|
10713
|
+
`FORM_ENSURE_REPLAY_TARGET_MISSING: ${formCode} 对应的确定性表单 ${formUuid} 在目标环境不存在;replay-local 禁止创建远端资源`
|
|
10714
|
+
);
|
|
10715
|
+
missing.code = 'FORM_ENSURE_REPLAY_TARGET_MISSING';
|
|
10716
|
+
missing.retryable = false;
|
|
10717
|
+
throw missing;
|
|
10718
|
+
}
|
|
10719
|
+
const localSchema = existingRemote
|
|
10720
|
+
? null
|
|
10721
|
+
: await loadLocalFormSchema(process.cwd(), formCode);
|
|
10722
|
+
if (!existingRemote && !localSchema) {
|
|
10723
|
+
fail(
|
|
10724
|
+
`FORM_ENSURE_SCHEMA_REQUIRED: ${formCode} 缺少 src/forms/${formCode}/schema.ts`
|
|
10725
|
+
);
|
|
10726
|
+
}
|
|
10702
10727
|
if (!existingRemote) {
|
|
10703
10728
|
await requestWithAuth(
|
|
10704
10729
|
config,
|
|
@@ -10718,12 +10743,16 @@ async function form(args) {
|
|
|
10718
10743
|
);
|
|
10719
10744
|
}
|
|
10720
10745
|
saveFormResource(target, formCode, formUuid, {
|
|
10721
|
-
name: localSchema
|
|
10746
|
+
name: localSchema?.name || existingRemote?.name || formCode,
|
|
10722
10747
|
});
|
|
10723
10748
|
results.push({
|
|
10724
10749
|
code: formCode,
|
|
10725
10750
|
formUuid,
|
|
10726
|
-
action:
|
|
10751
|
+
action: replayLocal
|
|
10752
|
+
? 'rebind-local'
|
|
10753
|
+
: existingRemote
|
|
10754
|
+
? 'bind'
|
|
10755
|
+
: 'create',
|
|
10727
10756
|
});
|
|
10728
10757
|
}
|
|
10729
10758
|
const result = {
|
|
@@ -10734,6 +10763,9 @@ async function form(args) {
|
|
|
10734
10763
|
create: results.filter(item => item.action === 'create').length,
|
|
10735
10764
|
bind: results.filter(item => item.action === 'bind').length,
|
|
10736
10765
|
noop: results.filter(item => item.action === 'noop').length,
|
|
10766
|
+
replayed: results.filter(item =>
|
|
10767
|
+
['verify-local', 'rebind-local'].includes(item.action)
|
|
10768
|
+
).length,
|
|
10737
10769
|
},
|
|
10738
10770
|
};
|
|
10739
10771
|
if (flags.json) return writeJson(result);
|
|
@@ -15642,7 +15674,13 @@ async function runtime(args) {
|
|
|
15642
15674
|
const files = collectRuntimeDistFiles(distDir, {
|
|
15643
15675
|
includeSourceMaps: Boolean(flags['include-sourcemaps']),
|
|
15644
15676
|
});
|
|
15677
|
+
const declaredReleaseFiles = runtimeReleaseFileDescriptors(files);
|
|
15645
15678
|
const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
15679
|
+
const reusableRuntimeRelease = await telemetry.runPhase(
|
|
15680
|
+
'reuse-check',
|
|
15681
|
+
async () =>
|
|
15682
|
+
findRuntimeReleaseByBuildId(config, target, buildId)
|
|
15683
|
+
);
|
|
15646
15684
|
printRuntimeProgress(
|
|
15647
15685
|
`runtime release upload: mode=${effectiveUploadMode} traceId=${traceId} files=${files.length} size=${formatBytes(totalBytes)} timeout=${uploadTimeoutMs}ms`
|
|
15648
15686
|
);
|
|
@@ -15650,6 +15688,20 @@ async function runtime(args) {
|
|
|
15650
15688
|
'upload',
|
|
15651
15689
|
{ fileCount: files.length, totalBytes, mode: effectiveUploadMode },
|
|
15652
15690
|
async () => {
|
|
15691
|
+
if (reusableRuntimeRelease) {
|
|
15692
|
+
assertRuntimeReleaseReusable(reusableRuntimeRelease, {
|
|
15693
|
+
appType: target.appType,
|
|
15694
|
+
buildId,
|
|
15695
|
+
files: declaredReleaseFiles,
|
|
15696
|
+
sourceRevision: runtimeLineage.sourceRevision,
|
|
15697
|
+
parentReleaseId: runtimeLineage.parentReleaseId,
|
|
15698
|
+
});
|
|
15699
|
+
effectiveUploadMode = 'reuse-existing';
|
|
15700
|
+
printRuntimeProgress(
|
|
15701
|
+
`runtime immutable release reused: releaseId=${reusableRuntimeRelease.id} buildId=${buildId} traceId=${traceId}`
|
|
15702
|
+
);
|
|
15703
|
+
return declaredReleaseFiles;
|
|
15704
|
+
}
|
|
15653
15705
|
if (uploadMode === 'legacy-json') {
|
|
15654
15706
|
effectiveUploadMode = 'legacy-json';
|
|
15655
15707
|
return files.map(file => ({
|
|
@@ -15682,6 +15734,27 @@ async function runtime(args) {
|
|
|
15682
15734
|
timeoutMs: uploadTimeoutMs,
|
|
15683
15735
|
});
|
|
15684
15736
|
} catch (error) {
|
|
15737
|
+
if (isRuntimeBuildImmutableConflict(error)) {
|
|
15738
|
+
const racedRuntimeRelease = await findRuntimeReleaseByBuildId(
|
|
15739
|
+
config,
|
|
15740
|
+
target,
|
|
15741
|
+
buildId
|
|
15742
|
+
);
|
|
15743
|
+
if (racedRuntimeRelease) {
|
|
15744
|
+
assertRuntimeReleaseReusable(racedRuntimeRelease, {
|
|
15745
|
+
appType: target.appType,
|
|
15746
|
+
buildId,
|
|
15747
|
+
files: declaredReleaseFiles,
|
|
15748
|
+
sourceRevision: runtimeLineage.sourceRevision,
|
|
15749
|
+
parentReleaseId: runtimeLineage.parentReleaseId,
|
|
15750
|
+
});
|
|
15751
|
+
effectiveUploadMode = 'reuse-existing';
|
|
15752
|
+
printRuntimeProgress(
|
|
15753
|
+
`runtime immutable release won upload race and was reused: releaseId=${racedRuntimeRelease.id} buildId=${buildId} traceId=${traceId}`
|
|
15754
|
+
);
|
|
15755
|
+
return declaredReleaseFiles;
|
|
15756
|
+
}
|
|
15757
|
+
}
|
|
15685
15758
|
if (uploadMode !== 'auto' || !isRuntimeStagedUploadBlocked(error)) {
|
|
15686
15759
|
throw error;
|
|
15687
15760
|
}
|
|
@@ -16044,6 +16117,134 @@ function collectRuntimeDistFiles(distDir, options = {}) {
|
|
|
16044
16117
|
return files;
|
|
16045
16118
|
}
|
|
16046
16119
|
|
|
16120
|
+
function runtimeReleaseFileDescriptors(files) {
|
|
16121
|
+
return (files || []).map(file => ({
|
|
16122
|
+
path: file.path,
|
|
16123
|
+
size: file.size,
|
|
16124
|
+
sha256: file.sha256,
|
|
16125
|
+
contentType: file.contentType,
|
|
16126
|
+
}));
|
|
16127
|
+
}
|
|
16128
|
+
|
|
16129
|
+
function calculateRuntimeReleaseContentHash(files) {
|
|
16130
|
+
const hash = crypto.createHash('sha256');
|
|
16131
|
+
for (const file of files || []) {
|
|
16132
|
+
const filePath = String(file?.path || '').trim();
|
|
16133
|
+
const fileSha256 = String(file?.sha256 || '').trim().toLowerCase();
|
|
16134
|
+
if (!filePath || !/^[a-f0-9]{64}$/.test(fileSha256)) {
|
|
16135
|
+
throw runtimeReleaseReuseError(
|
|
16136
|
+
'RUNTIME_RELEASE_REUSE_LOCAL_MANIFEST_INVALID',
|
|
16137
|
+
`本地 Runtime 文件描述不完整: ${filePath || '(unknown)'}`,
|
|
16138
|
+
{ filePath: filePath || null }
|
|
16139
|
+
);
|
|
16140
|
+
}
|
|
16141
|
+
hash.update(filePath);
|
|
16142
|
+
hash.update(fileSha256);
|
|
16143
|
+
}
|
|
16144
|
+
return hash.digest('hex');
|
|
16145
|
+
}
|
|
16146
|
+
|
|
16147
|
+
async function findRuntimeReleaseByBuildId(config, target, buildId) {
|
|
16148
|
+
const appPath = `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/runtime/releases`;
|
|
16149
|
+
try {
|
|
16150
|
+
const exact = await requestWithAuth(
|
|
16151
|
+
config,
|
|
16152
|
+
target.profileName,
|
|
16153
|
+
`${appPath}/by-build/${encodeURIComponent(buildId)}`
|
|
16154
|
+
);
|
|
16155
|
+
return exact?.id ? exact : null;
|
|
16156
|
+
} catch (error) {
|
|
16157
|
+
if (Number(error?.status) !== 404) throw error;
|
|
16158
|
+
}
|
|
16159
|
+
const releases = await requestWithAuth(
|
|
16160
|
+
config,
|
|
16161
|
+
target.profileName,
|
|
16162
|
+
appPath
|
|
16163
|
+
);
|
|
16164
|
+
const items = Array.isArray(releases)
|
|
16165
|
+
? releases
|
|
16166
|
+
: Array.isArray(releases?.items)
|
|
16167
|
+
? releases.items
|
|
16168
|
+
: [];
|
|
16169
|
+
return (
|
|
16170
|
+
items.find(item => String(item?.buildId || '') === String(buildId)) ||
|
|
16171
|
+
null
|
|
16172
|
+
);
|
|
16173
|
+
}
|
|
16174
|
+
|
|
16175
|
+
function assertRuntimeReleaseReusable(existing, expected) {
|
|
16176
|
+
const context = {
|
|
16177
|
+
appType: expected.appType,
|
|
16178
|
+
buildId: expected.buildId,
|
|
16179
|
+
existingReleaseId: existing?.id || null,
|
|
16180
|
+
};
|
|
16181
|
+
if (String(existing?.status || '') !== 'uploaded') {
|
|
16182
|
+
throw runtimeReleaseReuseError(
|
|
16183
|
+
'RUNTIME_RELEASE_REUSE_STATUS_INVALID',
|
|
16184
|
+
`相同 buildId 的 Runtime Release 状态为 ${existing?.status || '(unknown)'},仅 uploaded 状态可作为失败发布的暂存恢复点`,
|
|
16185
|
+
{ ...context, existingStatus: existing?.status || null }
|
|
16186
|
+
);
|
|
16187
|
+
}
|
|
16188
|
+
const expectedContentHash = calculateRuntimeReleaseContentHash(
|
|
16189
|
+
expected.files
|
|
16190
|
+
);
|
|
16191
|
+
if (
|
|
16192
|
+
String(existing?.contentHash || '').trim().toLowerCase() !==
|
|
16193
|
+
expectedContentHash
|
|
16194
|
+
) {
|
|
16195
|
+
throw runtimeReleaseReuseError(
|
|
16196
|
+
'RUNTIME_RELEASE_REUSE_CONTENT_MISMATCH',
|
|
16197
|
+
'相同 buildId 的历史 Runtime Release 内容与当前密封包不一致,拒绝复用',
|
|
16198
|
+
{
|
|
16199
|
+
...context,
|
|
16200
|
+
expectedContentHash,
|
|
16201
|
+
existingContentHash: existing?.contentHash || null,
|
|
16202
|
+
}
|
|
16203
|
+
);
|
|
16204
|
+
}
|
|
16205
|
+
if (
|
|
16206
|
+
!runtimeSourceRevisionEquals(
|
|
16207
|
+
existing?.sourceRevision,
|
|
16208
|
+
expected.sourceRevision
|
|
16209
|
+
)
|
|
16210
|
+
) {
|
|
16211
|
+
throw runtimeReleaseReuseError(
|
|
16212
|
+
'RUNTIME_RELEASE_REUSE_SOURCE_MISMATCH',
|
|
16213
|
+
'相同 buildId 的历史 Runtime Release 源码血缘与当前密封包不一致,拒绝复用',
|
|
16214
|
+
context
|
|
16215
|
+
);
|
|
16216
|
+
}
|
|
16217
|
+
const existingParentReleaseId =
|
|
16218
|
+
String(existing?.parentReleaseId || '').trim() || null;
|
|
16219
|
+
const expectedParentReleaseId =
|
|
16220
|
+
String(expected.parentReleaseId || '').trim() || null;
|
|
16221
|
+
if (existingParentReleaseId !== expectedParentReleaseId) {
|
|
16222
|
+
throw runtimeReleaseReuseError(
|
|
16223
|
+
'RUNTIME_RELEASE_REUSE_PARENT_MISMATCH',
|
|
16224
|
+
'相同 buildId 的历史 Runtime Release 父版本已与当前 Runtime head 不一致,拒绝复用',
|
|
16225
|
+
{
|
|
16226
|
+
...context,
|
|
16227
|
+
existingParentReleaseId,
|
|
16228
|
+
expectedParentReleaseId,
|
|
16229
|
+
}
|
|
16230
|
+
);
|
|
16231
|
+
}
|
|
16232
|
+
return existing;
|
|
16233
|
+
}
|
|
16234
|
+
|
|
16235
|
+
function runtimeSourceRevisionEquals(left, right) {
|
|
16236
|
+
return ['repo', 'baseCommit', 'treeHash'].every(
|
|
16237
|
+
key => String(left?.[key] || '').trim() === String(right?.[key] || '').trim()
|
|
16238
|
+
);
|
|
16239
|
+
}
|
|
16240
|
+
|
|
16241
|
+
function runtimeReleaseReuseError(code, message, data = {}) {
|
|
16242
|
+
const error = new Error(`${code}: ${message}`);
|
|
16243
|
+
error.code = code;
|
|
16244
|
+
error.data = data;
|
|
16245
|
+
return error;
|
|
16246
|
+
}
|
|
16247
|
+
|
|
16047
16248
|
function walkRuntimeDist(rootDir, currentDir, files, options) {
|
|
16048
16249
|
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
16049
16250
|
for (const entry of entries) {
|
|
@@ -16279,6 +16480,16 @@ function isRuntimeStagedUploadBlocked(error) {
|
|
|
16279
16480
|
return /^HTTP 403\b/.test(String(error?.message || ''));
|
|
16280
16481
|
}
|
|
16281
16482
|
|
|
16483
|
+
function isRuntimeBuildImmutableConflict(error) {
|
|
16484
|
+
return (
|
|
16485
|
+
Number(error?.status) === 409 &&
|
|
16486
|
+
(String(error?.code || '') === 'RUNTIME_BUILD_IMMUTABLE' ||
|
|
16487
|
+
/Runtime buildId .*禁止覆盖|RUNTIME_BUILD_IMMUTABLE/.test(
|
|
16488
|
+
String(error?.message || '')
|
|
16489
|
+
))
|
|
16490
|
+
);
|
|
16491
|
+
}
|
|
16492
|
+
|
|
16282
16493
|
function normalizeRuntimeUploadTimeoutMs(value) {
|
|
16283
16494
|
const candidate =
|
|
16284
16495
|
value === undefined || value === null || value === ''
|
|
@@ -20068,7 +20279,8 @@ async function prepareManifestJsCodeBundlesForPlan(manifest) {
|
|
|
20068
20279
|
sourceInfos.map(item => ({
|
|
20069
20280
|
sourceKind: item.sourceKind,
|
|
20070
20281
|
scriptCode: item.scriptCode,
|
|
20071
|
-
}))
|
|
20282
|
+
})),
|
|
20283
|
+
{ forceCanonical: Boolean(activeDeliveryV2Context) }
|
|
20072
20284
|
);
|
|
20073
20285
|
if (result.status !== 0) {
|
|
20074
20286
|
const legacyFallback = result.openxiangdaBuildMode === 'workspace-script'
|
|
@@ -28056,7 +28268,8 @@ function ensureJsCodeBundleForPlan(sourceInfo) {
|
|
|
28056
28268
|
sourceKind: sourceInfo.sourceKind,
|
|
28057
28269
|
scriptCode: sourceInfo.scriptCode,
|
|
28058
28270
|
},
|
|
28059
|
-
]
|
|
28271
|
+
],
|
|
28272
|
+
{ forceCanonical: Boolean(activeDeliveryV2Context) }
|
|
28060
28273
|
);
|
|
28061
28274
|
if (result.stdout) process.stderr.write(result.stdout);
|
|
28062
28275
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
@@ -28399,7 +28612,8 @@ async function resolveJsCodeBundlePath(localPath, scriptCode) {
|
|
|
28399
28612
|
function runWorkspaceJsCodeBuild(workspaceRoot, resolvedScriptCode, sourceKind) {
|
|
28400
28613
|
return runWorkspaceJsCodeBuildBatch(
|
|
28401
28614
|
workspaceRoot,
|
|
28402
|
-
[{ sourceKind, scriptCode: resolvedScriptCode }]
|
|
28615
|
+
[{ sourceKind, scriptCode: resolvedScriptCode }],
|
|
28616
|
+
{ forceCanonical: Boolean(activeDeliveryV2Context) }
|
|
28403
28617
|
);
|
|
28404
28618
|
}
|
|
28405
28619
|
|
|
@@ -30629,12 +30843,14 @@ function buildWorkspacePublishEnv(
|
|
|
30629
30843
|
}
|
|
30630
30844
|
|
|
30631
30845
|
module.exports = {
|
|
30846
|
+
assertRuntimeReleaseReusable,
|
|
30632
30847
|
buildResourceManifestSddTargets,
|
|
30633
30848
|
buildScopeGrantSourceSyncDecision,
|
|
30634
30849
|
deploymentEvidencePendingError,
|
|
30635
30850
|
deploymentQueueWaitDecision,
|
|
30636
30851
|
inspectSucceededPreproductionDeployment,
|
|
30637
30852
|
main,
|
|
30853
|
+
calculateRuntimeReleaseContentHash,
|
|
30638
30854
|
recoveredPreproductionShipPatch,
|
|
30639
30855
|
resolveReleaseCommandScopedFiles,
|
|
30640
30856
|
selectSucceededPreproductionDeployment,
|
package/lib/delivery-v2-cli.js
CHANGED
|
@@ -33,6 +33,7 @@ function createDeliveryV2Cli(dependencies) {
|
|
|
33
33
|
packageDigest: inspected.compiled.packageDigest,
|
|
34
34
|
summary: inspected.compiled.summary,
|
|
35
35
|
targets: inspected.targets,
|
|
36
|
+
plan: inspected.plan,
|
|
36
37
|
previousPackageDigest: inspected.previous?.packageDigest || null,
|
|
37
38
|
},
|
|
38
39
|
`Delivery V2 检查通过: ${inspected.compiled.packageDigest}`
|
|
@@ -62,8 +63,8 @@ function createDeliveryV2Cli(dependencies) {
|
|
|
62
63
|
if (!runId) {
|
|
63
64
|
throw usageError('用法: openxiangda status <runId> [--json]');
|
|
64
65
|
}
|
|
65
|
-
const
|
|
66
|
-
const result =
|
|
66
|
+
const resolved = await resolveRunTarget(config, flags, runId);
|
|
67
|
+
const result = resolved.run;
|
|
67
68
|
return output(
|
|
68
69
|
flags,
|
|
69
70
|
result,
|
|
@@ -75,7 +76,8 @@ function createDeliveryV2Cli(dependencies) {
|
|
|
75
76
|
if (!runId) {
|
|
76
77
|
throw usageError('用法: openxiangda retry <runId> [--json]');
|
|
77
78
|
}
|
|
78
|
-
const
|
|
79
|
+
const resolved = await resolveRunTarget(config, flags, runId);
|
|
80
|
+
const target = resolved.target;
|
|
79
81
|
const result = await executor.retry({
|
|
80
82
|
config,
|
|
81
83
|
target,
|
|
@@ -132,9 +134,83 @@ function createDeliveryV2Cli(dependencies) {
|
|
|
132
134
|
);
|
|
133
135
|
}
|
|
134
136
|
|
|
137
|
+
async function resolveRunTarget(config, flags, runId) {
|
|
138
|
+
const explicitEnvironment =
|
|
139
|
+
stringFlag(flags, 'environment') || stringFlag(flags, 'target');
|
|
140
|
+
if (explicitEnvironment) {
|
|
141
|
+
const target = resolveTarget(
|
|
142
|
+
config,
|
|
143
|
+
{ ...flags, environment: explicitEnvironment },
|
|
144
|
+
[],
|
|
145
|
+
false
|
|
146
|
+
);
|
|
147
|
+
return {
|
|
148
|
+
target,
|
|
149
|
+
run: await executor.status({ config, target, runId }),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const candidates = [];
|
|
154
|
+
const addCandidate = candidate => {
|
|
155
|
+
if (!candidate) return;
|
|
156
|
+
const key = [
|
|
157
|
+
candidate.profileName,
|
|
158
|
+
candidate.appType,
|
|
159
|
+
candidate.environmentId || 'direct',
|
|
160
|
+
].join(':');
|
|
161
|
+
if (candidates.some(item => item.key === key)) return;
|
|
162
|
+
candidates.push({ key, target: candidate });
|
|
163
|
+
};
|
|
164
|
+
try {
|
|
165
|
+
addCandidate(resolveTarget(config, flags, [], false));
|
|
166
|
+
} catch {
|
|
167
|
+
// A multi-environment workspace may require an explicit target; probe
|
|
168
|
+
// the canonical targets below before returning a not-found error.
|
|
169
|
+
}
|
|
170
|
+
for (const environment of ['preproduction', 'production']) {
|
|
171
|
+
try {
|
|
172
|
+
addCandidate(
|
|
173
|
+
deps.getWorkspaceTarget(
|
|
174
|
+
config,
|
|
175
|
+
stringFlag(flags, 'profile') || config.currentProfile,
|
|
176
|
+
{ ...flags, environment }
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
} catch {
|
|
180
|
+
// This workspace does not define the candidate environment.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let notFound = null;
|
|
185
|
+
for (const candidate of candidates) {
|
|
186
|
+
try {
|
|
187
|
+
const run = await executor.status({
|
|
188
|
+
config,
|
|
189
|
+
target: candidate.target,
|
|
190
|
+
runId,
|
|
191
|
+
});
|
|
192
|
+
return { target: candidate.target, run };
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (!isRunNotFoundError(error)) throw error;
|
|
195
|
+
notFound = error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (notFound) throw notFound;
|
|
199
|
+
throw usageError(
|
|
200
|
+
`无法解析 ReleaseRun ${runId} 所属环境;请显式添加 --environment preproduction|production`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
135
204
|
return { run };
|
|
136
205
|
}
|
|
137
206
|
|
|
207
|
+
function isRunNotFoundError(error) {
|
|
208
|
+
return (
|
|
209
|
+
Number(error?.status || error?.statusCode) === 404 ||
|
|
210
|
+
/DELIVERY_RUN_NOT_FOUND|HTTP 404/i.test(String(error?.message || ''))
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
138
214
|
function output(flags, value, message) {
|
|
139
215
|
if (flags.json) return writeJson(value);
|
|
140
216
|
print(message);
|
|
@@ -206,14 +282,21 @@ function printHelp(command) {
|
|
|
206
282
|
const lines = {
|
|
207
283
|
check: [
|
|
208
284
|
'用法: openxiangda check [--environment <name>] [--build] [--json]',
|
|
209
|
-
'
|
|
285
|
+
'编译并校验密封 App Package,输出精确资源差异和执行计划;默认复用现有 dist。',
|
|
286
|
+
'删除或未进入密封工具链的第三方构建依赖会在远程写入前失败。',
|
|
210
287
|
],
|
|
211
288
|
deploy: [
|
|
212
289
|
'用法: openxiangda deploy <environment> [--package <sha256>] [--json]',
|
|
213
290
|
'不带 --package 时从当前工作区构建;带 --package 时直接部署已封存制品。',
|
|
214
291
|
],
|
|
215
|
-
status: [
|
|
216
|
-
|
|
292
|
+
status: [
|
|
293
|
+
'用法: openxiangda status <runId> [--environment <name>] [--json]',
|
|
294
|
+
'未指定环境时自动在预发和生产定位 ReleaseRun。',
|
|
295
|
+
],
|
|
296
|
+
retry: [
|
|
297
|
+
'用法: openxiangda retry <runId> [--environment <name>] [--json]',
|
|
298
|
+
'从服务端检查点继续;隔离旧 attempt,并在跨机器场景重建本地 Form 绑定。',
|
|
299
|
+
],
|
|
217
300
|
rollback: [
|
|
218
301
|
'用法: openxiangda rollback <environment> --to <appReleaseId> [--json]',
|
|
219
302
|
],
|