openxiangda 1.0.176 → 1.0.178
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 +11 -3
- package/lib/cli.js +763 -140
- package/lib/form-field-contract.js +79 -0
- package/lib/integration-bundle.js +23 -2
- package/lib/publish-lease-heartbeat.js +1 -1
- package/lib/publish-lease.js +2 -2
- package/lib/release-error-classification.js +41 -0
- package/lib/sdd.js +131 -0
- package/lib/source-dependencies.js +190 -4
- package/lib/task-status.js +81 -18
- package/openxiangda-skills/SKILL.md +8 -1
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +5 -1
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +2 -2
- package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +8 -0
- package/package.json +3 -1
- package/templates/openxiangda-react-spa/AGENTS.md +3 -2
- package/templates/sy-lowcode-app-workspace/AGENTS.md +3 -1
package/lib/cli.js
CHANGED
|
@@ -82,6 +82,10 @@ const {
|
|
|
82
82
|
writeContentCache,
|
|
83
83
|
} = require('./content-cache');
|
|
84
84
|
const { explainReleaseExecution } = require('./release-explain');
|
|
85
|
+
const {
|
|
86
|
+
classifyReleaseExecutionFailure,
|
|
87
|
+
isDefinitelyPreWriteReleaseError,
|
|
88
|
+
} = require('./release-error-classification');
|
|
85
89
|
const { buildTaskStatus, findAppReleaseId } = require('./task-status');
|
|
86
90
|
const {
|
|
87
91
|
checkEngineeringPolicyMarkers,
|
|
@@ -94,6 +98,10 @@ const {
|
|
|
94
98
|
getImpactedResourceTargets,
|
|
95
99
|
validateManifestSourceBindings,
|
|
96
100
|
} = require('./source-dependencies');
|
|
101
|
+
const {
|
|
102
|
+
collectFormSnapshotFieldIds,
|
|
103
|
+
validateFormFieldContracts,
|
|
104
|
+
} = require('./form-field-contract');
|
|
97
105
|
const { getSkillStatusReport, installSkills } = require('./skills');
|
|
98
106
|
const {
|
|
99
107
|
assertOrClaimWorktreeOwner,
|
|
@@ -170,13 +178,32 @@ let readOnlyHttpGuardDepth = 0;
|
|
|
170
178
|
let readOnlyHttpGuardLabel = null;
|
|
171
179
|
let activePublishRequestContext = null;
|
|
172
180
|
let activePublishLeaseHeartbeat = null;
|
|
181
|
+
let activeCommandScopedPublishLease = null;
|
|
173
182
|
const genericGitBasePlanCache = new Map();
|
|
174
183
|
|
|
175
184
|
async function main(argv) {
|
|
185
|
+
let commandError = null;
|
|
176
186
|
try {
|
|
177
187
|
return await mainImpl(argv);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
commandError = error;
|
|
190
|
+
throw error;
|
|
178
191
|
} finally {
|
|
179
192
|
await stopActivePublishLeaseHeartbeat();
|
|
193
|
+
try {
|
|
194
|
+
await finalizeCommandScopedPublishLease({
|
|
195
|
+
succeeded: commandError === null,
|
|
196
|
+
});
|
|
197
|
+
} catch (cleanupError) {
|
|
198
|
+
if (!commandError) throw cleanupError;
|
|
199
|
+
commandError.publishLeaseCleanupError = {
|
|
200
|
+
code: cleanupError?.code || 'PUBLISH_LEASE_CLEANUP_FAILED',
|
|
201
|
+
message: maskText(cleanupError?.message || String(cleanupError)),
|
|
202
|
+
};
|
|
203
|
+
warn(
|
|
204
|
+
`发布命令失败后的租约清理也未完成:${commandError.publishLeaseCleanupError.message}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
180
207
|
activePublishRequestContext = null;
|
|
181
208
|
}
|
|
182
209
|
}
|
|
@@ -686,7 +713,7 @@ async function sdd(args) {
|
|
|
686
713
|
' openxiangda sdd quick tune-customer-copy --kind copy --pages customer_list --files src/pages/customer_list/index.tsx --summary "用户确认小范围文案调整"',
|
|
687
714
|
' openxiangda sdd approve add-customer-list --summary "用户确认设计"',
|
|
688
715
|
' openxiangda sdd ready add-customer-list',
|
|
689
|
-
' openxiangda sdd bundle mainline-release --changes add-customer-list,fix-order-log',
|
|
716
|
+
' openxiangda sdd bundle mainline-release --changes add-customer-list,fix-order-log [--source-base-ref <commit>]',
|
|
690
717
|
' openxiangda sdd context --change add-customer-list --json',
|
|
691
718
|
' openxiangda sdd verify --change add-customer-list --stage prepublish --json',
|
|
692
719
|
' openxiangda sdd render add-customer-list # 仅在确实需要完整文档时',
|
|
@@ -701,6 +728,7 @@ async function sdd(args) {
|
|
|
701
728
|
' - verify stage 可选 implementation、prepublish、postpublish、archive;不传保持旧版全量校验。',
|
|
702
729
|
' - 默认 streamlined 校验只把 approval、精确资源/文件范围、实际发布命令作为硬门禁;任务、证据和规格文案仅告警。配置 strictDocumentation: true 可恢复严格文档门禁。',
|
|
703
730
|
' - bundle 聚合多个 approved change;提交并推送该 bundle 后,在权威主分支执行一次 staged + atomic 发布。',
|
|
731
|
+
' - bundle 默认保留来源 change 创建时的共同 Git 基线;仅迁移旧 change 缺少 sourceBase 时使用 --source-base-ref 显式补充。',
|
|
704
732
|
' - 新 release 只能从已推送且与权威远端默认主分支一致的 clean HEAD 开始。',
|
|
705
733
|
].join('\n'));
|
|
706
734
|
return;
|
|
@@ -781,6 +809,7 @@ async function sdd(args) {
|
|
|
781
809
|
changes: flags.changes,
|
|
782
810
|
integration,
|
|
783
811
|
profile: flags.profile,
|
|
812
|
+
sourceBaseRef: flags['source-base-ref'],
|
|
784
813
|
force: Boolean(flags.force),
|
|
785
814
|
});
|
|
786
815
|
if (flags.json) return writeJson(result);
|
|
@@ -805,6 +834,7 @@ async function sdd(args) {
|
|
|
805
834
|
changeId,
|
|
806
835
|
targets: scope.targets,
|
|
807
836
|
changedFiles: scope.changedFiles,
|
|
837
|
+
sourceBase: scope.change?.sourceBase,
|
|
808
838
|
});
|
|
809
839
|
const ownerStatus = getWorktreeOwnerStatus({ cwd: process.cwd() });
|
|
810
840
|
if (
|
|
@@ -1005,6 +1035,10 @@ function releaseExecutionFile(changeId) {
|
|
|
1005
1035
|
return path.join(releaseExecutionDir(changeId), 'execution.json');
|
|
1006
1036
|
}
|
|
1007
1037
|
|
|
1038
|
+
function directPublishReceiptFile(changeId) {
|
|
1039
|
+
return path.join(releaseExecutionDir(changeId), 'direct-publish.json');
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1008
1042
|
function writePrivateJsonAtomic(file, value) {
|
|
1009
1043
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1010
1044
|
const tempFile = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
@@ -1026,6 +1060,39 @@ function readReleaseExecution(changeId) {
|
|
|
1026
1060
|
}
|
|
1027
1061
|
}
|
|
1028
1062
|
|
|
1063
|
+
function readDirectPublishReceipt(changeId) {
|
|
1064
|
+
const file = directPublishReceiptFile(changeId);
|
|
1065
|
+
if (!fs.existsSync(file)) return null;
|
|
1066
|
+
try {
|
|
1067
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
fail(
|
|
1070
|
+
`DIRECT_PUBLISH_RECEIPT_INVALID: ${path.relative(process.cwd(), file)} 无法读取: ${error.message}`
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function writeDirectPublishReceipt(changeId, input = {}) {
|
|
1076
|
+
if (!changeId) return null;
|
|
1077
|
+
const receipt = {
|
|
1078
|
+
schemaVersion: 'openxiangda_direct_publish_receipt_v1',
|
|
1079
|
+
changeId,
|
|
1080
|
+
status: 'completed',
|
|
1081
|
+
command: input.command || null,
|
|
1082
|
+
writeAttempted: Boolean(input.writeAttempted),
|
|
1083
|
+
selectors: unique(input.selectors || []).sort(),
|
|
1084
|
+
sourceRevision: input.sourceRevision || null,
|
|
1085
|
+
result: input.result || null,
|
|
1086
|
+
completedAt: new Date().toISOString(),
|
|
1087
|
+
};
|
|
1088
|
+
const file = directPublishReceiptFile(changeId);
|
|
1089
|
+
writePrivateJsonAtomic(file, receipt);
|
|
1090
|
+
return {
|
|
1091
|
+
file: path.relative(process.cwd(), file).replace(/\\/g, '/'),
|
|
1092
|
+
receipt,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1029
1096
|
function summarizeReleaseStepResult(result) {
|
|
1030
1097
|
const value =
|
|
1031
1098
|
result && typeof result === 'object' && !Array.isArray(result)
|
|
@@ -1082,30 +1149,6 @@ function releaseExecutionContextFromBegin(result, target, changeId) {
|
|
|
1082
1149
|
};
|
|
1083
1150
|
}
|
|
1084
1151
|
|
|
1085
|
-
function isDefinitelyPreWriteReleaseError(error) {
|
|
1086
|
-
const code = String(error?.code || '');
|
|
1087
|
-
if (new Set([
|
|
1088
|
-
'RELEASE_PUBLISH_REVISION_CHANGED',
|
|
1089
|
-
'RELEASE_SOURCE_DIRTY',
|
|
1090
|
-
'RELEASE_SOURCE_BRANCH_REQUIRED',
|
|
1091
|
-
'RELEASE_SOURCE_MAINLINE_REQUIRED',
|
|
1092
|
-
'RELEASE_SOURCE_MAINLINE_NOT_PUSHED',
|
|
1093
|
-
'RELEASE_SOURCE_BEHIND_MAIN',
|
|
1094
|
-
'RELEASE_GIT_AUTH_REQUIRED',
|
|
1095
|
-
'RELEASE_MAIN_BRANCH_UNRESOLVED',
|
|
1096
|
-
'RELEASE_MAIN_REF_UNVERIFIED',
|
|
1097
|
-
'SDD_PREPUBLISH_FAILED',
|
|
1098
|
-
'PUBLISH_CONTEXT_REQUIRED',
|
|
1099
|
-
'APP_RELEASE_STAGED_SCOPE_REQUIRED',
|
|
1100
|
-
'APP_RELEASE_STAGED_SCOPE_MISMATCH',
|
|
1101
|
-
]).has(code)) {
|
|
1102
|
-
return true;
|
|
1103
|
-
}
|
|
1104
|
-
return /^(?:SDD_|RELEASE_(?:SOURCE|GIT|MAINLINE|MAIN_BRANCH|MAIN_REF)|RUNTIME_(?:SOURCE|BUILD|PACKAGE|DEPENDENCY))/.test(
|
|
1105
|
-
code
|
|
1106
|
-
);
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
1152
|
function parseChildJsonOutput(stdout) {
|
|
1110
1153
|
const text = String(stdout || '').trim();
|
|
1111
1154
|
if (!text) return null;
|
|
@@ -1181,6 +1224,8 @@ async function waitForPublishLeaseAvailability(config, target, flags = {}) {
|
|
|
1181
1224
|
const deadline = startedAt + waitSeconds * 1000;
|
|
1182
1225
|
const clientSessionId = createPublishClientSessionId();
|
|
1183
1226
|
let attempts = 0;
|
|
1227
|
+
let lastNoticeFingerprint = '';
|
|
1228
|
+
let lastNoticeAt = 0;
|
|
1184
1229
|
while (true) {
|
|
1185
1230
|
const remote = await fetchPublishLeaseStatus(
|
|
1186
1231
|
config,
|
|
@@ -1191,10 +1236,11 @@ async function waitForPublishLeaseAvailability(config, target, flags = {}) {
|
|
|
1191
1236
|
return { waitedMs: Date.now() - startedAt, attempts, remote };
|
|
1192
1237
|
}
|
|
1193
1238
|
const remoteHolder =
|
|
1194
|
-
(typeof remote?.holder === 'string' ? remote.holder : null) ||
|
|
1195
|
-
remote?.holder?.changeId ||
|
|
1196
1239
|
remote?.changeId ||
|
|
1240
|
+
remote?.clientSessionId ||
|
|
1241
|
+
remote?.holder?.changeId ||
|
|
1197
1242
|
remote?.holder?.clientSessionId ||
|
|
1243
|
+
(typeof remote?.holder === 'string' ? remote.holder : null) ||
|
|
1198
1244
|
'another release';
|
|
1199
1245
|
if (Date.now() >= deadline) {
|
|
1200
1246
|
fail(
|
|
@@ -1210,9 +1256,20 @@ async function waitForPublishLeaseAvailability(config, target, flags = {}) {
|
|
|
1210
1256
|
30000,
|
|
1211
1257
|
Math.max(1000, deadline - Date.now())
|
|
1212
1258
|
);
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1259
|
+
const noticeFingerprint = [
|
|
1260
|
+
remoteHolder,
|
|
1261
|
+
remote?.leaseId || '',
|
|
1262
|
+
].join(':');
|
|
1263
|
+
if (
|
|
1264
|
+
noticeFingerprint !== lastNoticeFingerprint ||
|
|
1265
|
+
Date.now() - lastNoticeAt >= 120_000
|
|
1266
|
+
) {
|
|
1267
|
+
warn(
|
|
1268
|
+
`发布队列等待中: app=${target.appType} holder=${remoteHolder} retry=${Math.ceil(delayMs / 1000)}s${remote?.expiresAt ? ` expiresAt=${remote.expiresAt}` : ''}`
|
|
1269
|
+
);
|
|
1270
|
+
lastNoticeFingerprint = noticeFingerprint;
|
|
1271
|
+
lastNoticeAt = Date.now();
|
|
1272
|
+
}
|
|
1216
1273
|
await sleep(delayMs);
|
|
1217
1274
|
}
|
|
1218
1275
|
}
|
|
@@ -1443,6 +1500,9 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1443
1500
|
execution: null,
|
|
1444
1501
|
};
|
|
1445
1502
|
}
|
|
1503
|
+
const plannedSourceRevision = prepareReleaseSourceRevision({
|
|
1504
|
+
cwd: process.cwd(),
|
|
1505
|
+
});
|
|
1446
1506
|
const existing = readReleaseExecution(changeId);
|
|
1447
1507
|
if (
|
|
1448
1508
|
existing &&
|
|
@@ -1462,6 +1522,11 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1462
1522
|
'RELEASE_WRITE_REVIEW_REQUIRED: 上次写请求结果不确定;请先只读核对 staged release,确认可重试后追加 --resume-after-review'
|
|
1463
1523
|
);
|
|
1464
1524
|
}
|
|
1525
|
+
if (existing?.releaseSourceRevision) {
|
|
1526
|
+
assertReleaseSourceRevisionStable(existing.releaseSourceRevision, {
|
|
1527
|
+
cwd: process.cwd(),
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1465
1530
|
const now = new Date().toISOString();
|
|
1466
1531
|
const execution = existing || {
|
|
1467
1532
|
schemaVersion: 'openxiangda_release_execution_v1',
|
|
@@ -1469,6 +1534,7 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1469
1534
|
profile: target.profileName,
|
|
1470
1535
|
changeId,
|
|
1471
1536
|
planHash,
|
|
1537
|
+
releaseSourceRevision: plannedSourceRevision,
|
|
1472
1538
|
status: 'planned',
|
|
1473
1539
|
createdAt: now,
|
|
1474
1540
|
updatedAt: now,
|
|
@@ -1493,6 +1559,9 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1493
1559
|
},
|
|
1494
1560
|
],
|
|
1495
1561
|
};
|
|
1562
|
+
if (!execution.releaseSourceRevision) {
|
|
1563
|
+
execution.releaseSourceRevision = plannedSourceRevision;
|
|
1564
|
+
}
|
|
1496
1565
|
if (execution.status === 'completed') {
|
|
1497
1566
|
return {
|
|
1498
1567
|
completed: true,
|
|
@@ -1609,11 +1678,11 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1609
1678
|
execution.writeAttempted = true;
|
|
1610
1679
|
}
|
|
1611
1680
|
}
|
|
1612
|
-
execution.status =
|
|
1613
|
-
|
|
1614
|
-
: execution.
|
|
1615
|
-
|
|
1616
|
-
|
|
1681
|
+
execution.status = classifyReleaseExecutionFailure({
|
|
1682
|
+
error,
|
|
1683
|
+
stagedWriteOccurred: execution.stagedWriteOccurred,
|
|
1684
|
+
writeAttempted: execution.writeAttempted,
|
|
1685
|
+
});
|
|
1617
1686
|
execution.updatedAt = new Date().toISOString();
|
|
1618
1687
|
writePrivateJsonAtomic(releaseExecutionFile(changeId), execution);
|
|
1619
1688
|
if (!execution.writeAttempted) {
|
|
@@ -1662,7 +1731,7 @@ async function release(args) {
|
|
|
1662
1731
|
' - begin 获取应用级单写者 promotion lease;不同工作区仍可并行开发和验证。',
|
|
1663
1732
|
' - 带 --change 的 resource/runtime 写命令在没有本地 lease 时会自动 begin,并在后续命令复用。',
|
|
1664
1733
|
' - 子 Runtime/Page/Backend/Form Release 默认先 stage;app-finalize 用 --staged-resources-json 覆盖 capture 中的 changed children,并在同一事务中原子激活。',
|
|
1665
|
-
' - 默认 TTL 为
|
|
1734
|
+
' - 默认 TTL 为 120 秒;活动发布每 30 秒心跳并在到期前续租,客户端退出后孤儿租约最多阻塞约 2 分钟。',
|
|
1666
1735
|
' - begin 仅接受 clean 且与权威远端默认 main/master 完全一致的 HEAD;feature branch 先合并并 push,再由主分支一次发布。',
|
|
1667
1736
|
' - 因发布源已在远端主线,成功激活后 integration-status 会立即通过,end 不再等待补合并。',
|
|
1668
1737
|
' - integration-status --check 可作为 CI 门禁;尚未回合时返回非零。',
|
|
@@ -2080,7 +2149,14 @@ async function loadTaskStatusSnapshot(config, target, changeId) {
|
|
|
2080
2149
|
process.cwd(),
|
|
2081
2150
|
changeId
|
|
2082
2151
|
);
|
|
2083
|
-
const
|
|
2152
|
+
const execution = readReleaseExecution(changeId);
|
|
2153
|
+
const directPublish = readDirectPublishReceipt(changeId);
|
|
2154
|
+
const sourceRevision =
|
|
2155
|
+
execution?.releaseSourceRevision ||
|
|
2156
|
+
execution?.releaseContext?.releaseSourceRevision ||
|
|
2157
|
+
directPublish?.sourceRevision ||
|
|
2158
|
+
null;
|
|
2159
|
+
let integration = taskResult
|
|
2084
2160
|
? inspectIntegrationBundleCommits(
|
|
2085
2161
|
{
|
|
2086
2162
|
requiredCommits: [
|
|
@@ -2095,13 +2171,26 @@ async function loadTaskStatusSnapshot(config, target, changeId) {
|
|
|
2095
2171
|
{ cwd: process.cwd() }
|
|
2096
2172
|
)
|
|
2097
2173
|
: null;
|
|
2098
|
-
|
|
2174
|
+
let integrationError = null;
|
|
2175
|
+
if (!integration && sourceRevision) {
|
|
2176
|
+
try {
|
|
2177
|
+
integration = inspectReleaseSourceIntegration(sourceRevision, {
|
|
2178
|
+
cwd: process.cwd(),
|
|
2179
|
+
});
|
|
2180
|
+
} catch (error) {
|
|
2181
|
+
integrationError = {
|
|
2182
|
+
code: error?.code || 'RELEASE_INTEGRATION_STATUS_UNAVAILABLE',
|
|
2183
|
+
message: maskText(error?.message || String(error)),
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2099
2187
|
const localLease = getStoredPublishLease(target);
|
|
2100
2188
|
const baseline = getStoredChangeBaseline(target, {
|
|
2101
2189
|
access: 'reconciliation-read',
|
|
2102
2190
|
});
|
|
2103
2191
|
const clientSessionId =
|
|
2104
2192
|
localLease?.clientSessionId ||
|
|
2193
|
+
execution?.releaseContext?.clientSessionId ||
|
|
2105
2194
|
baseline?.clientSessionId ||
|
|
2106
2195
|
createPublishClientSessionId();
|
|
2107
2196
|
let remoteLease = null;
|
|
@@ -2145,6 +2234,8 @@ async function loadTaskStatusSnapshot(config, target, changeId) {
|
|
|
2145
2234
|
taskResult,
|
|
2146
2235
|
integration,
|
|
2147
2236
|
execution,
|
|
2237
|
+
directPublish,
|
|
2238
|
+
sourceRevision,
|
|
2148
2239
|
remoteLease,
|
|
2149
2240
|
postCommit,
|
|
2150
2241
|
}),
|
|
@@ -2152,6 +2243,7 @@ async function loadTaskStatusSnapshot(config, target, changeId) {
|
|
|
2152
2243
|
diagnostics: {
|
|
2153
2244
|
remoteLeaseError,
|
|
2154
2245
|
postCommitError,
|
|
2246
|
+
integrationError,
|
|
2155
2247
|
},
|
|
2156
2248
|
};
|
|
2157
2249
|
}
|
|
@@ -3717,9 +3809,25 @@ function resolveChangeSourceBase(changeId, flags = {}) {
|
|
|
3717
3809
|
if (!explicitBaseRef && changeId) {
|
|
3718
3810
|
try {
|
|
3719
3811
|
const scope = getSddChangeScope({ cwd: process.cwd(), changeId });
|
|
3720
|
-
const recorded =
|
|
3812
|
+
const recorded =
|
|
3813
|
+
scope?.change?.sourceBase ||
|
|
3814
|
+
scope?.release?.sourceBase;
|
|
3721
3815
|
if (recorded?.baseCommit && recorded?.treeHash) return recorded;
|
|
3722
|
-
|
|
3816
|
+
const recordedBaseRevision =
|
|
3817
|
+
scope?.change?.baseRevision ||
|
|
3818
|
+
scope?.release?.baseRevision;
|
|
3819
|
+
if (recordedBaseRevision) {
|
|
3820
|
+
return readGitSourceBase(process.cwd(), recordedBaseRevision);
|
|
3821
|
+
}
|
|
3822
|
+
if (scope?.change?.changeMode === 'mainline-bundle') {
|
|
3823
|
+
const error = new Error(
|
|
3824
|
+
`SDD_SOURCE_BASE_REQUIRED: mainline bundle ${changeId} 缺少 sourceBase/baseRevision,禁止退化为当前 HEAD`
|
|
3825
|
+
);
|
|
3826
|
+
error.code = 'SDD_SOURCE_BASE_REQUIRED';
|
|
3827
|
+
throw error;
|
|
3828
|
+
}
|
|
3829
|
+
} catch (error) {
|
|
3830
|
+
if (error?.code === 'SDD_SOURCE_BASE_REQUIRED') throw error;
|
|
3723
3831
|
// A direct release may use --change without a local SDD record. In that
|
|
3724
3832
|
// case the current Git HEAD is still a deterministic, auditable base.
|
|
3725
3833
|
}
|
|
@@ -3918,6 +4026,7 @@ async function resolvePublishLeaseForWrite(config, target, flags = {}) {
|
|
|
3918
4026
|
const explicitLeaseId = readStringFlag(flags, 'publish-lease-id');
|
|
3919
4027
|
const changeId = readStringFlag(flags, 'change');
|
|
3920
4028
|
const storedLease = getStoredPublishLease(target);
|
|
4029
|
+
let acquiredByCommand = false;
|
|
3921
4030
|
assertOrClaimWorktreeOwner({
|
|
3922
4031
|
cwd: process.cwd(),
|
|
3923
4032
|
changeId: changeId || storedLease?.changeId || 'unscoped-publish',
|
|
@@ -3981,6 +4090,7 @@ async function resolvePublishLeaseForWrite(config, target, flags = {}) {
|
|
|
3981
4090
|
...flags,
|
|
3982
4091
|
'client-session-id': clientSessionId,
|
|
3983
4092
|
});
|
|
4093
|
+
acquiredByCommand = true;
|
|
3984
4094
|
} catch (error) {
|
|
3985
4095
|
clearRejectedPendingBaseline(target, baseline, error);
|
|
3986
4096
|
throw error;
|
|
@@ -4017,6 +4127,17 @@ async function resolvePublishLeaseForWrite(config, target, flags = {}) {
|
|
|
4017
4127
|
if (shouldRenewPublishLease(lease)) {
|
|
4018
4128
|
lease = await heartbeat.beforeWrite({ forceRenew: true });
|
|
4019
4129
|
}
|
|
4130
|
+
if (acquiredByCommand) {
|
|
4131
|
+
activeCommandScopedPublishLease = {
|
|
4132
|
+
config,
|
|
4133
|
+
target,
|
|
4134
|
+
leaseId: lease.leaseId,
|
|
4135
|
+
changeId: lease.changeId || changeId,
|
|
4136
|
+
writeAttempted: false,
|
|
4137
|
+
retain: false,
|
|
4138
|
+
retentionReason: null,
|
|
4139
|
+
};
|
|
4140
|
+
}
|
|
4020
4141
|
return lease;
|
|
4021
4142
|
}
|
|
4022
4143
|
|
|
@@ -4084,6 +4205,112 @@ async function stopActivePublishLeaseHeartbeat() {
|
|
|
4084
4205
|
if (active?.heartbeat) await active.heartbeat.stop();
|
|
4085
4206
|
}
|
|
4086
4207
|
|
|
4208
|
+
function markCommandScopedPublishWriteAttempted() {
|
|
4209
|
+
if (activeCommandScopedPublishLease) {
|
|
4210
|
+
activeCommandScopedPublishLease.writeAttempted = true;
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
|
|
4214
|
+
function retainCommandScopedPublishLease(reason = 'staged-release') {
|
|
4215
|
+
if (!activeCommandScopedPublishLease) return false;
|
|
4216
|
+
activeCommandScopedPublishLease.retain = true;
|
|
4217
|
+
activeCommandScopedPublishLease.retentionReason = String(reason || 'staged-release');
|
|
4218
|
+
return true;
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
async function finalizeCommandScopedPublishLease(options = {}) {
|
|
4222
|
+
const active = activeCommandScopedPublishLease;
|
|
4223
|
+
if (!active) return null;
|
|
4224
|
+
activeCommandScopedPublishLease = null;
|
|
4225
|
+
if (active.retain) {
|
|
4226
|
+
return {
|
|
4227
|
+
managed: true,
|
|
4228
|
+
released: false,
|
|
4229
|
+
retained: true,
|
|
4230
|
+
reason: active.retentionReason,
|
|
4231
|
+
};
|
|
4232
|
+
}
|
|
4233
|
+
if (options.succeeded === false && active.writeAttempted) {
|
|
4234
|
+
warn(
|
|
4235
|
+
`PUBLISH_WRITE_REVIEW_REQUIRED: change=${active.changeId || '-'} 的写请求结果未完整确认,租约已保留供只读核对和恢复`
|
|
4236
|
+
);
|
|
4237
|
+
return {
|
|
4238
|
+
managed: true,
|
|
4239
|
+
released: false,
|
|
4240
|
+
retained: true,
|
|
4241
|
+
reason: 'write-result-review',
|
|
4242
|
+
};
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
await stopActivePublishLeaseHeartbeat();
|
|
4246
|
+
const stored = getStoredPublishLease(active.target);
|
|
4247
|
+
if (!stored || stored.leaseId !== active.leaseId) {
|
|
4248
|
+
return {
|
|
4249
|
+
managed: true,
|
|
4250
|
+
released: false,
|
|
4251
|
+
retained: false,
|
|
4252
|
+
reason: 'local-lease-already-cleared',
|
|
4253
|
+
};
|
|
4254
|
+
}
|
|
4255
|
+
const baseline = getStoredChangeBaseline(active.target, {
|
|
4256
|
+
access: 'reconciliation-read',
|
|
4257
|
+
});
|
|
4258
|
+
const integration = assertReleaseSourceIntegrated(
|
|
4259
|
+
baseline?.releaseSourceRevision,
|
|
4260
|
+
{ cwd: process.cwd() }
|
|
4261
|
+
);
|
|
4262
|
+
try {
|
|
4263
|
+
await requestWithAuth(
|
|
4264
|
+
active.config,
|
|
4265
|
+
active.target.profileName,
|
|
4266
|
+
publishLeaseApiPath(
|
|
4267
|
+
active.target,
|
|
4268
|
+
`/${encodeURIComponent(active.leaseId)}/release`
|
|
4269
|
+
),
|
|
4270
|
+
{
|
|
4271
|
+
method: 'POST',
|
|
4272
|
+
body: {
|
|
4273
|
+
completion:
|
|
4274
|
+
options.succeeded === false
|
|
4275
|
+
? 'prewrite-failed'
|
|
4276
|
+
: 'direct-publish-completed',
|
|
4277
|
+
sourceCommit: integration.sourceCommit,
|
|
4278
|
+
mainBranch: integration.mainBranch,
|
|
4279
|
+
mainTipCommit: integration.mainTipCommit,
|
|
4280
|
+
},
|
|
4281
|
+
}
|
|
4282
|
+
);
|
|
4283
|
+
} catch (error) {
|
|
4284
|
+
if (
|
|
4285
|
+
!['PUBLISH_LEASE_EXPIRED', 'PUBLISH_LEASE_INVALID'].includes(
|
|
4286
|
+
error?.code
|
|
4287
|
+
)
|
|
4288
|
+
) {
|
|
4289
|
+
activeCommandScopedPublishLease = active;
|
|
4290
|
+
throw error;
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
clearPublishLease(active.target, active.leaseId);
|
|
4294
|
+
if (baseline) {
|
|
4295
|
+
clearChangeBaseline(
|
|
4296
|
+
active.target,
|
|
4297
|
+
baseline.baselineId || baseline.id
|
|
4298
|
+
);
|
|
4299
|
+
}
|
|
4300
|
+
releaseWorktreeOwner({ cwd: process.cwd() });
|
|
4301
|
+
activePublishRequestContext = null;
|
|
4302
|
+
return {
|
|
4303
|
+
managed: true,
|
|
4304
|
+
released: true,
|
|
4305
|
+
retained: false,
|
|
4306
|
+
completion:
|
|
4307
|
+
options.succeeded === false
|
|
4308
|
+
? 'prewrite-failed'
|
|
4309
|
+
: 'direct-publish-completed',
|
|
4310
|
+
integration,
|
|
4311
|
+
};
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4087
4314
|
function publishContextRequiredError(label, detail) {
|
|
4088
4315
|
const suffix = detail ? `(${detail})` : '';
|
|
4089
4316
|
const error = new Error(
|
|
@@ -5773,7 +6000,12 @@ async function workspace(args) {
|
|
|
5773
6000
|
flags
|
|
5774
6001
|
);
|
|
5775
6002
|
}
|
|
5776
|
-
runReactSpaFormSchemaPublish(
|
|
6003
|
+
await runReactSpaFormSchemaPublish(
|
|
6004
|
+
profileName,
|
|
6005
|
+
profile,
|
|
6006
|
+
bound.appType,
|
|
6007
|
+
publishOptions
|
|
6008
|
+
);
|
|
5777
6009
|
} else {
|
|
5778
6010
|
fail(
|
|
5779
6011
|
[
|
|
@@ -5794,7 +6026,12 @@ async function workspace(args) {
|
|
|
5794
6026
|
flags
|
|
5795
6027
|
);
|
|
5796
6028
|
}
|
|
5797
|
-
runWorkspacePublish(
|
|
6029
|
+
await runWorkspacePublish(
|
|
6030
|
+
profileName,
|
|
6031
|
+
profile,
|
|
6032
|
+
bound.appType,
|
|
6033
|
+
publishOptions.workspaceArgs
|
|
6034
|
+
);
|
|
5798
6035
|
}
|
|
5799
6036
|
if (publishOptions.includeResources) {
|
|
5800
6037
|
if (publishOptions.dryRun) {
|
|
@@ -5828,6 +6065,7 @@ async function workspace(args) {
|
|
|
5828
6065
|
} else if (!publishOptions.quietResourceSkip) {
|
|
5829
6066
|
print('已跳过 src/resources 发布;如需同时发布资源,传 --resources。');
|
|
5830
6067
|
}
|
|
6068
|
+
await finalizeCommandScopedPublishLease({ succeeded: true });
|
|
5831
6069
|
return;
|
|
5832
6070
|
}
|
|
5833
6071
|
|
|
@@ -6644,6 +6882,12 @@ async function page(args) {
|
|
|
6644
6882
|
},
|
|
6645
6883
|
}
|
|
6646
6884
|
);
|
|
6885
|
+
const leaseCompletion = await finalizeCommandScopedPublishLease({
|
|
6886
|
+
succeeded: true,
|
|
6887
|
+
});
|
|
6888
|
+
if (leaseCompletion && data && typeof data === 'object') {
|
|
6889
|
+
data.publishLeaseLifecycle = leaseCompletion;
|
|
6890
|
+
}
|
|
6647
6891
|
return outputPageResult(data, flags);
|
|
6648
6892
|
}
|
|
6649
6893
|
|
|
@@ -6719,6 +6963,15 @@ async function page(args) {
|
|
|
6719
6963
|
legacyFormUuid: item.legacyFormUuid,
|
|
6720
6964
|
});
|
|
6721
6965
|
}
|
|
6966
|
+
if (!activate) {
|
|
6967
|
+
retainCommandScopedPublishLease('staged-page');
|
|
6968
|
+
}
|
|
6969
|
+
const leaseCompletion = await finalizeCommandScopedPublishLease({
|
|
6970
|
+
succeeded: true,
|
|
6971
|
+
});
|
|
6972
|
+
if (leaseCompletion && data && typeof data === 'object') {
|
|
6973
|
+
data.publishLeaseLifecycle = leaseCompletion;
|
|
6974
|
+
}
|
|
6722
6975
|
return outputPageResult(data, flags);
|
|
6723
6976
|
}
|
|
6724
6977
|
|
|
@@ -10244,6 +10497,18 @@ async function resource(args) {
|
|
|
10244
10497
|
error.missingSecretRefs = missing;
|
|
10245
10498
|
throw error;
|
|
10246
10499
|
}
|
|
10500
|
+
if (publishPlan.formFieldContracts?.valid === false) {
|
|
10501
|
+
const violations = publishPlan.formFieldContracts.errors || [];
|
|
10502
|
+
const error = new Error(
|
|
10503
|
+
`FORM_FIELD_CONTRACT_INVALID: ${violations
|
|
10504
|
+
.slice(0, 8)
|
|
10505
|
+
.map(item => item.message)
|
|
10506
|
+
.join('; ')}`
|
|
10507
|
+
);
|
|
10508
|
+
error.code = 'FORM_FIELD_CONTRACT_INVALID';
|
|
10509
|
+
error.violations = violations;
|
|
10510
|
+
throw error;
|
|
10511
|
+
}
|
|
10247
10512
|
const pruneCandidates = flags.prune
|
|
10248
10513
|
? await telemetry.runPhase('prune-plan', async () =>
|
|
10249
10514
|
collectPruneResourceCandidates(config, target, manifest)
|
|
@@ -10347,6 +10612,8 @@ async function resource(args) {
|
|
|
10347
10612
|
: [];
|
|
10348
10613
|
const stagedChangeId =
|
|
10349
10614
|
flags.change || getStoredChangeBaseline(target)?.changeId;
|
|
10615
|
+
const directPublishSourceRevision =
|
|
10616
|
+
getStoredChangeBaseline(target)?.releaseSourceRevision || null;
|
|
10350
10617
|
if (stagedResources.length > 0 && stagedChangeId) {
|
|
10351
10618
|
result.stagedResourcesFile = recordStagedAppReleaseResources(
|
|
10352
10619
|
stagedChangeId,
|
|
@@ -10366,7 +10633,42 @@ async function resource(args) {
|
|
|
10366
10633
|
target
|
|
10367
10634
|
);
|
|
10368
10635
|
}
|
|
10636
|
+
if (stagedResources.length > 0 || flags['stage-only']) {
|
|
10637
|
+
retainCommandScopedPublishLease('staged-resource');
|
|
10638
|
+
}
|
|
10639
|
+
const leaseCompletion = await telemetry.runPhase(
|
|
10640
|
+
'lease-release',
|
|
10641
|
+
async () =>
|
|
10642
|
+
finalizeCommandScopedPublishLease({
|
|
10643
|
+
succeeded: true,
|
|
10644
|
+
})
|
|
10645
|
+
);
|
|
10646
|
+
if (leaseCompletion) {
|
|
10647
|
+
result.publishLeaseLifecycle = leaseCompletion;
|
|
10648
|
+
}
|
|
10369
10649
|
result.timings = telemetry.stop();
|
|
10650
|
+
if (leaseCompletion?.released && stagedChangeId) {
|
|
10651
|
+
const receipt = writeDirectPublishReceipt(stagedChangeId, {
|
|
10652
|
+
command: 'resource publish',
|
|
10653
|
+
writeAttempted:
|
|
10654
|
+
(result.published || []).some(item => item.action !== 'noop') ||
|
|
10655
|
+
(result.deleted || []).length > 0,
|
|
10656
|
+
selectors: [
|
|
10657
|
+
...(result.published || []).map(
|
|
10658
|
+
item => `${item.kind || 'resource'}:${item.code}`
|
|
10659
|
+
),
|
|
10660
|
+
...(result.deleted || []).map(
|
|
10661
|
+
item => `${item.kind || 'resource'}:${item.code}`
|
|
10662
|
+
),
|
|
10663
|
+
],
|
|
10664
|
+
sourceRevision: directPublishSourceRevision,
|
|
10665
|
+
result: {
|
|
10666
|
+
published: (result.published || []).length,
|
|
10667
|
+
deleted: (result.deleted || []).length,
|
|
10668
|
+
},
|
|
10669
|
+
});
|
|
10670
|
+
result.directPublishReceipt = receipt?.file || null;
|
|
10671
|
+
}
|
|
10370
10672
|
if (flags.json) return writeJson(result);
|
|
10371
10673
|
printResourceResult(result);
|
|
10372
10674
|
} finally {
|
|
@@ -10599,6 +10901,10 @@ async function runtime(args) {
|
|
|
10599
10901
|
target,
|
|
10600
10902
|
flags
|
|
10601
10903
|
);
|
|
10904
|
+
const directChangeId =
|
|
10905
|
+
flags.change || getStoredChangeBaseline(target)?.changeId;
|
|
10906
|
+
const directSourceRevision =
|
|
10907
|
+
getStoredChangeBaseline(target)?.releaseSourceRevision || null;
|
|
10602
10908
|
await runRuntimeChangeBaselinePreflight(config, target);
|
|
10603
10909
|
const activationPreconditions = await resolveRuntimeActivationPreconditions(
|
|
10604
10910
|
config,
|
|
@@ -10623,6 +10929,18 @@ async function runtime(args) {
|
|
|
10623
10929
|
}
|
|
10624
10930
|
);
|
|
10625
10931
|
saveRuntimeReleaseState(target, data);
|
|
10932
|
+
const leaseCompletion = await finalizeCommandScopedPublishLease({
|
|
10933
|
+
succeeded: true,
|
|
10934
|
+
});
|
|
10935
|
+
if (leaseCompletion?.released && directChangeId) {
|
|
10936
|
+
writeDirectPublishReceipt(directChangeId, {
|
|
10937
|
+
command: 'runtime activate',
|
|
10938
|
+
writeAttempted: true,
|
|
10939
|
+
selectors: ['runtime:active'],
|
|
10940
|
+
sourceRevision: directSourceRevision,
|
|
10941
|
+
result: { releaseId: data?.id || releaseId },
|
|
10942
|
+
});
|
|
10943
|
+
}
|
|
10626
10944
|
if (flags.json) return writeJson(data);
|
|
10627
10945
|
print(`runtime release 已激活: ${data.id || releaseId} build=${data.buildId || '-'}`);
|
|
10628
10946
|
return;
|
|
@@ -10666,8 +10984,8 @@ async function runtime(args) {
|
|
|
10666
10984
|
// Keep build after lease acquisition. In direct-storage modes the build
|
|
10667
10985
|
// embeds the provider-specific assetBaseUrl returned by the protected
|
|
10668
10986
|
// upload plan; substituting the gateway URL would change chunk/publicPath
|
|
10669
|
-
// behavior. The
|
|
10670
|
-
//
|
|
10987
|
+
// behavior. The async build keeps the short publish-lease heartbeat alive
|
|
10988
|
+
// instead of extending a crash-prone one-hour lease.
|
|
10671
10989
|
let publishLease = await telemetry.runPhase('lease', async () =>
|
|
10672
10990
|
resolveRuntimePublishLeaseForWrite(config, target, flags)
|
|
10673
10991
|
);
|
|
@@ -10748,19 +11066,8 @@ async function runtime(args) {
|
|
|
10748
11066
|
}
|
|
10749
11067
|
}
|
|
10750
11068
|
if (!flags['no-build']) {
|
|
10751
|
-
//
|
|
10752
|
-
//
|
|
10753
|
-
// server maximum first; if the build still outlives it, the first
|
|
10754
|
-
// subsequent write performs a real renew and fails closed.
|
|
10755
|
-
publishLease = await renewPublishLeaseFailClosed(
|
|
10756
|
-
config,
|
|
10757
|
-
target,
|
|
10758
|
-
publishLease,
|
|
10759
|
-
{
|
|
10760
|
-
...flags,
|
|
10761
|
-
ttl: 3600,
|
|
10762
|
-
}
|
|
10763
|
-
);
|
|
11069
|
+
// Runtime builds run in an asynchronous child process so the promotion
|
|
11070
|
+
// heartbeat keeps renewing the short lease while compilation proceeds.
|
|
10764
11071
|
await telemetry.runPhase('build', async () =>
|
|
10765
11072
|
runRuntimeBuild({
|
|
10766
11073
|
buildId,
|
|
@@ -10952,8 +11259,35 @@ async function runtime(args) {
|
|
|
10952
11259
|
traceId,
|
|
10953
11260
|
...(stagedResource ? { stagedResource } : {}),
|
|
10954
11261
|
...(stagedResourcesFile ? { stagedResourcesFile } : {}),
|
|
10955
|
-
timings: telemetry.stop(),
|
|
10956
11262
|
};
|
|
11263
|
+
if (stagedResource) {
|
|
11264
|
+
retainCommandScopedPublishLease('staged-runtime');
|
|
11265
|
+
}
|
|
11266
|
+
const leaseCompletion = await telemetry.runPhase(
|
|
11267
|
+
'lease-release',
|
|
11268
|
+
async () =>
|
|
11269
|
+
finalizeCommandScopedPublishLease({
|
|
11270
|
+
succeeded: true,
|
|
11271
|
+
})
|
|
11272
|
+
);
|
|
11273
|
+
if (leaseCompletion) {
|
|
11274
|
+
result.publishLeaseLifecycle = leaseCompletion;
|
|
11275
|
+
}
|
|
11276
|
+
result.timings = telemetry.stop();
|
|
11277
|
+
if (leaseCompletion?.released && stagedChangeId) {
|
|
11278
|
+
const receipt = writeDirectPublishReceipt(stagedChangeId, {
|
|
11279
|
+
command: 'runtime deploy',
|
|
11280
|
+
writeAttempted: true,
|
|
11281
|
+
selectors: ['runtime:active'],
|
|
11282
|
+
sourceRevision: runtimeLineage.sourceRevision,
|
|
11283
|
+
result: {
|
|
11284
|
+
releaseId: data?.id || null,
|
|
11285
|
+
buildId,
|
|
11286
|
+
activated: true,
|
|
11287
|
+
},
|
|
11288
|
+
});
|
|
11289
|
+
result.directPublishReceipt = receipt?.file || null;
|
|
11290
|
+
}
|
|
10957
11291
|
if (flags.json) return writeJson(result);
|
|
10958
11292
|
print(
|
|
10959
11293
|
[
|
|
@@ -11056,32 +11390,61 @@ async function createRuntimeOssUploadBasePlan(options) {
|
|
|
11056
11390
|
);
|
|
11057
11391
|
}
|
|
11058
11392
|
|
|
11059
|
-
function runRuntimeBuild(options) {
|
|
11393
|
+
async function runRuntimeBuild(options) {
|
|
11060
11394
|
const command = options.command || defaultRuntimeBuildCommand();
|
|
11061
11395
|
if (options.jsonOutput) {
|
|
11062
11396
|
printRuntimeProgress(`构建 React SPA runtime: ${command}`);
|
|
11063
11397
|
} else {
|
|
11064
11398
|
print(`构建 React SPA runtime: ${command}`);
|
|
11065
11399
|
}
|
|
11066
|
-
|
|
11067
|
-
|
|
11068
|
-
|
|
11069
|
-
|
|
11070
|
-
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
|
|
11077
|
-
|
|
11400
|
+
await new Promise((resolve, reject) => {
|
|
11401
|
+
let child;
|
|
11402
|
+
try {
|
|
11403
|
+
child = spawn(command, [], {
|
|
11404
|
+
cwd: process.cwd(),
|
|
11405
|
+
shell: true,
|
|
11406
|
+
stdio: options.jsonOutput
|
|
11407
|
+
? ['ignore', 'pipe', 'pipe']
|
|
11408
|
+
: 'inherit',
|
|
11409
|
+
env: {
|
|
11410
|
+
...process.env,
|
|
11411
|
+
OPENXIANGDA_APP_TYPE: options.appType,
|
|
11412
|
+
APP_TYPE: options.appType,
|
|
11413
|
+
OPENXIANGDA_BUILD_ID: options.buildId,
|
|
11414
|
+
OPENXIANGDA_RUNTIME_ASSET_BASE: options.assetBaseUrl,
|
|
11415
|
+
},
|
|
11416
|
+
});
|
|
11417
|
+
} catch (error) {
|
|
11418
|
+
reject(
|
|
11419
|
+
new Error(`runtime build 无法启动: ${error?.message || String(error)}`)
|
|
11420
|
+
);
|
|
11421
|
+
return;
|
|
11422
|
+
}
|
|
11423
|
+
if (options.jsonOutput) {
|
|
11424
|
+
child.stdout?.on('data', chunk => {
|
|
11425
|
+
process.stderr.write(maskText(String(chunk)));
|
|
11426
|
+
});
|
|
11427
|
+
child.stderr?.on('data', chunk => {
|
|
11428
|
+
process.stderr.write(maskText(String(chunk)));
|
|
11429
|
+
});
|
|
11430
|
+
}
|
|
11431
|
+
child.once('error', error => {
|
|
11432
|
+
reject(new Error(`runtime build 无法启动: ${error.message}`));
|
|
11433
|
+
});
|
|
11434
|
+
child.once('close', (code, signal) => {
|
|
11435
|
+
if (code === 0) {
|
|
11436
|
+
resolve();
|
|
11437
|
+
return;
|
|
11438
|
+
}
|
|
11439
|
+
reject(
|
|
11440
|
+
new Error(
|
|
11441
|
+
`runtime build 失败: ${
|
|
11442
|
+
Number.isInteger(code) ? `exit ${code}` : `signal ${signal || '-'}`
|
|
11443
|
+
}`
|
|
11444
|
+
)
|
|
11445
|
+
);
|
|
11446
|
+
});
|
|
11078
11447
|
});
|
|
11079
|
-
if (options.jsonOutput) {
|
|
11080
|
-
if (result.stdout) process.stderr.write(maskText(result.stdout));
|
|
11081
|
-
if (result.stderr) process.stderr.write(maskText(result.stderr));
|
|
11082
|
-
}
|
|
11083
|
-
if (result.error) fail(`runtime build 无法启动: ${result.error.message}`);
|
|
11084
|
-
if (result.status !== 0) fail(`runtime build 失败: exit ${result.status}`);
|
|
11085
11448
|
}
|
|
11086
11449
|
|
|
11087
11450
|
function assertRuntimeBuildReady(explicitCommand) {
|
|
@@ -14705,6 +15068,18 @@ async function publishResourcesForWorkspace(config, profileName, options = {}) {
|
|
|
14705
15068
|
}
|
|
14706
15069
|
const target = getWorkspaceTarget(config, profileName, {});
|
|
14707
15070
|
const plan = await buildResourcePlan(config, target, manifest);
|
|
15071
|
+
if (plan.formFieldContracts?.valid === false) {
|
|
15072
|
+
const violations = plan.formFieldContracts.errors || [];
|
|
15073
|
+
const error = new Error(
|
|
15074
|
+
`FORM_FIELD_CONTRACT_INVALID: ${violations
|
|
15075
|
+
.slice(0, 8)
|
|
15076
|
+
.map(item => item.message)
|
|
15077
|
+
.join('; ')}`
|
|
15078
|
+
);
|
|
15079
|
+
error.code = 'FORM_FIELD_CONTRACT_INVALID';
|
|
15080
|
+
error.violations = violations;
|
|
15081
|
+
throw error;
|
|
15082
|
+
}
|
|
14708
15083
|
const pruneCandidates = options.prune
|
|
14709
15084
|
? await collectPruneResourceCandidates(config, target, manifest)
|
|
14710
15085
|
: [];
|
|
@@ -14740,7 +15115,10 @@ async function buildResourcePlan(config, target, manifest) {
|
|
|
14740
15115
|
manifest
|
|
14741
15116
|
);
|
|
14742
15117
|
await prepareManifestJsCodeBundlesForPlan(manifest);
|
|
14743
|
-
const existing = await
|
|
15118
|
+
const [existing, formFieldContracts] = await Promise.all([
|
|
15119
|
+
fetchExistingResourceMaps(config, target, manifest),
|
|
15120
|
+
buildFormFieldContractPlan(config, target, manifest),
|
|
15121
|
+
]);
|
|
14744
15122
|
const actions = [];
|
|
14745
15123
|
addPlanActions(actions, 'role', manifest.roles, existing.roles, roleEquals);
|
|
14746
15124
|
addPlanActions(actions, 'menu', manifest.menus, existing.menus, (item, current) => menuEquals(target.bound, item, current));
|
|
@@ -14780,6 +15158,7 @@ async function buildResourcePlan(config, target, manifest) {
|
|
|
14780
15158
|
...(manifest.resourceCodeFilters ? { resourceCodeFilters: manifest.resourceCodeFilters } : {}),
|
|
14781
15159
|
actions,
|
|
14782
15160
|
summary: summarizeActions(actions),
|
|
15161
|
+
formFieldContracts,
|
|
14783
15162
|
...(functionSecretRefs
|
|
14784
15163
|
? {
|
|
14785
15164
|
capabilities: {
|
|
@@ -16170,7 +16549,14 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
|
|
|
16170
16549
|
}
|
|
16171
16550
|
|
|
16172
16551
|
await publishRoleResources(config, target, manifest.roles || [], result, publishOptions);
|
|
16173
|
-
await publishFormSettingsResources(
|
|
16552
|
+
await publishFormSettingsResources(
|
|
16553
|
+
config,
|
|
16554
|
+
target,
|
|
16555
|
+
manifest.formSettings || [],
|
|
16556
|
+
manifest.formPermissionGroups || [],
|
|
16557
|
+
result,
|
|
16558
|
+
publishOptions
|
|
16559
|
+
);
|
|
16174
16560
|
await publishMenuResources(config, target, manifest.menus || [], result, publishOptions);
|
|
16175
16561
|
await publishConnectorResources(config, target, manifest.connectors || [], result, publishOptions);
|
|
16176
16562
|
await publishNotificationResources(config, target, manifest.notifications || [], result, publishOptions);
|
|
@@ -16200,7 +16586,13 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
|
|
|
16200
16586
|
await publishStorageConfigResources(config, target, manifest.storageConfigs || [], result, publishOptions);
|
|
16201
16587
|
await publishPublicAccessPolicyResources(config, target, manifest.publicAccessPolicies || [], result, publishOptions);
|
|
16202
16588
|
await publishPagePermissionGroupResources(config, target, manifest.pagePermissionGroups || [], result, publishOptions);
|
|
16203
|
-
await publishFormPermissionGroupResources(
|
|
16589
|
+
await publishFormPermissionGroupResources(
|
|
16590
|
+
config,
|
|
16591
|
+
target,
|
|
16592
|
+
manifest.formPermissionGroups || [],
|
|
16593
|
+
result,
|
|
16594
|
+
publishOptions
|
|
16595
|
+
);
|
|
16204
16596
|
if (options.prune) {
|
|
16205
16597
|
await pruneResourceManifest(
|
|
16206
16598
|
config,
|
|
@@ -17591,22 +17983,82 @@ async function publishFormSettingsResources(
|
|
|
17591
17983
|
config,
|
|
17592
17984
|
target,
|
|
17593
17985
|
settingsItems,
|
|
17986
|
+
permissionGroups,
|
|
17594
17987
|
result,
|
|
17595
17988
|
options = {}
|
|
17596
17989
|
) {
|
|
17990
|
+
const settingsByForm = new Map();
|
|
17597
17991
|
for (const item of settingsItems) {
|
|
17598
17992
|
const code = item.code || item.formCode || item.formUuid;
|
|
17599
|
-
|
|
17600
|
-
|
|
17993
|
+
const formUuid = resolveManifestFormUuid(target.bound, item, {
|
|
17994
|
+
fallbackToCode: true,
|
|
17995
|
+
});
|
|
17996
|
+
if (!formUuid) fail(`表单设置 ${code} 无法解析 formUuid`);
|
|
17997
|
+
settingsByForm.set(formUuid, { item, code });
|
|
17998
|
+
}
|
|
17999
|
+
const changedGroupsByForm = new Map();
|
|
18000
|
+
options.formReleaseHandledPermissionCodes =
|
|
18001
|
+
options.formReleaseHandledPermissionCodes || new Set();
|
|
18002
|
+
for (const group of permissionGroups || []) {
|
|
18003
|
+
const code = group.code || group.resourceCode;
|
|
18004
|
+
if (shouldSkipNoopResource(options, 'formPermissionGroup', code)) {
|
|
18005
|
+
recordNoopResource(result, 'formPermissionGroup', code, options);
|
|
18006
|
+
options.formReleaseHandledPermissionCodes.add(code);
|
|
17601
18007
|
continue;
|
|
17602
18008
|
}
|
|
17603
|
-
const formUuid = resolveManifestFormUuid(target.bound,
|
|
17604
|
-
if (!formUuid)
|
|
17605
|
-
|
|
18009
|
+
const formUuid = resolveManifestFormUuid(target.bound, group);
|
|
18010
|
+
if (!formUuid) {
|
|
18011
|
+
fail(`表单权限组 ${code} 无法解析 formUuid,不能进入 FormRelease`);
|
|
18012
|
+
}
|
|
18013
|
+
if (!changedGroupsByForm.has(formUuid)) {
|
|
18014
|
+
changedGroupsByForm.set(formUuid, []);
|
|
18015
|
+
}
|
|
18016
|
+
changedGroupsByForm.get(formUuid).push(group);
|
|
18017
|
+
}
|
|
18018
|
+
const formUuids = unique([
|
|
18019
|
+
...settingsByForm.keys(),
|
|
18020
|
+
...changedGroupsByForm.keys(),
|
|
18021
|
+
]).sort();
|
|
18022
|
+
for (const formUuid of formUuids) {
|
|
18023
|
+
const setting = settingsByForm.get(formUuid);
|
|
18024
|
+
const item = setting?.item || null;
|
|
18025
|
+
const groups = changedGroupsByForm.get(formUuid) || [];
|
|
18026
|
+
const code =
|
|
18027
|
+
setting?.code ||
|
|
18028
|
+
groups[0]?.formCode ||
|
|
18029
|
+
groups[0]?.form ||
|
|
18030
|
+
formUuid;
|
|
18031
|
+
const settingNoop =
|
|
18032
|
+
item && shouldSkipNoopResource(options, 'formSetting', code);
|
|
18033
|
+
if (settingNoop) {
|
|
18034
|
+
recordNoopResource(result, 'formSetting', code, options);
|
|
18035
|
+
}
|
|
18036
|
+
if (settingNoop && groups.length === 0) continue;
|
|
18037
|
+
const action = item
|
|
18038
|
+
? getPlannedResourceAction(options, 'formSetting', code)
|
|
18039
|
+
: null;
|
|
17606
18040
|
const frozen =
|
|
17607
18041
|
frozenFormConfigFromPlanAction(action) ||
|
|
17608
18042
|
(await fetchFrozenFormConfig(config, target, formUuid));
|
|
17609
|
-
const bundleBody =
|
|
18043
|
+
const bundleBody =
|
|
18044
|
+
item && !settingNoop ? buildFormSettingBundleBody(item) : {};
|
|
18045
|
+
if (groups.length > 0) {
|
|
18046
|
+
bundleBody.permissionGroups = groups.map(group => {
|
|
18047
|
+
const body = {
|
|
18048
|
+
...withoutResourceMeta(group),
|
|
18049
|
+
resourceCode: group.code || group.resourceCode,
|
|
18050
|
+
name: group.name || group.code || group.resourceCode,
|
|
18051
|
+
type: group.type || 'view',
|
|
18052
|
+
roles: group.roles || [],
|
|
18053
|
+
};
|
|
18054
|
+
delete body.code;
|
|
18055
|
+
delete body.formCode;
|
|
18056
|
+
delete body.form;
|
|
18057
|
+
delete body.formUuid;
|
|
18058
|
+
return body;
|
|
18059
|
+
});
|
|
18060
|
+
bundleBody.permissionGroupsMode = 'merge';
|
|
18061
|
+
}
|
|
17610
18062
|
const activate = options.activateFormBundles === true;
|
|
17611
18063
|
const artifactId = stableFormArtifactId(
|
|
17612
18064
|
'config-bundle',
|
|
@@ -17627,7 +18079,19 @@ async function publishFormSettingsResources(
|
|
|
17627
18079
|
},
|
|
17628
18080
|
{ frozen, artifactId }
|
|
17629
18081
|
);
|
|
17630
|
-
|
|
18082
|
+
if (item) {
|
|
18083
|
+
saveResourceEntry(target, 'formSettings', code, { formUuid });
|
|
18084
|
+
}
|
|
18085
|
+
for (const group of groups) {
|
|
18086
|
+
const groupCode = group.code || group.resourceCode;
|
|
18087
|
+
saveFormPermissionGroupResource(
|
|
18088
|
+
target,
|
|
18089
|
+
groupCode,
|
|
18090
|
+
undefined,
|
|
18091
|
+
{ formUuid }
|
|
18092
|
+
);
|
|
18093
|
+
options.formReleaseHandledPermissionCodes.add(groupCode);
|
|
18094
|
+
}
|
|
17631
18095
|
const releaseId = data?.release?.id;
|
|
17632
18096
|
const contentHash = String(
|
|
17633
18097
|
data?.release?.contentHash || data?.contentHash || ''
|
|
@@ -17676,20 +18140,36 @@ async function publishFormSettingsResources(
|
|
|
17676
18140
|
delete result.stagedResource;
|
|
17677
18141
|
}
|
|
17678
18142
|
}
|
|
17679
|
-
|
|
17680
|
-
|
|
17681
|
-
|
|
17682
|
-
|
|
17683
|
-
|
|
17684
|
-
|
|
17685
|
-
|
|
17686
|
-
|
|
17687
|
-
|
|
17688
|
-
|
|
17689
|
-
|
|
17690
|
-
|
|
17691
|
-
|
|
17692
|
-
|
|
18143
|
+
if (item && !settingNoop) {
|
|
18144
|
+
result.published.push({
|
|
18145
|
+
kind: 'formSetting',
|
|
18146
|
+
code,
|
|
18147
|
+
action: data?.noop ? 'noop' : data?.active ? 'update' : 'stage',
|
|
18148
|
+
formUuid,
|
|
18149
|
+
revision: data?.revision,
|
|
18150
|
+
activeFormReleaseHead: data?.activeFormReleaseHead,
|
|
18151
|
+
staged: Boolean(data?.staged),
|
|
18152
|
+
active: Boolean(data?.active),
|
|
18153
|
+
releaseStatus: data?.releaseStatus,
|
|
18154
|
+
releaseId,
|
|
18155
|
+
contentHash: contentHash || undefined,
|
|
18156
|
+
...(stagedResource ? { stagedResource } : {}),
|
|
18157
|
+
});
|
|
18158
|
+
}
|
|
18159
|
+
for (const group of groups) {
|
|
18160
|
+
result.published.push({
|
|
18161
|
+
kind: 'formPermissionGroup',
|
|
18162
|
+
code: group.code || group.resourceCode,
|
|
18163
|
+
action: data?.noop ? 'noop' : data?.active ? 'update' : 'stage',
|
|
18164
|
+
formUuid,
|
|
18165
|
+
staged: Boolean(data?.staged),
|
|
18166
|
+
active: Boolean(data?.active),
|
|
18167
|
+
releaseStatus: data?.releaseStatus,
|
|
18168
|
+
releaseId,
|
|
18169
|
+
contentHash: contentHash || undefined,
|
|
18170
|
+
...(stagedResource ? { stagedResource } : {}),
|
|
18171
|
+
});
|
|
18172
|
+
}
|
|
17693
18173
|
}
|
|
17694
18174
|
}
|
|
17695
18175
|
|
|
@@ -18810,38 +19290,24 @@ async function findExistingPagePermissionGroup(config, target, code) {
|
|
|
18810
19290
|
|
|
18811
19291
|
async function publishFormPermissionGroupResources(config, target, groups, result, options = {}) {
|
|
18812
19292
|
for (const group of groups) {
|
|
19293
|
+
if (
|
|
19294
|
+
options.formReleaseHandledPermissionCodes?.has(
|
|
19295
|
+
group.code || group.resourceCode
|
|
19296
|
+
)
|
|
19297
|
+
) {
|
|
19298
|
+
continue;
|
|
19299
|
+
}
|
|
18813
19300
|
if (shouldSkipNoopResource(options, 'formPermissionGroup', group.code)) {
|
|
18814
19301
|
recordNoopResource(result, 'formPermissionGroup', group.code, options);
|
|
18815
19302
|
continue;
|
|
18816
19303
|
}
|
|
18817
|
-
const
|
|
18818
|
-
|
|
18819
|
-
|
|
18820
|
-
|
|
18821
|
-
|
|
18822
|
-
|
|
18823
|
-
|
|
18824
|
-
type: group.type || 'view',
|
|
18825
|
-
roles: group.roles || [],
|
|
18826
|
-
};
|
|
18827
|
-
delete body.code;
|
|
18828
|
-
delete body.formCode;
|
|
18829
|
-
delete body.form;
|
|
18830
|
-
const data = existing
|
|
18831
|
-
? await requestWithAuth(
|
|
18832
|
-
config,
|
|
18833
|
-
target.profileName,
|
|
18834
|
-
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/permission-groups/${encodeURIComponent(existing.id)}`,
|
|
18835
|
-
{ method: 'POST', body }
|
|
18836
|
-
)
|
|
18837
|
-
: await requestWithAuth(
|
|
18838
|
-
config,
|
|
18839
|
-
target.profileName,
|
|
18840
|
-
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/permission-groups`,
|
|
18841
|
-
{ method: 'POST', body }
|
|
18842
|
-
);
|
|
18843
|
-
if (data?.id) saveFormPermissionGroupResource(target, group.code, data.id, { formUuid, name: data.name, resourceCode: group.code });
|
|
18844
|
-
result.published.push({ kind: 'formPermissionGroup', code: group.code, action: existing ? 'update' : 'create', id: data?.id });
|
|
19304
|
+
const error = new Error(
|
|
19305
|
+
`FORM_PERMISSION_GROUP_STAGE_REQUIRED: ${
|
|
19306
|
+
group.code || group.resourceCode
|
|
19307
|
+
} 未进入 FormRelease,禁止回退为直接线上写入`
|
|
19308
|
+
);
|
|
19309
|
+
error.code = 'FORM_PERMISSION_GROUP_STAGE_REQUIRED';
|
|
19310
|
+
throw error;
|
|
18845
19311
|
}
|
|
18846
19312
|
}
|
|
18847
19313
|
|
|
@@ -23461,6 +23927,32 @@ function publishContextHeadersForRequest(config, profileName, apiPath, options =
|
|
|
23461
23927
|
);
|
|
23462
23928
|
}
|
|
23463
23929
|
|
|
23930
|
+
function isPublishMutationRequest(apiPath, options = {}) {
|
|
23931
|
+
const method = String(options.method || 'GET').toUpperCase();
|
|
23932
|
+
if (['GET', 'HEAD', 'OPTIONS'].includes(method)) return false;
|
|
23933
|
+
const normalizedPath = String(apiPath || '').split('?')[0];
|
|
23934
|
+
if (
|
|
23935
|
+
normalizedPath.includes('/publish-lease/') ||
|
|
23936
|
+
normalizedPath.endsWith('/publish-lease/acquire') ||
|
|
23937
|
+
normalizedPath.includes('/change-baselines')
|
|
23938
|
+
) {
|
|
23939
|
+
return false;
|
|
23940
|
+
}
|
|
23941
|
+
if (
|
|
23942
|
+
normalizedPath.endsWith('/oss-upload-plan') &&
|
|
23943
|
+
options.body?.planOnly === true
|
|
23944
|
+
) {
|
|
23945
|
+
return false;
|
|
23946
|
+
}
|
|
23947
|
+
if (
|
|
23948
|
+
normalizedPath.endsWith('/schema-storage-plan') ||
|
|
23949
|
+
normalizedPath.endsWith('/storage-plan')
|
|
23950
|
+
) {
|
|
23951
|
+
return false;
|
|
23952
|
+
}
|
|
23953
|
+
return true;
|
|
23954
|
+
}
|
|
23955
|
+
|
|
23464
23956
|
async function requestWithAuth(config, profileName, apiPath, options = {}) {
|
|
23465
23957
|
assertReadOnlyHttpRequest(apiPath, options);
|
|
23466
23958
|
const resolved = getProfile(config, profileName);
|
|
@@ -23482,6 +23974,9 @@ async function requestWithAuth(config, profileName, apiPath, options = {}) {
|
|
|
23482
23974
|
) {
|
|
23483
23975
|
await activePublishLeaseHeartbeat?.heartbeat?.beforeWrite();
|
|
23484
23976
|
}
|
|
23977
|
+
if (isPublishMutationRequest(apiPath, requestOptions)) {
|
|
23978
|
+
markCommandScopedPublishWriteAttempted();
|
|
23979
|
+
}
|
|
23485
23980
|
// Build headers after the lease check: a successful renew may replace the
|
|
23486
23981
|
// live client session/expiry metadata, and auth/transient retries must not
|
|
23487
23982
|
// replay stale publish context.
|
|
@@ -23542,6 +24037,7 @@ async function requestFormWithAuth(config, profileName, apiPath, formDataFactory
|
|
|
23542
24037
|
if (!skipPublishLeaseHeartbeat) {
|
|
23543
24038
|
await activePublishLeaseHeartbeat?.heartbeat?.beforeWrite();
|
|
23544
24039
|
}
|
|
24040
|
+
markCommandScopedPublishWriteAttempted();
|
|
23545
24041
|
const currentRequestOptions = {
|
|
23546
24042
|
...requestOptions,
|
|
23547
24043
|
headers: {
|
|
@@ -23759,7 +24255,7 @@ function normalizeWorkspacePublishOptions(flags) {
|
|
|
23759
24255
|
};
|
|
23760
24256
|
}
|
|
23761
24257
|
|
|
23762
|
-
function runWorkspacePublish(profileName, profile, appType, publishArgs = []) {
|
|
24258
|
+
async function runWorkspacePublish(profileName, profile, appType, publishArgs = []) {
|
|
23763
24259
|
const packageFile = path.join(process.cwd(), 'package.json');
|
|
23764
24260
|
if (!fs.existsSync(packageFile)) {
|
|
23765
24261
|
fail('当前目录没有 package.json,无法识别工作区发布脚本');
|
|
@@ -23781,14 +24277,13 @@ function runWorkspacePublish(profileName, profile, appType, publishArgs = []) {
|
|
|
23781
24277
|
? ['run', scriptName, '--', ...publishArgs]
|
|
23782
24278
|
: ['run', scriptName];
|
|
23783
24279
|
const globalEnv = loadGlobalEnv();
|
|
23784
|
-
|
|
24280
|
+
await runWorkspaceChildCommand(command, args, {
|
|
23785
24281
|
cwd: process.cwd(),
|
|
23786
|
-
stdio: 'inherit',
|
|
23787
24282
|
env: buildWorkspacePublishEnv(profileName, profile, appType, globalEnv),
|
|
24283
|
+
failureCode: 'WORKSPACE_PUBLISH_FAILED',
|
|
24284
|
+
label: scriptName,
|
|
24285
|
+
marksPublishWrite: true,
|
|
23788
24286
|
});
|
|
23789
|
-
if (result.status !== 0) {
|
|
23790
|
-
process.exit(result.status || 1);
|
|
23791
|
-
}
|
|
23792
24287
|
}
|
|
23793
24288
|
|
|
23794
24289
|
function isReactSpaWorkspace() {
|
|
@@ -23798,7 +24293,12 @@ function isReactSpaWorkspace() {
|
|
|
23798
24293
|
return /runtimeMode\s*:\s*['"]react-spa['"]/.test(content);
|
|
23799
24294
|
}
|
|
23800
24295
|
|
|
23801
|
-
function runReactSpaFormSchemaPublish(
|
|
24296
|
+
async function runReactSpaFormSchemaPublish(
|
|
24297
|
+
profileName,
|
|
24298
|
+
profile,
|
|
24299
|
+
appType,
|
|
24300
|
+
publishOptions
|
|
24301
|
+
) {
|
|
23802
24302
|
const packageFile = path.join(process.cwd(), 'package.json');
|
|
23803
24303
|
if (!fs.existsSync(packageFile)) {
|
|
23804
24304
|
fail('当前目录没有 package.json,无法识别工作区发布脚本');
|
|
@@ -23818,14 +24318,137 @@ function runReactSpaFormSchemaPublish(profileName, profile, appType, publishOpti
|
|
|
23818
24318
|
`React SPA 表单 schema 发布${publishOptions.dryRun ? ' (dry-run)' : ''}: ${publishOptions.targetForm}`
|
|
23819
24319
|
);
|
|
23820
24320
|
const globalEnv = loadGlobalEnv();
|
|
23821
|
-
|
|
24321
|
+
await runWorkspaceChildCommand(command, args, {
|
|
23822
24322
|
cwd: process.cwd(),
|
|
23823
|
-
stdio: 'inherit',
|
|
23824
24323
|
env: buildWorkspacePublishEnv(profileName, profile, appType, globalEnv),
|
|
24324
|
+
failureCode: 'FORM_SCHEMA_PUBLISH_FAILED',
|
|
24325
|
+
label: `form schema ${publishOptions.targetForm}`,
|
|
24326
|
+
marksPublishWrite: true,
|
|
23825
24327
|
});
|
|
23826
|
-
|
|
23827
|
-
|
|
24328
|
+
}
|
|
24329
|
+
|
|
24330
|
+
async function runWorkspaceChildCommand(command, args, options = {}) {
|
|
24331
|
+
await new Promise((resolve, reject) => {
|
|
24332
|
+
const child = spawn(command, args, {
|
|
24333
|
+
cwd: options.cwd || process.cwd(),
|
|
24334
|
+
stdio: 'inherit',
|
|
24335
|
+
env: options.env || process.env,
|
|
24336
|
+
});
|
|
24337
|
+
child.once('spawn', () => {
|
|
24338
|
+
if (options.marksPublishWrite) {
|
|
24339
|
+
markCommandScopedPublishWriteAttempted();
|
|
24340
|
+
}
|
|
24341
|
+
});
|
|
24342
|
+
child.once('error', reject);
|
|
24343
|
+
child.once('close', (code, signal) => {
|
|
24344
|
+
if (code === 0) {
|
|
24345
|
+
resolve();
|
|
24346
|
+
return;
|
|
24347
|
+
}
|
|
24348
|
+
const error = new Error(
|
|
24349
|
+
`${options.label || command} 执行失败${signal ? `(signal=${signal})` : `(exit=${code ?? 1})`}`
|
|
24350
|
+
);
|
|
24351
|
+
error.code = options.failureCode || 'WORKSPACE_CHILD_COMMAND_FAILED';
|
|
24352
|
+
error.exitCode = Number.isInteger(code) ? code : 1;
|
|
24353
|
+
reject(error);
|
|
24354
|
+
});
|
|
24355
|
+
});
|
|
24356
|
+
}
|
|
24357
|
+
|
|
24358
|
+
async function buildFormFieldContractPlan(config, target, manifest) {
|
|
24359
|
+
const targets = [];
|
|
24360
|
+
for (const [manifestKey, kind] of [
|
|
24361
|
+
['functions', 'Function'],
|
|
24362
|
+
['automations', 'Automation'],
|
|
24363
|
+
['workflows', 'Workflow'],
|
|
24364
|
+
['jsCodeNodes', 'JsCodeNode'],
|
|
24365
|
+
]) {
|
|
24366
|
+
for (const item of manifest[manifestKey] || []) {
|
|
24367
|
+
const analysis = item.__sourceAnalysis;
|
|
24368
|
+
if (!analysis?.formFieldReferences?.length) continue;
|
|
24369
|
+
const code =
|
|
24370
|
+
item.code ||
|
|
24371
|
+
item.functionCode ||
|
|
24372
|
+
item.resourceCode ||
|
|
24373
|
+
item.scriptCode;
|
|
24374
|
+
for (const reference of analysis.formFieldReferences) {
|
|
24375
|
+
const formUuid = resolveManifestFormBindingForContract(
|
|
24376
|
+
target,
|
|
24377
|
+
item,
|
|
24378
|
+
reference.formCode
|
|
24379
|
+
);
|
|
24380
|
+
targets.push({
|
|
24381
|
+
kind,
|
|
24382
|
+
code,
|
|
24383
|
+
formCode: reference.formCode,
|
|
24384
|
+
formUuid,
|
|
24385
|
+
field: reference.field,
|
|
24386
|
+
api: reference.api,
|
|
24387
|
+
file: reference.file,
|
|
24388
|
+
line: reference.line,
|
|
24389
|
+
});
|
|
24390
|
+
}
|
|
24391
|
+
}
|
|
24392
|
+
}
|
|
24393
|
+
if (targets.length === 0) {
|
|
24394
|
+
return {
|
|
24395
|
+
contractVersion: 'form_field_contract_v1',
|
|
24396
|
+
valid: true,
|
|
24397
|
+
checkedForms: 0,
|
|
24398
|
+
checkedReferences: 0,
|
|
24399
|
+
errors: [],
|
|
24400
|
+
};
|
|
23828
24401
|
}
|
|
24402
|
+
const snapshots = new Map();
|
|
24403
|
+
const formTargets = unique(
|
|
24404
|
+
targets.map(item => item.formUuid).filter(Boolean)
|
|
24405
|
+
).sort();
|
|
24406
|
+
await Promise.all(
|
|
24407
|
+
formTargets.map(async formUuid => {
|
|
24408
|
+
const frozen = await fetchFrozenFormConfig(
|
|
24409
|
+
config,
|
|
24410
|
+
target,
|
|
24411
|
+
formUuid
|
|
24412
|
+
);
|
|
24413
|
+
snapshots.set(formUuid, collectFormSnapshotFieldIds(frozen.snapshot));
|
|
24414
|
+
})
|
|
24415
|
+
);
|
|
24416
|
+
return validateFormFieldContracts(targets, snapshots);
|
|
24417
|
+
}
|
|
24418
|
+
|
|
24419
|
+
function resolveManifestFormBindingForContract(target, item, formCode) {
|
|
24420
|
+
const containers = [
|
|
24421
|
+
item.resourceBindings,
|
|
24422
|
+
item.resources,
|
|
24423
|
+
item.definitionJson?.resourceBindings,
|
|
24424
|
+
item.definitionJson?.resources,
|
|
24425
|
+
];
|
|
24426
|
+
if (item.definitionFile) {
|
|
24427
|
+
try {
|
|
24428
|
+
const definition = JSON.parse(
|
|
24429
|
+
fs.readFileSync(
|
|
24430
|
+
path.resolve(item.__dir || process.cwd(), item.definitionFile),
|
|
24431
|
+
'utf8'
|
|
24432
|
+
)
|
|
24433
|
+
);
|
|
24434
|
+
containers.push(definition.resourceBindings, definition.resources);
|
|
24435
|
+
} catch {
|
|
24436
|
+
// Resource validation reports invalid definition files separately.
|
|
24437
|
+
}
|
|
24438
|
+
}
|
|
24439
|
+
for (const container of containers) {
|
|
24440
|
+
const binding = container?.forms?.[formCode];
|
|
24441
|
+
if (typeof binding === 'string' && binding.trim()) return binding.trim();
|
|
24442
|
+
const formUuid = String(
|
|
24443
|
+
binding?.formUuid || binding?.id || binding?.uuid || ''
|
|
24444
|
+
).trim();
|
|
24445
|
+
if (formUuid) return formUuid;
|
|
24446
|
+
}
|
|
24447
|
+
return resolveManifestFormUuid(
|
|
24448
|
+
target.bound,
|
|
24449
|
+
{ formCode },
|
|
24450
|
+
{ fallbackToCode: false }
|
|
24451
|
+
);
|
|
23829
24452
|
}
|
|
23830
24453
|
|
|
23831
24454
|
function buildWorkspacePublishEnv(profileName, profile, appType, globalEnv) {
|